A Flutter application for reading novels with localization and Text-To-Speech (TTS) support.
Get up and running in 3 steps:
-
Install dependencies
make deps
-
Run the app
# Web (recommended for quick start) make dev-web # Or choose a platform: make dev-chrome # Chrome desktop app make macos # macOS app make build-android # Android APK release build
-
Verify setup
# Run tests make test # Check code quality make lint
That's it! 🎉
- Reads novels/chapters from the backend and supports offline caching.
- Saves reading progress.
- Provides TTS playback with locale mapping and configurable settings.
- Ships with Makefile targets to simplify development and release builds across platforms.
- Home AppBar uses a logo that opens the sidebar on tap.
- Home sidebar includes: Settings, Character Templates, Scene Templates, Prompts, New Novel, About.
- About page shows a large logo above the title.
- AI Coach (Snowflake) panel keeps chat history for the current session and persists per-novel coaching state.
- Editor enhancements: formatting toolbar, markdown preview, writing stats (word/char/read time + streak), Zen mode, focus timer, and writing prompts.
- UI polish does not require
flutter_animate/lottie/confetti/shimmer; shimmer usesskeletonizerand confetti is implemented in-app.
This app uses a token-driven design system to keep the Writer UI visually and functionally consistent.
- Prefer
ThemeDatafor global look-and-feel (buttons, inputs, dialogs, nav) over per-screen styling. - Prefer shared primitives in
lib/shared/widgets/over bespoke one-off components in screens. - When adding new UI, reuse an existing primitive or extend it; avoid duplicating variants.
- Use
ColorSchemeand tokenized semantic colors inlib/theme/design_tokens.dart(avoid hard-codedColors.*for UI). - Typography is controlled by theme and typography presets; do not set ad-hoc font sizes in screens unless it’s content-specific.
- Settings → Color Theme options are generated from the Theme Factory definitions in
lib/theme/themes.dart(themeFactoryThemes). - To add/modify a theme: update
themeFactoryThemeswith its name + palette colors, then runmake testto verify switching and previews. - Current themes:
- Ocean Depths
- Sunset Boulevard
- Forest Canopy
- Modern Minimalist
- Golden Hour
- Arctic Frost
- Desert Rose
- Tech Innovation
- Botanical Garden
- Midnight Galaxy
- Embedded Chinese font:
assets/fonts/NotoSansSC-{Regular,Bold}.ttf(family:Noto Sans SC, declared inpubspec.yaml). - Initialization preload:
lib/main.dartpreloads the embedded Chinese font to reduce first-render tofu/FOUT. - Fallback chain:
lib/theme/font_packs.dartapplies a platform-aware CJK fallback list to the appTextTheme(even when using Inter/Merriweather). - Custom font picker: Settings → Typography only shows curated CJK-capable font families (plus the embedded font).
- Apple:
PingFang SC,Hiragino Sans GB,Heiti SC,Songti SC - Windows:
Microsoft YaHei,Microsoft YaHei UI,Microsoft JhengHei,SimSun,SimHei - Linux/Android:
Noto Sans CJK SC,Noto Sans CJK,WenQuanYi Micro Hei,AR PL UMing CN,AR PL UKai CN - Embedded:
Noto Sans SC
web/index.htmldeclares@font-faceforNoto Sans SCand preloads the.ttffiles withfont-display: swap.
- If bundle size becomes a concern, subset the embedded font and replace the
assets/fonts/*files +pubspec.yamlentries. - One common approach is
fonttools(pyftsubset) to produce a reducedwoff2/ttfcontaining only needed Unicode ranges.
- Desktop browsers: Chrome, Firefox, Safari, Edge
- OS: Windows, macOS, Linux
- Mobile: iOS Safari, Android Chrome
- Verify: mixed Latin + Chinese + punctuation, bold text, and long paragraphs in Reader/Editor screens
- Use spacing/radius tokens (
Spacing.*,Radii.*,MobileSpacing.*) for padding, gaps, and corner rounding. - Maintain minimum tap targets (
MobileSpacing.touchTargetMin) for interactive controls.
- Use shared motion tokens (
Motion.*) for transitions and repeating animations. - Follow the app motion preference (reduce motion disables page transitions via theme builder).
- Inputs use the app
InputDecorationThemefor focus/error borders and fill colors. - Error UX should follow the same pattern: clear message, optional retry, and no layout shifts where possible.
- Buttons:
lib/shared/widgets/app_buttons.dart(AppButtons.primary/secondary/text/icon) - Form controls:
ThemeData.inputDecorationTheme,CheckboxThemeData,SwitchThemeData - Loading:
lib/shared/widgets/loading_state.dart(LoadingState) - Empty/error:
lib/shared/widgets/empty_state.dart,lib/shared/widgets/error_view.dart - Notifications:
lib/shared/widgets/feedback/enhanced_toast.dart(showEnhancedToast),SnackBarThemeData - Modals:
DialogThemeData,lib/shared/widgets/mobile_bottom_sheet.dart - Navigation:
NavigationBarThemeData,lib/shared/widgets/mobile_bottom_nav_bar.dart
- Use theme-driven contrast (avoid custom low-contrast grays).
- Ensure touch targets meet the minimum size and provide semantics/labels for icon-only controls (tooltip where appropriate).
- Prefer
FilledButton/OutlinedButton/TextButtonandInputDecorationThemeso focus and disabled states remain consistent.
- Flutter SDK installed (
flutter --version). - Platform toolchains as needed:
- Android: Android SDK/NDK, Java, Gradle via Flutter.
- iOS/macOS: Xcode (ensure iPhoneOS platform runtime is installed), CocoaPods.
- Windows/Linux: respective build toolchains.
- Node.js 18+ for data import scripts.
- Install dependencies:
make deps - Static analysis and formatting:
make lint
- Web (local dev server):
make dev-web WEB_PORT=5500 - Chrome device:
make dev-chrome - macOS device:
make macos - Android build (copies APK to
/tmp/):make build-android
- The app reads
AI_SERVICE_URLat build time and stores the value in preferences:- App-level fallback when no dart-define is provided:
http://localhost:5600/ - Makefile default used by
maketargets:https://ai.huangjien.com/ - Override at build/run:
--dart-define=AI_SERVICE_URL=https://your-backend.example.com/
- App-level fallback when no dart-define is provided:
- The URL can be edited at runtime in Settings → App Settings → AI Service URL.
- Backend endpoint:
POST /snowflake/refine - Returns coaching JSON with
status,critique,question, andsuggestions. Whenstatus = "refined", it includesrefined_summaryand the app applies it to the Summary field automatically. - Chat history is included in responses and rendered in the Coach panel; history is stored per novel.
- Run tests with coverage summary:
make test
test/routing/app_router_coverage_test.dartprovides comprehensive coverage for the app router.- It validates navigation to over 20 routes including:
- Auth screens (
/signup,/forgot-password, etc.) - Simple screens (
/about,/my-novels, etc.) - List screens (
/prompts,/patterns, etc.) - Admin screens (
/admin/users,/admin/logs) - Nested novel routes (
/novel/:id/summary,/novel/:id/characters, etc.)
- Auth screens (
- These tests use
Fakerepositories and services to isolate routing logic from backend dependencies.
- Obtain the access token from the current session:
final token = ref.watch(sessionProvider);
- Send it in the
Authorizationheader (Bearerscheme) when calling the backend:
import 'package:http/http.dart' as http;
Future<http.Response> callBackend(Uri url) async {
final token = ref.watch(sessionProvider);
final headers = {
if (token != null) 'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
return http.get(url, headers: headers);
}- The app attaches this token automatically for AI calls and health checks; if a session exists but no token is present, it refreshes the session before retrying.
- The backend verifies this token via the auth service (
/auth/verify). Requests without a token or with an invalid token receive401 Unauthorized. Premium-only routes return403 Forbiddenfor non-premium users.
POST /agents/qarequires authentication.POST /agents/respondrequires authentication and a premium plan.
- The app polls
/healthadaptively:- Healthy → next check after 8 minutes
- Unhealthy → next check after 2 minutes
- Implementation:
lib/features/ai_chat/state/ai_chat_providers.dart.
- Web (release):
make build-web - Serve built web:
make serve-web-build WEB_PORT=8080 - Android (APK release):
make build-android- Automatically runs a plugin patch to ensure
isar_flutter_libscompiles with SDK 36.
- Automatically runs a plugin patch to ensure
- Android (AAB for Play Console):
flutter build appbundle --release - Android (APK direct install):
flutter build apk --release - iOS (IPA for iPhone install):
make build-ios(alias ofmake build-ipa) - iOS (Xcode project, no codesign):
make build-ios-app - iOS (IPA):
- Signed (installable on device):
make build-ipa - No codesign (export-only, not installable):
make build-ipa-nocodesign
- Signed (installable on device):
- Desktop:
- macOS:
make build-macos - Windows:
make build-windows - Linux:
make build-linux
- macOS:
- A device-installable IPA must be signed (Apple Development / Ad Hoc / TestFlight).
- One-time setup:
- Open
ios/Runner.xcworkspacein Xcode - In the Runner target:
- Set a unique Bundle Identifier
- Set Signing Team (your Apple ID)
- Ensure “Automatically manage signing” is enabled (for development builds)
- In Xcode: install the required iOS platform/device support under Xcode Settings → Platforms
- Open
- Build:
make build-ios- Output:
build/ios/ipa/*.ipa
- Install:
- Auto-detect + install:
make install-ios- Optional:
make install-ios DEVICE=<udid> IPA_PATH=build/ios/ipa/your.ipa
- Optional:
- Or manual: Xcode → Window → Devices and Simulators → select your iPhone → drag the
.ipaonto the device
- Auto-detect + install:
- Android
- The project patches
isar_flutter_libsGradle (scripts/patch_isar.js) to setcompileSdkVersion 36and fix resource linking errors (e.g.,android:attr/lStar). - If you run
flutter pub upgradeand the plugin cache changes, the Makefile pre-step re-applies the patch before Android builds. - Package name/namespace: update
android/app/build.gradle.kts(namespace) andAndroidManifest.xmlbefore publishing. Current namespace:com.huangjien.writer.
- The project patches
- iOS
- Xcode 16 requires installing the iOS platform runtime (e.g., iPhoneOS 26.0). Install from
Xcode → Settings → Platforms. - For device builds and App Store distribution, configure signing in
Runner.xcworkspaceand usebuild-ipa.
- Xcode 16 requires installing the iOS platform runtime (e.g., iPhoneOS 26.0). Install from
- Web icons and favicon are located under
web/andweb/icons/. - These are currently replaced with assets from
/Users/huangjien/workspace/writer/assets/:icon-192x192.png→web/icons/Icon-192.png,web/icons/Icon-maskable-192.pngicon-512x512.png→web/icons/Icon-512.png,web/icons/Icon-maskable-512.pngfavicon.png→web/favicon.png,favicon.ico→web/favicon.ico
- If you’d like unified app icons across mobile/desktop, consider adding
flutter_launcher_iconstopubspec.yamland generating platform icons from a single source image.
-
Android resource linking error (
android:attr/lStar not found)- Ensure the isar plugin is patched (
make build-androidtriggers it). - If necessary, run
node scripts/patch_isar.js, thenmake clean, and rebuild.
- Ensure the isar plugin is patched (
-
iOS platform not installed
- Install the iOS platform runtime in Xcode as noted above.
-
Windows plugin CMake parse error (
flutter_tts)- Some versions of
flutter_ttsinclude CMake script constructs that break generation on certain runners. - CI removes
flutter_ttsfrom the Windows plugin list before building to ensure successful generation. If you need TTS on Windows locally, pin a compatible plugin version or exclude it for Windows.
- Some versions of
The Writer service can be deployed to Google Cloud Platform using the provided Makefile targets.
- Google Cloud SDK installed and configured
- Docker installed and running
- Authentication:
gcloud auth login gcloud auth configure-docker europe-west1-docker.pkg.dev
The following variables can be set in your environment or use defaults:
PROJECT_ID- GCP Project ID (default: inferred from gcloud)RUN_REGION- Cloud Run region (default: europe-west1)REPO_NAME- Artifact Registry repository name (default: writer)SERVICE_NAME- Cloud Run service name (default: writer-web)
# Complete deployment (recommended)
make gcp-deploy-full
# Individual steps
make docker-setup-gcp # Enable required APIs
make docker-create-repo # Create Artifact Registry repo
make docker-build-gar # Build & push Docker image
make docker-deploy-gcp # Deploy to Cloud Run
# Management
make gcp-status # Check deployment status
make gcp-delete-deployment # Delete service and repo- Writer Web: https://writer-web-1026073243556.europe-west1.run.app
- Backend API: https://authorconsole-api-md5e22izxa-ew.a.run.app
The deployment automatically configures these secrets from Secret Manager:
OPENAI_API_KEY- OpenAI API keySUPABASE_SERVICE_ROLE_KEY- Supabase service role key (backend only)
- Docker images are built for
linux/amd64platform to ensure Cloud Run compatibility - Uses Artifact Registry for container storage
- Cloud Run provides serverless hosting with automatic scaling
- HTTPS endpoints with custom domains can be configured in GCP Console
- Print environment passed to builds:
make env-print - Clean Flutter build outputs:
make clean
- Workflow:
.github/workflows/writer-ci.yml- Triggers on push, pull_request, and manual dispatch.
- Sets up Flutter (stable), caches pub packages, runs
make lint, and executes tests. - Current workflow is quality-gate only (lint + tests); release publishing is not part of this workflow.
- Android CI patches
isar_flutter_libsto addnamespaceand setcompileSdkVersion 36. flutter_ttsversion is managed inpubspec.yamland CI applies the Windows plugin patch step from the Makefile flow.
- Never store or ship secrets in the app or in public repos. Use them only server-side or in secure CI contexts.
- TTS locale mapping is covered by tests (
test/tts_locale_mapping_test.dart) and can be extended if new locales are added. - For documentation and command consistency maintenance, use
docs/docs_drift_checklist.md. - Canonical AI coding policy lives in
RULES.md;AGENTS.mdis a delegating pointer.