Files
dailyou/lib/features/model_download/model_download_screen.dart
T
stefwill e464c5621e feat: add model download screen and gate app on model presence
Adds a one-time download flow for the on-device Gemma 2B model (~1.1 GB)
with a progress bar, partial-download cleanup, and automatic navigation
to the app shell once the file is verified. App.dart now gates on
modelPresentProvider instead of onboarding state.
2026-04-26 14:42:39 +10:00

168 lines
5.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/providers/model_download_provider.dart';
class ModelDownloadScreen extends ConsumerWidget {
const ModelDownloadScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final downloadState = ref.watch(modelDownloadNotifierProvider);
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(32),
child: downloadState.when(
data: (progress) => progress == 0.0
? _IdleView(
onDownload: () => ref
.read(modelDownloadNotifierProvider.notifier)
.startDownload(),
)
: _DownloadingView(progress: progress),
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => _ErrorView(
error: e.toString(),
onRetry: () {
ref.read(modelDownloadNotifierProvider.notifier).reset();
},
),
),
),
),
);
}
}
class _IdleView extends StatelessWidget {
const _IdleView({required this.onDownload});
final VoidCallback onDownload;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
Icons.auto_awesome,
size: 64,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: 24),
Text(
'On-device AI',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 12),
Text(
'DailyYou uses an on-device AI model to spot patterns in your moods '
'and behaviour. Your data never leaves your phone.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'The model is ~1.1 GB and only needs to be downloaded once.',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
FilledButton.icon(
onPressed: onDownload,
icon: const Icon(Icons.download),
label: const Text('Download model'),
),
],
);
}
}
class _DownloadingView extends StatelessWidget {
const _DownloadingView({required this.progress});
final double progress;
@override
Widget build(BuildContext context) {
final pct = (progress * 100).toStringAsFixed(0);
final done = progress >= 1.0;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(
done ? Icons.check_circle : Icons.downloading,
size: 64,
color: done
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.secondary,
),
const SizedBox(height: 24),
Text(
done ? 'Ready!' : 'Downloading…',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
LinearProgressIndicator(value: progress),
const SizedBox(height: 8),
Text(
done ? 'Model downloaded successfully.' : '$pct% — keep the app open',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.outline,
),
textAlign: TextAlign.center,
),
],
);
}
}
class _ErrorView extends StatelessWidget {
const _ErrorView({required this.error, required this.onRetry});
final String error;
final VoidCallback onRetry;
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Icon(Icons.error_outline,
size: 64, color: Theme.of(context).colorScheme.error),
const SizedBox(height: 24),
Text(
'Download failed',
style: Theme.of(context).textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Check your connection and try again.',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 40),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('Try again'),
),
],
);
}
}