diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart index 7aac46cc..aef4c197 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart @@ -47,6 +47,7 @@ import 'package:weblibre/features/geckoview/features/browser/domain/services/pro import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart'; import 'package:weblibre/features/geckoview/features/preferences/data/repositories/preference_observer.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/features/share_intent/domain/entities/shared_content.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/cache.dart'; @@ -117,6 +118,16 @@ class _BrowserViewState extends ConsumerState DateTime.now().subtract(settings.historyAutoCleanInterval), ); } + + if (settings.unassignedTabsAutoCleanInterval > Duration.zero) { + await ref + .read(tabDataRepositoryProvider.notifier) + .deleteUnassignedTabsOlderThan( + DateTime.now().subtract( + settings.unassignedTabsAutoCleanInterval, + ), + ); + } }); // Clear data for containers with clearDataOnExit enabled diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart index 25649a70..5c25ece7 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart @@ -33,7 +33,6 @@ import 'package:weblibre/features/geckoview/features/find_in_page/domain/entitie import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; -import 'package:weblibre/presentation/widgets/non_focusable.dart'; import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart'; import 'package:weblibre/utils/ui_helper.dart' as ui_helper; diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart index 439fda2a..76984827 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart @@ -387,4 +387,15 @@ class TabDao extends DatabaseAccessor with $TabDaoMixin { ); } } + + Future> getUnassignedTabsOlderThan(DateTime threshold) { + final query = selectOnly(db.tab) + ..addColumns([db.tab.id]) + ..where( + db.tab.containerId.isNull() & + db.tab.timestamp.isSmallerThanValue(threshold), + ); + + return query.map((row) => row.read(db.tab.id)!).get(); + } } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart index 730cb67f..3b7b9f77 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -210,6 +210,19 @@ class TabDataRepository extends _$TabDataRepository { ); } + Future deleteUnassignedTabsOlderThan(DateTime threshold) async { + final tabIds = await ref + .read(tabDatabaseProvider) + .tabDao + .getUnassignedTabsOlderThan(threshold); + + if (tabIds.isNotEmpty) { + await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); + } + + return tabIds.length; + } + @override void build() {} } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 00681394..8a993036 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -41,7 +41,7 @@ final class TabDataRepositoryProvider } } -String _$tabDataRepositoryHash() => r'a01c38e94aad2145bd26aa0b8ff0bb1bcceb22f5'; +String _$tabDataRepositoryHash() => r'c3632e0a2d91811103b329659cb0c6d778b14cea'; abstract class _$TabDataRepository extends $Notifier { void build(); diff --git a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart index ae3d5988..f8e43f75 100644 --- a/app/lib/features/settings/presentation/screens/privacy_security_settings.dart +++ b/app/lib/features/settings/presentation/screens/privacy_security_settings.dart @@ -117,6 +117,7 @@ class _DataManagementSection extends StatelessWidget { SettingSection(name: 'Data Management'), _DeleteBrowsingDataTile(), _AutoClearHistorySection(), + _AutoClearUnassignedTabsSection(), ], ); } @@ -285,6 +286,7 @@ class _AutoClearHistorySection extends HookConsumerWidget { dropdownMenuEntries: const [ DropdownMenuEntry(value: Duration.zero, label: 'Never'), DropdownMenuEntry(value: Duration(days: 1), label: '1 Day'), + DropdownMenuEntry(value: Duration(days: 3), label: '3 Days'), DropdownMenuEntry(value: Duration(days: 7), label: '1 Week'), DropdownMenuEntry(value: Duration(days: 14), label: '2 Weeks'), DropdownMenuEntry(value: Duration(days: 30), label: '1 Month'), @@ -306,6 +308,68 @@ class _AutoClearHistorySection extends HookConsumerWidget { } } +class _AutoClearUnassignedTabsSection extends HookConsumerWidget { + const _AutoClearUnassignedTabsSection(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final unassignedTabsAutoCleanInterval = ref.watch( + generalSettingsWithDefaultsProvider.select( + (s) => s.unassignedTabsAutoCleanInterval, + ), + ); + + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Auto-Clear Unassigned Tabs'), + subtitle: Text( + 'Automatically close unassigned tabs older than the selected time period', + ), + leading: Icon(MdiIcons.tabRemove), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40.0), + child: DropdownMenu( + initialSelection: unassignedTabsAutoCleanInterval, + inputDecorationTheme: InputDecorationTheme( + prefixIconConstraints: BoxConstraints.tight( + const Size.square(24), + ), + ), + width: double.infinity, + dropdownMenuEntries: const [ + DropdownMenuEntry(value: Duration.zero, label: 'Never'), + DropdownMenuEntry(value: Duration(days: 1), label: '1 Day'), + DropdownMenuEntry(value: Duration(days: 3), label: '3 Days'), + DropdownMenuEntry(value: Duration(days: 7), label: '1 Week'), + DropdownMenuEntry(value: Duration(days: 14), label: '2 Weeks'), + DropdownMenuEntry(value: Duration(days: 30), label: '1 Month'), + DropdownMenuEntry(value: Duration(days: 90), label: '3 Months'), + ], + onSelected: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .unassignedTabsAutoCleanInterval( + value ?? Duration.zero, + ), + ); + }, + ), + ), + ], + ), + ); + } +} + class _GlobalPrivacyControlTile extends HookConsumerWidget { const _GlobalPrivacyControlTile(); diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 2eb9b89b..4dfa4a40 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -82,6 +82,7 @@ class GeneralSettings with FastEquatable { final QuickTabSwitcherMode quickTabSwitcherMode; final bool pullToRefreshEnabled; final bool doubleBackCloseTab; + final Duration unassignedTabsAutoCleanInterval; GeneralSettings({ required this.themeMode, @@ -106,6 +107,7 @@ class GeneralSettings with FastEquatable { required this.quickTabSwitcherMode, required this.pullToRefreshEnabled, required this.doubleBackCloseTab, + required this.unassignedTabsAutoCleanInterval, }); GeneralSettings.withDefaults({ @@ -131,6 +133,7 @@ class GeneralSettings with FastEquatable { QuickTabSwitcherMode? quickTabSwitcherMode, bool? pullToRefreshEnabled, bool? doubleBackCloseTab, + Duration? unassignedTabsAutoCleanInterval, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -155,7 +158,9 @@ class GeneralSettings with FastEquatable { quickTabSwitcherMode = quickTabSwitcherMode ?? QuickTabSwitcherMode.lastUsedTabs, pullToRefreshEnabled = pullToRefreshEnabled ?? true, - doubleBackCloseTab = doubleBackCloseTab ?? true; + doubleBackCloseTab = doubleBackCloseTab ?? true, + unassignedTabsAutoCleanInterval = + unassignedTabsAutoCleanInterval ?? Duration.zero; factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -186,5 +191,6 @@ class GeneralSettings with FastEquatable { quickTabSwitcherMode, pullToRefreshEnabled, doubleBackCloseTab, + unassignedTabsAutoCleanInterval, ]; } diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart index 5dce90dd..26bf0527 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -61,6 +61,10 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings doubleBackCloseTab(bool doubleBackCloseTab); + GeneralSettings unassignedTabsAutoCleanInterval( + Duration unassignedTabsAutoCleanInterval, + ); + /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// @@ -91,6 +95,7 @@ abstract class _$GeneralSettingsCWProxy { QuickTabSwitcherMode quickTabSwitcherMode, bool pullToRefreshEnabled, bool doubleBackCloseTab, + Duration unassignedTabsAutoCleanInterval, }); } @@ -193,6 +198,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings doubleBackCloseTab(bool doubleBackCloseTab) => call(doubleBackCloseTab: doubleBackCloseTab); + @override + GeneralSettings unassignedTabsAutoCleanInterval( + Duration unassignedTabsAutoCleanInterval, + ) => call(unassignedTabsAutoCleanInterval: unassignedTabsAutoCleanInterval); + @override /// Creates a new instance with the provided field values. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. @@ -224,6 +234,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? quickTabSwitcherMode = const $CopyWithPlaceholder(), Object? pullToRefreshEnabled = const $CopyWithPlaceholder(), Object? doubleBackCloseTab = const $CopyWithPlaceholder(), + Object? unassignedTabsAutoCleanInterval = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null @@ -354,6 +365,12 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.doubleBackCloseTab // ignore: cast_nullable_to_non_nullable : doubleBackCloseTab as bool, + unassignedTabsAutoCleanInterval: + unassignedTabsAutoCleanInterval == const $CopyWithPlaceholder() || + unassignedTabsAutoCleanInterval == null + ? _value.unassignedTabsAutoCleanInterval + // ignore: cast_nullable_to_non_nullable + : unassignedTabsAutoCleanInterval as Duration, ); } } @@ -420,6 +437,13 @@ GeneralSettings _$GeneralSettingsFromJson( ), pullToRefreshEnabled: json['pullToRefreshEnabled'] as bool?, doubleBackCloseTab: json['doubleBackCloseTab'] as bool?, + unassignedTabsAutoCleanInterval: + json['unassignedTabsAutoCleanInterval'] == null + ? null + : Duration( + microseconds: (json['unassignedTabsAutoCleanInterval'] as num) + .toInt(), + ), ); Map _$GeneralSettingsToJson( @@ -455,6 +479,8 @@ Map _$GeneralSettingsToJson( _$QuickTabSwitcherModeEnumMap[instance.quickTabSwitcherMode]!, 'pullToRefreshEnabled': instance.pullToRefreshEnabled, 'doubleBackCloseTab': instance.doubleBackCloseTab, + 'unassignedTabsAutoCleanInterval': + instance.unassignedTabsAutoCleanInterval.inMicroseconds, }; const _$ThemeModeEnumMap = { diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart index fab79928..4cbb806d 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -130,6 +130,11 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.bool, db.typeMapping, ), + 'unassignedTabsAutoCleanInterval': + settings['unassignedTabsAutoCleanInterval']?.readAs( + DriftSqlType.int, + db.typeMapping, + ), }); } diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart index 3af10422..e4a8cd71 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider } String _$generalSettingsRepositoryHash() => - r'ada58479ceea436f08e22780be200c99e3abedb7'; + r'08af8820ddd41036ae0d9531c87cf17c9c62db0a'; abstract class _$GeneralSettingsRepository extends $StreamNotifier {