Files
MrbWebLibre/apps/weblibre/lib/features/user/data/models/general_settings.dart
T

498 lines
19 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 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 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
import 'package:weblibre/features/intent_gatekeeper/domain/entities/intent_source_policy.dart';
import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
part 'general_settings.g.dart';
const _fallbackSearchProvider = BangKey(
group: BangGroup.general,
trigger: 'wikipedia',
);
const _fallbackAutocompleteProvider = SearchSuggestionProviders.none;
const defaultUiScaleFactor = 1.0;
const minUiScaleFactor = 0.5;
const maxUiScaleFactor = 1.5;
const uiScaleFactorStep = 0.05;
/// Max number of inline chevron glyphs shown on a quick tab switcher chip
/// before the depth indicator collapses into a single icon + count badge.
/// A value of 0 hides the hierarchy indicator entirely.
const defaultQuickTabSwitcherHierarchyGlyphs = 2;
const minQuickTabSwitcherHierarchyGlyphs = 0;
const maxQuickTabSwitcherHierarchyGlyphs = 4;
/// Controls the Android display refresh rate the app requests at startup.
///
/// Flutter does not request a high refresh rate by default, so on many devices
/// (Samsung, OnePlus, Xiaomi, …) the app is left at 60Hz even on a 90/120Hz
/// panel. [high] asks the OS for the fastest available mode, [low] for the
/// slowest (battery saving), and [system] leaves the OS-managed default in
/// place. Android-only; ignored on other platforms.
enum RefreshRateMode { system, high, low }
enum TabBarSwipeAction { switchLastOpened, navigateOrderedTabs }
enum QuickTabSwitcherMode { lastUsedTabs, containerTabs }
enum TabIntentOpenSetting { regular, private, isolated, ask }
enum TabDirection { newestFirst, oldestFirst }
enum TabBarPosition { top, bottom }
enum TabBarLayout { withTitle, compact }
enum DeleteBrowsingDataType {
tabs('Open tabs'),
history('Browsing history'),
cookies('Cookies and site data', 'Youll be logged out of most sites'),
cache('Cached images and files', 'Frees up storage space'),
permissions('Site permissions'),
downloads('Downloads');
final String title;
final String? description;
const DeleteBrowsingDataType(this.title, [this.description]);
}
@CopyWith()
@JsonSerializable(includeIfNull: true, constructor: 'withDefaults')
class GeneralSettings with FastEquatable {
final ThemeMode themeMode;
final double uiScaleFactor;
final bool disableAnimations;
/// Android display refresh rate requested at startup. See [RefreshRateMode].
final RefreshRateMode refreshRateMode;
final bool showModalBarrier;
final bool enableReadability;
final bool enforceReadability;
final Set<DeleteBrowsingDataType>? deleteBrowsingDataOnQuit;
final bool screenshotProtectionEnabled;
@BangKeyConverter()
final BangKey? defaultSearchProvider;
final SearchSuggestionProviders defaultSearchSuggestionsProvider;
final bool createChildTabsOption;
final bool enableLocalAiFeatures;
final bool showContainerUi;
final bool showIsolatedTabUi;
/// Whether the search / new-tab page shows a leading close button so it can
/// be dismissed without a system back button or back gesture (e.g. on e-ink
/// devices). Defaults to false. Only shown when the route can be popped.
final bool showSearchCloseButton;
@JsonKey(name: 'defaultCreateTabType')
final TabType storedDefaultCreateTabType;
final TabDirection tabListDirection;
final TabDirection tabBarDirection;
final TabIntentOpenSetting tabIntentOpenSetting;
final bool autoHideTabBar;
final TabBarSwipeAction tabBarSwipeAction;
final Duration historyAutoCleanInterval;
final bool tabViewBottomSheet;
final bool tabBarShowContextualBar;
final bool tabBarShowQuickTabSwitcherBar;
final TabBarPosition tabBarPosition;
final TabBarLayout tabBarLayout;
final QuickTabSwitcherMode quickTabSwitcherMode;
final bool pullToRefreshEnabled;
final bool useExternalDownloadManager;
final bool doubleBackCloseTab;
final Duration unassignedTabsAutoCleanInterval;
final int maxSearchHistoryEntries;
final bool allowClipboardAccess;
final bool tabListShowFavicons;
final bool quickTabSwitcherShowTitles;
final int quickTabSwitcherHierarchyGlyphs;
final bool quickTabSwitcherShowHistorySuggestions;
final String syncServerOverride;
final String syncTokenServerOverride;
final bool urlCleanerEnabled;
final bool urlCleanerAutoApply;
final bool urlCleanerAllowReferralMarketing;
final String urlCleanerCatalogUrl;
final String urlCleanerHashUrl;
final bool urlCleanerAutoUpdate;
final int? urlCleanerLastCheckEpochMs;
final bool urlCleanerLastUpdateWasAuto;
final TabType smallWebTabType;
final bool tabBarLongPressUrlCopy;
final bool unshortenerEnabled;
final String unshortenerToken;
final bool allowNonManifestPwaInstall;
final bool blockExternalAppsEnabled;
final Map<String, IntentSourcePolicy> externalAppIntentPolicies;
/// Whether the local search index (`history` table populated via tab→
/// history triggers) is active. When false, the SQL trigger guard returns
/// without writing; existing rows stay until the user clears them.
final bool enableLocalSearchIndex;
/// Whether private tabs feed the local search index. Defaults to false.
final bool indexPrivateTabs;
/// Whether pressing the keyboard submit/enter button should automatically
/// accept and complete an inline search suggestion. Defaults to false.
final bool acceptSuggestionOnSubmit;
/// Whether dark mode should use pure-black ("OLED"/high-contrast) surfaces.
/// Only takes effect when the effective brightness is dark. Defaults to false.
final bool pureBlack;
/// Browser-wide default desktop mode. When true, newly opened tabs request
/// the desktop version of sites by default. The per-tab desktop-mode toggle
/// still overrides this for an individual tab. Defaults to false.
final bool globalDesktopMode;
/// Hosts that should always load in desktop mode. A tab navigating to a
/// matching host (or any of its subdomains) is switched to desktop mode,
/// overriding [globalDesktopMode] for that visit. See `hostMatchesRule`.
final List<String> desktopModeSites;
GeneralSettings({
required this.themeMode,
required this.uiScaleFactor,
required this.disableAnimations,
required this.refreshRateMode,
required this.showModalBarrier,
required this.enableReadability,
required this.enforceReadability,
required this.deleteBrowsingDataOnQuit,
required this.screenshotProtectionEnabled,
required this.defaultSearchProvider,
required this.defaultSearchSuggestionsProvider,
required this.createChildTabsOption,
required this.enableLocalAiFeatures,
required this.showContainerUi,
required this.showIsolatedTabUi,
required this.showSearchCloseButton,
required this.storedDefaultCreateTabType,
required this.tabListDirection,
required this.tabBarDirection,
required this.tabIntentOpenSetting,
required this.autoHideTabBar,
required this.tabBarSwipeAction,
required this.historyAutoCleanInterval,
required this.tabViewBottomSheet,
required this.tabBarShowContextualBar,
required this.tabBarShowQuickTabSwitcherBar,
required this.tabBarPosition,
required this.tabBarLayout,
required this.quickTabSwitcherMode,
required this.pullToRefreshEnabled,
required this.useExternalDownloadManager,
required this.doubleBackCloseTab,
required this.unassignedTabsAutoCleanInterval,
required this.maxSearchHistoryEntries,
required this.allowClipboardAccess,
required this.tabListShowFavicons,
required this.quickTabSwitcherShowTitles,
required this.quickTabSwitcherHierarchyGlyphs,
required this.quickTabSwitcherShowHistorySuggestions,
required this.syncServerOverride,
required this.syncTokenServerOverride,
required this.urlCleanerEnabled,
required this.urlCleanerAutoApply,
required this.urlCleanerAllowReferralMarketing,
required this.urlCleanerCatalogUrl,
required this.urlCleanerHashUrl,
required this.urlCleanerAutoUpdate,
required this.urlCleanerLastCheckEpochMs,
required this.urlCleanerLastUpdateWasAuto,
required this.smallWebTabType,
required this.tabBarLongPressUrlCopy,
required this.unshortenerEnabled,
required this.unshortenerToken,
required this.allowNonManifestPwaInstall,
required this.blockExternalAppsEnabled,
required this.externalAppIntentPolicies,
required this.enableLocalSearchIndex,
required this.indexPrivateTabs,
required this.acceptSuggestionOnSubmit,
required this.pureBlack,
required this.globalDesktopMode,
required this.desktopModeSites,
});
GeneralSettings.withDefaults({
ThemeMode? themeMode,
double? uiScaleFactor,
bool? disableAnimations,
RefreshRateMode? refreshRateMode,
bool? showModalBarrier,
bool? enableReadability,
bool? enforceReadability,
this.deleteBrowsingDataOnQuit,
bool? screenshotProtectionEnabled,
BangKey? defaultSearchProvider,
SearchSuggestionProviders? defaultSearchSuggestionsProvider,
bool? createChildTabsOption,
bool? enableLocalAiFeatures,
bool? showContainerUi,
bool? showIsolatedTabUi,
bool? showSearchCloseButton,
TabType? storedDefaultCreateTabType,
TabDirection? tabListDirection,
TabDirection? tabBarDirection,
TabIntentOpenSetting? tabIntentOpenSetting,
bool? autoHideTabBar,
TabBarSwipeAction? tabBarSwipeAction,
Duration? historyAutoCleanInterval,
bool? tabViewBottomSheet,
bool? tabBarShowContextualBar,
bool? tabBarShowQuickTabSwitcherBar,
TabBarPosition? tabBarPosition,
TabBarLayout? tabBarLayout,
QuickTabSwitcherMode? quickTabSwitcherMode,
bool? pullToRefreshEnabled,
bool? useExternalDownloadManager,
bool? doubleBackCloseTab,
Duration? unassignedTabsAutoCleanInterval,
int? maxSearchHistoryEntries,
bool? allowClipboardAccess,
bool? tabListShowFavicons,
bool? quickTabSwitcherShowTitles,
int? quickTabSwitcherHierarchyGlyphs,
bool? quickTabSwitcherShowHistorySuggestions,
String? syncServerOverride,
String? syncTokenServerOverride,
bool? urlCleanerEnabled,
bool? urlCleanerAutoApply,
bool? urlCleanerAllowReferralMarketing,
String? urlCleanerCatalogUrl,
String? urlCleanerHashUrl,
bool? urlCleanerAutoUpdate,
this.urlCleanerLastCheckEpochMs,
bool? urlCleanerLastUpdateWasAuto,
TabType? smallWebTabType,
bool? tabBarLongPressUrlCopy,
bool? unshortenerEnabled,
String? unshortenerToken,
bool? allowNonManifestPwaInstall,
bool? blockExternalAppsEnabled,
Map<String, IntentSourcePolicy>? externalAppIntentPolicies,
bool? enableLocalSearchIndex,
bool? indexPrivateTabs,
bool? acceptSuggestionOnSubmit,
bool? pureBlack,
bool? globalDesktopMode,
List<String>? desktopModeSites,
}) : themeMode = themeMode ?? ThemeMode.dark,
uiScaleFactor = uiScaleFactor ?? defaultUiScaleFactor,
disableAnimations = disableAnimations ?? false,
refreshRateMode = refreshRateMode ?? RefreshRateMode.high,
showModalBarrier = showModalBarrier ?? true,
enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false,
screenshotProtectionEnabled = screenshotProtectionEnabled ?? false,
defaultSearchProvider = defaultSearchProvider ?? _fallbackSearchProvider,
defaultSearchSuggestionsProvider =
defaultSearchSuggestionsProvider ?? _fallbackAutocompleteProvider,
createChildTabsOption = createChildTabsOption ?? false,
enableLocalAiFeatures = enableLocalAiFeatures ?? true,
showContainerUi = showContainerUi ?? true,
showIsolatedTabUi = showIsolatedTabUi ?? true,
showSearchCloseButton = showSearchCloseButton ?? false,
storedDefaultCreateTabType =
storedDefaultCreateTabType ?? TabType.regular,
tabListDirection = tabListDirection ?? TabDirection.newestFirst,
tabBarDirection = tabBarDirection ?? TabDirection.newestFirst,
tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask,
autoHideTabBar = autoHideTabBar ?? true,
tabBarSwipeAction =
tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened,
historyAutoCleanInterval =
historyAutoCleanInterval ?? const Duration(days: 90),
tabViewBottomSheet = tabViewBottomSheet ?? false,
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true,
tabBarPosition = tabBarPosition ?? TabBarPosition.bottom,
tabBarLayout = tabBarLayout ?? TabBarLayout.compact,
quickTabSwitcherMode =
quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs,
pullToRefreshEnabled = pullToRefreshEnabled ?? true,
useExternalDownloadManager = useExternalDownloadManager ?? false,
doubleBackCloseTab = doubleBackCloseTab ?? true,
unassignedTabsAutoCleanInterval =
unassignedTabsAutoCleanInterval ?? Duration.zero,
maxSearchHistoryEntries = maxSearchHistoryEntries ?? 5,
allowClipboardAccess = allowClipboardAccess ?? true,
tabListShowFavicons = tabListShowFavicons ?? false,
quickTabSwitcherShowTitles = quickTabSwitcherShowTitles ?? true,
quickTabSwitcherHierarchyGlyphs =
quickTabSwitcherHierarchyGlyphs ??
defaultQuickTabSwitcherHierarchyGlyphs,
quickTabSwitcherShowHistorySuggestions =
quickTabSwitcherShowHistorySuggestions ?? true,
syncServerOverride = syncServerOverride ?? '',
syncTokenServerOverride = syncTokenServerOverride ?? '',
urlCleanerEnabled = urlCleanerEnabled ?? true,
urlCleanerAutoApply = urlCleanerAutoApply ?? false,
urlCleanerAllowReferralMarketing =
urlCleanerAllowReferralMarketing ?? false,
urlCleanerCatalogUrl =
urlCleanerCatalogUrl ??
'https://rules2.clearurls.xyz/data.minify.json',
urlCleanerHashUrl =
urlCleanerHashUrl ??
'https://rules2.clearurls.xyz/rules.minify.hash',
urlCleanerAutoUpdate = urlCleanerAutoUpdate ?? false,
urlCleanerLastUpdateWasAuto = urlCleanerLastUpdateWasAuto ?? false,
smallWebTabType = smallWebTabType ?? TabType.private,
tabBarLongPressUrlCopy = tabBarLongPressUrlCopy ?? true,
unshortenerEnabled = unshortenerEnabled ?? false,
unshortenerToken = unshortenerToken ?? '',
allowNonManifestPwaInstall = allowNonManifestPwaInstall ?? false,
blockExternalAppsEnabled = blockExternalAppsEnabled ?? false,
externalAppIntentPolicies = externalAppIntentPolicies ?? const {},
enableLocalSearchIndex = enableLocalSearchIndex ?? true,
indexPrivateTabs = indexPrivateTabs ?? false,
acceptSuggestionOnSubmit = acceptSuggestionOnSubmit ?? false,
pureBlack = pureBlack ?? false,
globalDesktopMode = globalDesktopMode ?? false,
desktopModeSites = desktopModeSites ?? const [];
factory GeneralSettings.fromJson(Map<String, dynamic> json) {
// Migrate legacy `newTabPosition` setting to direction settings.
// Old `first` (new tabs at top) → newestFirst; `end` → oldestFirst.
// TODO: Drop this fallback (and the `newTabPosition` row in the user
// settings DB) once enough releases have shipped that rolling back to a
// version without `tabListDirection`/`tabBarDirection` is no longer a
// concern.
final legacyNewTabPosition = json['newTabPosition'];
if (legacyNewTabPosition != null) {
final mapped = legacyNewTabPosition == 'end'
? 'oldestFirst'
: 'newestFirst';
json.putIfAbsent('tabListDirection', () => mapped);
json.putIfAbsent('tabBarDirection', () => mapped);
}
return _$GeneralSettingsFromJson(json);
}
Map<String, dynamic> toJson() => _$GeneralSettingsToJson(this);
TabType get effectiveDefaultCreateTabType {
if (!showIsolatedTabUi && storedDefaultCreateTabType == TabType.isolated) {
return TabType.regular;
}
return storedDefaultCreateTabType;
}
TabType get effectiveSmallWebTabType {
if (!showIsolatedTabUi && smallWebTabType == TabType.isolated) {
return TabType.private;
}
return smallWebTabType;
}
TabIntentOpenSetting get effectiveTabIntentOpenSetting {
if (!showIsolatedTabUi &&
tabIntentOpenSetting == TabIntentOpenSetting.isolated) {
return TabIntentOpenSetting.ask;
}
return tabIntentOpenSetting;
}
QuickTabSwitcherMode effectiveUiQuickTabSwitcherMode() {
if (!showContainerUi &&
quickTabSwitcherMode == QuickTabSwitcherMode.containerTabs) {
return QuickTabSwitcherMode.lastUsedTabs;
}
return quickTabSwitcherMode;
}
@override
List<Object?> get hashParameters => [
themeMode,
uiScaleFactor,
disableAnimations,
refreshRateMode,
showModalBarrier,
enableReadability,
enforceReadability,
deleteBrowsingDataOnQuit,
screenshotProtectionEnabled,
defaultSearchProvider,
defaultSearchSuggestionsProvider,
createChildTabsOption,
enableLocalAiFeatures,
showContainerUi,
showIsolatedTabUi,
showSearchCloseButton,
storedDefaultCreateTabType,
tabListDirection,
tabBarDirection,
tabIntentOpenSetting,
autoHideTabBar,
tabBarSwipeAction,
historyAutoCleanInterval,
tabViewBottomSheet,
tabBarShowContextualBar,
tabBarShowQuickTabSwitcherBar,
tabBarPosition,
tabBarLayout,
quickTabSwitcherMode,
pullToRefreshEnabled,
useExternalDownloadManager,
doubleBackCloseTab,
unassignedTabsAutoCleanInterval,
maxSearchHistoryEntries,
allowClipboardAccess,
tabListShowFavicons,
quickTabSwitcherShowTitles,
quickTabSwitcherHierarchyGlyphs,
quickTabSwitcherShowHistorySuggestions,
syncServerOverride,
syncTokenServerOverride,
urlCleanerEnabled,
urlCleanerAutoApply,
urlCleanerAllowReferralMarketing,
urlCleanerCatalogUrl,
urlCleanerHashUrl,
urlCleanerAutoUpdate,
urlCleanerLastCheckEpochMs,
urlCleanerLastUpdateWasAuto,
smallWebTabType,
tabBarLongPressUrlCopy,
unshortenerEnabled,
unshortenerToken,
allowNonManifestPwaInstall,
blockExternalAppsEnabled,
externalAppIntentPolicies,
enableLocalSearchIndex,
indexPrivateTabs,
acceptSuggestionOnSubmit,
pureBlack,
globalDesktopMode,
desktopModeSites,
];
}