From b01012c76c289a40363e58e2d0c1b3f684498a34 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Fri, 19 Sep 2025 10:27:38 +0200 Subject: [PATCH] finished implementing history with removal options --- .../presentation/dialogs/delete_data.dart | 60 +++++ .../widgets/browser_modules/browser_view.dart | 7 + .../entities/history_filter_options.dart | 18 +- .../entities/history_filter_options.g.dart | 26 +-- .../features/history/domain/providers.dart | 9 +- .../features/history/domain/providers.g.dart | 4 +- .../history/presentation/screens/history.dart | 219 +++++++++++++++--- .../screens/web_engine_settings.dart | 91 +++++++- .../user/data/models/general_settings.dart | 8 +- .../user/data/models/general_settings.g.dart | 19 ++ .../domain/repositories/general_settings.dart | 4 + .../repositories/general_settings.g.dart | 2 +- .../api/GeckoHistoryApiImpl.kt | 28 +++ .../pigeons/Gecko.g.kt | 42 ++++ .../src/domain/services/gecko_history.dart | 11 + .../lib/src/pigeons/gecko.g.dart | 46 ++++ .../pigeons/gecko.dart | 6 + 17 files changed, 525 insertions(+), 75 deletions(-) create mode 100644 app/lib/features/geckoview/features/browser/presentation/dialogs/delete_data.dart diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/delete_data.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/delete_data.dart new file mode 100644 index 00000000..c8d3ac80 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/delete_data.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:nullability/nullability.dart'; +import 'package:weblibre/features/geckoview/features/browser/domain/services/browser_data.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; + +class DeleteDataDialog extends HookConsumerWidget { + final Set initialSettings; + + const DeleteDataDialog({required this.initialSettings}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selections = useState(initialSettings); + + return SimpleDialog( + title: const Text('Delete Browsing Data'), + children: [ + for (final type in DeleteBrowsingDataType.values) + CheckboxListTile.adaptive( + value: selections.value.contains(type), + controlAffinity: ListTileControlAffinity.leading, + title: Text(type.title), + subtitle: type.description.mapNotNull( + (description) => Text(description), + ), + onChanged: (value) { + if (value == true) { + selections.value = {...selections.value, type}; + } else { + selections.value = {...selections.value}..remove(type); + } + }, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: FilledButton.icon( + onPressed: () async { + await ref + .read(browserDataServiceProvider.notifier) + .deleteData(selections.value); + + if (context.mounted) { + context.pop(); + } + }, + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.error, + foregroundColor: Theme.of(context).colorScheme.onError, + ), + label: const Text('Delete'), + icon: const Icon(Icons.delete_forever), + ), + ), + ], + ); + } +} 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 fffc31aa..45217b2a 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 @@ -92,6 +92,13 @@ class _BrowserViewState extends ConsumerState await ref .read(browserDataServiceProvider.notifier) .deleteDataOnEngineStart(settings.deleteBrowsingDataOnQuit); + + if (settings.historyAutoCleanInterval > Duration.zero) { + await GeckoHistoryService().deleteVisitsBetween( + DateTime(0), + DateTime.now().subtract(settings.historyAutoCleanInterval), + ); + } }); }); diff --git a/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.dart b/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.dart index 61f3cfa7..a37963e7 100644 --- a/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.dart +++ b/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.dart @@ -1,28 +1,20 @@ import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; part 'history_filter_options.g.dart'; @CopyWith() class HistoryFilterOptions with FastEquatable { - final DateTime? start; - final DateTime? end; + final DateTimeRange? dateRange; final Set visitTypes; - HistoryFilterOptions({ - required this.start, - required this.end, - required this.visitTypes, - }); + HistoryFilterOptions({required this.dateRange, required this.visitTypes}); HistoryFilterOptions.withDefaults() - : this( - start: null, - end: null, - visitTypes: {VisitType.link, VisitType.typed}, - ); + : this(dateRange: null, visitTypes: {VisitType.link, VisitType.typed}); @override - List get hashParameters => [start, end, visitTypes]; + List get hashParameters => [dateRange, visitTypes]; } diff --git a/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.g.dart b/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.g.dart index daae825c..a08b7190 100644 --- a/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.g.dart +++ b/app/lib/features/geckoview/features/history/domain/entities/history_filter_options.g.dart @@ -7,9 +7,7 @@ part of 'history_filter_options.dart'; // ************************************************************************** abstract class _$HistoryFilterOptionsCWProxy { - HistoryFilterOptions start(DateTime? start); - - HistoryFilterOptions end(DateTime? end); + HistoryFilterOptions dateRange(DateTimeRange? dateRange); HistoryFilterOptions visitTypes(Set visitTypes); @@ -20,8 +18,7 @@ abstract class _$HistoryFilterOptionsCWProxy { /// HistoryFilterOptions(...).copyWith(id: 12, name: "My name") /// ```` HistoryFilterOptions call({ - DateTime? start, - DateTime? end, + DateTimeRange? dateRange, Set visitTypes, }); } @@ -34,10 +31,8 @@ class _$HistoryFilterOptionsCWProxyImpl final HistoryFilterOptions _value; @override - HistoryFilterOptions start(DateTime? start) => this(start: start); - - @override - HistoryFilterOptions end(DateTime? end) => this(end: end); + HistoryFilterOptions dateRange(DateTimeRange? dateRange) => + this(dateRange: dateRange); @override HistoryFilterOptions visitTypes(Set visitTypes) => @@ -51,19 +46,14 @@ class _$HistoryFilterOptionsCWProxyImpl /// HistoryFilterOptions(...).copyWith(id: 12, name: "My name") /// ```` HistoryFilterOptions call({ - Object? start = const $CopyWithPlaceholder(), - Object? end = const $CopyWithPlaceholder(), + Object? dateRange = const $CopyWithPlaceholder(), Object? visitTypes = const $CopyWithPlaceholder(), }) { return HistoryFilterOptions( - start: start == const $CopyWithPlaceholder() - ? _value.start + dateRange: dateRange == const $CopyWithPlaceholder() + ? _value.dateRange // ignore: cast_nullable_to_non_nullable - : start as DateTime?, - end: end == const $CopyWithPlaceholder() - ? _value.end - // ignore: cast_nullable_to_non_nullable - : end as DateTime?, + : dateRange as DateTimeRange?, visitTypes: visitTypes == const $CopyWithPlaceholder() ? _value.visitTypes // ignore: cast_nullable_to_non_nullable diff --git a/app/lib/features/geckoview/features/history/domain/providers.dart b/app/lib/features/geckoview/features/history/domain/providers.dart index 3e481559..7890a080 100644 --- a/app/lib/features/geckoview/features/history/domain/providers.dart +++ b/app/lib/features/geckoview/features/history/domain/providers.dart @@ -17,6 +17,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ +import 'package:flutter/material.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -34,6 +35,10 @@ class HistoryFilter extends _$HistoryFilter { } } + void setDateRange(DateTimeRange? range) { + state = state.copyWith.dateRange(range); + } + @override HistoryFilterOptions build() { return HistoryFilterOptions.withDefaults(); @@ -47,8 +52,8 @@ Future> browsingHistory(Ref ref) { final service = GeckoHistoryService(); return service .getDetailedVisits( - options.start ?? DateTime(0), - options.end ?? DateTime(9999), + options.dateRange?.start ?? DateTime(0), + options.dateRange?.end ?? DateTime(9999), options.visitTypes, ) .then( diff --git a/app/lib/features/geckoview/features/history/domain/providers.g.dart b/app/lib/features/geckoview/features/history/domain/providers.g.dart index c2269713..cfd16f05 100644 --- a/app/lib/features/geckoview/features/history/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/history/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$browsingHistoryHash() => r'a684c34fc370474a771c5fcc7982a200de2294e5'; +String _$browsingHistoryHash() => r'6447fd4d8209c3befb07f2d82fa41a24bd3accb1'; /// See also [browsingHistory]. @ProviderFor(browsingHistory) @@ -24,7 +24,7 @@ final browsingHistoryProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef BrowsingHistoryRef = AutoDisposeFutureProviderRef>; -String _$historyFilterHash() => r'271d60ce79f96ac49d450f01d21cbfdad684ce53'; +String _$historyFilterHash() => r'68a604fe857476f7c15be89352a7b10bc456c3d6'; /// See also [HistoryFilter]. @ProviderFor(HistoryFilter) diff --git a/app/lib/features/geckoview/features/history/presentation/screens/history.dart b/app/lib/features/geckoview/features/history/presentation/screens/history.dart index 23866edc..1dae046f 100644 --- a/app/lib/features/geckoview/features/history/presentation/screens/history.dart +++ b/app/lib/features/geckoview/features/history/presentation/screens/history.dart @@ -2,27 +2,34 @@ import 'package:collection/collection.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:nullability/nullability.dart'; import 'package:sliver_tools/sliver_tools.dart'; import 'package:timeago/timeago.dart' as timeago; +import 'package:weblibre/features/geckoview/domain/repositories/tab.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart'; import 'package:weblibre/features/geckoview/features/history/domain/providers.dart'; +import 'package:weblibre/features/user/data/models/general_settings.dart'; import 'package:weblibre/presentation/hooks/menu_controller.dart'; import 'package:weblibre/presentation/widgets/failure_widget.dart'; import 'package:weblibre/presentation/widgets/url_icon.dart'; class Section extends MultiSliver { - static final _datePattern = DateFormat('yMMMMd').addPattern('Hm'); + static final _datePattern = DateFormat.MMMd().addPattern('Hm'); Section({ - Key? key, + super.key, required BuildContext context, required String title, required List items, + required Set selectedItems, + required void Function(VisitInfo) onTap, + required void Function(VisitInfo) onLongPress, }) : super( - key: key, pushPinnedChildren: true, children: [ SliverPinnedHeader( @@ -50,16 +57,40 @@ class Section extends MultiSliver { crossAxisAlignment: CrossAxisAlignment.start, children: [ ListTile( - leading: UrlIcon([Uri.parse(item.url)], iconSize: 24), + leading: selectedItems.contains(item) + ? const CircleAvatar( + radius: 12, + child: Icon(Icons.check, size: 12), + ) + : UrlIcon([Uri.parse(item.url)], iconSize: 24), title: item.title.mapNotNull((title) => Text(title)), subtitle: Text( item.url, maxLines: 2, overflow: TextOverflow.ellipsis, ), + trailing: Consumer( + builder: (context, ref, _) { + return IconButton( + onPressed: () async { + final service = GeckoHistoryService(); + await service.deleteVisit(item); + // ignore: unused_result + await ref.refresh(browsingHistoryProvider.future); + }, + icon: const Icon(MdiIcons.closeCircle), + ); + }, + ), + onTap: () { + onTap(item); + }, + onLongPress: () { + onLongPress(item); + }, ), Padding( - padding: const EdgeInsets.only(left: 54), + padding: const EdgeInsets.only(left: 54, right: 16), child: Wrap( spacing: 8.0, children: [ @@ -113,43 +144,126 @@ class HistoryScreen extends HookConsumerWidget { final historyEntries = ref.watch(browsingHistoryProvider); + final selectedItems = useState({}); + return Scaffold( appBar: AppBar( + title: selectedItems.value.isEmpty + ? const Text('History') + : Text('${selectedItems.value.length} selected'), actions: [ + if (selectedItems.value.isNotEmpty) + IconButton( + onPressed: () async { + for (final item in selectedItems.value) { + final service = GeckoHistoryService(); + await service.deleteVisit(item); + } + + selectedItems.value = {}; + // ignore: unused_result + await ref.refresh(browsingHistoryProvider.future); + }, + icon: const Icon(Icons.delete), + ) + else + IconButton( + onPressed: () async { + await showDialog( + context: context, + builder: (context) { + return const DeleteDataDialog( + initialSettings: {DeleteBrowsingDataType.history}, + ); + }, + ); + + // ignore: unused_result + await ref.refresh(browsingHistoryProvider.future); + }, + icon: const Icon(Icons.delete), + ), MenuAnchor( controller: menuController, menuChildren: [ - ...VisitType.values - .whereNot( - (element) => const {VisitType.bookmark}.contains(element), - ) - .map( - (type) => CheckboxMenuButton( - value: historyFilter.visitTypes.contains(type), - onChanged: (value) { - if (value != null) { - ref - .read(historyFilterProvider.notifier) - .updateVisitType(type, value); - } - }, - child: switch (type) { - VisitType.link => const Text('Followed Links'), - VisitType.typed => const Text('Typed Addresses'), - VisitType.embed => const Text('Embedded Page Elements'), - VisitType.redirectPermanent => const Text( - 'Temporary Redirects', - ), - VisitType.redirectTemporary => const Text( - 'Permanent Redirects', - ), - VisitType.download => const Text('Downloads'), - VisitType.framedLink => const Text('Frames'), - VisitType.reload => const Text('Page Reloads'), - VisitType.bookmark => throw UnimplementedError(), - }, - ), + MenuItemButton( + leadingIcon: const Icon(MdiIcons.calendarRange), + trailingIcon: historyFilter.dateRange.mapNotNull( + (_) => IconButton( + onPressed: () { + ref + .read(historyFilterProvider.notifier) + .setDateRange(null); + }, + icon: const Icon(Icons.clear), ), + ), + child: + historyFilter.dateRange.mapNotNull( + (range) => Text( + '${DateFormat.yMd().format(range.start)} - ${DateFormat.yMd().format(range.end)}', + ), + ) ?? + const Text('Filter Date'), + onPressed: () async { + final range = await showDateRangePicker( + context: context, + initialDateRange: historyFilter.dateRange, + firstDate: DateTime.now().subtract( + const Duration(days: 365), + ), + lastDate: DateTime.now(), + ); + + ref + .read(historyFilterProvider.notifier) + .setDateRange( + range.mapNotNull( + (range) => DateTimeRange( + start: range.start, + //Make sure to include last day fully + end: range.end.add( + const Duration(days: 1) - + const Duration(milliseconds: 1), + ), + ), + ), + ); + }, + ), + const Divider(), + ...{ + VisitType.link, + VisitType.typed, + VisitType.reload, + VisitType.download, + }.map( + (type) => CheckboxMenuButton( + value: historyFilter.visitTypes.contains(type), + onChanged: (value) { + if (value != null) { + ref + .read(historyFilterProvider.notifier) + .updateVisitType(type, value); + } + }, + child: switch (type) { + VisitType.link => const Text('Followed Links'), + VisitType.typed => const Text('Typed Addresses'), + VisitType.embed => const Text('Embedded Page Elements'), + VisitType.redirectPermanent => const Text( + 'Temporary Redirects', + ), + VisitType.redirectTemporary => const Text( + 'Permanent Redirects', + ), + VisitType.download => const Text('Downloads'), + VisitType.framedLink => const Text('Frames'), + VisitType.reload => const Text('Page Reloads'), + VisitType.bookmark => throw UnimplementedError(), + }, + ), + ), ], child: IconButton( onPressed: () { @@ -159,7 +273,7 @@ class HistoryScreen extends HookConsumerWidget { menuController.open(); } }, - icon: const Icon(Icons.more_vert), + icon: const Icon(MdiIcons.filter), ), ), ], @@ -182,10 +296,41 @@ class HistoryScreen extends HookConsumerWidget { [EquatableValue(data)], ); + void toggleSelected(VisitInfo item) { + if (selectedItems.value.contains(item)) { + selectedItems.value = {...selectedItems.value} + ..remove(item); + } else { + selectedItems.value = {...selectedItems.value, item}; + } + } + return CustomScrollView( slivers: [ for (final MapEntry(:key, :value) in groups.entries) - Section(context: context, title: key, items: value), + Section( + context: context, + title: key, + items: value, + selectedItems: selectedItems.value, + onLongPress: toggleSelected, + onTap: (item) async { + if (selectedItems.value.isNotEmpty) { + toggleSelected(item); + } else { + await ref + .read(tabRepositoryProvider.notifier) + .addTab( + url: Uri.parse(item.url), + private: false, + ); + + if (context.mounted) { + context.pop(); + } + } + }, + ), ], ); }, diff --git a/app/lib/features/settings/presentation/screens/web_engine_settings.dart b/app/lib/features/settings/presentation/screens/web_engine_settings.dart index 5d82cb2b..e3ae63ac 100644 --- a/app/lib/features/settings/presentation/screens/web_engine_settings.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_settings.dart @@ -24,6 +24,7 @@ import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:nullability/nullability.dart'; import 'package:weblibre/core/routing/routes.dart'; +import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/delete_data.dart'; import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart'; import 'package:weblibre/features/user/data/models/engine_settings.dart'; import 'package:weblibre/features/user/data/models/general_settings.dart'; @@ -49,7 +50,7 @@ class WebEngineSettingsScreen extends HookConsumerWidget { SwitchListTile.adaptive( title: const Text('Incognito Mode'), subtitle: const Text( - 'Deletes all browsing data upon app restart for enhanced privacy.', + 'Deletes selected browsing data upon app restart for enhanced privacy.', ), secondary: const Icon(MdiIcons.incognito), value: generalSettings.deleteBrowsingDataOnQuit != null, @@ -114,6 +115,94 @@ class WebEngineSettingsScreen extends HookConsumerWidget { ], ), ), + ListTile( + title: const Text('Delete Browsing Data'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.delete), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await showDialog( + context: context, + builder: (context) { + return const DeleteDataDialog(initialSettings: {}); + }, + ); + }, + ), + 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 History'), + subtitle: Text( + 'Automatically delete browsing history older than the selected time period', + ), + leading: Icon(MdiIcons.deleteClock), + contentPadding: EdgeInsets.zero, + ), + Padding( + padding: const EdgeInsets.only(left: 40.0), + child: DropdownMenu( + initialSelection: + generalSettings.historyAutoCleanInterval, + 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: 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 + .historyAutoCleanInterval( + value ?? Duration.zero, + ), + ); + }, + ), + ), + ], + ), + ), SwitchListTile.adaptive( title: const Text('Global Privacy Control (GPC)'), secondary: const Icon(MdiIcons.incognitoCircleOff), diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart index 3e5dde05..5e0fa23f 100644 --- a/app/lib/features/user/data/models/general_settings.dart +++ b/app/lib/features/user/data/models/general_settings.dart @@ -63,6 +63,7 @@ class GeneralSettings with FastEquatable { final TabIntentOpenSetting tabIntentOpenSetting; final bool autoHideTabBar; final TabBarSwipeAction tabBarSwipeAction; + final Duration historyAutoCleanInterval; GeneralSettings({ required this.themeMode, @@ -78,6 +79,7 @@ class GeneralSettings with FastEquatable { required this.tabIntentOpenSetting, required this.autoHideTabBar, required this.tabBarSwipeAction, + required this.historyAutoCleanInterval, }); GeneralSettings.withDefaults({ @@ -94,6 +96,7 @@ class GeneralSettings with FastEquatable { TabIntentOpenSetting? tabIntentOpenSetting, bool? autoHideTabBar, TabBarSwipeAction? tabBarSwipeAction, + Duration? historyAutoCleanInterval, }) : themeMode = themeMode ?? ThemeMode.dark, enableReadability = enableReadability ?? true, enforceReadability = enforceReadability ?? false, @@ -107,7 +110,9 @@ class GeneralSettings with FastEquatable { tabIntentOpenSetting = tabIntentOpenSetting ?? TabIntentOpenSetting.ask, autoHideTabBar = autoHideTabBar ?? true, tabBarSwipeAction = - tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened; + tabBarSwipeAction ?? TabBarSwipeAction.switchLastOpened, + historyAutoCleanInterval = + historyAutoCleanInterval ?? const Duration(days: 90); factory GeneralSettings.fromJson(Map json) => _$GeneralSettingsFromJson(json); @@ -129,5 +134,6 @@ class GeneralSettings with FastEquatable { tabIntentOpenSetting, autoHideTabBar, tabBarSwipeAction, + historyAutoCleanInterval, ]; } 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 8e30456e..bff6fae5 100644 --- a/app/lib/features/user/data/models/general_settings.g.dart +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -39,6 +39,8 @@ abstract class _$GeneralSettingsCWProxy { GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction); + GeneralSettings historyAutoCleanInterval(Duration historyAutoCleanInterval); + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// /// Usage @@ -59,6 +61,7 @@ abstract class _$GeneralSettingsCWProxy { TabIntentOpenSetting tabIntentOpenSetting, bool autoHideTabBar, TabBarSwipeAction tabBarSwipeAction, + Duration historyAutoCleanInterval, }); } @@ -122,6 +125,10 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { GeneralSettings tabBarSwipeAction(TabBarSwipeAction tabBarSwipeAction) => this(tabBarSwipeAction: tabBarSwipeAction); + @override + GeneralSettings historyAutoCleanInterval(Duration historyAutoCleanInterval) => + this(historyAutoCleanInterval: historyAutoCleanInterval); + @override /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. /// @@ -143,6 +150,7 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { Object? tabIntentOpenSetting = const $CopyWithPlaceholder(), Object? autoHideTabBar = const $CopyWithPlaceholder(), Object? tabBarSwipeAction = const $CopyWithPlaceholder(), + Object? historyAutoCleanInterval = const $CopyWithPlaceholder(), }) { return GeneralSettings( themeMode: themeMode == const $CopyWithPlaceholder() @@ -203,6 +211,11 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { ? _value.tabBarSwipeAction // ignore: cast_nullable_to_non_nullable : tabBarSwipeAction as TabBarSwipeAction, + historyAutoCleanInterval: + historyAutoCleanInterval == const $CopyWithPlaceholder() + ? _value.historyAutoCleanInterval + // ignore: cast_nullable_to_non_nullable + : historyAutoCleanInterval as Duration, ); } } @@ -247,6 +260,11 @@ GeneralSettings _$GeneralSettingsFromJson(Map json) => _$TabBarSwipeActionEnumMap, json['tabBarSwipeAction'], ), + historyAutoCleanInterval: json['historyAutoCleanInterval'] == null + ? null + : Duration( + microseconds: (json['historyAutoCleanInterval'] as num).toInt(), + ), ); Map _$GeneralSettingsToJson( @@ -270,6 +288,7 @@ Map _$GeneralSettingsToJson( _$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!, 'autoHideTabBar': instance.autoHideTabBar, 'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!, + 'historyAutoCleanInterval': instance.historyAutoCleanInterval.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 d8663881..3760fc87 100644 --- a/app/lib/features/user/domain/repositories/general_settings.dart +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -96,6 +96,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository { DriftSqlType.string, db.typeMapping, ), + 'historyAutoCleanInterval': settings['historyAutoCleanInterval']?.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 db5a7d59..17df857e 100644 --- a/app/lib/features/user/domain/repositories/general_settings.g.dart +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider = typedef GeneralSettingsWithDefaultsRef = AutoDisposeProviderRef; String _$generalSettingsRepositoryHash() => - r'0d207ae4ae9fd94b1257eb8aa4cfd73d4a47caf2'; + r'3e2a07b8956094cd9376a254d9aedaa80a3c45c4'; /// See also [GeneralSettingsRepository]. @ProviderFor(GeneralSettingsRepository) diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoHistoryApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoHistoryApiImpl.kt index 269507c0..fa6f6122 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoHistoryApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/api/GeckoHistoryApiImpl.kt @@ -69,4 +69,32 @@ class GeckoHistoryApiImpl() : GeckoHistoryApi { } } } + + override fun deleteVisit( + url: String, + timestamp: Long, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.historyStorage.deleteVisit(url, timestamp); + + callback(Result.success(Unit)) + } + } + } + + override fun deleteVisitsBetween( + startMillis: Long, + endMillis: Long, + callback: (Result) -> Unit + ) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.historyStorage.deleteVisitsBetween(startMillis, endMillis); + + callback(Result.success(Unit)) + } + } + } } \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt index 95862c34..867aa4ff 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/weblibre/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -4750,6 +4750,8 @@ interface GeckoDeleteBrowsingDataController { /** Generated interface from Pigeon that represents a handler of messages from Flutter. */ interface GeckoHistoryApi { fun getDetailedVisits(startMillis: Long, endMillis: Long, excludeTypes: List, callback: (Result>) -> Unit) + fun deleteVisit(url: String, timestamp: Long, callback: (Result) -> Unit) + fun deleteVisitsBetween(startMillis: Long, endMillis: Long, callback: (Result) -> Unit) companion object { /** The codec used by GeckoHistoryApi. */ @@ -4782,6 +4784,46 @@ interface GeckoHistoryApi { channel.setMessageHandler(null) } } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val urlArg = args[0] as String + val timestampArg = args[1] as Long + api.deleteVisit(urlArg, timestampArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + reply.reply(GeckoPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { message, reply -> + val args = message as List + val startMillisArg = args[0] as Long + val endMillisArg = args[1] as Long + api.deleteVisitsBetween(startMillisArg, endMillisArg) { result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(GeckoPigeonUtils.wrapError(error)) + } else { + reply.reply(GeckoPigeonUtils.wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } } } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart index 01f1a383..2df11baa 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_history.dart @@ -24,4 +24,15 @@ class GeckoHistoryService { VisitType.values.toSet().difference(types).toList(), ); } + + Future deleteVisit(VisitInfo info) { + return _api.deleteVisit(info.url, info.visitTime); + } + + Future deleteVisitsBetween(DateTime start, DateTime end) { + return _api.deleteVisitsBetween( + start.millisecondsSinceEpoch, + end.millisecondsSinceEpoch, + ); + } } diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index e3d97a0e..205ee1a3 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -5731,6 +5731,52 @@ class GeckoHistoryApi { return (pigeonVar_replyList[0] as List?)!.cast(); } } + + Future deleteVisit(String url, int timestamp) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([url, timestamp]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } + + Future deleteVisitsBetween(int startMillis, int endMillis) async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final Future pigeonVar_sendFuture = pigeonVar_channel.send([startMillis, endMillis]); + final List? pigeonVar_replyList = + await pigeonVar_sendFuture as List?; + if (pigeonVar_replyList == null) { + throw _createConnectionError(pigeonVar_channelName); + } else if (pigeonVar_replyList.length > 1) { + throw PlatformException( + code: pigeonVar_replyList[0]! as String, + message: pigeonVar_replyList[1] as String?, + details: pigeonVar_replyList[2], + ); + } else { + return; + } + } } class GeckoDownloadsApi { diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index f66a11b4..6287ebd4 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -1248,6 +1248,12 @@ abstract class GeckoHistoryApi { int endMillis, List excludeTypes, ); + + @async + void deleteVisit(String url, int timestamp); + + @async + void deleteVisitsBetween(int startMillis, int endMillis); } @HostApi()