add separate history download screen

This commit is contained in:
Fabian Freund
2026-02-21 10:59:56 +01:00
parent 7c14e7b3a3
commit f8d497f991
6 changed files with 372 additions and 115 deletions
+28
View File
@@ -1143,6 +1143,13 @@ RouteBase get $historyRoute => GoRouteData.$route(
path: '/history',
name: 'HistoryRoute',
factory: $HistoryRoute._fromState,
routes: [
GoRouteData.$route(
path: 'downloads',
name: 'HistoryDownloadsRoute',
factory: $HistoryDownloadsRoute._fromState,
),
],
);
mixin $HistoryRoute on GoRouteData {
@@ -1165,6 +1172,27 @@ mixin $HistoryRoute on GoRouteData {
void replace(BuildContext context) => context.replace(location);
}
mixin $HistoryDownloadsRoute on GoRouteData {
static HistoryDownloadsRoute _fromState(GoRouterState state) =>
const HistoryDownloadsRoute();
@override
String get location => GoRouteData.$location('/history/downloads');
@override
void go(BuildContext context) => context.go(location);
@override
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
@override
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
@override
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $profileListRoute => GoRouteData.$route(
path: '/profiles',
name: 'ProfileListRoute',
+19 -1
View File
@@ -19,7 +19,16 @@
*/
part of 'routes.dart';
@TypedGoRoute<HistoryRoute>(name: 'HistoryRoute', path: '/history')
@TypedGoRoute<HistoryRoute>(
name: 'HistoryRoute',
path: '/history',
routes: [
TypedGoRoute<HistoryDownloadsRoute>(
name: 'HistoryDownloadsRoute',
path: 'downloads',
),
],
)
class HistoryRoute extends GoRouteData with $HistoryRoute {
const HistoryRoute();
@@ -28,3 +37,12 @@ class HistoryRoute extends GoRouteData with $HistoryRoute {
return const HistoryScreen();
}
}
class HistoryDownloadsRoute extends GoRouteData with $HistoryDownloadsRoute {
const HistoryDownloadsRoute();
@override
Widget build(BuildContext context, GoRouterState state) {
return const HistoryScreen(mode: HistoryScreenMode.downloads);
}
}
@@ -54,12 +54,12 @@ class BrowserNavigationDrawer extends HookConsumerWidget {
return NavigationDrawer(
backgroundColor: colorScheme.surface,
header: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [_ProfileHeader(), _SyncTile(), const Divider()],
),
children: [
// Profile Header
_ProfileHeader(),
_SyncTile(),
const Divider(),
// Section 1: Tools & Configuration
_ExtensionsSection(),
@@ -107,6 +107,15 @@ class BrowserNavigationDrawer extends HookConsumerWidget {
},
),
ListTile(
leading: const Icon(MdiIcons.fileDownload),
title: const Text('Downloads'),
onTap: () async {
Navigator.of(context).pop();
await const HistoryDownloadsRoute().push(context);
},
),
ListTile(
leading: const Icon(MdiIcons.bookmarkMultiple),
title: const Text('Bookmarks'),
@@ -30,7 +30,7 @@ part 'providers.g.dart';
@Riverpod(keepAlive: true)
@JsonPersist()
class HistoryFilter extends _$HistoryFilter {
class HistoryVisitsFilter extends _$HistoryVisitsFilter {
void updateVisitType(VisitType type, bool value) {
if (value) {
state = state.copyWith.visitTypes({...state.visitTypes, type});
@@ -51,16 +51,54 @@ class HistoryFilter extends _$HistoryFilter {
HistoryFilterOptions build() {
persist(
ref.watch(riverpodDatabaseStorageProvider),
key: 'HistoryFilterOptions',
key: 'HistoryVisitsFilterOptions',
);
return stateOrNull ?? HistoryFilterOptions.withDefaults();
}
}
@Riverpod(keepAlive: true)
@JsonPersist()
class HistoryDownloadsFilter extends _$HistoryDownloadsFilter {
void reset() {
state = HistoryFilterOptions(
dateRange: null,
visitTypes: const {VisitType.download},
);
}
void setDateRange(DateTimeRange<DateTime>? range) {
state = state.copyWith.dateRange(range);
}
@override
HistoryFilterOptions build() {
persist(
ref.watch(riverpodDatabaseStorageProvider),
key: 'HistoryDownloadsFilterOptions',
);
return stateOrNull ??
HistoryFilterOptions(
dateRange: null,
visitTypes: const {VisitType.download},
);
}
}
@Riverpod()
Future<List<VisitInfo>> browsingHistory(Ref ref) {
final options = ref.watch(historyFilterProvider);
final options = ref.watch(historyVisitsFilterProvider);
return ref
.read(historyRepositoryProvider.notifier)
.getDetailedVisits(options);
}
@Riverpod()
Future<List<VisitInfo>> browsingDownloads(Ref ref) {
final options = ref.watch(historyDownloadsFilterProvider);
return ref
.read(historyRepositoryProvider.notifier)
@@ -9,30 +9,30 @@ part of 'providers.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(HistoryFilter)
@ProviderFor(HistoryVisitsFilter)
@JsonPersist()
final historyFilterProvider = HistoryFilterProvider._();
final historyVisitsFilterProvider = HistoryVisitsFilterProvider._();
@JsonPersist()
final class HistoryFilterProvider
extends $NotifierProvider<HistoryFilter, HistoryFilterOptions> {
HistoryFilterProvider._()
final class HistoryVisitsFilterProvider
extends $NotifierProvider<HistoryVisitsFilter, HistoryFilterOptions> {
HistoryVisitsFilterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'historyFilterProvider',
name: r'historyVisitsFilterProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$historyFilterHash();
String debugGetCreateSourceHash() => _$historyVisitsFilterHash();
@$internal
@override
HistoryFilter create() => HistoryFilter();
HistoryVisitsFilter create() => HistoryVisitsFilter();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(HistoryFilterOptions value) {
@@ -43,10 +43,69 @@ final class HistoryFilterProvider
}
}
String _$historyFilterHash() => r'685b92590011ec450ef0c19820e4a6750d26b79d';
String _$historyVisitsFilterHash() =>
r'a4bf6c41c9180166365084cb7c7981558ac7ad36';
@JsonPersist()
abstract class _$HistoryFilterBase extends $Notifier<HistoryFilterOptions> {
abstract class _$HistoryVisitsFilterBase
extends $Notifier<HistoryFilterOptions> {
HistoryFilterOptions build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<HistoryFilterOptions, HistoryFilterOptions>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<HistoryFilterOptions, HistoryFilterOptions>,
HistoryFilterOptions,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(HistoryDownloadsFilter)
@JsonPersist()
final historyDownloadsFilterProvider = HistoryDownloadsFilterProvider._();
@JsonPersist()
final class HistoryDownloadsFilterProvider
extends $NotifierProvider<HistoryDownloadsFilter, HistoryFilterOptions> {
HistoryDownloadsFilterProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'historyDownloadsFilterProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$historyDownloadsFilterHash();
@$internal
@override
HistoryDownloadsFilter create() => HistoryDownloadsFilter();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(HistoryFilterOptions value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<HistoryFilterOptions>(value),
);
}
}
String _$historyDownloadsFilterHash() =>
r'b44de5aced7dadba361fddf69b8462ff92252a99';
@JsonPersist()
abstract class _$HistoryDownloadsFilterBase
extends $Notifier<HistoryFilterOptions> {
HistoryFilterOptions build();
@$mustCallSuper
@override
@@ -101,17 +160,88 @@ final class BrowsingHistoryProvider
}
}
String _$browsingHistoryHash() => r'3b6ee5853387481d7544bc9733741a5519f84f40';
String _$browsingHistoryHash() => r'6f26228da28f6f67844551bf430025b161731bb2';
@ProviderFor(browsingDownloads)
final browsingDownloadsProvider = BrowsingDownloadsProvider._();
final class BrowsingDownloadsProvider
extends
$FunctionalProvider<
AsyncValue<List<VisitInfo>>,
List<VisitInfo>,
FutureOr<List<VisitInfo>>
>
with $FutureModifier<List<VisitInfo>>, $FutureProvider<List<VisitInfo>> {
BrowsingDownloadsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'browsingDownloadsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$browsingDownloadsHash();
@$internal
@override
$FutureProviderElement<List<VisitInfo>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<VisitInfo>> create(Ref ref) {
return browsingDownloads(ref);
}
}
String _$browsingDownloadsHash() => r'd05e3c079c6de349ff9e910ca597ad7c78f87a16';
// **************************************************************************
// JsonGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
abstract class _$HistoryFilter extends _$HistoryFilterBase {
abstract class _$HistoryVisitsFilter extends _$HistoryVisitsFilterBase {
/// The default key used by [persist].
String get key {
const resolvedKey = "HistoryFilter";
const resolvedKey = "HistoryVisitsFilter";
return resolvedKey;
}
/// A variant of [persist], for JSON-specific encoding.
///
/// You can override [key] to customize the key used for storage.
PersistResult persist(
FutureOr<Storage<String, String>> storage, {
String? key,
String Function(HistoryFilterOptions state)? encode,
HistoryFilterOptions Function(String encoded)? decode,
StorageOptions options = const StorageOptions(),
}) {
return NotifierPersistX(this).persist<String, String>(
storage,
key: key ?? this.key,
encode: encode ?? $jsonCodex.encode,
decode:
decode ??
(encoded) {
final e = $jsonCodex.decode(encoded);
return HistoryFilterOptions.fromJson(e as Map<String, Object?>);
},
options: options,
);
}
}
abstract class _$HistoryDownloadsFilter extends _$HistoryDownloadsFilterBase {
/// The default key used by [persist].
String get key {
const resolvedKey = "HistoryDownloadsFilter";
return resolvedKey;
}
@@ -57,6 +57,7 @@ class Section extends MultiSliver {
required Set<VisitInfo> selectedItems,
required void Function(VisitInfo) onTap,
required void Function(VisitInfo) onLongPress,
required Future<void> Function(VisitInfo) onDelete,
}) : super(
pushPinnedChildren: true,
children: [
@@ -103,37 +104,11 @@ class Section extends MultiSliver {
),
),
subtitle: UriBreadcrumb(uri: uri),
trailing: Consumer(
builder: (context, ref, _) {
return IconButton(
onPressed: () async {
await ref
.read(historyRepositoryProvider.notifier)
.deleteVisit(item);
final downloadedFile = item.title.mapNotNull(
(title) => File(title),
);
if (await downloadedFile?.exists() == true) {
if (context.mounted) {
final delete = await showDeleteFileDialog(
context,
downloadedFile.toString(),
);
if (delete?.delete == true) {
await downloadedFile!.delete();
}
}
}
// ignore: unused_result
await ref.refresh(browsingHistoryProvider.future);
},
icon: const Icon(MdiIcons.closeCircle),
);
trailing: IconButton(
onPressed: () async {
await onDelete(item);
},
icon: const Icon(MdiIcons.closeCircle),
),
onTap: () {
onTap(item);
@@ -196,21 +171,74 @@ class Section extends MultiSliver {
);
}
enum HistoryScreenMode { history, downloads }
class HistoryScreen extends HookConsumerWidget {
const HistoryScreen({super.key});
const HistoryScreen({super.key, this.mode = HistoryScreenMode.history});
final HistoryScreenMode mode;
@override
Widget build(BuildContext context, WidgetRef ref) {
final historyFilter = ref.watch(historyFilterProvider);
final isDownloadsMode = mode == HistoryScreenMode.downloads;
final textFilterEnabled = useState(false);
final textFilterController = useTextEditingController();
final menuController = useMenuController();
final historyEntries = ref.watch(browsingHistoryProvider);
final historyFilter = isDownloadsMode
? ref.watch(historyDownloadsFilterProvider)
: ref.watch(historyVisitsFilterProvider);
final historyEntries = isDownloadsMode
? ref.watch(browsingDownloadsProvider)
: ref.watch(browsingHistoryProvider);
final selectedItems = useState(<VisitInfo>{});
final defaultDownloadsFilter = HistoryFilterOptions(
dateRange: null,
visitTypes: const {VisitType.download},
);
final hasActiveFilter = isDownloadsMode
? historyFilter != defaultDownloadsFilter
: historyFilter != HistoryFilterOptions.withDefaults();
Future<void> refreshHistoryEntries() async {
if (isDownloadsMode) {
// ignore: unused_result
await ref.refresh(browsingDownloadsProvider.future);
} else {
// ignore: unused_result
await ref.refresh(browsingHistoryProvider.future);
}
}
void setDateRange(DateTimeRange<DateTime>? range) {
if (isDownloadsMode) {
ref.read(historyDownloadsFilterProvider.notifier).setDateRange(range);
} else {
ref.read(historyVisitsFilterProvider.notifier).setDateRange(range);
}
}
Future<void> deleteHistoryItem(VisitInfo item) async {
await ref.read(historyRepositoryProvider.notifier).deleteVisit(item);
final downloadedFile = item.title.mapNotNull((title) => File(title));
if (await downloadedFile?.exists() == true && context.mounted) {
final delete = await showDeleteFileDialog(
context,
downloadedFile.toString(),
);
if (delete?.delete == true) {
await downloadedFile!.delete();
}
}
await refreshHistoryEntries();
}
return Scaffold(
appBar: AppBar(
@@ -220,7 +248,9 @@ class HistoryScreen extends HookConsumerWidget {
decoration: InputDecoration(
contentPadding: const EdgeInsets.only(top: 12),
border: InputBorder.none,
hintText: 'Filter history...',
hintText: isDownloadsMode
? 'Filter downloads...'
: 'Filter history...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: IconButton(
onPressed: () {
@@ -235,7 +265,7 @@ class HistoryScreen extends HookConsumerWidget {
),
)
: selectedItems.value.isEmpty
? const Text('History')
? Text(isDownloadsMode ? 'Downloads' : 'History')
: Text('${selectedItems.value.length} selected'),
actions: [
if (selectedItems.value.isNotEmpty)
@@ -272,8 +302,7 @@ class HistoryScreen extends HookConsumerWidget {
}
selectedItems.value = {};
// ignore: unused_result
await ref.refresh(browsingHistoryProvider.future);
await refreshHistoryEntries();
},
icon: const Icon(Icons.delete),
)
@@ -282,11 +311,15 @@ class HistoryScreen extends HookConsumerWidget {
onPressed: () async {
await showDeleteDataDialog(
context,
initialSettings: {DeleteBrowsingDataType.history},
initialSettings: {
if (isDownloadsMode)
DeleteBrowsingDataType.downloads
else
DeleteBrowsingDataType.history,
},
);
// ignore: unused_result
await ref.refresh(browsingHistoryProvider.future);
await refreshHistoryEntries();
},
icon: const Icon(Icons.delete),
),
@@ -307,9 +340,7 @@ class HistoryScreen extends HookConsumerWidget {
trailingIcon: historyFilter.dateRange.mapNotNull(
(_) => IconButton(
onPressed: () {
ref
.read(historyFilterProvider.notifier)
.setDateRange(null);
setDateRange(null);
},
icon: const Icon(Icons.clear),
),
@@ -331,53 +362,52 @@ class HistoryScreen extends HookConsumerWidget {
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),
),
),
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.reload, VisitType.download}.map(
(type) => CheckboxMenuButton(
closeOnActivate: false,
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(
'VisitType.bookmark filter not implemented',
),
},
if (!isDownloadsMode) const Divider(),
if (!isDownloadsMode)
...{VisitType.link, VisitType.reload, VisitType.download}.map(
(type) => CheckboxMenuButton(
closeOnActivate: false,
value: historyFilter.visitTypes.contains(type),
onChanged: (value) {
if (value != null) {
ref
.read(historyVisitsFilterProvider.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(
'VisitType.bookmark filter not implemented',
),
},
),
),
),
const Divider(),
MenuItemButton(
leadingIcon: const Icon(MdiIcons.restore),
@@ -385,8 +415,11 @@ class HistoryScreen extends HookConsumerWidget {
onPressed: () {
textFilterController.clear();
textFilterEnabled.value = false;
ref.read(historyFilterProvider.notifier).reset();
if (isDownloadsMode) {
ref.read(historyDownloadsFilterProvider.notifier).reset();
} else {
ref.read(historyVisitsFilterProvider.notifier).reset();
}
},
),
],
@@ -399,8 +432,7 @@ class HistoryScreen extends HookConsumerWidget {
}
},
icon: Badge(
isLabelVisible:
historyFilter != HistoryFilterOptions.withDefaults(),
isLabelVisible: hasActiveFilter,
child: const Icon(MdiIcons.filter),
),
),
@@ -413,8 +445,7 @@ class HistoryScreen extends HookConsumerWidget {
data: (data) {
return RefreshIndicator(
onRefresh: () async {
// ignore: unused_result
await ref.refresh(browsingHistoryProvider.future);
await refreshHistoryEntries();
},
child: HookBuilder(
builder: (context) {
@@ -464,6 +495,7 @@ class HistoryScreen extends HookConsumerWidget {
items: value,
selectedItems: selectedItems.value,
onLongPress: toggleSelected,
onDelete: deleteHistoryItem,
onTap: (item) async {
if (selectedItems.value.isNotEmpty) {
toggleSelected(item);
@@ -492,7 +524,9 @@ class HistoryScreen extends HookConsumerWidget {
},
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load History',
title: isDownloadsMode
? 'Failed to load Downloads'
: 'Failed to load History',
exception: error,
),
),