2026-10-11 09:33:25 +03:00
|
|
|
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';
|
2026-10-11 13:05:07 +03:00
|
|
|
import '../application/internal_actions.dart';
|
2026-10-11 11:48:24 +03:00
|
|
|
import '../application/internal_pages.dart';
|
2026-10-11 09:33:25 +03:00
|
|
|
import '../database/database.dart';
|
|
|
|
|
import '../l10n/strings.dart';
|
2026-10-11 11:48:24 +03:00
|
|
|
import '../mews/document.dart';
|
|
|
|
|
import 'reader/palettes.dart';
|
2026-10-11 09:33:25 +03:00
|
|
|
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 {
|
2026-10-11 11:48:24 +03:00
|
|
|
AppDependencies({
|
|
|
|
|
MewsDatabase? database,
|
|
|
|
|
Actor<FetchCommand, FetchResult>? fetch,
|
|
|
|
|
Strings? strings,
|
|
|
|
|
}) : db = database ?? MewsDatabase(),
|
|
|
|
|
fetch = fetch ?? Actor.create(FetchHandler.new),
|
|
|
|
|
strings = strings ?? Strings() {
|
2026-10-11 09:33:25 +03:00
|
|
|
settings = ReaderSettingsController(db);
|
|
|
|
|
library = LibraryController(db);
|
2026-10-11 12:14:32 +03:00
|
|
|
internalPages = InternalPages(
|
|
|
|
|
db: db, library: library, settings: settings, strings: this.strings);
|
2026-10-11 13:05:07 +03:00
|
|
|
internalActions = InternalActions(settings: settings);
|
|
|
|
|
browser = BrowserController(
|
|
|
|
|
db: db,
|
|
|
|
|
fetch: this.fetch,
|
|
|
|
|
internalPages: internalPages,
|
|
|
|
|
internalActions: internalActions,
|
|
|
|
|
);
|
2026-10-11 09:33:25 +03:00
|
|
|
}
|
|
|
|
|
|
2026-10-11 11:48:24 +03:00
|
|
|
/// Localized strings, also used to title the internal pages.
|
|
|
|
|
final Strings strings;
|
|
|
|
|
|
2026-10-11 09:33:25 +03:00
|
|
|
final MewsDatabase db;
|
|
|
|
|
final Actor<FetchCommand, FetchResult> fetch;
|
|
|
|
|
late final BrowserController browser;
|
|
|
|
|
late final ReaderSettingsController settings;
|
|
|
|
|
late final LibraryController library;
|
2026-10-11 11:48:24 +03:00
|
|
|
late final InternalPages internalPages;
|
2026-10-11 13:05:07 +03:00
|
|
|
late final InternalActions internalActions;
|
2026-10-11 09:33:25 +03:00
|
|
|
|
|
|
|
|
Future<void> 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<AppScope>()!;
|
|
|
|
|
|
|
|
|
|
@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,
|
2026-10-11 10:28:54 +03:00
|
|
|
child: StringsScope(
|
|
|
|
|
strings: strings,
|
|
|
|
|
child: const _ThemedApp(),
|
|
|
|
|
),
|
2026-10-11 09:33:25 +03:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-11 12:51:18 +03:00
|
|
|
/// The chrome (address bar, dialogs, text selection) follows the same palette
|
2026-10-11 11:48:24 +03:00
|
|
|
/// the reader selected for pages, so the whole app shares one color scheme.
|
2026-10-11 09:33:25 +03:00
|
|
|
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,
|
|
|
|
|
};
|
2026-10-11 11:48:24 +03:00
|
|
|
final settings = deps.settings;
|
|
|
|
|
final page = deps.browser.currentPage.value;
|
|
|
|
|
final variant = settings.variant.value == 'site'
|
|
|
|
|
? (page?.variant ?? MewsVariant.plain)
|
|
|
|
|
: pageVariantFromName(settings.variant.value);
|
|
|
|
|
|
2026-10-11 09:33:25 +03:00
|
|
|
return MaterialApp(
|
|
|
|
|
title: 'Mews',
|
|
|
|
|
themeMode: mode,
|
2026-10-11 11:48:24 +03:00
|
|
|
theme: _themeFor(paletteFor(pageVariant: variant, readerVariant: 'site', dark: false)),
|
|
|
|
|
darkTheme: _themeFor(paletteFor(pageVariant: variant, readerVariant: 'site', dark: true)),
|
2026-10-11 09:33:25 +03:00
|
|
|
home: const ReaderScreen(),
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-10-11 11:48:24 +03:00
|
|
|
|
|
|
|
|
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,
|
|
|
|
|
// Selection highlights and the caret follow the palette too.
|
|
|
|
|
textSelectionTheme: TextSelectionThemeData(
|
|
|
|
|
cursorColor: palette.link,
|
|
|
|
|
selectionColor: palette.link.withValues(alpha: 0.3),
|
|
|
|
|
selectionHandleColor: palette.link,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-10-11 09:33:25 +03:00
|
|
|
}
|