188 lines
5.7 KiB
Dart
188 lines
5.7 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'app_theme.dart';
|
|
import 'tp_launch_page.dart';
|
|
|
|
/// Paint a light first frame before asking plugins to read saved data.
|
|
Future<void> launchApplication({
|
|
Future<void>? ready,
|
|
Widget? child,
|
|
Future<Widget> Function()? bootstrap,
|
|
VoidCallback? onHomeReady,
|
|
}) async {
|
|
assert(bootstrap != null || (ready != null && child != null));
|
|
// Attach error handling immediately, even if a caller already started work.
|
|
final legacy = bootstrap == null ? _legacyReady(ready!, child!) : null;
|
|
runApp(
|
|
TpStartup(bootstrap: bootstrap ?? () => legacy!, onHomeReady: onHomeReady),
|
|
);
|
|
}
|
|
|
|
Future<Widget> _legacyReady(Future<void> ready, Widget child) async {
|
|
try {
|
|
await ready;
|
|
} catch (_) {
|
|
// AppController exposes failed storage reads through the recovery screen.
|
|
}
|
|
return child;
|
|
}
|
|
|
|
class TpStartup extends StatefulWidget {
|
|
const TpStartup({super.key, required this.bootstrap, this.onHomeReady});
|
|
final Future<Widget> Function() bootstrap;
|
|
final VoidCallback? onHomeReady;
|
|
@override
|
|
State<TpStartup> createState() => _TpStartupState();
|
|
}
|
|
|
|
class _TpStartupState extends State<TpStartup>
|
|
with SingleTickerProviderStateMixin {
|
|
late final _fade =
|
|
AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(milliseconds: 300),
|
|
)..addStatusListener((status) {
|
|
if (status == AnimationStatus.completed && mounted) {
|
|
setState(() => _revealed = true);
|
|
}
|
|
});
|
|
Widget? _home;
|
|
bool _failed = false, _revealed = false;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (mounted) _bootstrap();
|
|
});
|
|
}
|
|
|
|
void _bootstrap() {
|
|
setState(() => _failed = false);
|
|
Future<Widget>.sync(widget.bootstrap).then(
|
|
(home) {
|
|
if (!mounted) return;
|
|
setState(() => _home = home);
|
|
// Paint home behind an opaque brand frame before fading it out.
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
final reduced =
|
|
MediaQuery.maybeOf(context)?.disableAnimations ??
|
|
WidgetsBinding
|
|
.instance
|
|
.platformDispatcher
|
|
.accessibilityFeatures
|
|
.disableAnimations;
|
|
if (reduced) {
|
|
_fade.value = 1;
|
|
} else {
|
|
_fade.forward();
|
|
}
|
|
widget.onHomeReady?.call();
|
|
});
|
|
},
|
|
onError: (Object error, StackTrace stack) {
|
|
if (mounted) setState(() => _failed = true);
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_fade.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
Widget content = Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Theme(
|
|
data: hepanTheme(),
|
|
child: AnimatedBuilder(
|
|
animation: _fade,
|
|
builder: (context, _) {
|
|
final progress = Curves.easeOutCubic.transform(_fade.value);
|
|
return AnnotatedRegion<SystemUiOverlayStyle>(
|
|
value:
|
|
(progress < 0.5
|
|
? SystemUiOverlayStyle.light
|
|
: SystemUiOverlayStyle.dark)
|
|
.copyWith(
|
|
statusBarColor: Colors.transparent,
|
|
systemNavigationBarColor: Color.lerp(
|
|
const Color(0xFF38B3FF),
|
|
canvas,
|
|
progress,
|
|
),
|
|
),
|
|
child: ColoredBox(
|
|
color: const Color(0xFF38B3FF),
|
|
child: Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
if (_home != null)
|
|
ExcludeSemantics(
|
|
excluding: !_revealed,
|
|
child: ExcludeFocus(
|
|
excluding: !_revealed,
|
|
child: IgnorePointer(
|
|
ignoring: !_revealed,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 6 * (1 - progress)),
|
|
child: _home,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (!_revealed)
|
|
IgnorePointer(
|
|
ignoring: !_failed,
|
|
child: Opacity(
|
|
key: const Key('startup-brand-overlay'),
|
|
opacity: 1 - progress,
|
|
child: _failed
|
|
? _recovery()
|
|
: const TpLaunchPage(nativeContinuity: true),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
if (MediaQuery.maybeOf(context) == null) {
|
|
content = MediaQuery.fromView(view: View.of(context), child: content);
|
|
}
|
|
return content;
|
|
}
|
|
|
|
Widget _recovery() => Material(
|
|
color: const Color(0xFF38B3FF),
|
|
child: Center(
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
const Text(
|
|
'启动未完成,请重试',
|
|
style: TextStyle(color: Colors.white, fontSize: 18),
|
|
),
|
|
const SizedBox(height: 20),
|
|
FilledButton(
|
|
style: FilledButton.styleFrom(
|
|
backgroundColor: Colors.white,
|
|
foregroundColor: river,
|
|
),
|
|
onPressed: _bootstrap,
|
|
child: const Text('重新打开'),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|