import 'package:actors/actors.dart'; import 'package:flutter/material.dart'; import 'package:signals/signals_flutter.dart' show SignalBuilder; import '../application/actors/fetch_actor.dart'; import '../application/controllers/browser_controller.dart'; import '../application/controllers/reader_settings_controller.dart'; import '../application/internal_pages.dart'; import '../database/database.dart'; import '../l10n/strings.dart'; import '../mews/document.dart'; import 'reader/palettes.dart'; import 'reader/reader_screen.dart'; /// Wires the app's dependencies together and provides them to the tree. /// Everything is local-first: the database is created up front and the fetch /// actor is the only path to the network. class AppDependencies { AppDependencies({ MewsDatabase? database, Actor? fetch, Strings? strings, }) : db = database ?? MewsDatabase(), fetch = fetch ?? Actor.create(FetchHandler.new), strings = strings ?? Strings() { settings = ReaderSettingsController(db); library = LibraryController(db); internalPages = InternalPages( db: db, library: library, settings: settings, strings: this.strings); browser = BrowserController(db: db, fetch: this.fetch, internalPages: internalPages); } /// Localized strings, also used to title the internal pages. final Strings strings; final MewsDatabase db; final Actor fetch; late final BrowserController browser; late final ReaderSettingsController settings; late final LibraryController library; late final InternalPages internalPages; Future start() async { await settings.load(); await library.load(); await browser.restoreLastSession(); } } class AppScope extends InheritedWidget { const AppScope({ required this.deps, required this.strings, required super.child, super.key, }); final AppDependencies deps; final Strings strings; static AppScope of(BuildContext context) => context.getInheritedWidgetOfExactType()!; @override bool updateShouldNotify(AppScope oldWidget) => false; } class MewsApp extends StatelessWidget { const MewsApp({required this.deps, required this.strings, super.key}); final AppDependencies deps; final Strings strings; @override Widget build(BuildContext context) { return AppScope( deps: deps, strings: strings, child: StringsScope( strings: strings, child: const _ThemedApp(), ), ); } } /// The chrome (address bar, FOB, sheet, dialogs) follows the same palette /// the reader selected for pages, so the whole app shares one color scheme. class _ThemedApp extends StatelessWidget { const _ThemedApp(); @override Widget build(BuildContext context) { final deps = AppScope.of(context).deps; return SignalBuilder(builder: (context) { final mode = switch (deps.settings.themeMode.value) { 'light' => ThemeMode.light, 'dark' => ThemeMode.dark, _ => ThemeMode.system, }; final settings = deps.settings; final page = deps.browser.currentPage.value; final variant = settings.variant.value == 'site' ? (page?.variant ?? MewsVariant.plain) : pageVariantFromName(settings.variant.value); return MaterialApp( title: 'Mews', themeMode: mode, theme: _themeFor(paletteFor(pageVariant: variant, readerVariant: 'site', dark: false)), darkTheme: _themeFor(paletteFor(pageVariant: variant, readerVariant: 'site', dark: true)), home: const ReaderScreen(), ); }); } ThemeData _themeFor(MewsPalette palette) { final dark = palette.background.computeLuminance() < 0.5; final scheme = ColorScheme( brightness: dark ? Brightness.dark : Brightness.light, primary: palette.link, onPrimary: palette.background, primaryContainer: palette.link, onPrimaryContainer: palette.background, secondary: palette.border, onSecondary: palette.foreground, surface: palette.background, onSurface: palette.foreground, onSurfaceVariant: palette.foreground, surfaceContainerHighest: palette.preBackground, outline: palette.border, error: const Color(0xffba1a1a), onError: Colors.white, ); return ThemeData( useMaterial3: true, colorScheme: scheme, scaffoldBackgroundColor: palette.background, // The FOB follows the palette, not the M3 container defaults. floatingActionButtonTheme: FloatingActionButtonThemeData( backgroundColor: palette.link, foregroundColor: palette.background, ), // Selection highlights and the caret follow the palette too. textSelectionTheme: TextSelectionThemeData( cursorColor: palette.link, selectionColor: palette.link.withValues(alpha: 0.3), selectionHandleColor: palette.link, ), ); } }