add feature container drafting
This commit is contained in:
@@ -33,6 +33,10 @@ part of 'routes.dart';
|
||||
name: 'ContextMenuRoute',
|
||||
path: 'context_menu',
|
||||
),
|
||||
TypedGoRoute<ContainerDraftRoute>(
|
||||
name: 'ContainerDraftRoute',
|
||||
path: 'container_draft',
|
||||
),
|
||||
TypedGoRoute<ContainerListRoute>(
|
||||
name: 'ContainerListRoute',
|
||||
path: 'containers',
|
||||
@@ -107,6 +111,13 @@ class TorProxyRoute extends GoRouteData with $TorProxyRoute {
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerDraftRoute extends GoRouteData with $ContainerDraftRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const ContainerDraftSuggestionsScreen();
|
||||
}
|
||||
}
|
||||
|
||||
class ContainerListRoute extends GoRouteData with $ContainerListRoute {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import 'package:weblibre/features/geckoview/features/contextmenu/presentation/co
|
||||
import 'package:weblibre/features/geckoview/features/history/presentation/screens/history.dart';
|
||||
import 'package:weblibre/features/geckoview/features/search/presentation/screens/search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_draft_suggestions.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_list.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/screens/container_selection.dart';
|
||||
|
||||
@@ -322,6 +322,11 @@ RouteBase get $browserRoute => GoRouteData.$route(
|
||||
name: 'ContextMenuRoute',
|
||||
factory: $ContextMenuRoute._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'container_draft',
|
||||
name: 'ContainerDraftRoute',
|
||||
factory: $ContainerDraftRoute._fromState,
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'containers',
|
||||
name: 'ContainerListRoute',
|
||||
@@ -487,6 +492,27 @@ mixin $ContextMenuRoute on GoRouteData {
|
||||
context.replace(location, extra: _self.$extra);
|
||||
}
|
||||
|
||||
mixin $ContainerDraftRoute on GoRouteData {
|
||||
static ContainerDraftRoute _fromState(GoRouterState state) =>
|
||||
ContainerDraftRoute();
|
||||
|
||||
@override
|
||||
String get location => GoRouteData.$location('/container_draft');
|
||||
|
||||
@override
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
@override
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
@override
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
@override
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
mixin $ContainerListRoute on GoRouteData {
|
||||
static ContainerListRoute _fromState(GoRouterState state) =>
|
||||
ContainerListRoute();
|
||||
|
||||
+11
-10
@@ -42,7 +42,7 @@ final class GeckoInferenceRepositoryProvider
|
||||
}
|
||||
|
||||
String _$geckoInferenceRepositoryHash() =>
|
||||
r'eb174c9f80f3537bc075ebbffcef787ec55ad217';
|
||||
r'5f99fd1e46a8ae291b63df044fe11917cd5d415c';
|
||||
|
||||
abstract class _$GeckoInferenceRepository extends $Notifier<void> {
|
||||
void build();
|
||||
@@ -138,13 +138,13 @@ const suggestClustersProvider = SuggestClustersProvider._();
|
||||
final class SuggestClustersProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<({List<String> tabIds, String? topic})>?>,
|
||||
List<({List<String> tabIds, String? topic})>?,
|
||||
FutureOr<List<({List<String> tabIds, String? topic})>?>
|
||||
AsyncValue<List<SuggestedContainer>?>,
|
||||
List<SuggestedContainer>?,
|
||||
FutureOr<List<SuggestedContainer>?>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<({List<String> tabIds, String? topic})>?>,
|
||||
$FutureProvider<List<({List<String> tabIds, String? topic})>?> {
|
||||
$FutureModifier<List<SuggestedContainer>?>,
|
||||
$FutureProvider<List<SuggestedContainer>?> {
|
||||
const SuggestClustersProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
@@ -161,16 +161,17 @@ final class SuggestClustersProvider
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<({List<String> tabIds, String? topic})>?>
|
||||
$createElement($ProviderPointer pointer) => $FutureProviderElement(pointer);
|
||||
$FutureProviderElement<List<SuggestedContainer>?> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<({List<String> tabIds, String? topic})>?> create(Ref ref) {
|
||||
FutureOr<List<SuggestedContainer>?> create(Ref ref) {
|
||||
return suggestClusters(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$suggestClustersHash() => r'592058ed092384c37a185dcd5657276a3dde5b4e';
|
||||
String _$suggestClustersHash() => r'3b7da171b3596f708a8039658578251ae61655ea';
|
||||
|
||||
@ProviderFor(containerTabSuggestions)
|
||||
const containerTabSuggestionsProvider = ContainerTabSuggestionsFamily._();
|
||||
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/core/uuid.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.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/domain/repositories/gecko_inference.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/presentation/widgets/failure_widget.dart';
|
||||
import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
||||
const ContainerDraftSuggestionsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final suggestionsAsync = ref.watch(suggestClustersProvider);
|
||||
|
||||
final selectedContainer = useState<SuggestedContainer?>(null);
|
||||
final selectedContainerTabs = useState(<SuggestedContainer, Set<String>>{});
|
||||
|
||||
final selectedTabs = selectedContainer.value != null
|
||||
? selectedContainerTabs.value[selectedContainer.value!]
|
||||
: null;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Draft Containers')),
|
||||
body: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: suggestionsAsync.when(
|
||||
data: (suggestions) {
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
|
||||
final crossAxisCount = useMemoized(() {
|
||||
final calculatedCount = calculateCrossAxisItemCount(
|
||||
screenWidth: screenWidth,
|
||||
horizontalPadding: 4.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
);
|
||||
|
||||
return math.max(
|
||||
math.min(
|
||||
calculatedCount,
|
||||
selectedContainer.value?.tabIds.length ?? 0,
|
||||
),
|
||||
2,
|
||||
);
|
||||
}, [screenWidth, selectedContainer.value?.tabIds.length]);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 48,
|
||||
child: SelectableChips(
|
||||
deleteIcon: false,
|
||||
itemId: (container) => container,
|
||||
itemAvatar: (container) =>
|
||||
const Icon(MdiIcons.creation, size: 20),
|
||||
itemLabel: (container) =>
|
||||
Text(container.topic ?? 'Untitled'),
|
||||
itemBadgeCount: (container) => container.tabIds.length,
|
||||
availableItems: suggestions!,
|
||||
selectedItem: selectedContainer.value,
|
||||
onSelected: (item) {
|
||||
selectedContainer.value = item;
|
||||
},
|
||||
),
|
||||
),
|
||||
const Divider(),
|
||||
const SizedBox(height: 4),
|
||||
if (selectedContainer.value != null)
|
||||
Expanded(
|
||||
child: GridView.builder(
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: crossAxisCount,
|
||||
),
|
||||
itemCount: selectedContainer.value!.tabIds.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tabId = selectedContainer.value!.tabIds[index];
|
||||
final equatable = selectedContainer.value!;
|
||||
|
||||
return (selectedContainerTabs.value[equatable]
|
||||
?.contains(tabId) ==
|
||||
true)
|
||||
? TabPreview(
|
||||
tabId: tabId,
|
||||
isActive: false,
|
||||
onDelete: () {
|
||||
selectedContainerTabs.value = {
|
||||
...selectedContainerTabs.value,
|
||||
equatable: {
|
||||
...?selectedContainerTabs
|
||||
.value[equatable],
|
||||
}..remove(tabId),
|
||||
};
|
||||
},
|
||||
)
|
||||
: SuggestedSingleTabPreview(
|
||||
tabId: tabId,
|
||||
activeTabId: null,
|
||||
onTap: () {
|
||||
selectedContainerTabs.value = {
|
||||
...selectedContainerTabs.value,
|
||||
equatable: {
|
||||
...?selectedContainerTabs
|
||||
.value[equatable],
|
||||
tabId,
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
return FailureWidget(
|
||||
title: 'Failed to create suggestions',
|
||||
onRetry: () {
|
||||
ref.invalidate(suggestClustersProvider);
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
),
|
||||
floatingActionButton: (selectedTabs?.isNotEmpty ?? false)
|
||||
? FloatingActionButton(
|
||||
child: const Icon(MdiIcons.folderPlus),
|
||||
onPressed: () async {
|
||||
final initialColor = await ref
|
||||
.read(containerRepositoryProvider.notifier)
|
||||
.unusedRandomContainerColor();
|
||||
|
||||
final initialContainer = ContainerData(
|
||||
id: uuid.v7(),
|
||||
color: initialColor,
|
||||
name: selectedContainer.value?.topic,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
final result = await ContainerCreateRoute(
|
||||
initialContainer,
|
||||
).push<ContainerData?>(context);
|
||||
|
||||
if (result != null) {
|
||||
for (final tab in selectedTabs!) {
|
||||
await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.assignContainer(tab, result);
|
||||
}
|
||||
|
||||
selectedContainerTabs.value = {};
|
||||
selectedContainer.value = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
: null,
|
||||
);
|
||||
}
|
||||
}
|
||||
+107
-113
@@ -23,6 +23,7 @@ import 'package:flutter_hooks/flutter_hooks.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/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart';
|
||||
@@ -85,126 +86,119 @@ class ContainerChips extends HookConsumerWidget {
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
if (selectedContainer != null || availableContainers.isNotEmpty)
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
deleteIcon: false,
|
||||
itemId: (container) => container.id,
|
||||
itemAvatar: (container) => Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
color: container.color,
|
||||
shape: BoxShape.circle,
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
deleteIcon: false,
|
||||
itemId: (container) => container.id,
|
||||
itemAvatar: (container) => Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
color: container.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
itemLabel: (container) =>
|
||||
ContainerTitle(container: container),
|
||||
itemBadgeCount: (container) => container.tabCount,
|
||||
itemWrap: (child, container) {
|
||||
return TabDragContainerTarget(
|
||||
container: container,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
prefixListItems: [
|
||||
TabDragContainerTarget(
|
||||
container: null,
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabCount = ref.watch(
|
||||
containerTabCountProvider(
|
||||
// ignore: provider_parameters
|
||||
ContainerFilterById(containerId: null),
|
||||
).select((value) => value.value ?? 0),
|
||||
);
|
||||
|
||||
return FilterChip(
|
||||
avatar: const Icon(MdiIcons.folderHidden),
|
||||
labelPadding: (tabCount > 0)
|
||||
? null
|
||||
: const EdgeInsets.only(right: 2.0),
|
||||
label: (tabCount > 0)
|
||||
? Text(tabCount.toString())
|
||||
: const SizedBox.shrink(),
|
||||
selected: selectedContainer == null,
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {
|
||||
if (value) {
|
||||
onSelected?.call(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
itemLabel: (container) =>
|
||||
ContainerTitle(container: container),
|
||||
itemBadgeCount: (container) => container.tabCount,
|
||||
itemWrap: (child, container) {
|
||||
return TabDragContainerTarget(
|
||||
container: container,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
prefixListItems: [
|
||||
TabDragContainerTarget(
|
||||
container: null,
|
||||
child: Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabCount = ref.watch(
|
||||
containerTabCountProvider(
|
||||
// ignore: provider_parameters
|
||||
ContainerFilterById(containerId: null),
|
||||
).select((value) => value.value ?? 0),
|
||||
);
|
||||
if (showGroupSuggestions)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabSuggestionsEnabled = ref.watch(
|
||||
tabSuggestionsControllerProvider,
|
||||
);
|
||||
final enableAiFeatures = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.enableLocalAiFeatures,
|
||||
),
|
||||
);
|
||||
|
||||
return FilterChip(
|
||||
avatar: const Icon(MdiIcons.folderHidden),
|
||||
labelPadding: (tabCount > 0)
|
||||
? null
|
||||
: const EdgeInsets.only(right: 2.0),
|
||||
label: (tabCount > 0)
|
||||
? Text(tabCount.toString())
|
||||
: const SizedBox.shrink(),
|
||||
selected: selectedContainer == null,
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {
|
||||
if (value) {
|
||||
onSelected?.call(null);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (showGroupSuggestions)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabSuggestionsEnabled = ref.watch(
|
||||
tabSuggestionsControllerProvider,
|
||||
);
|
||||
final enableAiFeatures = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select(
|
||||
(settings) => settings.enableLocalAiFeatures,
|
||||
),
|
||||
);
|
||||
if (!enableAiFeatures || !tabSuggestionsEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (!enableAiFeatures || !tabSuggestionsEnabled) {
|
||||
final suggestions = ref.watch(
|
||||
suggestClustersProvider,
|
||||
);
|
||||
|
||||
return suggestions.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FilterChip(
|
||||
avatar: const Icon(MdiIcons.autoFix),
|
||||
label: Text(data!.length.toString()),
|
||||
showCheckmark: false,
|
||||
onSelected: (_) async {
|
||||
await ContainerDraftRoute().push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Error suggesting containers',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final suggestions = ref.watch(
|
||||
suggestClustersProvider,
|
||||
);
|
||||
|
||||
return suggestions.when(
|
||||
data: (data) {
|
||||
if (data.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return FilterChip(
|
||||
avatar: const Icon(MdiIcons.autoFix),
|
||||
label: Text(data!.length.toString()),
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {},
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) {
|
||||
logger.e(
|
||||
'Error suggesting containers',
|
||||
error: error,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
loading: () {
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
availableItems: availableContainers,
|
||||
selectedItem: selectedContainer,
|
||||
onSelected: onSelected,
|
||||
onDeleted: onDeleted,
|
||||
),
|
||||
)
|
||||
else
|
||||
Visibility(
|
||||
visible: displayMenu,
|
||||
child: Expanded(
|
||||
child: Text(
|
||||
"Press '>' to manage Containers.",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
},
|
||||
loading: () {
|
||||
return const FilterChip(
|
||||
avatar: Icon(MdiIcons.autoFix),
|
||||
label: Skeletonizer(child: Text('0')),
|
||||
showCheckmark: false,
|
||||
onSelected: null,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
availableItems: availableContainers,
|
||||
selectedItem: selectedContainer,
|
||||
onSelected: onSelected,
|
||||
onDeleted: onDeleted,
|
||||
),
|
||||
),
|
||||
if (displayMenu)
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
|
||||
Reference in New Issue
Block a user