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.
168 lines
5.1 KiB
Dart
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'),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|