From 22c2b15de6ae0868ab63ff700f935a45f2febe62 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 31 Jul 2026 05:46:26 +0200 Subject: [PATCH] bookmar default open setting --- .../domain/providers/tab_detail_state.g.dart | 8 +- .../domain/providers/tab_state.g.dart | 2 +- .../presentation/screens/bookmark_list.dart | 81 +++++++++++++--- .../repositories/singbox_proxy_logs.g.dart | 30 +++++- .../screens/browsing_settings.dart | 92 +++++++++++++++++++ .../user/data/models/general_settings.dart | 21 +++++ .../user/data/models/general_settings.g.dart | 28 ++++++ .../domain/repositories/general_settings.dart | 4 + .../repositories/general_settings.g.dart | 2 +- 9 files changed, 250 insertions(+), 18 deletions(-) diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/tab_detail_state.g.dart b/apps/weblibre/lib/features/geckoview/domain/providers/tab_detail_state.g.dart index ad8437a5..f943bac6 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/tab_detail_state.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/tab_detail_state.g.dart @@ -181,7 +181,7 @@ final class TabThumbnailsProvider } } -String _$tabThumbnailsHash() => r'16c4bb6edff2e6413100d9ce02c1ae7948740cfe'; +String _$tabThumbnailsHash() => r'c3066c4d05520d9edefcafc03810cda47302f43e'; /// Page screenshots per tab. Refreshed on a 10s timer for the selected tab and /// consumed only by the tab tray previews. @@ -323,7 +323,7 @@ final class TabHistoryStatesProvider } } -String _$tabHistoryStatesHash() => r'3920410334c4354ca66da605d9bfe85b4853f3bf'; +String _$tabHistoryStatesHash() => r'e27d36cbb16f7c025fa9a5034699151706d7ba21'; /// Session history (back/forward stack) per tab. @@ -464,7 +464,7 @@ final class TabFindResultStatesProvider } String _$tabFindResultStatesHash() => - r'f57dee1658eae789002ac3cff15808cbc96b7883'; + r'e9d68ea9ed3d8d4f319073204415fa27420de29d'; /// Find-in-page match counters per tab. Emitted at a high rate by Gecko while /// a search is running. @@ -612,7 +612,7 @@ final class TabTranslationStatesProvider } String _$tabTranslationStatesHash() => - r'f04004026dc5fd638319528a55e2f10e0f64c9b3'; + r'28daf6c0d421a3ed60ccb5087021d5fc078c2927'; /// Translation progress/result per tab. diff --git a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart index 1daecb8a..160bc32e 100644 --- a/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/apps/weblibre/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -41,7 +41,7 @@ final class TabStatesProvider } } -String _$tabStatesHash() => r'1ae4e941890116eabf1803809014b3e575c33e73'; +String _$tabStatesHash() => r'6009e702c84e913c81d674585d391bcefb2d7cc1'; abstract class _$TabStates extends $Notifier> { Map build(); diff --git a/apps/weblibre/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart b/apps/weblibre/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart index b8a2b5e1..7585f2a3 100644 --- a/apps/weblibre/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart +++ b/apps/weblibre/lib/features/geckoview/features/bookmarks/presentation/screens/bookmark_list.dart @@ -30,6 +30,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:share_plus/share_plus.dart'; import 'package:weblibre/core/logger.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/extensions/uri.dart'; +import 'package:weblibre/features/geckoview/domain/entities/tab_container_selection.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/bookmarks/domain/entities/bookmark_item.dart'; @@ -47,6 +49,10 @@ import 'package:weblibre/features/geckoview/features/bookmarks/presentation/dial import 'package:weblibre/features/geckoview/features/bookmarks/presentation/dialogs/select_bookmark_folder_dialog.dart'; import 'package:weblibre/features/geckoview/features/bookmarks/utils/bookmark_import_isolate.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart'; +import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; @@ -572,17 +578,7 @@ class BookmarkListScreen extends HookConsumerWidget { trailing: _buildEntryMenu(context, ref, bookmark), title: Text(bookmark.title, maxLines: 3, overflow: TextOverflow.ellipsis), subtitle: UriBreadcrumb(uri: bookmark.url), - onTap: () async { - final result = await OpenSharedContentRoute( - sharedUrl: bookmark.url.toString(), - ).push(context); - - if (result == true) { - if (context.mounted) { - const BrowserRoute().go(context); - } - } - }, + onTap: () => _openBookmark(context, ref, bookmark.url), onLongPress: () { uiStateNotifier.enterSelectionMode(initialGuid: bookmark.guid); }, @@ -1016,6 +1012,69 @@ class BookmarkListScreen extends HookConsumerWidget { // -- Tab Opening Helper -- + /// Opens a tapped bookmark according to [BookmarkOpenSetting]. `ask` shows + /// the "open in..." sheet (the historical, default behavior); the other + /// values open the bookmark directly with no intermediate prompt. + Future _openBookmark( + BuildContext context, + WidgetRef ref, + Uri url, + ) async { + final settings = ref.read(generalSettingsWithDefaultsProvider); + + switch (settings.effectiveBookmarkOpenSetting) { + case BookmarkOpenSetting.ask: + final result = await OpenSharedContentRoute( + sharedUrl: url.toString(), + ).push(context); + + if (result == true && context.mounted) { + const BrowserRoute().go(context); + } + case BookmarkOpenSetting.regular: + case BookmarkOpenSetting.isolated: + final isolated = + settings.effectiveBookmarkOpenSetting == + BookmarkOpenSetting.isolated; + + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: url, + tabMode: isolated ? TabMode.newIsolated() : TabMode.regular, + selectTab: true, + containerSelection: isolated + ? const TabContainerSelection.unassigned() + : const TabContainerSelection.useSelected(), + ); + + if (context.mounted) { + const BrowserRoute().go(context); + } + case BookmarkOpenSetting.customTab: + final containerRepo = ref.read(containerRepositoryProvider.notifier); + + ContainerData? container; + if (url.hasAuthority && url.isHttpOrHttps) { + final siteAssignedId = await containerRepo.siteAssignedContainerId( + url, + ); + if (siteAssignedId != null) { + container = await containerRepo.getContainerData(siteAssignedId); + } + } + container ??= await ref + .read(selectedContainerProvider.notifier) + .fetchData(); + + await GeckoBrowserService().openInCustomTab( + url: url, + private: false, + contextId: container?.metadata.contextualIdentity, + ); + } + } + Future _openInNewTab( BuildContext context, WidgetRef ref, diff --git a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart index e9e100d7..fc110a23 100644 --- a/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart +++ b/apps/weblibre/lib/features/proxy/domain/repositories/singbox_proxy_logs.g.dart @@ -9,14 +9,35 @@ part of 'singbox_proxy_logs.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning /// Snapshot of buffered log entries. Most-recent-last (chronological). +/// +/// This notifier is `keepAlive` and subscribed from app start (see +/// `main.dart`) so startup messages are retained even before any UI mounts. +/// Appending and *publishing* are therefore deliberately decoupled: lines +/// always land in [_buffer], but a new immutable snapshot is only produced +/// while the log screen is on screen ([setLivePublishing]) and at most once +/// per [_publishInterval]. @ProviderFor(SingboxProxyLogs) final singboxProxyLogsProvider = SingboxProxyLogsProvider._(); /// Snapshot of buffered log entries. Most-recent-last (chronological). +/// +/// This notifier is `keepAlive` and subscribed from app start (see +/// `main.dart`) so startup messages are retained even before any UI mounts. +/// Appending and *publishing* are therefore deliberately decoupled: lines +/// always land in [_buffer], but a new immutable snapshot is only produced +/// while the log screen is on screen ([setLivePublishing]) and at most once +/// per [_publishInterval]. final class SingboxProxyLogsProvider extends $NotifierProvider> { /// Snapshot of buffered log entries. Most-recent-last (chronological). + /// + /// This notifier is `keepAlive` and subscribed from app start (see + /// `main.dart`) so startup messages are retained even before any UI mounts. + /// Appending and *publishing* are therefore deliberately decoupled: lines + /// always land in [_buffer], but a new immutable snapshot is only produced + /// while the log screen is on screen ([setLivePublishing]) and at most once + /// per [_publishInterval]. SingboxProxyLogsProvider._() : super( from: null, @@ -44,9 +65,16 @@ final class SingboxProxyLogsProvider } } -String _$singboxProxyLogsHash() => r'9fa30201ed4c128335142227226d937022f79d47'; +String _$singboxProxyLogsHash() => r'9ce114b70829de5b7b1c05f359c13d1d52d8e4ad'; /// Snapshot of buffered log entries. Most-recent-last (chronological). +/// +/// This notifier is `keepAlive` and subscribed from app start (see +/// `main.dart`) so startup messages are retained even before any UI mounts. +/// Appending and *publishing* are therefore deliberately decoupled: lines +/// always land in [_buffer], but a new immutable snapshot is only produced +/// while the log screen is on screen ([setLivePublishing]) and at most once +/// per [_publishInterval]. abstract class _$SingboxProxyLogs extends $Notifier> { List build(); diff --git a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart index 331f3907..dac9b94c 100644 --- a/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart +++ b/apps/weblibre/lib/features/settings/presentation/screens/browsing_settings.dart @@ -172,6 +172,17 @@ const List browsingSettingsSections = [ ), ], ), + SettingsSectionDefinition( + title: 'Bookmarks', + entries: [ + SettingsEntryDefinition( + title: 'Bookmark Open Behavior', + subtitle: 'Choose how tapping a bookmark opens it', + keywords: ['bookmarks', 'open', 'custom tab', 'isolated'], + child: _BookmarkOpenBehaviorSection(), + ), + ], + ), ]; class BrowsingSettingsScreen extends StatelessWidget { @@ -422,6 +433,81 @@ class _ExternalLinkHandlingSection extends HookConsumerWidget { } } +class _BookmarkOpenBehaviorSection extends HookConsumerWidget { + const _BookmarkOpenBehaviorSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(generalSettingsWithDefaultsProvider); + final bookmarkOpenSetting = settings.effectiveBookmarkOpenSetting; + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Bookmark Open Behavior'), + subtitle: Text('Choose how tapping a bookmark opens it'), + leading: Icon(MdiIcons.bookmarkMultiple), + contentPadding: EdgeInsets.zero, + ), + RadioGroup( + groupValue: bookmarkOpenSetting, + onChanged: (value) async { + if (value != null) { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.bookmarkOpenSetting(value), + ); + } + }, + child: Column( + children: [ + const RadioListTile.adaptive( + value: BookmarkOpenSetting.ask, + title: Text('Prompt'), + subtitle: Text('Ask how the bookmark should open'), + secondary: Icon(MdiIcons.messageQuestion), + ), + const RadioListTile.adaptive( + value: BookmarkOpenSetting.regular, + title: Text('Regular'), + subtitle: Text('Open the bookmark in a regular tab'), + secondary: Icon(MdiIcons.tab), + ), + const RadioListTile.adaptive( + value: BookmarkOpenSetting.customTab, + title: Text('Custom Tab'), + subtitle: Text( + 'Open the bookmark in a lightweight custom tab', + ), + secondary: Icon(MdiIcons.applicationOutline), + ), + if (settings.showIsolatedTabUi) + RadioListTile.adaptive( + value: BookmarkOpenSetting.isolated, + title: const Text('Isolated'), + subtitle: const Text( + 'Open the bookmark in an isolated tab', + ), + secondary: Icon( + MdiIcons.snowflake, + color: AppColors.of(context).isolatedTabTeal, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + class _TabListDirectionSection extends HookConsumerWidget { const _TabListDirectionSection(); @@ -639,6 +725,12 @@ class _ShowIsolatedTabUiTile extends HookConsumerWidget { if (!value && updated.smallWebTabType == TabType.isolated) { updated = updated.copyWith.smallWebTabType(TabType.private); } + if (!value && + updated.bookmarkOpenSetting == BookmarkOpenSetting.isolated) { + updated = updated.copyWith.bookmarkOpenSetting( + BookmarkOpenSetting.ask, + ); + } return updated; }); }, diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.dart b/apps/weblibre/lib/features/user/data/models/general_settings.dart index 03d7ecbf..c5011f1b 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.dart @@ -89,6 +89,11 @@ enum TabBarStackingMode { enum TabIntentOpenSetting { regular, private, isolated, ask } +/// Determines what happens when a bookmark is tapped in the bookmark list. +/// [ask] shows the "open in..." sheet (today's behavior, and the default); +/// the other values open the bookmark directly with no intermediate prompt. +enum BookmarkOpenSetting { regular, isolated, customTab, ask } + enum TabDirection { newestFirst, oldestFirst } enum TabBarPosition { @@ -157,6 +162,10 @@ class GeneralSettings with FastEquatable { final TabDirection tabListDirection; final TabDirection tabBarDirection; final TabIntentOpenSetting tabIntentOpenSetting; + + /// Determines what happens when a bookmark is tapped. See + /// [BookmarkOpenSetting] and [effectiveBookmarkOpenSetting]. + final BookmarkOpenSetting bookmarkOpenSetting; final bool autoHideTabBar; final TabBarSwipeAction tabBarSwipeAction; final Duration historyAutoCleanInterval; @@ -286,6 +295,7 @@ class GeneralSettings with FastEquatable { required this.tabListDirection, required this.tabBarDirection, required this.tabIntentOpenSetting, + required this.bookmarkOpenSetting, required this.autoHideTabBar, required this.tabBarSwipeAction, required this.historyAutoCleanInterval, @@ -358,6 +368,7 @@ class GeneralSettings with FastEquatable { TabDirection? tabListDirection, TabDirection? tabBarDirection, TabIntentOpenSetting? tabIntentOpenSetting, + BookmarkOpenSetting? bookmarkOpenSetting, bool? autoHideTabBar, TabBarSwipeAction? tabBarSwipeAction, Duration? historyAutoCleanInterval, @@ -428,6 +439,7 @@ class GeneralSettings with FastEquatable { tabListDirection = tabListDirection ?? TabDirection.newestFirst, tabBarDirection = tabBarDirection ?? TabDirection.newestFirst, tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask, + bookmarkOpenSetting = bookmarkOpenSetting ?? BookmarkOpenSetting.ask, autoHideTabBar = autoHideTabBar ?? true, tabBarSwipeAction = tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened, @@ -550,6 +562,14 @@ class GeneralSettings with FastEquatable { return tabIntentOpenSetting; } + BookmarkOpenSetting get effectiveBookmarkOpenSetting { + if (!showIsolatedTabUi && + bookmarkOpenSetting == BookmarkOpenSetting.isolated) { + return BookmarkOpenSetting.ask; + } + return bookmarkOpenSetting; + } + /// Container-dependent stacking modes degrade to a single recently-used /// row when the container UI is disabled. Two-level stacking additionally /// degrades to accordion (the default mode, which has a vertical form) on the @@ -595,6 +615,7 @@ class GeneralSettings with FastEquatable { tabListDirection, tabBarDirection, tabIntentOpenSetting, + bookmarkOpenSetting, autoHideTabBar, tabBarSwipeAction, historyAutoCleanInterval, diff --git a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart index 7cd836cf..94e9f373 100644 --- a/apps/weblibre/lib/features/user/data/models/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/data/models/general_settings.g.dart @@ -55,6 +55,8 @@ abstract class _$GeneralSettingsCWProxy { TabIntentOpenSetting tabIntentOpenSetting, ); + GeneralSettings bookmarkOpenSetting(BookmarkOpenSetting bookmarkOpenSetting); + GeneralSettings autoHideTabBar(bool autoHideTabBar); GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction); @@ -195,6 +197,7 @@ abstract class _$GeneralSettingsCWProxy { TabDirection tabListDirection, TabDirection tabBarDirection, TabIntentOpenSetting tabIntentOpenSetting, + BookmarkOpenSetting bookmarkOpenSetting, bool autoHideTabBar, TabBarSwipeAction tabBarSwipeAction, Duration historyAutoCleanInterval, @@ -338,6 +341,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { TabIntentOpenSetting tabIntentOpenSetting, ) => call(tabIntentOpenSetting: tabIntentOpenSetting); + @override + GeneralSettings bookmarkOpenSetting( + BookmarkOpenSetting bookmarkOpenSetting, + ) => call(bookmarkOpenSetting: bookmarkOpenSetting); + @override GeneralSettings autoHideTabBar(bool autoHideTabBar) => call(autoHideTabBar: autoHideTabBar); @@ -578,6 +586,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? tabListDirection = const $CopyWithPlaceholder(), Object? tabBarDirection = const $CopyWithPlaceholder(), Object? tabIntentOpenSetting = const $CopyWithPlaceholder(), + Object? bookmarkOpenSetting = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(), Object? tabBarSwipeAction = const $CopyWithPlaceholder(), Object? historyAutoCleanInterval = const $CopyWithPlaceholder(), @@ -746,6 +755,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.tabIntentOpenSetting // ignore: cast_nullable_to_non_nullable : tabIntentOpenSetting as TabIntentOpenSetting, + bookmarkOpenSetting: + bookmarkOpenSetting == const $CopyWithPlaceholder() || + bookmarkOpenSetting == null + ? _value.bookmarkOpenSetting + // ignore: cast_nullable_to_non_nullable + : bookmarkOpenSetting as BookmarkOpenSetting, autoHideTabBar: autoHideTabBar == const $CopyWithPlaceholder() || autoHideTabBar == null @@ -1096,6 +1111,10 @@ GeneralSettings _$GeneralSettingsFromJson( _$TabIntentOpenSettingEnumMap, json['tabIntentOpenSetting'], ), + bookmarkOpenSetting: $enumDecodeNullable( + _$BookmarkOpenSettingEnumMap, + json['bookmarkOpenSetting'], + ), autoHideTabBar: json['autoHideTabBar'] as bool?, tabBarSwipeAction: $enumDecodeNullable( _$TabBarSwipeActionEnumMap, @@ -1221,6 +1240,8 @@ Map _$GeneralSettingsToJson( 'tabBarDirection': _$TabDirectionEnumMap[instance.tabBarDirection]!, 'tabIntentOpenSetting': _$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!, + 'bookmarkOpenSetting': + _$BookmarkOpenSettingEnumMap[instance.bookmarkOpenSetting]!, 'autoHideTabBar': instance.autoHideTabBar, 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, 'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds, @@ -1329,6 +1350,13 @@ const _$TabIntentOpenSettingEnumMap = { TabIntentOpenSetting.ask: 'ask', }; +const _$BookmarkOpenSettingEnumMap = { + BookmarkOpenSetting.regular: 'regular', + BookmarkOpenSetting.isolated: 'isolated', + BookmarkOpenSetting.customTab: 'customTab', + BookmarkOpenSetting.ask: 'ask', +}; + const _$TabBarSwipeActionEnumMap = { TabBarSwipeAction.switchLastOpened: 'switchLastOpened', TabBarSwipeAction.navigateOrderedTabs: 'navigateOrderedTabs', diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart index e7613885..45bdd8f7 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.dart @@ -122,6 +122,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), + 'bookmarkOpenSetting': settings['bookmarkOpenSetting']?.readAs( + DriftSqlType.string, + db.typeMapping, + ), 'autoHideTabBar': settings['autoHideTabBar']?.readAs( DriftSqlType.bool, db.typeMapping, diff --git a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart index c502ea31..baed8122 100644 --- a/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart +++ b/apps/weblibre/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'7020706aafbac7ee64f678f918ef9fc24c3b98fb'; + r'3c458f146b63ae219a55a5f0488a667b70c44f4b'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {