prepare for multiple apps
This commit is contained in:
+48
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:weblibre/data/database/converters/date_time_range.dart';
|
||||
|
||||
part 'history_filter_options.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class HistoryFilterOptions with FastEquatable {
|
||||
@DateTimeRangeConverter()
|
||||
final DateTimeRange<DateTime>? dateRange;
|
||||
final Set<VisitType> visitTypes;
|
||||
|
||||
HistoryFilterOptions({required this.dateRange, required this.visitTypes});
|
||||
|
||||
HistoryFilterOptions.withDefaults()
|
||||
: this(dateRange: null, visitTypes: {VisitType.link});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [dateRange, visitTypes];
|
||||
|
||||
factory HistoryFilterOptions.fromJson(Map<String, dynamic> json) =>
|
||||
_$HistoryFilterOptionsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$HistoryFilterOptionsToJson(this);
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'history_filter_options.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$HistoryFilterOptionsCWProxy {
|
||||
HistoryFilterOptions dateRange(DateTimeRange<DateTime>? dateRange);
|
||||
|
||||
HistoryFilterOptions visitTypes(Set<VisitType> visitTypes);
|
||||
|
||||
/// 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 `HistoryFilterOptions(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// HistoryFilterOptions(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
HistoryFilterOptions call({
|
||||
DateTimeRange<DateTime>? dateRange,
|
||||
Set<VisitType> visitTypes,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfHistoryFilterOptions.copyWith(...)` or call `instanceOfHistoryFilterOptions.copyWith.fieldName(value)` for a single field.
|
||||
class _$HistoryFilterOptionsCWProxyImpl
|
||||
implements _$HistoryFilterOptionsCWProxy {
|
||||
const _$HistoryFilterOptionsCWProxyImpl(this._value);
|
||||
|
||||
final HistoryFilterOptions _value;
|
||||
|
||||
@override
|
||||
HistoryFilterOptions dateRange(DateTimeRange<DateTime>? dateRange) =>
|
||||
call(dateRange: dateRange);
|
||||
|
||||
@override
|
||||
HistoryFilterOptions visitTypes(Set<VisitType> visitTypes) =>
|
||||
call(visitTypes: visitTypes);
|
||||
|
||||
@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 `HistoryFilterOptions(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// HistoryFilterOptions(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
HistoryFilterOptions call({
|
||||
Object? dateRange = const $CopyWithPlaceholder(),
|
||||
Object? visitTypes = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return HistoryFilterOptions(
|
||||
dateRange: dateRange == const $CopyWithPlaceholder()
|
||||
? _value.dateRange
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: dateRange as DateTimeRange<DateTime>?,
|
||||
visitTypes:
|
||||
visitTypes == const $CopyWithPlaceholder() || visitTypes == null
|
||||
? _value.visitTypes
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: visitTypes as Set<VisitType>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $HistoryFilterOptionsCopyWith on HistoryFilterOptions {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfHistoryFilterOptions.copyWith(...)` or `instanceOfHistoryFilterOptions.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$HistoryFilterOptionsCWProxy get copyWith =>
|
||||
_$HistoryFilterOptionsCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
HistoryFilterOptions _$HistoryFilterOptionsFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => HistoryFilterOptions(
|
||||
dateRange: const DateTimeRangeConverter().fromJson(
|
||||
json['dateRange'] as Map<String, dynamic>?,
|
||||
),
|
||||
visitTypes: (json['visitTypes'] as List<dynamic>)
|
||||
.map((e) => $enumDecode(_$VisitTypeEnumMap, e))
|
||||
.toSet(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$HistoryFilterOptionsToJson(
|
||||
HistoryFilterOptions instance,
|
||||
) => <String, dynamic>{
|
||||
'dateRange': const DateTimeRangeConverter().toJson(instance.dateRange),
|
||||
'visitTypes': instance.visitTypes.map((e) => _$VisitTypeEnumMap[e]!).toList(),
|
||||
};
|
||||
|
||||
const _$VisitTypeEnumMap = {
|
||||
VisitType.link: 'link',
|
||||
VisitType.typed: 'typed',
|
||||
VisitType.bookmark: 'bookmark',
|
||||
VisitType.embed: 'embed',
|
||||
VisitType.redirectPermanent: 'redirectPermanent',
|
||||
VisitType.redirectTemporary: 'redirectTemporary',
|
||||
VisitType.download: 'download',
|
||||
VisitType.framedLink: 'framedLink',
|
||||
VisitType.reload: 'reload',
|
||||
};
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:riverpod_annotation/experimental/json_persist.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||
import 'package:weblibre/features/user/data/providers.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
@JsonPersist()
|
||||
class HistoryVisitsFilter extends _$HistoryVisitsFilter {
|
||||
void updateVisitType(VisitType type, bool value) {
|
||||
if (value) {
|
||||
state = state.copyWith.visitTypes({...state.visitTypes, type});
|
||||
} else {
|
||||
state = state.copyWith.visitTypes({...state.visitTypes}..remove(type));
|
||||
}
|
||||
}
|
||||
|
||||
void reset() {
|
||||
state = HistoryFilterOptions.withDefaults();
|
||||
}
|
||||
|
||||
void setDateRange(DateTimeRange<DateTime>? range) {
|
||||
state = state.copyWith.dateRange(range);
|
||||
}
|
||||
|
||||
@override
|
||||
HistoryFilterOptions build() {
|
||||
persist(
|
||||
ref.watch(riverpodDatabaseStorageProvider),
|
||||
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(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)
|
||||
.getDetailedVisits(options);
|
||||
}
|
||||
@@ -0,0 +1,271 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(HistoryVisitsFilter)
|
||||
@JsonPersist()
|
||||
final historyVisitsFilterProvider = HistoryVisitsFilterProvider._();
|
||||
|
||||
@JsonPersist()
|
||||
final class HistoryVisitsFilterProvider
|
||||
extends $NotifierProvider<HistoryVisitsFilter, HistoryFilterOptions> {
|
||||
HistoryVisitsFilterProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'historyVisitsFilterProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$historyVisitsFilterHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
HistoryVisitsFilter create() => HistoryVisitsFilter();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(HistoryFilterOptions value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<HistoryFilterOptions>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$historyVisitsFilterHash() =>
|
||||
r'a4bf6c41c9180166365084cb7c7981558ac7ad36';
|
||||
|
||||
@JsonPersist()
|
||||
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
|
||||
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(browsingHistory)
|
||||
final browsingHistoryProvider = BrowsingHistoryProvider._();
|
||||
|
||||
final class BrowsingHistoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<VisitInfo>>,
|
||||
List<VisitInfo>,
|
||||
FutureOr<List<VisitInfo>>
|
||||
>
|
||||
with $FutureModifier<List<VisitInfo>>, $FutureProvider<List<VisitInfo>> {
|
||||
BrowsingHistoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'browsingHistoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$browsingHistoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<VisitInfo>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<VisitInfo>> create(Ref ref) {
|
||||
return browsingHistory(ref);
|
||||
}
|
||||
}
|
||||
|
||||
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 _$HistoryVisitsFilter extends _$HistoryVisitsFilterBase {
|
||||
/// The default key used by [persist].
|
||||
String get key {
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
||||
|
||||
part 'history.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class HistoryRepository extends _$HistoryRepository {
|
||||
final _service = GeckoHistoryService();
|
||||
|
||||
Future<void> deleteVisitsBetween(DateTime start, DateTime end) {
|
||||
return _service.deleteVisitsBetween(start, end);
|
||||
}
|
||||
|
||||
Future<List<VisitInfo>> getDetailedVisits(HistoryFilterOptions options) {
|
||||
return _service
|
||||
.getDetailedVisits(
|
||||
options.dateRange?.start ?? DateTime(0),
|
||||
options.dateRange?.end ?? DateTime(9999),
|
||||
options.visitTypes,
|
||||
)
|
||||
.then(
|
||||
(visits) =>
|
||||
visits..sort((a, b) => b.visitTime.compareTo(a.visitTime)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<VisitInfo>> getVisitsPaginated({
|
||||
required int count,
|
||||
int offset = 0,
|
||||
Set<VisitType> types = const {VisitType.link},
|
||||
}) {
|
||||
return _service.getVisitsPaginated(offset, count, types);
|
||||
}
|
||||
|
||||
Future<void> deleteVisit(VisitInfo info) {
|
||||
return _service.deleteVisit(info);
|
||||
}
|
||||
|
||||
Future<List<HistoryHighlight>> getHistoryHighlights({
|
||||
double viewTimeWeight = 10.0,
|
||||
double frequencyWeight = 4.0,
|
||||
required int limit,
|
||||
}) {
|
||||
return _service.getHistoryHighlights(
|
||||
weights: HistoryHighlightWeights(
|
||||
viewTime: viewTimeWeight,
|
||||
frequency: frequencyWeight,
|
||||
),
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<TopFrecentSiteInfo>> getTopFrecentSites({
|
||||
required int limit,
|
||||
FrecencyThresholdOption frecencyThreshold =
|
||||
FrecencyThresholdOption.skipOneTimePages,
|
||||
}) {
|
||||
return _service.getTopFrecentSites(
|
||||
limit: limit,
|
||||
frecencyThreshold: frecencyThreshold,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'history.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(HistoryRepository)
|
||||
final historyRepositoryProvider = HistoryRepositoryProvider._();
|
||||
|
||||
final class HistoryRepositoryProvider
|
||||
extends $NotifierProvider<HistoryRepository, void> {
|
||||
HistoryRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'historyRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$historyRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
HistoryRepository create() => HistoryRepository();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$historyRepositoryHash() => r'414b68ec6be3cc2eca3681cbc00990e53e6127ce';
|
||||
|
||||
abstract class _$HistoryRepository extends $Notifier<void> {
|
||||
void build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final ref = this.ref as $Ref<void, void>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<void, void>,
|
||||
void,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
typedef DeleteDecision = ({bool delete, bool remember});
|
||||
|
||||
Future<DeleteDecision?> showDeleteFileDialog(
|
||||
BuildContext context,
|
||||
String filePath, {
|
||||
bool multiFileMode = false,
|
||||
}) {
|
||||
return showDialog<DeleteDecision>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
final fileName = p.basename(filePath);
|
||||
|
||||
return HookBuilder(
|
||||
builder: (context) {
|
||||
final remember = useState(false);
|
||||
|
||||
return AlertDialog(
|
||||
icon: const Icon(Icons.warning),
|
||||
title: const Text('Delete Downloaded File?'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
children: [
|
||||
const TextSpan(text: 'Would you like to delete '),
|
||||
TextSpan(
|
||||
text: fileName,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const TextSpan(text: ' from your device?'),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'This action cannot be undone.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (multiFileMode) ...[
|
||||
const SizedBox(height: 16),
|
||||
CheckboxListTile.adaptive(
|
||||
value: remember.value,
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
onChanged: (value) {
|
||||
remember.value = value!;
|
||||
},
|
||||
title: const Text('Remember my choice for remaining files'),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.pop<DeleteDecision>((
|
||||
delete: false,
|
||||
remember: remember.value,
|
||||
));
|
||||
},
|
||||
child: const Text('Keep File'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
context.pop<DeleteDecision>((
|
||||
delete: true,
|
||||
remember: remember.value,
|
||||
));
|
||||
},
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.error,
|
||||
foregroundColor: Theme.of(context).colorScheme.onError,
|
||||
),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+538
@@ -0,0 +1,538 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fading_scroll/fading_scroll.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:path/path.dart' as p;
|
||||
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/entities/history_filter_options.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||
import 'package:weblibre/features/geckoview/features/history/presentation/dialogs/delete_file.dart';
|
||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.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/uri_breadcrumb.dart';
|
||||
import 'package:weblibre/presentation/widgets/url_icon.dart';
|
||||
|
||||
class Section extends MultiSliver {
|
||||
static final _datePattern = DateFormat.MMMd().addPattern('Hm');
|
||||
|
||||
Section({
|
||||
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,
|
||||
required Future<void> Function(VisitInfo) onDelete,
|
||||
}) : super(
|
||||
pushPinnedChildren: true,
|
||||
children: [
|
||||
SliverPinnedHeader(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 24, top: 8),
|
||||
color: Theme.of(context).canvasColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(title, style: Theme.of(context).textTheme.bodyLarge),
|
||||
const Divider(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverList.builder(
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
final uri = Uri.parse(item.url);
|
||||
|
||||
return Column(
|
||||
key: ValueKey(item.hashCode),
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: selectedItems.contains(item)
|
||||
? const CircleAvatar(
|
||||
radius: 12,
|
||||
child: Icon(Icons.check, size: 12),
|
||||
)
|
||||
: UrlIcon([uri], iconSize: 24),
|
||||
title: item.title.mapNotNull(
|
||||
(title) => Text(
|
||||
switch (item.visitType) {
|
||||
VisitType.download => p.basename(title),
|
||||
_ => title,
|
||||
},
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
subtitle: UriBreadcrumb(uri: uri),
|
||||
trailing: IconButton(
|
||||
onPressed: () async {
|
||||
await onDelete(item);
|
||||
},
|
||||
icon: const Icon(MdiIcons.closeCircle),
|
||||
),
|
||||
onTap: () {
|
||||
onTap(item);
|
||||
},
|
||||
onLongPress: () {
|
||||
onLongPress(item);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 54, right: 16),
|
||||
child: Wrap(
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
Chip(
|
||||
avatar: switch (item.visitType) {
|
||||
VisitType.link => const Icon(MdiIcons.openInNew),
|
||||
VisitType.download => const Icon(
|
||||
MdiIcons.fileDownload,
|
||||
),
|
||||
VisitType.reload => const Icon(MdiIcons.reload),
|
||||
_ => null,
|
||||
},
|
||||
label: switch (item.visitType) {
|
||||
VisitType.link => const Text('Followed Link'),
|
||||
VisitType.typed => const Text('Typed Address'),
|
||||
VisitType.embed => const Text(
|
||||
'Embedded Page Element',
|
||||
),
|
||||
VisitType.redirectPermanent => const Text(
|
||||
'Temporary Redirect',
|
||||
),
|
||||
VisitType.redirectTemporary => const Text(
|
||||
'Permanent Redirect',
|
||||
),
|
||||
VisitType.download => const Text('Download'),
|
||||
VisitType.framedLink => const Text('Frame'),
|
||||
VisitType.reload => const Text('Page Reload'),
|
||||
VisitType.bookmark => throw UnimplementedError(
|
||||
'VisitType.bookmark chip display not implemented',
|
||||
),
|
||||
},
|
||||
),
|
||||
Chip(
|
||||
label: Text(
|
||||
_datePattern.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
item.visitTime,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
enum HistoryScreenMode { history, downloads }
|
||||
|
||||
class HistoryScreen extends HookConsumerWidget {
|
||||
const HistoryScreen({super.key, this.mode = HistoryScreenMode.history});
|
||||
|
||||
final HistoryScreenMode mode;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isDownloadsMode = mode == HistoryScreenMode.downloads;
|
||||
|
||||
final textFilterEnabled = useState(false);
|
||||
final textFilterController = useTextEditingController();
|
||||
|
||||
final menuController = useMenuController();
|
||||
|
||||
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(
|
||||
title: textFilterEnabled.value
|
||||
? TextField(
|
||||
controller: textFilterController,
|
||||
decoration: InputDecoration(
|
||||
contentPadding: const EdgeInsets.only(top: 12),
|
||||
border: InputBorder.none,
|
||||
hintText: isDownloadsMode
|
||||
? 'Filter downloads...'
|
||||
: 'Filter history...',
|
||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||
suffixIcon: IconButton(
|
||||
onPressed: () {
|
||||
if (textFilterController.text.isNotEmpty) {
|
||||
textFilterController.clear();
|
||||
} else {
|
||||
textFilterEnabled.value = false;
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.clear),
|
||||
),
|
||||
),
|
||||
)
|
||||
: selectedItems.value.isEmpty
|
||||
? Text(isDownloadsMode ? 'Downloads' : 'History')
|
||||
: Text('${selectedItems.value.length} selected'),
|
||||
actions: [
|
||||
if (selectedItems.value.isNotEmpty)
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
DeleteDecision? deleteDecision;
|
||||
|
||||
for (final item in selectedItems.value) {
|
||||
await ref
|
||||
.read(historyRepositoryProvider.notifier)
|
||||
.deleteVisit(item);
|
||||
|
||||
final downloadedFile = item.title.mapNotNull(
|
||||
(title) => File(title),
|
||||
);
|
||||
|
||||
if (await downloadedFile?.exists() == true) {
|
||||
if (deleteDecision?.remember == true) {
|
||||
if (deleteDecision?.delete == true) {
|
||||
await downloadedFile!.delete();
|
||||
}
|
||||
} else if (context.mounted) {
|
||||
deleteDecision = await showDeleteFileDialog(
|
||||
context,
|
||||
downloadedFile.toString(),
|
||||
multiFileMode: true,
|
||||
);
|
||||
|
||||
if (deleteDecision?.delete == true) {
|
||||
await downloadedFile!.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
selectedItems.value = {};
|
||||
await refreshHistoryEntries();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await showDeleteDataDialog(
|
||||
context,
|
||||
initialSettings: {
|
||||
if (isDownloadsMode)
|
||||
DeleteBrowsingDataType.downloads
|
||||
else
|
||||
DeleteBrowsingDataType.history,
|
||||
},
|
||||
);
|
||||
|
||||
await refreshHistoryEntries();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
),
|
||||
MenuAnchor(
|
||||
controller: menuController,
|
||||
consumeOutsideTap: true,
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
child: const Text('Text Filter'),
|
||||
onPressed: () {
|
||||
textFilterEnabled.value = true;
|
||||
},
|
||||
),
|
||||
MenuItemButton(
|
||||
closeOnActivate: false,
|
||||
leadingIcon: const Icon(MdiIcons.calendarRange),
|
||||
trailingIcon: historyFilter.dateRange.mapNotNull(
|
||||
(_) => IconButton(
|
||||
onPressed: () {
|
||||
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(),
|
||||
);
|
||||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
child: const Text('Reset Filter'),
|
||||
onPressed: () {
|
||||
textFilterController.clear();
|
||||
textFilterEnabled.value = false;
|
||||
if (isDownloadsMode) {
|
||||
ref.read(historyDownloadsFilterProvider.notifier).reset();
|
||||
} else {
|
||||
ref.read(historyVisitsFilterProvider.notifier).reset();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
child: IconButton(
|
||||
onPressed: () {
|
||||
if (menuController.isOpen) {
|
||||
menuController.close();
|
||||
} else {
|
||||
menuController.open();
|
||||
}
|
||||
},
|
||||
icon: Badge(
|
||||
isLabelVisible: hasActiveFilter,
|
||||
child: const Icon(MdiIcons.filter),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
child: historyEntries.when(
|
||||
skipLoadingOnReload: true,
|
||||
data: (data) {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await refreshHistoryEntries();
|
||||
},
|
||||
child: HookBuilder(
|
||||
builder: (context) {
|
||||
final textFilter = useListenableSelector(
|
||||
textFilterController,
|
||||
() => textFilterController.text.toLowerCase(),
|
||||
);
|
||||
|
||||
final groups = useMemoized(
|
||||
() => data
|
||||
.where(
|
||||
(visit) =>
|
||||
textFilter.isEmpty ||
|
||||
visit.title?.toLowerCase().contains(textFilter) ==
|
||||
true ||
|
||||
visit.url.toLowerCase().contains(textFilter),
|
||||
)
|
||||
.groupListsBy(
|
||||
(element) => timeago.format(
|
||||
DateTime.fromMillisecondsSinceEpoch(
|
||||
element.visitTime,
|
||||
),
|
||||
),
|
||||
),
|
||||
[EquatableValue(data), textFilter],
|
||||
);
|
||||
|
||||
void toggleSelected(VisitInfo item) {
|
||||
if (selectedItems.value.contains(item)) {
|
||||
selectedItems.value = {...selectedItems.value}
|
||||
..remove(item);
|
||||
} else {
|
||||
selectedItems.value = {...selectedItems.value, item};
|
||||
}
|
||||
}
|
||||
|
||||
return FadingScroll(
|
||||
startFadingSize: 0.0,
|
||||
builder: (context, controller) {
|
||||
return CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
for (final MapEntry(:key, :value) in groups.entries)
|
||||
Section(
|
||||
context: context,
|
||||
title: key,
|
||||
items: value,
|
||||
selectedItems: selectedItems.value,
|
||||
onLongPress: toggleSelected,
|
||||
onDelete: deleteHistoryItem,
|
||||
onTap: (item) async {
|
||||
if (selectedItems.value.isNotEmpty) {
|
||||
toggleSelected(item);
|
||||
} else {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.addTab(
|
||||
url: Uri.parse(item.url),
|
||||
tabMode: TabMode.regular,
|
||||
selectTab: true,
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
context.pop();
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
error: (error, stackTrace) => Center(
|
||||
child: FailureWidget(
|
||||
title: isDownloadsMode
|
||||
? 'Failed to load Downloads'
|
||||
: 'Failed to load History',
|
||||
exception: error,
|
||||
),
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user