finished implementing history with removal options
This commit is contained in:
@@ -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<DeleteBrowsingDataType> 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+7
@@ -92,6 +92,13 @@ class _BrowserViewState extends ConsumerState<BrowserView>
|
||||
await ref
|
||||
.read(browserDataServiceProvider.notifier)
|
||||
.deleteDataOnEngineStart(settings.deleteBrowsingDataOnQuit);
|
||||
|
||||
if (settings.historyAutoCleanInterval > Duration.zero) {
|
||||
await GeckoHistoryService().deleteVisitsBetween(
|
||||
DateTime(0),
|
||||
DateTime.now().subtract(settings.historyAutoCleanInterval),
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+5
-13
@@ -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<DateTime>? dateRange;
|
||||
final Set<VisitType> 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<Object?> get hashParameters => [start, end, visitTypes];
|
||||
List<Object?> get hashParameters => [dateRange, visitTypes];
|
||||
}
|
||||
|
||||
+8
-18
@@ -7,9 +7,7 @@ part of 'history_filter_options.dart';
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$HistoryFilterOptionsCWProxy {
|
||||
HistoryFilterOptions start(DateTime? start);
|
||||
|
||||
HistoryFilterOptions end(DateTime? end);
|
||||
HistoryFilterOptions dateRange(DateTimeRange<DateTime>? dateRange);
|
||||
|
||||
HistoryFilterOptions visitTypes(Set<VisitType> visitTypes);
|
||||
|
||||
@@ -20,8 +18,7 @@ abstract class _$HistoryFilterOptionsCWProxy {
|
||||
/// HistoryFilterOptions(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
HistoryFilterOptions call({
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
DateTimeRange<DateTime>? dateRange,
|
||||
Set<VisitType> 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<DateTime>? dateRange) =>
|
||||
this(dateRange: dateRange);
|
||||
|
||||
@override
|
||||
HistoryFilterOptions visitTypes(Set<VisitType> 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<DateTime>?,
|
||||
visitTypes: visitTypes == const $CopyWithPlaceholder()
|
||||
? _value.visitTypes
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
* 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: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<DateTime>? range) {
|
||||
state = state.copyWith.dateRange(range);
|
||||
}
|
||||
|
||||
@override
|
||||
HistoryFilterOptions build() {
|
||||
return HistoryFilterOptions.withDefaults();
|
||||
@@ -47,8 +52,8 @@ Future<List<VisitInfo>> 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(
|
||||
|
||||
@@ -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<List<VisitInfo>>;
|
||||
String _$historyFilterHash() => r'271d60ce79f96ac49d450f01d21cbfdad684ce53';
|
||||
String _$historyFilterHash() => r'68a604fe857476f7c15be89352a7b10bc456c3d6';
|
||||
|
||||
/// See also [HistoryFilter].
|
||||
@ProviderFor(HistoryFilter)
|
||||
|
||||
@@ -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<VisitInfo> items,
|
||||
required Set<VisitInfo> 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(<VisitInfo>{});
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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<String, dynamic> json) =>
|
||||
_$GeneralSettingsFromJson(json);
|
||||
@@ -129,5 +134,6 @@ class GeneralSettings with FastEquatable {
|
||||
tabIntentOpenSetting,
|
||||
autoHideTabBar,
|
||||
tabBarSwipeAction,
|
||||
historyAutoCleanInterval,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> json) =>
|
||||
_$TabBarSwipeActionEnumMap,
|
||||
json['tabBarSwipeAction'],
|
||||
),
|
||||
historyAutoCleanInterval: json['historyAutoCleanInterval'] == null
|
||||
? null
|
||||
: Duration(
|
||||
microseconds: (json['historyAutoCleanInterval'] as num).toInt(),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GeneralSettingsToJson(
|
||||
@@ -270,6 +288,7 @@ Map<String, dynamic> _$GeneralSettingsToJson(
|
||||
_$TabIntentOpenSettingEnumMap[instance.tabIntentOpenSetting]!,
|
||||
'autoHideTabBar': instance.autoHideTabBar,
|
||||
'tabBarSwipeAction': _$TabBarSwipeActionEnumMap[instance.tabBarSwipeAction]!,
|
||||
'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds,
|
||||
};
|
||||
|
||||
const _$ThemeModeEnumMap = {
|
||||
|
||||
@@ -96,6 +96,10 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
|
||||
DriftSqlType.string,
|
||||
db.typeMapping,
|
||||
),
|
||||
'historyAutoCleanInterval': settings['historyAutoCleanInterval']?.readAs(
|
||||
DriftSqlType.int,
|
||||
db.typeMapping,
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ final generalSettingsWithDefaultsProvider =
|
||||
typedef GeneralSettingsWithDefaultsRef =
|
||||
AutoDisposeProviderRef<GeneralSettings>;
|
||||
String _$generalSettingsRepositoryHash() =>
|
||||
r'0d207ae4ae9fd94b1257eb8aa4cfd73d4a47caf2';
|
||||
r'3e2a07b8956094cd9376a254d9aedaa80a3c45c4';
|
||||
|
||||
/// See also [GeneralSettingsRepository].
|
||||
@ProviderFor(GeneralSettingsRepository)
|
||||
|
||||
+28
@@ -69,4 +69,32 @@ class GeckoHistoryApiImpl() : GeckoHistoryApi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun deleteVisit(
|
||||
url: String,
|
||||
timestamp: Long,
|
||||
callback: (Result<Unit>) -> 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>) -> Unit
|
||||
) {
|
||||
coroutineScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
components.core.historyStorage.deleteVisitsBetween(startMillis, endMillis);
|
||||
|
||||
callback(Result.success(Unit))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+42
@@ -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<VisitType>, callback: (Result<List<VisitInfo>>) -> Unit)
|
||||
fun deleteVisit(url: String, timestamp: Long, callback: (Result<Unit>) -> Unit)
|
||||
fun deleteVisitsBetween(startMillis: Long, endMillis: Long, callback: (Result<Unit>) -> Unit)
|
||||
|
||||
companion object {
|
||||
/** The codec used by GeckoHistoryApi. */
|
||||
@@ -4782,6 +4784,46 @@ interface GeckoHistoryApi {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
run {
|
||||
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val urlArg = args[0] as String
|
||||
val timestampArg = args[1] as Long
|
||||
api.deleteVisit(urlArg, timestampArg) { result: Result<Unit> ->
|
||||
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<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$separatedMessageChannelSuffix", codec)
|
||||
if (api != null) {
|
||||
channel.setMessageHandler { message, reply ->
|
||||
val args = message as List<Any?>
|
||||
val startMillisArg = args[0] as Long
|
||||
val endMillisArg = args[1] as Long
|
||||
api.deleteVisitsBetween(startMillisArg, endMillisArg) { result: Result<Unit> ->
|
||||
val error = result.exceptionOrNull()
|
||||
if (error != null) {
|
||||
reply.reply(GeckoPigeonUtils.wrapError(error))
|
||||
} else {
|
||||
reply.reply(GeckoPigeonUtils.wrapResult(null))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
channel.setMessageHandler(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,4 +24,15 @@ class GeckoHistoryService {
|
||||
VisitType.values.toSet().difference(types).toList(),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteVisit(VisitInfo info) {
|
||||
return _api.deleteVisit(info.url, info.visitTime);
|
||||
}
|
||||
|
||||
Future<void> deleteVisitsBetween(DateTime start, DateTime end) {
|
||||
return _api.deleteVisitsBetween(
|
||||
start.millisecondsSinceEpoch,
|
||||
end.millisecondsSinceEpoch,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5731,6 +5731,52 @@ class GeckoHistoryApi {
|
||||
return (pigeonVar_replyList[0] as List<Object?>?)!.cast<VisitInfo>();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteVisit(String url, int timestamp) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisit$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[url, timestamp]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
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<void> deleteVisitsBetween(int startMillis, int endMillis) async {
|
||||
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoHistoryApi.deleteVisitsBetween$pigeonVar_messageChannelSuffix';
|
||||
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||
pigeonVar_channelName,
|
||||
pigeonChannelCodec,
|
||||
binaryMessenger: pigeonVar_binaryMessenger,
|
||||
);
|
||||
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[startMillis, endMillis]);
|
||||
final List<Object?>? pigeonVar_replyList =
|
||||
await pigeonVar_sendFuture as List<Object?>?;
|
||||
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 {
|
||||
|
||||
@@ -1248,6 +1248,12 @@ abstract class GeckoHistoryApi {
|
||||
int endMillis,
|
||||
List<VisitType> excludeTypes,
|
||||
);
|
||||
|
||||
@async
|
||||
void deleteVisit(String url, int timestamp);
|
||||
|
||||
@async
|
||||
void deleteVisitsBetween(int startMillis, int endMillis);
|
||||
}
|
||||
|
||||
@HostApi()
|
||||
|
||||
Reference in New Issue
Block a user