Add drag-and-drop tab to create new container
This commit is contained in:
@@ -152,7 +152,7 @@ class ContainerEditRoute extends GoRouteData with $ContainerEditRoute {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
return ContainerEditScreen.edit(
|
return ContainerEditScreen.edit(
|
||||||
initialContainer: ContainerData.fromJson(
|
initialContainer: ContainerDataWithCount.fromJson(
|
||||||
jsonDecode(containerData) as Map<String, dynamic>,
|
jsonDecode(containerData) as Map<String, dynamic>,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -161,15 +161,20 @@ class ContainerEditRoute extends GoRouteData with $ContainerEditRoute {
|
|||||||
|
|
||||||
class ContainerCreateRoute extends GoRouteData with $ContainerCreateRoute {
|
class ContainerCreateRoute extends GoRouteData with $ContainerCreateRoute {
|
||||||
final String containerData;
|
final String containerData;
|
||||||
|
final String tabIds;
|
||||||
|
|
||||||
ContainerCreateRoute({required this.containerData});
|
ContainerCreateRoute({required this.containerData, this.tabIds = '[]'});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, GoRouterState state) {
|
Widget build(BuildContext context, GoRouterState state) {
|
||||||
|
final tabIdsList = jsonDecode(tabIds) as List;
|
||||||
|
final tabIdsSet = tabIdsList.cast<String>().toSet();
|
||||||
|
|
||||||
return ContainerEditScreen.create(
|
return ContainerEditScreen.create(
|
||||||
initialContainer: ContainerData.fromJson(
|
initialContainer: ContainerData.fromJson(
|
||||||
jsonDecode(containerData) as Map<String, dynamic>,
|
jsonDecode(containerData) as Map<String, dynamic>,
|
||||||
),
|
),
|
||||||
|
tabIds: tabIdsSet.isNotEmpty ? tabIdsSet : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -758,6 +758,7 @@ mixin $ContainerCreateRoute on GoRouteData {
|
|||||||
static ContainerCreateRoute _fromState(GoRouterState state) =>
|
static ContainerCreateRoute _fromState(GoRouterState state) =>
|
||||||
ContainerCreateRoute(
|
ContainerCreateRoute(
|
||||||
containerData: state.pathParameters['containerData']!,
|
containerData: state.pathParameters['containerData']!,
|
||||||
|
tabIds: state.uri.queryParameters['tab-ids'] ?? '[]',
|
||||||
);
|
);
|
||||||
|
|
||||||
ContainerCreateRoute get _self => this as ContainerCreateRoute;
|
ContainerCreateRoute get _self => this as ContainerCreateRoute;
|
||||||
@@ -765,6 +766,7 @@ mixin $ContainerCreateRoute on GoRouteData {
|
|||||||
@override
|
@override
|
||||||
String get location => GoRouteData.$location(
|
String get location => GoRouteData.$location(
|
||||||
'/browser/containers/create/${Uri.encodeComponent(_self.containerData)}',
|
'/browser/containers/create/${Uri.encodeComponent(_self.containerData)}',
|
||||||
|
queryParams: {if (_self.tabIds != '[]') 'tab-ids': _self.tabIds},
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
+109
@@ -0,0 +1,109 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2024-2026 Fabian Freund.
|
||||||
|
*
|
||||||
|
* This file is part of WebLibre
|
||||||
|
* (see https://weblibre.eu).
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as
|
||||||
|
* published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
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/repositories/container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart';
|
||||||
|
|
||||||
|
/// A widget that wraps a tab and enables creating a new container
|
||||||
|
/// when another tab is dropped onto it.
|
||||||
|
///
|
||||||
|
/// When a tab is dragged and dropped onto this widget:
|
||||||
|
/// 1. Both tab titles are collected
|
||||||
|
/// 2. An AI-suggested container name is generated
|
||||||
|
/// 3. User is redirected to container creation screen to confirm/modify settings
|
||||||
|
/// 4. Both tabs are assigned to the new container if user confirms
|
||||||
|
class TabDropTarget extends HookConsumerWidget {
|
||||||
|
/// The tab entity that serves as the drop target
|
||||||
|
final String targetTabId;
|
||||||
|
|
||||||
|
/// The widget to display (typically the tab preview)
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
/// Whether drag-and-drop to create containers is enabled
|
||||||
|
final bool enabled;
|
||||||
|
|
||||||
|
const TabDropTarget({
|
||||||
|
required this.targetTabId,
|
||||||
|
required this.child,
|
||||||
|
this.enabled = true,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (!enabled) {
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
|
||||||
|
return DragTarget<TabDragData>(
|
||||||
|
onWillAcceptWithDetails: (details) {
|
||||||
|
// Only accept if dragging a different tab
|
||||||
|
return details.data.tabId != targetTabId;
|
||||||
|
},
|
||||||
|
onAcceptWithDetails: (details) async {
|
||||||
|
final draggedTabId = details.data.tabId;
|
||||||
|
|
||||||
|
final containerRepo = ref.read(containerRepositoryProvider.notifier);
|
||||||
|
final newContainer = await containerRepo.createNewContainer();
|
||||||
|
|
||||||
|
if (!context.mounted) return;
|
||||||
|
|
||||||
|
final result = await ContainerCreateRoute(
|
||||||
|
containerData: jsonEncode(newContainer.toJson()),
|
||||||
|
tabIds: jsonEncode([draggedTabId, targetTabId]),
|
||||||
|
).push<ContainerData?>(context);
|
||||||
|
|
||||||
|
if (result != null) {
|
||||||
|
final tabRepo = ref.read(tabDataRepositoryProvider.notifier);
|
||||||
|
await tabRepo.assignContainer(draggedTabId, result);
|
||||||
|
await tabRepo.assignContainer(targetTabId, result);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
showInfoMessage(
|
||||||
|
context,
|
||||||
|
'Created container "${result.name ?? 'New Container'}"',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, candidateData, rejectedData) {
|
||||||
|
if (candidateData.isEmpty) return child;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
-16
@@ -36,6 +36,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||||
@@ -388,26 +389,33 @@ class _TabGrid extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final Widget tab;
|
final TabEntity entity;
|
||||||
|
final String? suggestedId;
|
||||||
|
|
||||||
if (index < filteredTabEntities.value.length) {
|
if (index < filteredTabEntities.value.length) {
|
||||||
final entity = filteredTabEntities.value[index];
|
entity = filteredTabEntities.value[index];
|
||||||
tab = CustomDraggable(
|
suggestedId = null;
|
||||||
key: Key(entity.tabId),
|
|
||||||
data: TabDragData(entity.tabId),
|
|
||||||
child: _TabDraggable(entity: entity, onClose: onClose),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final suggestedIndex = index - filteredTabEntities.value.length;
|
final suggestedIndex = index - filteredTabEntities.value.length;
|
||||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
suggestedId = suggestedContainerId;
|
||||||
|
}
|
||||||
|
|
||||||
tab = CustomDraggable(
|
final tab = CustomDraggable(
|
||||||
key: Key('suggested_${entity.tabId}'),
|
key: Key(
|
||||||
child: _TabDraggable(
|
suggestedId != null ? 'suggested_${entity.tabId}' : entity.tabId,
|
||||||
entity: entity,
|
),
|
||||||
onClose: onClose,
|
data: suggestedId != null ? null : TabDragData(entity.tabId),
|
||||||
suggestedContainerId: suggestedContainerId,
|
child: _TabDraggable(
|
||||||
),
|
entity: entity,
|
||||||
);
|
onClose: onClose,
|
||||||
|
suggestedContainerId: suggestedId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Only add DragTarget for non-suggested tabs in non-reorder mode
|
||||||
|
if (suggestedId == null && itemBuilder == null) {
|
||||||
|
return TabDropTarget(targetTabId: entity.tabId, child: tab);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
||||||
|
|||||||
+41
-35
@@ -35,6 +35,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_drop_target.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
@@ -236,51 +237,56 @@ class _TabListView extends HookConsumerWidget {
|
|||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemExtent: _itemHeight,
|
itemExtent: _itemHeight,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final CustomDraggable tab;
|
final TabEntity entity;
|
||||||
|
final String? suggestedId;
|
||||||
|
|
||||||
if (index < filteredTabEntities.value.length) {
|
if (index < filteredTabEntities.value.length) {
|
||||||
final entity = filteredTabEntities.value[index];
|
entity = filteredTabEntities.value[index];
|
||||||
tab = CustomDraggable(
|
suggestedId = null;
|
||||||
key: Key(entity.tabId),
|
|
||||||
data: TabDragData(entity.tabId),
|
|
||||||
child: _TabDraggable(
|
|
||||||
entity: entity,
|
|
||||||
onClose: onClose,
|
|
||||||
height: _itemHeight,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
final suggestedIndex =
|
final suggestedIndex =
|
||||||
index - filteredTabEntities.value.length;
|
index - filteredTabEntities.value.length;
|
||||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
suggestedId = containerId;
|
||||||
tab = CustomDraggable(
|
|
||||||
key: Key('suggested_${entity.tabId}'),
|
|
||||||
child: _TabDraggable(
|
|
||||||
entity: entity,
|
|
||||||
onClose: onClose,
|
|
||||||
suggestedContainerId: containerId,
|
|
||||||
height: _itemHeight,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return LongPressDraggable(
|
final tab = CustomDraggable(
|
||||||
feedback: Material(
|
key: Key(
|
||||||
color: Colors
|
suggestedId != null
|
||||||
.transparent, // removes white corners when having shadow
|
? 'suggested_${entity.tabId}'
|
||||||
child: Transform.scale(
|
: entity.tabId,
|
||||||
scale: 1.05,
|
),
|
||||||
child: SizedBox(
|
data: suggestedId != null
|
||||||
height: _itemHeight,
|
? null
|
||||||
width: MediaQuery.of(context).size.width,
|
: TabDragData(entity.tabId),
|
||||||
child: tab.child,
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
suggestedContainerId: suggestedId,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return TabDropTarget(
|
||||||
|
targetTabId: entity.tabId,
|
||||||
|
enabled: suggestedId == null,
|
||||||
|
child: LongPressDraggable(
|
||||||
|
feedback: Material(
|
||||||
|
color: Colors
|
||||||
|
.transparent, // removes white corners when having shadow
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.05,
|
||||||
|
child: SizedBox(
|
||||||
|
height: _itemHeight,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: tab.child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
data: tab.data,
|
||||||
|
childWhenDragging: const SizedBox(height: _itemHeight),
|
||||||
|
child: tab.child,
|
||||||
),
|
),
|
||||||
data: tab.data,
|
|
||||||
childWhenDragging: const SizedBox(height: _itemHeight),
|
|
||||||
child: tab.child,
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|||||||
+34
-2
@@ -18,19 +18,51 @@
|
|||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
|
||||||
part 'container_topic.g.dart';
|
part 'container_topic.g.dart';
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
class ContainerTopicController extends _$ContainerTopicController {
|
class ContainerTopicController extends _$ContainerTopicController {
|
||||||
Future<String?> predictDocumentTopic(String containerId) async {
|
Future<String?> predictDocumentTopic(String containerId) async {
|
||||||
|
final tabData = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getContainerTabsData(containerId);
|
||||||
|
|
||||||
|
if (tabData.isEmpty) return null;
|
||||||
|
|
||||||
|
final titles = tabData.map((t) => t.title).nonNulls.toSet();
|
||||||
|
return _predictFromTitles(titles);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> predictTopicFromTabIds(Set<String> tabIds) async {
|
||||||
|
final tabStates = ref.read(tabStatesProvider);
|
||||||
|
final titles = tabIds
|
||||||
|
.map((tabId) => tabStates[tabId]?.title)
|
||||||
|
.nonNulls
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
return _predictFromTitles(titles);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _predictFromTitles(Set<String> titles) async {
|
||||||
|
if (titles.isEmpty) return null;
|
||||||
|
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
|
|
||||||
final result = await AsyncValue.guard(() async {
|
final result = await AsyncValue.guard(() async {
|
||||||
return await ref.read(containerTopicProvider(containerId).future);
|
final topicResult = await ref
|
||||||
|
.read(geckoInferenceRepositoryProvider.notifier)
|
||||||
|
.predictDocumentTopic(titles);
|
||||||
|
|
||||||
|
return topicResult.fold((topic) => topic, onFailure: (_) => null);
|
||||||
});
|
});
|
||||||
state = result;
|
|
||||||
|
if (ref.mounted) {
|
||||||
|
state = result;
|
||||||
|
}
|
||||||
|
|
||||||
return result.value;
|
return result.value;
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ final class ContainerTopicControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$containerTopicControllerHash() =>
|
String _$containerTopicControllerHash() =>
|
||||||
r'e89fbdaaa9106f163ff00a829eb40752dd4af63a';
|
r'0b54e0be899e2abb4defbd5614ba231917ad74e5';
|
||||||
|
|
||||||
abstract class _$ContainerTopicController extends $Notifier<AsyncValue<void>> {
|
abstract class _$ContainerTopicController extends $Notifier<AsyncValue<void>> {
|
||||||
AsyncValue<void> build();
|
AsyncValue<void> build();
|
||||||
|
|||||||
@@ -40,22 +40,28 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
final _DialogMode _mode;
|
final _DialogMode _mode;
|
||||||
|
|
||||||
final ContainerData initialContainer;
|
final ContainerData initialContainer;
|
||||||
|
final Set<String>? tabIds;
|
||||||
|
|
||||||
const ContainerEditScreen._({
|
const ContainerEditScreen._({
|
||||||
required _DialogMode mode,
|
required _DialogMode mode,
|
||||||
required this.initialContainer,
|
required this.initialContainer,
|
||||||
|
this.tabIds,
|
||||||
}) : _mode = mode;
|
}) : _mode = mode;
|
||||||
|
|
||||||
factory ContainerEditScreen.create({
|
factory ContainerEditScreen.create({
|
||||||
required ContainerData initialContainer,
|
required ContainerData initialContainer,
|
||||||
|
Set<String>? tabIds,
|
||||||
}) {
|
}) {
|
||||||
return ContainerEditScreen._(
|
return ContainerEditScreen._(
|
||||||
mode: _DialogMode.create,
|
mode: _DialogMode.create,
|
||||||
initialContainer: initialContainer,
|
initialContainer: initialContainer,
|
||||||
|
tabIds: tabIds,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
factory ContainerEditScreen.edit({required ContainerData initialContainer}) {
|
factory ContainerEditScreen.edit({
|
||||||
|
required ContainerDataWithCount initialContainer,
|
||||||
|
}) {
|
||||||
return ContainerEditScreen._(
|
return ContainerEditScreen._(
|
||||||
mode: _DialogMode.edit,
|
mode: _DialogMode.edit,
|
||||||
initialContainer: initialContainer,
|
initialContainer: initialContainer,
|
||||||
@@ -76,11 +82,6 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
text: initialContainer.name,
|
text: initialContainer.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
final containerHasTabs = switch (initialContainer) {
|
|
||||||
ContainerDataWithCount(:final tabCount?) when tabCount > 0 => true,
|
|
||||||
_ => false,
|
|
||||||
};
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(switch (_mode) {
|
title: Text(switch (_mode) {
|
||||||
@@ -147,38 +148,11 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
label: const Text('Name'),
|
label: const Text('Name'),
|
||||||
suffixIcon:
|
suffixIcon: _buildMagicWandButton(
|
||||||
(_mode == _DialogMode.edit && containerHasTabs)
|
context,
|
||||||
? Consumer(
|
ref,
|
||||||
builder: (context, ref, child) {
|
textController,
|
||||||
final isLoading = ref.watch(
|
),
|
||||||
containerTopicControllerProvider.select(
|
|
||||||
(value) => value.isLoading,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return IconButton(
|
|
||||||
onPressed: isLoading
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
final topic = await ref
|
|
||||||
.read(
|
|
||||||
containerTopicControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.predictDocumentTopic(
|
|
||||||
initialContainer.id,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (topic != null) {
|
|
||||||
textController.text = topic;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(MdiIcons.creation),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: null,
|
|
||||||
),
|
),
|
||||||
controller: textController,
|
controller: textController,
|
||||||
),
|
),
|
||||||
@@ -311,4 +285,49 @@ class ContainerEditScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget? _buildMagicWandButton(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
TextEditingController textController,
|
||||||
|
) {
|
||||||
|
final predict = switch (_mode) {
|
||||||
|
_DialogMode.edit => switch (initialContainer) {
|
||||||
|
ContainerDataWithCount(:final tabCount?) when tabCount > 0 =>
|
||||||
|
(WidgetRef ref) => ref
|
||||||
|
.read(containerTopicControllerProvider.notifier)
|
||||||
|
.predictDocumentTopic(initialContainer.id),
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
_DialogMode.create => switch (tabIds) {
|
||||||
|
final ids? when ids.isNotEmpty =>
|
||||||
|
(WidgetRef ref) => ref
|
||||||
|
.read(containerTopicControllerProvider.notifier)
|
||||||
|
.predictTopicFromTabIds(ids),
|
||||||
|
_ => null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (predict == null) return null;
|
||||||
|
|
||||||
|
return Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final isLoading = ref.watch(
|
||||||
|
containerTopicControllerProvider.select((value) => value.isLoading),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
onPressed: isLoading
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
final topic = await predict(ref);
|
||||||
|
if (topic != null) {
|
||||||
|
textController.text = topic;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.creation),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user