container topic inference

This commit is contained in:
Fabian Freund
2025-07-20 00:37:51 +02:00
parent 2a3e0b6b6d
commit dc14a7d718
29 changed files with 1117 additions and 12 deletions
+1 -1
View File
@@ -110,7 +110,7 @@ class ContainerSelectionRoute extends GoRouteData
}
class ContainerEditRoute extends GoRouteData with _$ContainerEditRoute {
final ContainerData $extra;
final ContainerDataWithCount $extra;
ContainerEditRoute(this.$extra);
+1 -1
View File
@@ -517,7 +517,7 @@ mixin _$ContainerCreateRoute on GoRouteData {
mixin _$ContainerEditRoute on GoRouteData {
static ContainerEditRoute _fromState(GoRouterState state) =>
ContainerEditRoute(state.extra as ContainerData);
ContainerEditRoute(state.extra as ContainerDataWithCount);
ContainerEditRoute get _self => this as ContainerEditRoute;
@@ -64,3 +64,9 @@ Stream<Map<String, String?>> tabDescendants(Ref ref, String tabId) {
);
});
}
@Riverpod()
Stream<List<TabData>> containerTabsData(Ref ref, String containerId) {
final db = ref.watch(tabDatabaseProvider);
return db.containerDao.getContainerTabsData(containerId).watch();
}
@@ -442,5 +442,127 @@ class _TabDescendantsProviderElement
String get tabId => (origin as TabDescendantsProvider).tabId;
}
String _$containerTabsDataHash() => r'1987b2d69f2ba663a7343e93572aad3c31a29be3';
/// See also [containerTabsData].
@ProviderFor(containerTabsData)
const containerTabsDataProvider = ContainerTabsDataFamily();
/// See also [containerTabsData].
class ContainerTabsDataFamily extends Family<AsyncValue<List<TabData>>> {
/// See also [containerTabsData].
const ContainerTabsDataFamily();
/// See also [containerTabsData].
ContainerTabsDataProvider call(String containerId) {
return ContainerTabsDataProvider(containerId);
}
@override
ContainerTabsDataProvider getProviderOverride(
covariant ContainerTabsDataProvider provider,
) {
return call(provider.containerId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'containerTabsDataProvider';
}
/// See also [containerTabsData].
class ContainerTabsDataProvider
extends AutoDisposeStreamProvider<List<TabData>> {
/// See also [containerTabsData].
ContainerTabsDataProvider(String containerId)
: this._internal(
(ref) => containerTabsData(ref as ContainerTabsDataRef, containerId),
from: containerTabsDataProvider,
name: r'containerTabsDataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$containerTabsDataHash,
dependencies: ContainerTabsDataFamily._dependencies,
allTransitiveDependencies:
ContainerTabsDataFamily._allTransitiveDependencies,
containerId: containerId,
);
ContainerTabsDataProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
}) : super.internal();
final String containerId;
@override
Override overrideWith(
Stream<List<TabData>> Function(ContainerTabsDataRef provider) create,
) {
return ProviderOverride(
origin: this,
override: ContainerTabsDataProvider._internal(
(ref) => create(ref as ContainerTabsDataRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
),
);
}
@override
AutoDisposeStreamProviderElement<List<TabData>> createElement() {
return _ContainerTabsDataProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ContainerTabsDataProvider &&
other.containerId == containerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ContainerTabsDataRef on AutoDisposeStreamProviderRef<List<TabData>> {
/// The parameter `containerId` of this provider.
String get containerId;
}
class _ContainerTabsDataProviderElement
extends AutoDisposeStreamProviderElement<List<TabData>>
with ContainerTabsDataRef {
_ContainerTabsDataProviderElement(super.provider);
@override
String get containerId => (origin as ContainerTabsDataProvider).containerId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -38,8 +38,7 @@ class SelectedContainer extends _$SelectedContainer {
.read(localAuthenticationServiceProvider.notifier)
.authenticate(
authKey: 'container_access::${container.id}',
localizedReason:
'Require authentication for container ${container.name ?? 'New Container'}',
localizedReason: 'Require authentication for container',
settings: container.metadata.authSettings,
useAuthCache: true,
);
@@ -25,7 +25,7 @@ final selectedContainerDataProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
String _$selectedContainerHash() => r'736a60b8f19273d3bdc25d1717e3a613384e59a6';
String _$selectedContainerHash() => r'34457f0adc45d437a9ab817387be4b1664cd2e7a';
/// See also [SelectedContainer].
@ProviderFor(SelectedContainer)
@@ -0,0 +1,70 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:synchronized/synchronized.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/utils/lru_cache.dart';
part 'container_topic.g.dart';
@Riverpod(keepAlive: true)
class ContainerTopicRepository extends _$ContainerTopicRepository {
final _service = GeckoMlService();
final _lock = Lock();
final _cache = LRUCache<Set<String>, String>(
50,
equals: (a, b) {
return const DeepCollectionEquality.unordered().equals(a, b);
},
hashCode: (key) {
return const DeepCollectionEquality.unordered().hash(key);
},
);
Future<String?> getContainerTopic(Set<String> titles) async {
if (titles.isNotEmpty) {
if (_cache.get(titles) case final String title) {
return title;
}
try {
final title = await _lock.synchronized(() async {
final title = await _service.getContainerTopic(titles);
return _cache.set(titles, title);
}, timeout: const Duration(seconds: 120));
return title;
} on TimeoutException {
return null;
}
}
return null;
}
@override
void build() {}
}
@Riverpod(keepAlive: true)
Future<String?> containerTopic(Ref ref, String containerId) async {
final titles = await ref.watch(
containerTabsDataProvider(containerId).selectAsync(
(tabData) =>
EquatableValue(tabData.map((tab) => tab.title).nonNulls.toSet()),
),
);
final topic = await ref
.read(containerTopicRepositoryProvider.notifier)
.getContainerTopic(titles.value);
return topic;
}
@@ -0,0 +1,167 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'container_topic.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$containerTopicHash() => r'aa3fd27f26bb94b6c9e794d8470537240b8d2713';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [containerTopic].
@ProviderFor(containerTopic)
const containerTopicProvider = ContainerTopicFamily();
/// See also [containerTopic].
class ContainerTopicFamily extends Family<AsyncValue<String?>> {
/// See also [containerTopic].
const ContainerTopicFamily();
/// See also [containerTopic].
ContainerTopicProvider call(String containerId) {
return ContainerTopicProvider(containerId);
}
@override
ContainerTopicProvider getProviderOverride(
covariant ContainerTopicProvider provider,
) {
return call(provider.containerId);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'containerTopicProvider';
}
/// See also [containerTopic].
class ContainerTopicProvider extends FutureProvider<String?> {
/// See also [containerTopic].
ContainerTopicProvider(String containerId)
: this._internal(
(ref) => containerTopic(ref as ContainerTopicRef, containerId),
from: containerTopicProvider,
name: r'containerTopicProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$containerTopicHash,
dependencies: ContainerTopicFamily._dependencies,
allTransitiveDependencies:
ContainerTopicFamily._allTransitiveDependencies,
containerId: containerId,
);
ContainerTopicProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
}) : super.internal();
final String containerId;
@override
Override overrideWith(
FutureOr<String?> Function(ContainerTopicRef provider) create,
) {
return ProviderOverride(
origin: this,
override: ContainerTopicProvider._internal(
(ref) => create(ref as ContainerTopicRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
),
);
}
@override
FutureProviderElement<String?> createElement() {
return _ContainerTopicProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ContainerTopicProvider && other.containerId == containerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin ContainerTopicRef on FutureProviderRef<String?> {
/// The parameter `containerId` of this provider.
String get containerId;
}
class _ContainerTopicProviderElement extends FutureProviderElement<String?>
with ContainerTopicRef {
_ContainerTopicProviderElement(super.provider);
@override
String get containerId => (origin as ContainerTopicProvider).containerId;
}
String _$containerTopicRepositoryHash() =>
r'20ae14772821ac8a4ec0c16bc551e29098556189';
/// See also [ContainerTopicRepository].
@ProviderFor(ContainerTopicRepository)
final containerTopicRepositoryProvider =
NotifierProvider<ContainerTopicRepository, void>.internal(
ContainerTopicRepository.new,
name: r'containerTopicRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$containerTopicRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ContainerTopicRepository = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -0,0 +1,23 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
part 'container_topic.g.dart';
@Riverpod()
class ContainerTopicController extends _$ContainerTopicController {
Future<String?> getContainerTopic(String containerId) async {
state = const AsyncLoading();
final result = await AsyncValue.guard(() async {
return await ref.read(containerTopicProvider(containerId).future);
});
state = result;
return result.valueOrNull;
}
@override
AsyncValue<void> build() {
return const AsyncData(null);
}
}
@@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'container_topic.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$containerTopicControllerHash() =>
r'229709202edecb21b20432f8e4d0d7aee293f623';
/// See also [ContainerTopicController].
@ProviderFor(ContainerTopicController)
final containerTopicControllerProvider =
AutoDisposeNotifierProvider<
ContainerTopicController,
AsyncValue<void>
>.internal(
ContainerTopicController.new,
name: r'containerTopicControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$containerTopicControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ContainerTopicController = AutoDisposeNotifier<AsyncValue<void>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -6,6 +6,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:weblibre/core/uuid.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/controllers/container_topic.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart';
import 'package:weblibre/features/user/domain/services/local_authentication.dart';
import 'package:weblibre/presentation/icons/tor_icons.dart';
@@ -59,6 +60,11 @@ class ContainerEditScreen extends HookConsumerWidget {
text: initialContainer.name,
);
final containerHasTabs = switch (initialContainer) {
ContainerDataWithCount(:final tabCount?) when tabCount > 0 => true,
_ => false,
};
return Scaffold(
appBar: AppBar(
title: Text(switch (_mode) {
@@ -89,8 +95,7 @@ class ContainerEditScreen extends HookConsumerWidget {
.read(localAuthenticationServiceProvider.notifier)
.authenticate(
authKey: 'container_access::${container.id}',
localizedReason:
'Require authentication for container ${container.name ?? 'New Container'}',
localizedReason: 'Require authentication for container',
);
if (!authResult) {
@@ -140,6 +145,38 @@ class ContainerEditScreen extends HookConsumerWidget {
),
),
label: const Text('Name'),
suffixIcon:
(_mode == _DialogMode.edit && containerHasTabs)
? Consumer(
builder: (context, ref, child) {
final isLoading = ref.watch(
containerTopicControllerProvider.select(
(value) => value.isLoading,
),
);
return IconButton(
onPressed: isLoading
? null
: () async {
final topic = await ref
.read(
containerTopicControllerProvider
.notifier,
)
.getContainerTopic(
initialContainer.id,
);
if (topic != null) {
textController.text = topic;
}
},
icon: const Icon(MdiIcons.creation),
);
},
)
: null,
),
controller: textController,
),
@@ -9,6 +9,7 @@ import 'package:weblibre/data/models/drag_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart';
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
class ContainerChips extends HookConsumerWidget {
@@ -75,7 +76,7 @@ class ContainerChips extends HookConsumerWidget {
),
),
itemLabel: (container) =>
Text(container.name ?? 'New Container'),
ContainerTitle(container: container),
itemBadgeCount: (container) => container.tabCount,
itemWrap: (child, container) {
return HookBuilder(
@@ -169,6 +170,7 @@ class ContainerChips extends HookConsumerWidget {
),
if (displayMenu)
IconButton(
visualDensity: VisualDensity.compact,
onPressed: () async {
await ContainerListRoute().push(context);
},
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_title.dart';
class ContainerListTile extends HookWidget {
final ContainerData container;
@@ -22,7 +23,7 @@ class ContainerListTile extends HookWidget {
child: ListTile(
selected: isSelected,
leading: CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'),
title: ContainerTitle(container: container),
onTap: onTap,
trailing: const Icon(Icons.chevron_right),
),
@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:weblibre/core/logger.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container_topic.dart';
class ContainerTitle extends HookConsumerWidget {
final ContainerData container;
const ContainerTitle({super.key, required this.container});
@override
Widget build(BuildContext context, WidgetRef ref) {
if (container.name.isNotEmpty) {
return Text(container.name!);
}
final topicAsync = ref.watch(containerTopicProvider(container.id));
return topicAsync.when(
skipLoadingOnReload: true,
data: (data) =>
data.mapNotNull(
(name) => RichText(
text: TextSpan(
children: [
TextSpan(text: name),
const WidgetSpan(child: SizedBox(width: 4)),
const WidgetSpan(child: Icon(MdiIcons.creation, size: 16)),
],
),
),
) ??
const Text('New Container'),
error: (error, stackTrace) {
logger.e(
'Could not determine container name ${container.id}',
error: error,
stackTrace: stackTrace,
);
return const Text('New Container');
},
loading: () => const Skeletonizer(child: Text('container')),
);
}
}
+1
View File
@@ -72,6 +72,7 @@ dependencies:
url: https://github.com/FaFre/speech_to_text_google_dialog.git
sqlite3: ^2.7.7
sqlite3_flutter_libs: ^0.5.36
synchronized: ^3.4.0
text_scroll: ^0.2.0
timeago: ^3.7.1
tor: