Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View 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,
);
}

View 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();
}

View 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),
),
],
),
);
}
}

View 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");
}
}
}