kirakira/lib/presentation/app_widget.dart

100 lines
3.5 KiB
Dart

import "dart:async";
import "package:app_links/app_links.dart";
import "package:auto_route/auto_route.dart";
import "package:flutter/material.dart";
import "package:flutter_riverpod/flutter_riverpod.dart";
import "package:smol_mail/data/providers/providers.dart";
import "package:smol_mail/presentation/routes/app_router.dart";
import "package:smol_mail/presentation/routes/app_router.gr.dart";
import "package:smol_mail/presentation/theme/theme.dart";
import "package:smol_mail/smol/link.dart";
/// SnackBars need no context through this key, so the client's trust
/// warnings can be shown from anywhere in the protocol stack.
final rootScaffoldMessengerKey = GlobalKey<ScaffoldMessengerState>();
class AppWidget extends ConsumerStatefulWidget {
const AppWidget({super.key});
@override
ConsumerState<AppWidget> createState() => _AppWidgetState();
}
class _AppWidgetState extends ConsumerState<AppWidget> {
late final AppRouter _router = AppRouter(ref);
final AppLinks _appLinks = AppLinks();
StreamSubscription<Uri>? _linkSub;
@override
void initState() {
super.initState();
// §4/§8 trust warnings stay on screen until dismissed; ordinary notices
// do not use this channel.
ref.read(clientProvider).onWarning = (message) {
rootScaffoldMessengerKey.currentState?.showSnackBar(
SnackBar(
backgroundColor: const Color.fromRGBO(198, 40, 40, 1),
content: Text(message, style: const TextStyle(color: Colors.white)),
duration: const Duration(days: 1),
action: SnackBarAction(
label: "Dismiss",
textColor: Colors.white,
onPressed: () =>
rootScaffoldMessengerKey.currentState?.hideCurrentSnackBar(),
),
),
);
};
_linkSub = _appLinks.uriLinkStream.listen(_handleLink);
}
// A smol:// link is the self-certifying form: the §8 strong path,
// confirmed in the import dialog route before its key is bound. The push
// waits a frame — the stream can deliver the cold-start link while the
// first frame is still being laid out.
void _handleLink(Uri uri) {
final link = parseSmolLink(uri.toString());
if (link == null) return;
if (ref.read(identityProvider) == null) {
rootScaffoldMessengerKey.currentState?.showSnackBar(
const SnackBar(
content: Text("finish onboarding before using smol:// links")),
);
return;
}
WidgetsBinding.instance.addPostFrameCallback(
(_) => _router.push(ImportContactDialogRoute(prefill: link.source)),
);
}
@override
void dispose() {
_linkSub?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: "kirakira",
theme: appThemeLight,
darkTheme: appThemeDark,
themeMode: ThemeMode.system,
debugShowCheckedModeBanner: false,
scaffoldMessengerKey: rootScaffoldMessengerKey,
// smol:// URIs are for app_links, not the route table. The startup link
// must still resolve to a real route — auto_route throws "Can not
// resolve initial route" on an unmatched link and leaves a blank
// screen — so it lands on the normal entry route, whose guard
// redirects to home exactly like a launcher start. Every later
// platform link is ignored by the router; the stream handler reacts.
routerConfig: _router.config(
deepLinkBuilder: (link) =>
link.initial ? DeepLink.single(const OnboardingRoute()) : DeepLink.none,
),
);
}
}