sync initial
This commit is contained in:
@@ -39,8 +39,8 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/keep_tab_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||
@@ -55,6 +55,7 @@ import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/move_to_background.dart';
|
||||
@@ -303,6 +304,44 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
},
|
||||
);
|
||||
|
||||
useOnAppLifecycleStateChange((previous, current) {
|
||||
switch (current) {
|
||||
case AppLifecycleState.resumed:
|
||||
if (current != AppLifecycleState.resumed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!ref.read(syncIsAuthenticatedProvider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
unawaited(() async {
|
||||
try {
|
||||
final openedTabs = await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.pollIncomingTabsAndOpen();
|
||||
|
||||
if (openedTabs > 0 && context.mounted) {
|
||||
ui_helper.showOpenedTabsFromAnotherDeviceMessage(
|
||||
context,
|
||||
openedTabs,
|
||||
);
|
||||
}
|
||||
} catch (e, s) {
|
||||
logger.e(
|
||||
'Failed polling incoming sync tabs on resume',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
}
|
||||
}());
|
||||
case AppLifecycleState.detached:
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
case AppLifecycleState.paused:
|
||||
}
|
||||
});
|
||||
|
||||
final pointerMoveEventsController = useStreamController<Offset>();
|
||||
|
||||
// Watch sheet state for rendering in Stack
|
||||
@@ -317,8 +356,7 @@ class BrowserScreen extends HookConsumerWidget {
|
||||
|
||||
// Toolbar is visible when: sheet is shown OR (not fullscreen AND controller says visible)
|
||||
// The controller handles loading-start show internally via ref.listen on isLoading
|
||||
final effectiveAppBarVisible =
|
||||
toolbarState == ToolbarVisibility.visible;
|
||||
final effectiveAppBarVisible = toolbarState == ToolbarVisibility.visible;
|
||||
final topToolbarVisible =
|
||||
sheetDisplayed || (!tabInFullScreen && effectiveAppBarVisible);
|
||||
final bottomToolbarVisible =
|
||||
@@ -1020,6 +1058,16 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
||||
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
||||
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
||||
|
||||
final isSyncedScope = ref.watch(
|
||||
effectiveTabsTrayScopeProvider.select(
|
||||
(scope) => scope == TabsTrayScope.synced,
|
||||
),
|
||||
);
|
||||
|
||||
final effectiveTabsViewMode = isSyncedScope
|
||||
? TabsViewMode.list
|
||||
: tabsViewMode;
|
||||
|
||||
final draggableScrollableController = useDraggableScrollableController(
|
||||
keys: [tabsReorderable],
|
||||
);
|
||||
@@ -1037,7 +1085,7 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
||||
topRight: Radius.circular(28),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: switch (tabsViewMode) {
|
||||
child: switch (effectiveTabsViewMode) {
|
||||
TabsViewMode.list => ViewTabListWidget(
|
||||
scrollController: scrollController,
|
||||
showNewTabFab: true,
|
||||
|
||||
@@ -26,6 +26,7 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/contro
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/scroll_visibility.dart';
|
||||
|
||||
@@ -37,6 +38,16 @@ class TabViewScreen extends HookConsumerWidget {
|
||||
final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
|
||||
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
|
||||
|
||||
final isSyncedScope = ref.watch(
|
||||
effectiveTabsTrayScopeProvider.select(
|
||||
(scope) => scope == TabsTrayScope.synced,
|
||||
),
|
||||
);
|
||||
|
||||
final effectiveTabsViewMode = isSyncedScope
|
||||
? TabsViewMode.list
|
||||
: tabsViewMode;
|
||||
|
||||
final scrollController = useScrollController(keys: [tabsReorderable]);
|
||||
|
||||
// Track FAB visibility based on scroll direction
|
||||
@@ -45,7 +56,7 @@ class TabViewScreen extends HookConsumerWidget {
|
||||
return Dialog.fullscreen(
|
||||
child: Scaffold(
|
||||
body: SafeArea(
|
||||
child: switch (tabsViewMode) {
|
||||
child: switch (effectiveTabsViewMode) {
|
||||
TabsViewMode.list => ViewTabListWidget(
|
||||
key: ValueKey(tabsReorderable),
|
||||
scrollController: scrollController,
|
||||
@@ -73,31 +84,35 @@ class TabViewScreen extends HookConsumerWidget {
|
||||
),
|
||||
},
|
||||
),
|
||||
floatingActionButton: AnimatedSlide(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: isFabVisible.value ? 1.0 : 0.0,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
floatingActionButton: isSyncedScope
|
||||
? null
|
||||
: AnimatedSlide(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
opacity: isFabVisible.value ? 1.0 : 0.0,
|
||||
child: FloatingActionButton(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(
|
||||
generalSettingsWithDefaultsProvider,
|
||||
);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.defaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -670,6 +670,7 @@ class ShareMenuButton extends HookConsumerWidget {
|
||||
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShareMenuItemButton(selectedTabId: selectedTabId),
|
||||
SendTabToDeviceMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
||||
],
|
||||
builder: (context, controller, child) {
|
||||
|
||||
+79
-1
@@ -18,6 +18,8 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -31,12 +33,15 @@ import 'package:weblibre/features/geckoview/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/web_extensions_state.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
|
||||
import 'package:weblibre/features/sync/domain/entities/sync_repository_state.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/tor/domain/services/tor_proxy.dart';
|
||||
import 'package:weblibre/features/user/domain/presentation/dialogs/quit_browser_dialog.dart';
|
||||
import 'package:weblibre/features/user/domain/providers.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/icons/tor_icons.dart';
|
||||
import 'package:weblibre/utils/exit_app.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
/// Navigation drawer for the browser screen.
|
||||
/// Contains all navigation destinations and settings.
|
||||
@@ -52,7 +57,7 @@ class BrowserNavigationDrawer extends HookConsumerWidget {
|
||||
children: [
|
||||
// Profile Header
|
||||
_ProfileHeader(),
|
||||
|
||||
_SyncTile(),
|
||||
const Divider(),
|
||||
|
||||
// Section 1: Tools & Configuration
|
||||
@@ -327,3 +332,76 @@ class _ExtensionsSection extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sync tile widget shown in navigation drawer when sync is active.
|
||||
/// Displays sync status and allows manual sync trigger.
|
||||
class _SyncTile extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||
|
||||
// Only show if sync is active
|
||||
if (!isAuthenticated) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final syncInfo = ref.watch(
|
||||
syncRepositoryProvider.select((value) => value.value?.account),
|
||||
);
|
||||
|
||||
final syncStarted = ref.watch(
|
||||
syncEventProvider.select(
|
||||
(value) => value.isLoading || value.value?.$1 == SyncEvent.started,
|
||||
),
|
||||
);
|
||||
final isSyncing = syncStarted || syncInfo?.syncing == true;
|
||||
|
||||
final controller = useAnimationController(
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
if (isSyncing) {
|
||||
unawaited(controller.repeat());
|
||||
} else {
|
||||
controller.stop();
|
||||
controller.reset();
|
||||
}
|
||||
return null;
|
||||
}, [isSyncing]);
|
||||
|
||||
return ListTile(
|
||||
leading: RotationTransition(
|
||||
turns: Tween<double>(begin: 0, end: -1).animate(controller),
|
||||
child: const Icon(Icons.sync),
|
||||
),
|
||||
title: const Text('Sync Now'),
|
||||
onTap: () async {
|
||||
await ref.read(syncRepositoryProvider.notifier).syncNow();
|
||||
|
||||
final openedTabs = await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.pollIncomingTabsAndOpen();
|
||||
|
||||
if (context.mounted) {
|
||||
if (openedTabs > 0) {
|
||||
ui_helper.showOpenedTabsFromAnotherDeviceMessage(
|
||||
context,
|
||||
openedTabs,
|
||||
);
|
||||
} else {
|
||||
ui_helper.showInfoMessage(
|
||||
context,
|
||||
'Synchronization complete',
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+97
@@ -26,13 +26,16 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/content_selection_dialog.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/presentation/hooks/cached_future.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class ShareMenuItemButton extends HookConsumerWidget {
|
||||
const ShareMenuItemButton({super.key, required this.selectedTabId});
|
||||
@@ -287,3 +290,97 @@ class CopyAddressMenuItemButton extends HookConsumerWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SendTabToDeviceMenuItemButton extends HookConsumerWidget {
|
||||
final String? selectedTabId;
|
||||
|
||||
const SendTabToDeviceMenuItemButton({super.key, required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
if (selectedTabId == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||
final devices = ref.watch(syncDevicesProvider);
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: devices.isLoading && devices.value == null,
|
||||
child: SubmenuButton(
|
||||
leadingIcon: const Icon(Icons.send_outlined),
|
||||
menuChildren: devices.when(
|
||||
data: (deviceList) {
|
||||
final targets = deviceList
|
||||
.where((device) => !device.isCurrentDevice && device.canSendTab)
|
||||
.toList(growable: false);
|
||||
|
||||
if (targets.isEmpty) {
|
||||
return const [MenuItemButton(child: Text('No target devices'))];
|
||||
}
|
||||
|
||||
return targets
|
||||
.map((device) {
|
||||
return MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: const Icon(Icons.devices_other),
|
||||
child: Text(device.displayName),
|
||||
onPressed: () async {
|
||||
final tabState = ref.read(
|
||||
tabStateProvider(selectedTabId),
|
||||
);
|
||||
if (tabState == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final title = tabState.title.isNotEmpty
|
||||
? tabState.title
|
||||
: tabState.url.toString();
|
||||
|
||||
final success = await ref
|
||||
.read(syncRepositoryProvider.notifier)
|
||||
.sendTabToDevice(
|
||||
deviceId: device.deviceId,
|
||||
title: title,
|
||||
url: tabState.url.toString(),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
if (success) {
|
||||
ui_helper.showInfoMessage(
|
||||
context,
|
||||
'Sent tab to ${device.displayName}',
|
||||
);
|
||||
} else {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
'Failed to send tab',
|
||||
);
|
||||
}
|
||||
|
||||
MenuController.maybeOf(context)?.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.toList(growable: false);
|
||||
},
|
||||
loading: () => const [
|
||||
MenuItemButton(
|
||||
leadingIcon: Icon(Icons.devices_other),
|
||||
child: Text('Loading devices...'),
|
||||
),
|
||||
],
|
||||
error: (_, _) => const [
|
||||
MenuItemButton(child: Text('Failed to load devices')),
|
||||
],
|
||||
),
|
||||
child: const Text('Send To Device'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,6 +475,7 @@ class TabMenu extends HookConsumerWidget {
|
||||
OpenInAppMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShareMenuItemButton(selectedTabId: selectedTabId),
|
||||
SendTabToDeviceMenuItemButton(selectedTabId: selectedTabId),
|
||||
ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
|
||||
],
|
||||
leadingIcon: const Icon(Icons.share),
|
||||
|
||||
+59
-2
@@ -44,6 +44,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
|
||||
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/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
|
||||
class _TabDraggable extends HookConsumerWidget {
|
||||
@@ -149,8 +150,58 @@ class _TabListView extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// final screenWidth = MediaQuery.of(context).size.width;
|
||||
final scope = ref.watch(effectiveTabsTrayScopeProvider);
|
||||
|
||||
return switch (scope) {
|
||||
TabsTrayScope.synced => _buildSyncedTabsView(context, ref),
|
||||
_ => _buildLocalTabsView(context, ref),
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildSyncedTabsView(BuildContext context, WidgetRef ref) {
|
||||
final syncedTabs = ref.watch(syncedTabsForSelectedDeviceProvider);
|
||||
|
||||
return syncedTabs.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (tabs) {
|
||||
if (tabs.isEmpty) {
|
||||
return const Center(child: Text('No synced tabs available'));
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: ListView.builder(
|
||||
controller: scrollController,
|
||||
itemCount: tabs.length,
|
||||
itemBuilder: (context, index) {
|
||||
final tab = tabs[index].tab;
|
||||
final uri = Uri.tryParse(tab.url);
|
||||
|
||||
if (uri == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return SyncedListTabPreview(
|
||||
title: tab.title.isNotEmpty ? tab.title : tab.url,
|
||||
url: uri,
|
||||
deviceName: tabs[index].deviceName,
|
||||
onTap: () async {
|
||||
await OpenSharedContentRoute(
|
||||
sharedUrl: uri.toString(),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, _) =>
|
||||
Center(child: Text('Failed to load synced tabs: $error')),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocalTabsView(BuildContext context, WidgetRef ref) {
|
||||
final containerId = ref.watch(selectedContainerProvider);
|
||||
|
||||
final filteredTabEntities = ref.watch(
|
||||
@@ -417,6 +468,12 @@ class ViewTabListWidget extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSyncedScope = ref.watch(
|
||||
effectiveTabsTrayScopeProvider.select(
|
||||
(scope) => scope == TabsTrayScope.synced,
|
||||
),
|
||||
);
|
||||
|
||||
final isFabVisible = useState(true);
|
||||
final lastSheetSize = useRef(0.0);
|
||||
final isInitialized = useRef(false);
|
||||
@@ -516,7 +573,7 @@ class ViewTabListWidget extends HookConsumerWidget {
|
||||
onClose: onClose,
|
||||
),
|
||||
),
|
||||
if (showNewTabFab)
|
||||
if (showNewTabFab && !isSyncedScope)
|
||||
AnimatedSlide(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
offset: isFabVisible.value ? Offset.zero : const Offset(0, 2),
|
||||
|
||||
+46
@@ -36,6 +36,7 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
import 'package:weblibre/presentation/widgets/safe_raw_image.dart';
|
||||
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
class GridTabItemContainer extends StatelessWidget {
|
||||
@@ -378,6 +379,51 @@ class ListTabPreview extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SyncedListTabPreview extends StatelessWidget {
|
||||
const SyncedListTabPreview({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.deviceName,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final Uri url;
|
||||
final String deviceName;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
onTap: onTap,
|
||||
contentPadding: const EdgeInsets.only(left: 8, right: 6),
|
||||
leading: UrlIcon([url], iconSize: 20),
|
||||
title: Text(title, maxLines: 2, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.devices, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
deviceName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
UriBreadcrumb(uri: url),
|
||||
],
|
||||
),
|
||||
trailing: const Icon(Icons.open_in_new),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SingleGridTabPreview extends HookConsumerWidget {
|
||||
final String tabId;
|
||||
final String? activeTabId;
|
||||
|
||||
+279
-172
@@ -43,6 +43,7 @@ import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selec
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||
import 'package:weblibre/features/sync/domain/repositories/sync.dart';
|
||||
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
||||
import 'package:weblibre/features/tor/presentation/widgets/tor_dialog.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
@@ -50,6 +51,151 @@ import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||
|
||||
/// Widget for tab filters (container chips with synced option)
|
||||
class _TabFilters extends ConsumerWidget {
|
||||
final TabsViewMode tabsViewMode;
|
||||
|
||||
const _TabFilters({required this.tabsViewMode});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isSyncedScope = ref.watch(
|
||||
effectiveTabsTrayScopeProvider.select(
|
||||
(scope) => scope == TabsTrayScope.synced,
|
||||
),
|
||||
);
|
||||
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider.select((value) => value.value),
|
||||
);
|
||||
|
||||
final isAuthenticated = ref.watch(syncIsAuthenticatedProvider);
|
||||
final syncedTabCountAsync = ref.watch(syncedTabsTotalCountProvider);
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ContainerChips(
|
||||
showGroupSuggestions: switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
},
|
||||
enableDragAndDrop: switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
},
|
||||
showSyncedChip: isAuthenticated && tabsViewMode != TabsViewMode.tree,
|
||||
syncedChipSelected: isSyncedScope,
|
||||
syncedTabCount: syncedTabCountAsync.when(
|
||||
data: (count) => count,
|
||||
loading: () => 0,
|
||||
error: (_, _) => 0,
|
||||
),
|
||||
onSyncedChipSelected: () {
|
||||
ref.read(tabsTrayScopeControllerProvider.notifier).showSynced();
|
||||
},
|
||||
selectedContainer: selectedContainer,
|
||||
onSelected: (container) async {
|
||||
ref.read(tabsTrayScopeControllerProvider.notifier).showLocal();
|
||||
|
||||
if (container != null) {
|
||||
final result = await ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
|
||||
if (context.mounted &&
|
||||
result == SetContainerResult.successHasProxy) {
|
||||
final shouldStartProxy = await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.shouldPromptProxyStart();
|
||||
|
||||
if (!context.mounted || !shouldStartProxy) return;
|
||||
|
||||
final dialogResult = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const TorDialog();
|
||||
},
|
||||
);
|
||||
|
||||
if (dialogResult == true) {
|
||||
await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.startProxy();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ref.read(selectedContainerProvider.notifier).clearContainer();
|
||||
}
|
||||
},
|
||||
onDeleted: (container) {
|
||||
ref.read(tabsTrayScopeControllerProvider.notifier).showLocal();
|
||||
ref.read(selectedContainerProvider.notifier).clearContainer();
|
||||
},
|
||||
onLongPress: (container) async {
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
),
|
||||
if (isSyncedScope) ...[
|
||||
const SizedBox(height: 8),
|
||||
_SyncedDeviceSelector(),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Widget for synced device selector
|
||||
class _SyncedDeviceSelector extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final remoteDevicesAsync = ref.watch(syncRemoteTabsProvider);
|
||||
final selectedDeviceId = ref.watch(selectedSyncedTabsDeviceIdProvider);
|
||||
|
||||
return remoteDevicesAsync.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (devices) {
|
||||
if (devices.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final effectiveSelectedDeviceId =
|
||||
selectedDeviceId != null &&
|
||||
devices.any((device) => device.deviceId == selectedDeviceId)
|
||||
? selectedDeviceId
|
||||
: devices.first.deviceId;
|
||||
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: devices
|
||||
.map((device) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 6.0),
|
||||
child: ChoiceChip(
|
||||
label: Text(device.deviceName),
|
||||
selected: effectiveSelectedDeviceId == device.deviceId,
|
||||
onSelected: (_) {
|
||||
ref
|
||||
.read(selectedSyncedTabsDeviceIdProvider.notifier)
|
||||
.selectDevice(device.deviceId);
|
||||
},
|
||||
),
|
||||
);
|
||||
})
|
||||
.toList(growable: false),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => const SizedBox.shrink(),
|
||||
error: (_, _) => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabViewHeader extends HookConsumerWidget {
|
||||
static const headerSize = 124.0;
|
||||
|
||||
@@ -82,6 +228,13 @@ class TabViewHeader extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final selectedContainerId = ref.watch(selectedContainerProvider);
|
||||
final isSyncedScope = ref.watch(
|
||||
effectiveTabsTrayScopeProvider.select(
|
||||
(scope) => scope == TabsTrayScope.synced,
|
||||
),
|
||||
);
|
||||
|
||||
useOnListenableChange(searchTextController, () async {
|
||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||
await ref
|
||||
@@ -153,13 +306,15 @@ class TabViewHeader extends HookConsumerWidget {
|
||||
.toList(),
|
||||
child: IconButton(
|
||||
tooltip: 'Change view mode',
|
||||
onPressed: () {
|
||||
if (viewModeMenuController.isOpen) {
|
||||
viewModeMenuController.close();
|
||||
} else {
|
||||
viewModeMenuController.open();
|
||||
}
|
||||
},
|
||||
onPressed: isSyncedScope
|
||||
? null
|
||||
: () {
|
||||
if (viewModeMenuController.isOpen) {
|
||||
viewModeMenuController.close();
|
||||
} else {
|
||||
viewModeMenuController.open();
|
||||
}
|
||||
},
|
||||
icon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -278,122 +433,133 @@ class TabViewHeader extends HookConsumerWidget {
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.closeCircle),
|
||||
onPressed: isSyncedScope
|
||||
? null
|
||||
: () async {
|
||||
final result = await showCloseAllTabsDialog(
|
||||
context,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final count = await ref
|
||||
.read(
|
||||
tabDataRepositoryProvider.notifier,
|
||||
)
|
||||
.closeContainerTabs(
|
||||
selectedContainerId,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(
|
||||
tabRepositoryProvider.notifier,
|
||||
)
|
||||
.undoClose,
|
||||
count: count.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Close All Tabs'),
|
||||
onPressed: () async {
|
||||
final result = await showCloseAllTabsDialog(
|
||||
context,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final container = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.closeContainerTabs(container);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.undoClose,
|
||||
count: count.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.incognitoCircleOff),
|
||||
onPressed: isSyncedScope
|
||||
? null
|
||||
: () async {
|
||||
final result =
|
||||
await showCloseAllPrivateTabsDialog(
|
||||
context,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final count = await ref
|
||||
.read(
|
||||
tabDataRepositoryProvider.notifier,
|
||||
)
|
||||
.closeContainerTabs(
|
||||
selectedContainerId,
|
||||
includeRegular: false,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(
|
||||
tabRepositoryProvider.notifier,
|
||||
)
|
||||
.undoClose,
|
||||
count: count.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Close Private Tabs'),
|
||||
onPressed: () async {
|
||||
final result = await showCloseAllPrivateTabsDialog(
|
||||
context,
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
final container = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final count = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.closeContainerTabs(
|
||||
container,
|
||||
includeRegular: false,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showTabUndoClose(
|
||||
context,
|
||||
ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.undoClose,
|
||||
count: count.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(),
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||
child: const Text('Bookmark all'),
|
||||
onPressed: () async {
|
||||
final choice = await showBookmarkAllDialog(context);
|
||||
if (choice == null || !context.mounted) return;
|
||||
|
||||
final containerId = ref.read(
|
||||
selectedContainerProvider,
|
||||
);
|
||||
|
||||
final tabData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabsData(containerId);
|
||||
|
||||
if (choice == BookmarkAllChoice.fast) {
|
||||
if (!context.mounted) return;
|
||||
final folderGuid = await showSelectFolderDialog(
|
||||
context,
|
||||
);
|
||||
if (folderGuid == null) return;
|
||||
|
||||
final repo = ref.read(
|
||||
bookmarksRepositoryProvider.notifier,
|
||||
);
|
||||
for (final tab in tabData) {
|
||||
if (tab.url != null) {
|
||||
await repo.addBookmark(
|
||||
parentGuid: folderGuid,
|
||||
url: tab.url!,
|
||||
title: tab.title ?? tab.url.toString(),
|
||||
onPressed: isSyncedScope
|
||||
? null
|
||||
: () async {
|
||||
final choice = await showBookmarkAllDialog(
|
||||
context,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (choice == null || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showInfoMessage(
|
||||
context,
|
||||
'${tabData.length} bookmark(s) added',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (final tab in tabData) {
|
||||
if (context.mounted) {
|
||||
await BookmarkEntryAddRoute(
|
||||
bookmarkInfo: jsonEncode(
|
||||
BookmarkInfo(
|
||||
title: tab.title,
|
||||
url: tab.url.toString(),
|
||||
).encode(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
final tabData = await ref
|
||||
.read(tabDataRepositoryProvider.notifier)
|
||||
.getContainerTabsData(
|
||||
selectedContainerId,
|
||||
);
|
||||
|
||||
if (choice == BookmarkAllChoice.fast) {
|
||||
if (!context.mounted) return;
|
||||
final folderGuid =
|
||||
await showSelectFolderDialog(context);
|
||||
if (folderGuid == null) return;
|
||||
|
||||
final repo = ref.read(
|
||||
bookmarksRepositoryProvider.notifier,
|
||||
);
|
||||
for (final tab in tabData) {
|
||||
if (tab.url != null) {
|
||||
await repo.addBookmark(
|
||||
parentGuid: folderGuid,
|
||||
url: tab.url!,
|
||||
title:
|
||||
tab.title ?? tab.url.toString(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
ui_helper.showInfoMessage(
|
||||
context,
|
||||
'${tabData.length} bookmark(s) added',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
for (final tab in tabData) {
|
||||
if (context.mounted) {
|
||||
await BookmarkEntryAddRoute(
|
||||
bookmarkInfo: jsonEncode(
|
||||
BookmarkInfo(
|
||||
title: tab.title,
|
||||
url: tab.url.toString(),
|
||||
).encode(),
|
||||
),
|
||||
).push(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
child: const Text('Bookmark all'),
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
@@ -582,66 +748,7 @@ class TabViewHeader extends HookConsumerWidget {
|
||||
),
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final selectedContainer = ref.watch(
|
||||
selectedContainerDataProvider.select(
|
||||
(value) => value.value,
|
||||
),
|
||||
);
|
||||
|
||||
return ContainerChips(
|
||||
showGroupSuggestions: switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
},
|
||||
enableDragAndDrop: switch (tabsViewMode) {
|
||||
TabsViewMode.list || TabsViewMode.grid => true,
|
||||
TabsViewMode.tree => false,
|
||||
},
|
||||
selectedContainer: selectedContainer,
|
||||
onSelected: (container) async {
|
||||
if (container != null) {
|
||||
final result = await ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.setContainerId(container.id);
|
||||
|
||||
if (context.mounted &&
|
||||
result == SetContainerResult.successHasProxy) {
|
||||
final shouldStartProxy = await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.shouldPromptProxyStart();
|
||||
|
||||
if (!context.mounted || !shouldStartProxy) return;
|
||||
|
||||
final dialogResult = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return const TorDialog();
|
||||
},
|
||||
);
|
||||
|
||||
if (dialogResult == true) {
|
||||
await ref
|
||||
.read(startProxyControllerProvider.notifier)
|
||||
.startProxy();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
}
|
||||
},
|
||||
onDeleted: (container) {
|
||||
ref
|
||||
.read(selectedContainerProvider.notifier)
|
||||
.clearContainer();
|
||||
},
|
||||
onLongPress: (container) async {
|
||||
await ContainerEditRoute(
|
||||
containerData: jsonEncode(container.toJson()),
|
||||
).push(context);
|
||||
},
|
||||
);
|
||||
return _TabFilters(tabsViewMode: tabsViewMode);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
+1
-1
@@ -178,7 +178,7 @@ class FeedSearch extends HookConsumerWidget {
|
||||
child: Text(
|
||||
ref
|
||||
.read(formatProvider.notifier)
|
||||
.fullDateTimeWithTimezone(articleDate),
|
||||
.fullDateTime(articleDate),
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
|
||||
@@ -186,7 +186,7 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
|
||||
);
|
||||
}
|
||||
|
||||
SingleSelectable<String> siteAssignedContainerId(Uri uri) {
|
||||
Selectable<String> siteAssignedContainerId(Uri uri) {
|
||||
return db.definitionsDrift.siteAssignedContainerId(uri: uri.origin);
|
||||
}
|
||||
|
||||
|
||||
@@ -174,7 +174,8 @@ class ContainerRepository extends _$ContainerRepository {
|
||||
.read(tabDatabaseProvider)
|
||||
.containerDao
|
||||
.siteAssignedContainerId(uri)
|
||||
.getSingle();
|
||||
.get()
|
||||
.then((value) => value.firstOrNull);
|
||||
}
|
||||
|
||||
Future<List<String>> getContainersToClearOnExit() async {
|
||||
|
||||
@@ -42,7 +42,7 @@ final class ContainerRepositoryProvider
|
||||
}
|
||||
|
||||
String _$containerRepositoryHash() =>
|
||||
r'62c2b06970db4385ea0d2edc68bcf076c291a9b6';
|
||||
r'5e46b6d9d3510aeeb70646ede75d71c2db9efb0f';
|
||||
|
||||
abstract class _$ContainerRepository extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -39,12 +39,12 @@ import 'package:weblibre/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class _UnassignedContainerChip extends ConsumerWidget {
|
||||
final int? Function()? containerBadgeCount;
|
||||
final ContainerData? selectedContainer;
|
||||
final bool selected;
|
||||
final void Function(ContainerDataWithCount?)? onSelected;
|
||||
|
||||
const _UnassignedContainerChip({
|
||||
required this.containerBadgeCount,
|
||||
required this.selectedContainer,
|
||||
required this.selected,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@@ -65,7 +65,7 @@ class _UnassignedContainerChip extends ConsumerWidget {
|
||||
label: (tabCount > 0)
|
||||
? Text(tabCount.toString())
|
||||
: const SizedBox.shrink(),
|
||||
selected: selectedContainer == null,
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {
|
||||
if (value) {
|
||||
@@ -76,6 +76,34 @@ class _UnassignedContainerChip extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _SyncedTabsChip extends ConsumerWidget {
|
||||
final bool selected;
|
||||
final int count;
|
||||
final VoidCallback onSelected;
|
||||
|
||||
const _SyncedTabsChip({
|
||||
required this.selected,
|
||||
required this.count,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return FilterChip(
|
||||
avatar: const Icon(Icons.devices_other),
|
||||
labelPadding: count > 0 ? null : const EdgeInsets.only(right: 2.0),
|
||||
label: count > 0 ? Text(count.toString()) : const SizedBox.shrink(),
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
onSelected: (value) {
|
||||
if (value) {
|
||||
onSelected();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContainerSuggestionsChip extends ConsumerWidget {
|
||||
const _ContainerSuggestionsChip();
|
||||
|
||||
@@ -135,6 +163,11 @@ class ContainerChips extends HookConsumerWidget {
|
||||
final bool showUnassignedChip;
|
||||
final bool showGroupSuggestions;
|
||||
final bool enableDragAndDrop;
|
||||
final bool showSyncedChip;
|
||||
final bool syncedChipSelected;
|
||||
final bool? unassignedChipSelected;
|
||||
final int syncedTabCount;
|
||||
final VoidCallback? onSyncedChipSelected;
|
||||
|
||||
final ContainerData? selectedContainer;
|
||||
final bool Function(ContainerDataWithCount)? containerFilter;
|
||||
@@ -157,6 +190,11 @@ class ContainerChips extends HookConsumerWidget {
|
||||
this.showUnassignedChip = true,
|
||||
this.showGroupSuggestions = false,
|
||||
this.enableDragAndDrop = true,
|
||||
this.showSyncedChip = false,
|
||||
this.syncedChipSelected = false,
|
||||
this.unassignedChipSelected,
|
||||
this.syncedTabCount = 0,
|
||||
this.onSyncedChipSelected,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -217,6 +255,12 @@ class ContainerChips extends HookConsumerWidget {
|
||||
}
|
||||
: null,
|
||||
prefixListItems: [
|
||||
if (showSyncedChip)
|
||||
_SyncedTabsChip(
|
||||
selected: syncedChipSelected,
|
||||
count: syncedTabCount,
|
||||
onSelected: onSyncedChipSelected ?? () {},
|
||||
),
|
||||
if (showUnassignedChip)
|
||||
enableDragAndDrop
|
||||
? TabDragContainerTarget(
|
||||
@@ -224,14 +268,20 @@ class ContainerChips extends HookConsumerWidget {
|
||||
child: _UnassignedContainerChip(
|
||||
containerBadgeCount: () =>
|
||||
containerBadgeCount?.call(null),
|
||||
selectedContainer: selectedContainer,
|
||||
selected:
|
||||
unassignedChipSelected ??
|
||||
(selectedContainer == null &&
|
||||
!syncedChipSelected),
|
||||
onSelected: onSelected,
|
||||
),
|
||||
)
|
||||
: _UnassignedContainerChip(
|
||||
containerBadgeCount: () =>
|
||||
containerBadgeCount?.call(null),
|
||||
selectedContainer: selectedContainer,
|
||||
selected:
|
||||
unassignedChipSelected ??
|
||||
(selectedContainer == null &&
|
||||
!syncedChipSelected),
|
||||
onSelected: onSelected,
|
||||
),
|
||||
if (showGroupSuggestions)
|
||||
|
||||
Reference in New Issue
Block a user