Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
13
lib/Peach/see/splash/ConnectSplash.dart
Normal file
13
lib/Peach/see/splash/ConnectSplash.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import '../../about/internal/application/DeploymentRecord.dart';
|
||||
import '../../about/internal/application/ServerProfile.dart';
|
||||
|
||||
/// What [ViewSplash] hands back to the screen once the app's stored state has
|
||||
/// been read. The ViewModel never touches widgets — it calls these.
|
||||
abstract class ConnectSplash {
|
||||
/// Everything needed to open Home, loaded and ready.
|
||||
void onBootstrapped(
|
||||
List<ServerProfile> servers,
|
||||
List<DeploymentRecord> history,
|
||||
String themeMode,
|
||||
);
|
||||
}
|
||||
10
lib/Peach/see/splash/Splash.dart
Normal file
10
lib/Peach/see/splash/Splash.dart
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
import 'SplashState.dart';
|
||||
|
||||
class Splash extends StatefulWidget {
|
||||
const Splash({super.key});
|
||||
|
||||
@override
|
||||
State<Splash> createState() => SplashState();
|
||||
}
|
||||
168
lib/Peach/see/splash/SplashState.dart
Normal file
168
lib/Peach/see/splash/SplashState.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:stacked/stacked.dart';
|
||||
|
||||
import '../../about/internal/application/DeploymentRecord.dart';
|
||||
import '../../about/internal/application/NavigatorType.dart';
|
||||
import '../../about/internal/application/ServerProfile.dart';
|
||||
import '../../about/internal/application/TextType.dart';
|
||||
import '../../configs/Navigator.dart';
|
||||
import '../../configs/ThemeController.dart';
|
||||
import '../../designs/text/Text.dart';
|
||||
import '../../utils/Colors.dart';
|
||||
import '../home/Home.dart';
|
||||
import 'ConnectSplash.dart';
|
||||
import 'Splash.dart';
|
||||
import 'ViewSplash.dart';
|
||||
|
||||
class SplashState extends State<Splash>
|
||||
with SingleTickerProviderStateMixin
|
||||
implements ConnectSplash {
|
||||
ViewSplash? _model;
|
||||
|
||||
late final AnimationController _pulse;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_pulse = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 900),
|
||||
)..repeat(reverse: true);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pulse.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ViewModelBuilder<ViewSplash>.reactive(
|
||||
viewModelBuilder: () => ViewSplash(context, this),
|
||||
onViewModelReady: _onReady,
|
||||
builder: (context, viewModel, child) => Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_brandMark(),
|
||||
const SizedBox(height: 24),
|
||||
text(
|
||||
'Peach',
|
||||
36,
|
||||
TextType.Bold,
|
||||
context: context,
|
||||
letterSpacing: -0.8,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
text(
|
||||
'DEPLOYMENT COCKPIT',
|
||||
11.5,
|
||||
TextType.Bold,
|
||||
color: context.jp.textFaint,
|
||||
letterSpacing: 4,
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
child: LinearProgressIndicator(
|
||||
minHeight: 3,
|
||||
backgroundColor: context.jp.surface2,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onReady(ViewSplash model) {
|
||||
_model = model;
|
||||
// onViewModelReady fires mid-build; a synchronous setState from the
|
||||
// first load would dirty this State while it is still being built.
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) _initiate();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _initiate() async {
|
||||
_model?.bootstrap();
|
||||
}
|
||||
|
||||
// ── ConnectSplash ──────────────────────────────────────────────────────────
|
||||
|
||||
@override
|
||||
void onBootstrapped(
|
||||
List<ServerProfile> servers,
|
||||
List<DeploymentRecord> history,
|
||||
String themeMode,
|
||||
) {
|
||||
if (!mounted) return;
|
||||
// Apply the stored theme before the dashboard paints, so it never flashes
|
||||
// the default and then switches.
|
||||
ThemeController.apply(themeMode);
|
||||
PeachNavigation().navigateToPage(
|
||||
NavigatorType.makeNewMain,
|
||||
Home(servers: servers, history: history),
|
||||
context,
|
||||
);
|
||||
}
|
||||
|
||||
// ── screen-only widgets ────────────────────────────────────────────────────
|
||||
|
||||
/// Layered logo: a soft pulsing halo behind a beveled brand tile.
|
||||
Widget _brandMark() {
|
||||
return SizedBox(
|
||||
width: 132,
|
||||
height: 132,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ScaleTransition(
|
||||
scale: Tween(
|
||||
begin: 0.9,
|
||||
end: 1.12,
|
||||
).animate(CurvedAnimation(parent: _pulse, curve: Curves.easeInOut)),
|
||||
child: Container(
|
||||
width: 132,
|
||||
height: 132,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
PeachTheme.seed.withValues(alpha: 0.28),
|
||||
PeachTheme.seed.withValues(alpha: 0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
width: 90,
|
||||
height: 90,
|
||||
decoration: BoxDecoration(
|
||||
gradient: PeachTheme.brandGradient,
|
||||
borderRadius: BorderRadius.circular(26),
|
||||
border: Border.all(
|
||||
color: colorWhite.withValues(alpha: 0.18),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: PeachTheme.glow(
|
||||
PeachTheme.seed,
|
||||
opacity: 0.5,
|
||||
blur: 44,
|
||||
spread: 0,
|
||||
),
|
||||
),
|
||||
child: Icon(Icons.rocket_launch, color: colorWhite, size: 44),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
28
lib/Peach/see/splash/ViewSplash.dart
Normal file
28
lib/Peach/see/splash/ViewSplash.dart
Normal file
@@ -0,0 +1,28 @@
|
||||
import '../parent/ParentViewModel.dart';
|
||||
import 'ConnectSplash.dart';
|
||||
|
||||
class ViewSplash extends ParentViewModel {
|
||||
ConnectSplash connection;
|
||||
|
||||
ViewSplash(super.context, this.connection);
|
||||
|
||||
/// Reads the stored servers, deployment history and theme preference, then
|
||||
/// hands them to the screen. The splash is deliberately held for a minimum
|
||||
/// duration so a fast read doesn't make it flash.
|
||||
void bootstrap() async {
|
||||
final started = DateTime.now();
|
||||
try {
|
||||
final servers = await getDataManager().getServers();
|
||||
final history = await getDataManager().getDeployments();
|
||||
final themeMode = await getDataManager().getThemeMode();
|
||||
|
||||
const minShow = Duration(milliseconds: 1300);
|
||||
final elapsed = DateTime.now().difference(started);
|
||||
if (elapsed < minShow) await Future.delayed(minShow - elapsed);
|
||||
|
||||
connection.onBootstrapped(servers, history, themeMode);
|
||||
} catch (error) {
|
||||
handleError(error, bootstrap, () {}, "Retry");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user