prepare for multiple apps

This commit is contained in:
Fabian Freund
2026-04-06 12:23:11 +02:00
parent bd1600e8dc
commit 5afc323f04
904 changed files with 29 additions and 29 deletions
@@ -0,0 +1,102 @@
/*
* 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:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
import 'package:weblibre/features/bangs/domain/repositories/sync.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
part 'bangs.g.dart';
@Riverpod(keepAlive: true)
Stream<BangData?> defaultSearchBangData(Ref ref) {
final key = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.defaultSearchProvider,
),
);
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBang(key);
}
@Riverpod()
Stream<BangData?> bangData(Ref ref, BangKey key) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBang(key);
}
@Riverpod()
Stream<Map<String, List<String>>> bangCategories(Ref ref) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchCategories();
}
@Riverpod()
Stream<List<BangData>> bangList(
Ref ref, {
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
}) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBangs(
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
orderMostFrequentFirst: orderMostFrequentFirst,
);
}
@Riverpod()
Stream<List<BangData>> frequentBangList(Ref ref) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchFrequentBangs();
}
@Riverpod()
Stream<List<SearchHistoryEntry>> searchHistory(Ref ref) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
final maxSearchHistoryEntries = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.maxSearchHistoryEntries,
),
);
return repository.watchSearchHistory(limit: maxSearchHistoryEntries);
}
@Riverpod()
Stream<DateTime?> lastSyncOfGroup(Ref ref, BangGroup group) {
final repository = ref.watch(bangSyncRepositoryProvider.notifier);
return repository.watchLastSyncOfGroup(group);
}
@Riverpod()
Stream<int> bangCountOfGroup(Ref ref, BangGroup group) {
final repository = ref.watch(bangDataRepositoryProvider.notifier);
return repository.watchBangCount(group);
}
@@ -0,0 +1,496 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'bangs.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(defaultSearchBangData)
final defaultSearchBangDataProvider = DefaultSearchBangDataProvider._();
final class DefaultSearchBangDataProvider
extends
$FunctionalProvider<AsyncValue<BangData?>, BangData?, Stream<BangData?>>
with $FutureModifier<BangData?>, $StreamProvider<BangData?> {
DefaultSearchBangDataProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'defaultSearchBangDataProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$defaultSearchBangDataHash();
@$internal
@override
$StreamProviderElement<BangData?> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<BangData?> create(Ref ref) {
return defaultSearchBangData(ref);
}
}
String _$defaultSearchBangDataHash() =>
r'5f43b8989219cf3cb2f5ca65df351b6cb100427f';
@ProviderFor(bangData)
final bangDataProvider = BangDataFamily._();
final class BangDataProvider
extends
$FunctionalProvider<AsyncValue<BangData?>, BangData?, Stream<BangData?>>
with $FutureModifier<BangData?>, $StreamProvider<BangData?> {
BangDataProvider._({
required BangDataFamily super.from,
required BangKey super.argument,
}) : super(
retry: null,
name: r'bangDataProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangDataHash();
@override
String toString() {
return r'bangDataProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<BangData?> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<BangData?> create(Ref ref) {
final argument = this.argument as BangKey;
return bangData(ref, argument);
}
@override
bool operator ==(Object other) {
return other is BangDataProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$bangDataHash() => r'bd9f5ec8b29aab74620a9b5a4246cb7e3b2fd377';
final class BangDataFamily extends $Family
with $FunctionalFamilyOverride<Stream<BangData?>, BangKey> {
BangDataFamily._()
: super(
retry: null,
name: r'bangDataProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
BangDataProvider call(BangKey key) =>
BangDataProvider._(argument: key, from: this);
@override
String toString() => r'bangDataProvider';
}
@ProviderFor(bangCategories)
final bangCategoriesProvider = BangCategoriesProvider._();
final class BangCategoriesProvider
extends
$FunctionalProvider<
AsyncValue<Map<String, List<String>>>,
Map<String, List<String>>,
Stream<Map<String, List<String>>>
>
with
$FutureModifier<Map<String, List<String>>>,
$StreamProvider<Map<String, List<String>>> {
BangCategoriesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bangCategoriesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangCategoriesHash();
@$internal
@override
$StreamProviderElement<Map<String, List<String>>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<Map<String, List<String>>> create(Ref ref) {
return bangCategories(ref);
}
}
String _$bangCategoriesHash() => r'947fcfd2dffcc7f585c6ed7379d319f4fe72293a';
@ProviderFor(bangList)
final bangListProvider = BangListFamily._();
final class BangListProvider
extends
$FunctionalProvider<
AsyncValue<List<BangData>>,
List<BangData>,
Stream<List<BangData>>
>
with $FutureModifier<List<BangData>>, $StreamProvider<List<BangData>> {
BangListProvider._({
required BangListFamily super.from,
required ({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
})
super.argument,
}) : super(
retry: null,
name: r'bangListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangListHash();
@override
String toString() {
return r'bangListProvider'
''
'$argument';
}
@$internal
@override
$StreamProviderElement<List<BangData>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<BangData>> create(Ref ref) {
final argument =
this.argument
as ({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
});
return bangList(
ref,
triggers: argument.triggers,
groups: argument.groups,
domain: argument.domain,
categoryFilter: argument.categoryFilter,
orderMostFrequentFirst: argument.orderMostFrequentFirst,
);
}
@override
bool operator ==(Object other) {
return other is BangListProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$bangListHash() => r'd1e0bb9fa4f523ce516e075c0d149bf7803ebb2b';
final class BangListFamily extends $Family
with
$FunctionalFamilyOverride<
Stream<List<BangData>>,
({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
})
> {
BangListFamily._()
: super(
retry: null,
name: r'bangListProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
BangListProvider call({
List<String>? triggers,
List<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
}) => BangListProvider._(
argument: (
triggers: triggers,
groups: groups,
domain: domain,
categoryFilter: categoryFilter,
orderMostFrequentFirst: orderMostFrequentFirst,
),
from: this,
);
@override
String toString() => r'bangListProvider';
}
@ProviderFor(frequentBangList)
final frequentBangListProvider = FrequentBangListProvider._();
final class FrequentBangListProvider
extends
$FunctionalProvider<
AsyncValue<List<BangData>>,
List<BangData>,
Stream<List<BangData>>
>
with $FutureModifier<List<BangData>>, $StreamProvider<List<BangData>> {
FrequentBangListProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'frequentBangListProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$frequentBangListHash();
@$internal
@override
$StreamProviderElement<List<BangData>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<BangData>> create(Ref ref) {
return frequentBangList(ref);
}
}
String _$frequentBangListHash() => r'2c1ecb7e9416772fc1c32d01d767e1eb4f865975';
@ProviderFor(searchHistory)
final searchHistoryProvider = SearchHistoryProvider._();
final class SearchHistoryProvider
extends
$FunctionalProvider<
AsyncValue<List<SearchHistoryEntry>>,
List<SearchHistoryEntry>,
Stream<List<SearchHistoryEntry>>
>
with
$FutureModifier<List<SearchHistoryEntry>>,
$StreamProvider<List<SearchHistoryEntry>> {
SearchHistoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'searchHistoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchHistoryHash();
@$internal
@override
$StreamProviderElement<List<SearchHistoryEntry>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<SearchHistoryEntry>> create(Ref ref) {
return searchHistory(ref);
}
}
String _$searchHistoryHash() => r'5f9508a6b286bfcd1b641bd429de46ad052a3dfe';
@ProviderFor(lastSyncOfGroup)
final lastSyncOfGroupProvider = LastSyncOfGroupFamily._();
final class LastSyncOfGroupProvider
extends
$FunctionalProvider<AsyncValue<DateTime?>, DateTime?, Stream<DateTime?>>
with $FutureModifier<DateTime?>, $StreamProvider<DateTime?> {
LastSyncOfGroupProvider._({
required LastSyncOfGroupFamily super.from,
required BangGroup super.argument,
}) : super(
retry: null,
name: r'lastSyncOfGroupProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$lastSyncOfGroupHash();
@override
String toString() {
return r'lastSyncOfGroupProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<DateTime?> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<DateTime?> create(Ref ref) {
final argument = this.argument as BangGroup;
return lastSyncOfGroup(ref, argument);
}
@override
bool operator ==(Object other) {
return other is LastSyncOfGroupProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$lastSyncOfGroupHash() => r'23d07f3132ba9bb35a31f74e3a69698d31d4c569';
final class LastSyncOfGroupFamily extends $Family
with $FunctionalFamilyOverride<Stream<DateTime?>, BangGroup> {
LastSyncOfGroupFamily._()
: super(
retry: null,
name: r'lastSyncOfGroupProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
LastSyncOfGroupProvider call(BangGroup group) =>
LastSyncOfGroupProvider._(argument: group, from: this);
@override
String toString() => r'lastSyncOfGroupProvider';
}
@ProviderFor(bangCountOfGroup)
final bangCountOfGroupProvider = BangCountOfGroupFamily._();
final class BangCountOfGroupProvider
extends $FunctionalProvider<AsyncValue<int>, int, Stream<int>>
with $FutureModifier<int>, $StreamProvider<int> {
BangCountOfGroupProvider._({
required BangCountOfGroupFamily super.from,
required BangGroup super.argument,
}) : super(
retry: null,
name: r'bangCountOfGroupProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangCountOfGroupHash();
@override
String toString() {
return r'bangCountOfGroupProvider'
''
'($argument)';
}
@$internal
@override
$StreamProviderElement<int> $createElement($ProviderPointer pointer) =>
$StreamProviderElement(pointer);
@override
Stream<int> create(Ref ref) {
final argument = this.argument as BangGroup;
return bangCountOfGroup(ref, argument);
}
@override
bool operator ==(Object other) {
return other is BangCountOfGroupProvider && other.argument == argument;
}
@override
int get hashCode {
return argument.hashCode;
}
}
String _$bangCountOfGroupHash() => r'211ffcd7f49b637a7953f913dc5eafda344423f3';
final class BangCountOfGroupFamily extends $Family
with $FunctionalFamilyOverride<Stream<int>, BangGroup> {
BangCountOfGroupFamily._()
: super(
retry: null,
name: r'bangCountOfGroupProvider',
dependencies: null,
$allTransitiveDependencies: null,
isAutoDispose: true,
);
BangCountOfGroupProvider call(BangGroup group) =>
BangCountOfGroupProvider._(argument: group, from: this);
@override
String toString() => r'bangCountOfGroupProvider';
}
@@ -0,0 +1,102 @@
/*
* 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:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
import 'package:weblibre/features/bangs/data/providers.dart';
import 'package:weblibre/features/bangs/domain/providers/bangs.dart';
import 'package:weblibre/features/bangs/domain/repositories/data.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
part 'search.g.dart';
@Riverpod()
class BangSearch extends _$BangSearch {
late StreamController<List<BangData>> _streamController;
Future<Uri> triggerBangSearch(BangData bang, String searchQuery) async {
final bangDataNotifier = ref.read(bangDataRepositoryProvider.notifier);
final settings = ref.read(generalSettingsWithDefaultsProvider);
await bangDataNotifier.increaseFrequency(bang.toKey());
await bangDataNotifier.addSearchEntry(
bang.group,
bang.trigger,
searchQuery,
maxEntryCount: settings.maxSearchHistoryEntries,
);
return bang.getTemplateUrl(searchQuery);
}
Future<void> search(String input) async {
if (input.isNotEmpty) {
await ref.read(bangDatabaseProvider).bangDao.queryBangs(input).get().then(
(value) {
if (!_streamController.isClosed) {
_streamController.add(value);
}
},
);
}
}
@override
Stream<List<BangData>> build() {
_streamController = StreamController();
// Emit initial empty list so UI doesn't show loading state
_streamController.add([]);
ref.onDispose(() async {
await _streamController.close();
});
return _streamController.stream;
}
}
@Riverpod()
class SeamlessBang extends _$SeamlessBang {
bool _hasSearch = false;
void search(String input) {
if (input.isNotEmpty) {
if (!_hasSearch) {
_hasSearch = true;
ref.invalidateSelf();
}
//Don't block
unawaited(ref.read(bangSearchProvider.notifier).search(input));
} else if (_hasSearch) {
_hasSearch = false;
ref.invalidateSelf();
}
}
@override
AsyncValue<List<BangData>> build() {
return _hasSearch
? ref.watch(bangSearchProvider)
: ref.watch(frequentBangListProvider);
}
}
@@ -0,0 +1,111 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(BangSearch)
final bangSearchProvider = BangSearchProvider._();
final class BangSearchProvider
extends $StreamNotifierProvider<BangSearch, List<BangData>> {
BangSearchProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bangSearchProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangSearchHash();
@$internal
@override
BangSearch create() => BangSearch();
}
String _$bangSearchHash() => r'feed24edfe703b0697f4a855be9c7359c456b0f2';
abstract class _$BangSearch extends $StreamNotifier<List<BangData>> {
Stream<List<BangData>> build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<AsyncValue<List<BangData>>, List<BangData>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<BangData>>, List<BangData>>,
AsyncValue<List<BangData>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@ProviderFor(SeamlessBang)
final seamlessBangProvider = SeamlessBangProvider._();
final class SeamlessBangProvider
extends $NotifierProvider<SeamlessBang, AsyncValue<List<BangData>>> {
SeamlessBangProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'seamlessBangProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$seamlessBangHash();
@$internal
@override
SeamlessBang create() => SeamlessBang();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(AsyncValue<List<BangData>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<AsyncValue<List<BangData>>>(value),
);
}
}
String _$seamlessBangHash() => r'8bd7a2cbe4c302ae08f85167290666a7437f8b9b';
abstract class _$SeamlessBang extends $Notifier<AsyncValue<List<BangData>>> {
AsyncValue<List<BangData>> build();
@$mustCallSuper
@override
void runBuild() {
final ref =
this.ref
as $Ref<AsyncValue<List<BangData>>, AsyncValue<List<BangData>>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<
AsyncValue<List<BangData>>,
AsyncValue<List<BangData>>
>,
AsyncValue<List<BangData>>,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}
@@ -0,0 +1,167 @@
/*
* 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:convert';
import 'package:drift/drift.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/bangs/data/models/bang.dart';
import 'package:weblibre/features/bangs/data/models/bang_data.dart';
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
import 'package:weblibre/features/bangs/data/models/bang_key.dart';
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
import 'package:weblibre/features/bangs/data/providers.dart';
part 'data.g.dart';
@Riverpod(keepAlive: true)
class BangDataRepository extends _$BangDataRepository {
@override
void build() {}
Stream<BangData?> watchBang(BangKey? key) {
if (key != null) {
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangData(key.group, key.trigger)
.watchSingleOrNull();
} else {
return Stream.value(null);
}
}
Stream<Map<String, List<String>>> watchCategories() {
return ref
.read(bangDatabaseProvider)
.definitionsDrift
.categoriesJson()
.watchSingle()
.map((json) {
final decoded = jsonDecode(json) as Map<String, dynamic>;
return decoded.map(
(key, value) => MapEntry(key, (value as List<dynamic>).cast()),
);
});
}
Stream<int> watchBangCount(BangGroup group) {
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangCount(groups: [group])
.watchSingle();
}
Stream<List<BangData>> watchBangs({
Iterable<String>? triggers,
Iterable<BangGroup>? groups,
String? domain,
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
}) {
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangDataList(
triggers: triggers,
groups: groups,
domain: domain,
category: categoryFilter?.category,
subCategory: categoryFilter?.subCategory,
orderMostFrequentFirst: orderMostFrequentFirst,
)
.watch();
}
Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup>? groups}) {
return ref
.read(bangDatabaseProvider)
.bangDao
.getFrequentBangDataList(groups: groups)
.watch();
}
Stream<List<SearchHistoryEntry>> watchSearchHistory({required int limit}) {
return ref
.read(bangDatabaseProvider)
.definitionsDrift
.searchHistoryEntries(limit: limit)
.watch();
}
Future<void> increaseFrequency(BangKey key) {
return ref.read(bangDatabaseProvider).bangDao.increaseBangFrequency(key);
}
Future<void> addSearchEntry(
BangGroup group,
String trigger,
String searchQuery, {
required int maxEntryCount,
}) async {
// Skip capturing history if maxEntryCount is 0
if (maxEntryCount <= 0) {
return;
}
final db = ref.read(bangDatabaseProvider);
//Pack in a transaction to bundle rebuilds of watch() queries
return db.transaction(() async {
await db.bangDao.addSearchEntry(group, trigger, searchQuery);
await db.definitionsDrift.evictHistoryEntries(limit: maxEntryCount);
});
}
Future<void> removeSearchEntry(String searchQuery) {
return ref
.read(bangDatabaseProvider)
.bangDao
.removeSearchEntry(searchQuery);
}
Future<int> resetFrequencies() {
return ref.read(bangDatabaseProvider).bangFrequency.deleteAll();
}
Future<int> resetFrequency(String trigger) {
return ref
.read(bangDatabaseProvider)
.bangFrequency
.deleteWhere((t) => t.trigger.equals(trigger));
}
Future<BangData?> getBang(BangKey key) {
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangData(key.group, key.trigger)
.getSingleOrNull();
}
Future<void> upsertBang(Bang bang) {
return ref.read(bangDatabaseProvider).bangDao.upsertBang(bang);
}
Future<void> deleteBang(BangKey key) {
return ref.read(bangDatabaseProvider).syncDao.deleteBangs(key.group, [
key.trigger,
]);
}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'data.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(BangDataRepository)
final bangDataRepositoryProvider = BangDataRepositoryProvider._();
final class BangDataRepositoryProvider
extends $NotifierProvider<BangDataRepository, void> {
BangDataRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bangDataRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangDataRepositoryHash();
@$internal
@override
BangDataRepository create() => BangDataRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$bangDataRepositoryHash() =>
r'c562ef10d75ca6dee13805f84d2491cf2a89aaac';
abstract class _$BangDataRepository 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);
}
}
@@ -0,0 +1,191 @@
/*
* 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:exceptions/exceptions.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/bangs/data/database/database.dart';
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
import 'package:weblibre/features/bangs/data/providers.dart';
import 'package:weblibre/features/bangs/data/services/data_source.dart';
part 'sync.g.dart';
@Riverpod(keepAlive: true)
class BangSyncRepository extends _$BangSyncRepository {
static Future<Result<void>> _fetchAndSyncRemote({
required BangDataSourceService sourceService,
required BangDatabase db,
required Uri url,
required BangGroup group,
required Duration? syncInterval,
}) async {
if (syncInterval != null) {
final lastSync = await db.syncDao
.getLastSyncOfGroup(group)
.getSingleOrNull();
if (lastSync != null &&
DateTime.now().difference(lastSync) < syncInterval) {
return Result.success(null);
}
}
final result = await sourceService.fetchRemoteBangs(url, group);
return result.flatMapAsync((remoteBangs) async {
await db.syncDao.syncBangs(
group: group,
remoteBangs: remoteBangs,
syncTime: DateTime.now(),
);
await db.definitionsDrift.optimizeBangFtsIndex();
await db.definitionsDrift.optimizeTriggerFtsIndex();
});
}
static Future<Result<void>> _fetchAndSyncBundled({
required BangDataSourceService sourceService,
required BangDatabase db,
required BangGroup group,
}) async {
if (group.bundled == null) {
return Result.failure(
const ErrorMessage(source: 'BangSync', message: 'Not bundled'),
);
}
if (group.remote == null) {
return Result.failure(
const ErrorMessage(source: 'BangSync', message: 'No remote source'),
);
}
final lastSync = await db.syncDao
.getLastSyncOfGroup(group)
.getSingleOrNull();
final sourceDate = await sourceService.getBundledBangDate(
'assets/bangs/last_sync.txt',
);
if (lastSync != null &&
(sourceDate == lastSync ||
sourceDate.difference(lastSync).isNegative)) {
return Result.success(null);
}
final result = await sourceService.getBundledBangs(group.bundled!, group);
return result.flatMapAsync((remoteBangs) async {
await db.syncDao.syncBangs(
group: group,
remoteBangs: remoteBangs,
syncTime: sourceDate,
);
await db.definitionsDrift.optimizeBangFtsIndex();
await db.definitionsDrift.optimizeTriggerFtsIndex();
});
}
Future<Result<void>> syncRemoteBangGroup(
BangGroup group,
Duration? syncInterval,
) async {
try {
return Result.success(
await ref
.read(bangDatabaseProvider)
.computeWithDatabase(
connect: BangDatabase.new,
computation: (db) async {
final ref = ProviderContainer();
final result = await _fetchAndSyncRemote(
sourceService: ref.read(
bangDataSourceServiceProvider.notifier,
),
db: db,
url: Uri.parse(group.remote!),
group: group,
syncInterval: syncInterval,
);
//Throw if necessary
return result.value;
},
),
);
} catch (e) {
return Result.failure(
ErrorMessage(
message: "Failed to sync Bangs (${group.name})",
source: 'BangSync',
details: e,
),
);
}
}
Future<Result<void>> syncBundledBangGroup(BangGroup group) async {
try {
final db = ref.read(bangDatabaseProvider);
final result = await _fetchAndSyncBundled(
sourceService: ref.read(bangDataSourceServiceProvider.notifier),
db: db,
group: group,
);
//Throw if necessary
return result;
} catch (e) {
return Result.failure(
ErrorMessage(
message: "Failed to sync Bangs (${group.name})",
source: 'BangSync',
details: e,
),
);
}
}
Stream<DateTime?> watchLastSyncOfGroup(BangGroup group) {
return ref
.read(bangDatabaseProvider)
.syncDao
.getLastSyncOfGroup(group)
.watchSingleOrNull();
}
Future<Map<BangGroup, Result<void>>> syncBundledBangGroups({
Set<BangGroup>? groups,
}) async {
//Default to all sources
groups ??= BangGroup.values.where((e) => e.bundled != null).toSet();
//Run isolated operations
final futures = groups.map(
(source) => syncBundledBangGroup(
source,
).then((result) => MapEntry(source, result)),
);
return Map.fromEntries(await Future.wait(futures));
}
@override
void build() {}
}
@@ -0,0 +1,63 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'sync.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(BangSyncRepository)
final bangSyncRepositoryProvider = BangSyncRepositoryProvider._();
final class BangSyncRepositoryProvider
extends $NotifierProvider<BangSyncRepository, void> {
BangSyncRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'bangSyncRepositoryProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$bangSyncRepositoryHash();
@$internal
@override
BangSyncRepository create() => BangSyncRepository();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$bangSyncRepositoryHash() =>
r'1347dcbd03f6a1a4fa3bbbae5d9302098d260c50';
abstract class _$BangSyncRepository 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);
}
}
@@ -0,0 +1,46 @@
/*
* 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:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:weblibre/features/bangs/data/providers.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
part 'search_history_cleanup.g.dart';
/// Service that listens to maxSearchHistoryEntries setting changes
/// and cleans up search history when the limit is reduced.
@Riverpod(keepAlive: true)
class SearchHistoryCleanupService extends _$SearchHistoryCleanupService {
@override
void build() {
ref.listen(
generalSettingsWithDefaultsProvider.select(
(settings) => settings.maxSearchHistoryEntries,
),
(previous, next) async {
// Only cleanup when limit is reduced (including to 0)
if (previous != null && next < previous) {
final db = ref.read(bangDatabaseProvider);
await db.definitionsDrift.evictHistoryEntries(limit: next);
}
},
);
}
}
@@ -0,0 +1,73 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'search_history_cleanup.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Service that listens to maxSearchHistoryEntries setting changes
/// and cleans up search history when the limit is reduced.
@ProviderFor(SearchHistoryCleanupService)
final searchHistoryCleanupServiceProvider =
SearchHistoryCleanupServiceProvider._();
/// Service that listens to maxSearchHistoryEntries setting changes
/// and cleans up search history when the limit is reduced.
final class SearchHistoryCleanupServiceProvider
extends $NotifierProvider<SearchHistoryCleanupService, void> {
/// Service that listens to maxSearchHistoryEntries setting changes
/// and cleans up search history when the limit is reduced.
SearchHistoryCleanupServiceProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'searchHistoryCleanupServiceProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$searchHistoryCleanupServiceHash();
@$internal
@override
SearchHistoryCleanupService create() => SearchHistoryCleanupService();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
);
}
}
String _$searchHistoryCleanupServiceHash() =>
r'4e84ab50fa2ad55c615bb63a667c95383715570b';
/// Service that listens to maxSearchHistoryEntries setting changes
/// and cleans up search history when the limit is reduced.
abstract class _$SearchHistoryCleanupService 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);
}
}