ml download progress reporting

This commit is contained in:
Fabian Freund
2025-12-29 16:35:58 +01:00
parent 50621179a2
commit cc8984ca1b
17 changed files with 975 additions and 246 deletions
@@ -205,3 +205,43 @@ class EngineReadyState extends _$EngineReadyState {
return currentState;
}
}
/// Stream of ML model progress events
@Riverpod(keepAlive: true)
Stream<MlProgressData> mlProgressEvents(Ref ref) {
final service = ref.watch(eventServiceProvider);
return service.mlProgressEvents;
}
/// Tracks active ML model downloads
@Riverpod()
class MlDownloadState extends _$MlDownloadState {
@override
MlProgressData? build() {
ref.listen(
mlProgressEventsProvider,
(previous, next) {
next.whenData((progress) {
if (progress.type == MlProgressType.downloading) {
if (progress.status == MlProgressStatus.done) {
// Keep showing for 2 seconds after completion
Future.delayed(const Duration(seconds: 2), () {
if (state != null && state!.id == progress.id) {
state = null;
}
});
} else {
state = progress;
}
}
});
},
);
return null;
}
void clear() {
state = null;
}
}
@@ -298,3 +298,106 @@ abstract class _$EngineReadyState extends $Notifier<bool> {
element.handleValue(ref, created);
}
}
/// Stream of ML model progress events
@ProviderFor(mlProgressEvents)
const mlProgressEventsProvider = MlProgressEventsProvider._();
/// Stream of ML model progress events
final class MlProgressEventsProvider
extends
$FunctionalProvider<
AsyncValue<MlProgressData>,
MlProgressData,
Stream<MlProgressData>
>
with $FutureModifier<MlProgressData>, $StreamProvider<MlProgressData> {
/// Stream of ML model progress events
const MlProgressEventsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'mlProgressEventsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mlProgressEventsHash();
@$internal
@override
$StreamProviderElement<MlProgressData> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<MlProgressData> create(Ref ref) {
return mlProgressEvents(ref);
}
}
String _$mlProgressEventsHash() => r'41c1e6aece7f9ee2bebe5d189c6bec753c2fefc8';
/// Tracks active ML model downloads
@ProviderFor(MlDownloadState)
const mlDownloadStateProvider = MlDownloadStateProvider._();
/// Tracks active ML model downloads
final class MlDownloadStateProvider
extends $NotifierProvider<MlDownloadState, MlProgressData?> {
/// Tracks active ML model downloads
const MlDownloadStateProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'mlDownloadStateProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$mlDownloadStateHash();
@$internal
@override
MlDownloadState create() => MlDownloadState();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(MlProgressData? value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<MlProgressData?>(value),
);
}
}
String _$mlDownloadStateHash() => r'10be3767d448ea8fca8bf35578c343f3b9ddbb4e';
/// Tracks active ML model downloads
abstract class _$MlDownloadState extends $Notifier<MlProgressData?> {
MlProgressData? build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<MlProgressData?, MlProgressData?>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<MlProgressData?, MlProgressData?>,
MlProgressData?,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@@ -30,8 +30,12 @@ part 'tab_view_controllers.g.dart';
@Riverpod(keepAlive: true)
class TabSuggestionsController extends _$TabSuggestionsController {
void toggle() {
state = !state;
void enable() {
state = true;
}
void disable() {
state = false;
}
void hide() {
@@ -42,7 +42,7 @@ final class TabSuggestionsControllerProvider
}
String _$tabSuggestionsControllerHash() =>
r'c60912e474177f2a0f15167743db439d157cd852';
r'27a22674f9e4efb01e70120338202e95acef4791';
abstract class _$TabSuggestionsController extends $Notifier<bool> {
bool build();
@@ -6,6 +6,7 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/domain/providers.dart';
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
@@ -142,20 +143,75 @@ class TabViewHeader extends HookConsumerWidget {
final tabSuggestionsEnabled = ref.watch(
tabSuggestionsControllerProvider,
);
final downloadProgress = ref.watch(
mlDownloadStateProvider,
);
return IconButton.filledTonal(
icon: const Icon(MdiIcons.imageAutoAdjust),
isSelected: tabSuggestionsEnabled,
iconSize: 18,
padding: EdgeInsets.zero,
onPressed: () {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
return Badge(
isLabelVisible: downloadProgress != null,
offset: const Offset(-2, 2),
label: downloadProgress != null
? Text(
'${downloadProgress.progress.toInt()}%',
style: const TextStyle(fontSize: 10),
)
.toggle();
},
: null,
child: IconButton.filledTonal(
icon: const Icon(MdiIcons.imageAutoAdjust),
isSelected: tabSuggestionsEnabled,
iconSize: 18,
padding: EdgeInsets.zero,
onPressed: () async {
if (!tabSuggestionsEnabled) {
final result = await showDialog<bool?>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
icon: const Icon(MdiIcons.download),
title: const Text(
'Enable AI Tab Suggestions',
),
content: const Text(
'Enabling this feature may require downloading AI models. '
'The download size and progress cannot be determined in advance.\n\n'
'Do you want to continue?',
),
actions: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text('Enable'),
),
],
);
},
);
if (result == true) {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
)
.enable();
}
} else {
ref
.read(
tabSuggestionsControllerProvider
.notifier,
)
.disable();
}
},
),
);
},
),