bookmar default open setting

This commit is contained in:
Fabian Freund
2026-07-31 05:46:26 +02:00
parent bdfc851681
commit 22c2b15de6
9 changed files with 250 additions and 18 deletions
@@ -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.
@@ -41,7 +41,7 @@ final class TabStatesProvider
}
}
String _$tabStatesHash() => r'1ae4e941890116eabf1803809014b3e575c33e73';
String _$tabStatesHash() => r'6009e702c84e913c81d674585d391bcefb2d7cc1';
abstract class _$TabStates extends $Notifier<Map<String, TabState>> {
Map<String, TabState> build();
@@ -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<bool>(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<void> _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<bool>(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<void> _openInNewTab(
BuildContext context,
WidgetRef ref,
@@ -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<SingboxProxyLogs, List<ProxyLogMessage>> {
/// 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<ProxyLogMessage>> {
List<ProxyLogMessage> build();
@@ -172,6 +172,17 @@ const List<SettingsSectionDefinition> 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;
});
},
@@ -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,
@@ -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<String, dynamic> _$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',
@@ -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,
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
}
String _$generalSettingsRepositoryHash() =>
r'7020706aafbac7ee64f678f918ef9fc24c3b98fb';
r'3c458f146b63ae219a55a5f0488a667b70c44f4b';
abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> {