From b1e2b65a0c6aef85a6a596001a2619fd823ab588 Mon Sep 17 00:00:00 2001 From: Fabian Freund Date: Wed, 12 Jun 2024 08:34:48 +0200 Subject: [PATCH] refactored sync & bangs --- .../bangs/data/database/daos/bang.dart | 34 +- .../bangs/data/database/daos/sync.dart | 85 ++++ .../bangs/data/database/daos/sync.g.dart | 6 + .../bangs/data/database/database.dart | 6 +- .../bangs/data/database/database.drift | 7 +- .../bangs/data/database/database.g.dart | 276 ++++++++++- app/lib/features/bangs/data/models/bang.dart | 1 + app/lib/features/bangs/domain/providers.dart | 44 +- .../features/bangs/domain/providers.g.dart | 455 ++++++++++++++---- .../bangs/domain/repositories/bang.dart | 154 ------ .../bangs/domain/repositories/bang_data.dart | 74 +++ .../domain/repositories/bang_data.g.dart | 27 ++ .../bangs/domain/repositories/sync.dart | 133 +++++ .../repositories/{bang.g.dart => sync.g.dart} | 20 +- .../bangs/presentation/screens/bang.dart | 2 +- .../bangs/presentation/widgets/bang_icon.dart | 2 +- .../search_browser/domain/providers.dart | 4 +- .../search_browser/domain/providers.g.dart | 2 +- .../presentation/widgets/bang_chips.dart | 2 +- .../presentation/widgets/tabs/search_tab.dart | 8 +- .../presentation/widgets/web_view.dart | 2 +- app/lib/main.dart | 6 +- 22 files changed, 1023 insertions(+), 327 deletions(-) create mode 100644 app/lib/features/bangs/data/database/daos/sync.dart create mode 100644 app/lib/features/bangs/data/database/daos/sync.g.dart delete mode 100644 app/lib/features/bangs/domain/repositories/bang.dart create mode 100644 app/lib/features/bangs/domain/repositories/bang_data.dart create mode 100644 app/lib/features/bangs/domain/repositories/bang_data.g.dart create mode 100644 app/lib/features/bangs/domain/repositories/sync.dart rename app/lib/features/bangs/domain/repositories/{bang.g.dart => sync.g.dart} (55%) diff --git a/app/lib/features/bangs/data/database/daos/bang.dart b/app/lib/features/bangs/data/database/daos/bang.dart index 9a871984..8de9504c 100644 --- a/app/lib/features/bangs/data/database/daos/bang.dart +++ b/app/lib/features/bangs/data/database/daos/bang.dart @@ -9,7 +9,7 @@ part 'bang.g.dart'; class BangDao extends DatabaseAccessor with _$BangDaoMixin { BangDao(super.db); - Selectable getBangs({Iterable? groups}) { + Selectable getBangList({Iterable? groups}) { final selectable = select(db.bang); if (groups != null) { selectable.where((t) => t.group.isInValues(groups)); @@ -18,11 +18,17 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { return selectable; } + SingleSelectable getBangCount({Iterable? groups}) { + return db.bang.count( + where: (groups != null) ? (t) => t.group.isInValues(groups) : null, + ); + } + SingleOrNullSelectable getBangData(String trigger) { return select(db.bangDataView)..where((t) => t.trigger.equals(trigger)); } - Selectable getBangDataList({Iterable? groups}) { + Selectable getBangDataList({Iterable? groups}) { final selectable = select(db.bangDataView); if (groups != null) { selectable.where((t) => t.group.isInValues(groups)); @@ -33,9 +39,10 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { return selectable; } - Selectable getFrequentBangDataList({Iterable? groups}) { + Selectable getFrequentBangDataList({Iterable? groups}) { final selectable = select(db.bangDataView) ..where((t) => t.frequency.isBiggerThanValue(0)); + if (groups != null) { selectable.where((t) => t.group.isInValues(groups)); } @@ -48,7 +55,7 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { return selectable; } - Future increaseFrequency(String trigger) { + Future increaseBangFrequency(String trigger) { return db.bangFrequency.insertOne( BangFrequencyCompanion.insert( trigger: trigger, @@ -68,11 +75,7 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { return db.bangQuery(query: db.buildQuery(searchString)); } - Future insertBangs(Iterable bangs) { - return db.bang.insertAll(bangs); - } - - Future upsertIcon(String trigger, Uint8List iconData) { + Future upsertBangIcon(String trigger, Uint8List iconData) { return db.bangIcon.insertOne( BangIconCompanion.insert( trigger: trigger, @@ -82,17 +85,4 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { mode: InsertMode.insertOrReplace, ); } - - Future replaceBangs(Iterable bangs) { - return batch( - (batch) { - batch.replaceAll(db.bang, bangs); - }, - ); - } - - Future deleteBangs(Iterable triggers) { - final statement = delete(db.bang)..where((t) => t.trigger.isIn(triggers)); - return statement.go(); - } } diff --git a/app/lib/features/bangs/data/database/daos/sync.dart b/app/lib/features/bangs/data/database/daos/sync.dart new file mode 100644 index 00000000..a8499437 --- /dev/null +++ b/app/lib/features/bangs/data/database/daos/sync.dart @@ -0,0 +1,85 @@ +import 'package:bang_navigator/features/bangs/data/database/database.dart'; +import 'package:bang_navigator/features/bangs/data/models/bang.dart'; +import 'package:drift/drift.dart'; + +part 'sync.g.dart'; + +@DriftAccessor() +class SyncDao extends DatabaseAccessor with _$SyncDaoMixin { + SyncDao(super.db); + + SingleOrNullSelectable lastSyncOfGroup(BangGroup group) { + final query = selectOnly(db.bangSync) + ..addColumns([db.bangSync.lastSync]) + ..where(db.bangSync.group.equalsValue(group)); + + return query.map((row) => row.read(db.bangSync.lastSync)); + } + + Future upsertLastSyncOfGroup(BangGroup group, DateTime lastSync) { + return db.bangSync.insertOne( + BangSyncCompanion.insert( + group: Value(group), + lastSync: lastSync, + ), + onConflict: DoUpdate( + (old) => BangSyncCompanion.custom(lastSync: Variable(lastSync)), + ), + ); + } + + Future insertBangs(Iterable bangs) { + return db.bang.insertAll(bangs); + } + + Future replaceBangs(Iterable bangs) { + return batch( + (batch) { + batch.replaceAll(db.bang, bangs); + }, + ); + } + + Future deleteBangs(Iterable triggers) { + final statement = delete(db.bang)..where((t) => t.trigger.isIn(triggers)); + return statement.go(); + } + + Future syncBangs({ + required BangGroup group, + required Iterable remoteBangs, + required DateTime syncTime, + }) async { + final remoteBangMap = + Map.fromEntries(remoteBangs.map((e) => MapEntry(e.trigger, e))); + final localBangMap = + await db.bangDao.getBangList(groups: [group]).get().then( + (bangs) => + Map.fromEntries(bangs.map((e) => MapEntry(e.trigger, e))), + ); + + final remoteBangTriggers = remoteBangMap.keys.toSet(); + final localBangTriggers = localBangMap.keys.toSet(); + + final removedBangs = localBangTriggers.difference(remoteBangTriggers); + final addedBangs = remoteBangTriggers.difference(localBangTriggers).map( + (e) => remoteBangMap[e]!, + ); + + final changedBangs = remoteBangTriggers + .intersection(localBangTriggers) + .where((e) => remoteBangMap[e] != localBangMap[e]) + .map( + (e) => remoteBangMap[e]!, + ); + + await db.transaction( + () async { + await deleteBangs(removedBangs); + await insertBangs(addedBangs); + await replaceBangs(changedBangs); + await upsertLastSyncOfGroup(group, syncTime); + }, + ); + } +} diff --git a/app/lib/features/bangs/data/database/daos/sync.g.dart b/app/lib/features/bangs/data/database/daos/sync.g.dart new file mode 100644 index 00000000..6ba5a943 --- /dev/null +++ b/app/lib/features/bangs/data/database/daos/sync.g.dart @@ -0,0 +1,6 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sync.dart'; + +// ignore_for_file: type=lint +mixin _$SyncDaoMixin on DatabaseAccessor {} diff --git a/app/lib/features/bangs/data/database/database.dart b/app/lib/features/bangs/data/database/database.dart index 431fe668..52fb1c9d 100644 --- a/app/lib/features/bangs/data/database/database.dart +++ b/app/lib/features/bangs/data/database/database.dart @@ -1,4 +1,5 @@ import 'package:bang_navigator/features/bangs/data/database/daos/bang.dart'; +import 'package:bang_navigator/features/bangs/data/database/daos/sync.dart'; import 'package:bang_navigator/features/bangs/data/database/drift/converters/bang_format.dart'; import 'package:bang_navigator/features/bangs/data/models/bang.dart'; import 'package:bang_navigator/features/bangs/data/models/bang_data.dart'; @@ -7,7 +8,10 @@ import 'package:drift/drift.dart'; part 'database.g.dart'; -@DriftDatabase(include: {'database.drift'}, daos: [BangDao]) +@DriftDatabase( + include: {'database.drift'}, + daos: [BangDao, SyncDao], +) class BangDatabase extends _$BangDatabase with PrefixQueryBuilderMixin { @override final int schemaVersion = 1; diff --git a/app/lib/features/bangs/data/database/database.drift b/app/lib/features/bangs/data/database/database.drift index 135ce30f..30413633 100644 --- a/app/lib/features/bangs/data/database/database.drift +++ b/app/lib/features/bangs/data/database/database.drift @@ -4,7 +4,7 @@ import 'package:bang_navigator/features/bangs/data/models/bang.dart'; CREATE TABLE bang ( "trigger" TEXT PRIMARY KEY NOT NULL, - "group" ENUM(BangGroup), + "group" ENUM(BangGroup) NOT NULL, website_name TEXT NOT NULL, domain TEXT NOT NULL, url_template TEXT NOT NULL, @@ -13,6 +13,11 @@ CREATE TABLE bang ( format TEXT MAPPED BY `const BangFormatConverter()` ) WITH Bang; +CREATE TABLE bang_sync ( + "group" ENUM(BangGroup) PRIMARY KEY NOT NULL, + last_sync DATETIME NOT NULL +); + CREATE TABLE bang_frequency ( "trigger" TEXT PRIMARY KEY NOT NULL REFERENCES bang ("trigger") ON DELETE CASCADE, frequency INTEGER NOT NULL, diff --git a/app/lib/features/bangs/data/database/database.g.dart b/app/lib/features/bangs/data/database/database.g.dart index b26e7811..a3269b0c 100644 --- a/app/lib/features/bangs/data/database/database.g.dart +++ b/app/lib/features/bangs/data/database/database.g.dart @@ -13,12 +13,12 @@ class BangTable extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'PRIMARY KEY NOT NULL'); - late final GeneratedColumnWithTypeConverter group = - GeneratedColumn('group', aliasedName, true, + late final GeneratedColumnWithTypeConverter group = + GeneratedColumn('group', aliasedName, false, type: DriftSqlType.int, - requiredDuringInsert: false, - $customConstraints: '') - .withConverter(BangTable.$convertergroupn); + requiredDuringInsert: true, + $customConstraints: 'NOT NULL') + .withConverter(BangTable.$convertergroup); late final GeneratedColumn websiteName = GeneratedColumn( 'website_name', aliasedName, false, type: DriftSqlType.string, @@ -80,8 +80,8 @@ class BangTable extends Table with TableInfo { .read(DriftSqlType.string, data['${effectivePrefix}trigger'])!, urlTemplate: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}url_template'])!, - group: BangTable.$convertergroupn.fromSql(attachedDatabase.typeMapping - .read(DriftSqlType.int, data['${effectivePrefix}group'])), + group: BangTable.$convertergroup.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}group'])!), category: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}category']), subCategory: attachedDatabase.typeMapping @@ -98,8 +98,6 @@ class BangTable extends Table with TableInfo { static JsonTypeConverter2 $convertergroup = const EnumIndexConverter(BangGroup.values); - static JsonTypeConverter2 $convertergroupn = - JsonTypeConverter2.asNullable($convertergroup); static TypeConverter?, String?> $converterformat = const BangFormatConverter(); @override @@ -108,7 +106,7 @@ class BangTable extends Table with TableInfo { class BangCompanion extends UpdateCompanion { final Value trigger; - final Value group; + final Value group; final Value websiteName; final Value domain; final Value urlTemplate; @@ -129,7 +127,7 @@ class BangCompanion extends UpdateCompanion { }); BangCompanion.insert({ required String trigger, - this.group = const Value.absent(), + required BangGroup group, required String websiteName, required String domain, required String urlTemplate, @@ -138,6 +136,7 @@ class BangCompanion extends UpdateCompanion { this.format = const Value.absent(), this.rowid = const Value.absent(), }) : trigger = Value(trigger), + group = Value(group), websiteName = Value(websiteName), domain = Value(domain), urlTemplate = Value(urlTemplate); @@ -167,7 +166,7 @@ class BangCompanion extends UpdateCompanion { BangCompanion copyWith( {Value? trigger, - Value? group, + Value? group, Value? websiteName, Value? domain, Value? urlTemplate, @@ -196,7 +195,7 @@ class BangCompanion extends UpdateCompanion { } if (group.present) { map['group'] = - Variable(BangTable.$convertergroupn.toSql(group.value)); + Variable(BangTable.$convertergroup.toSql(group.value)); } if (websiteName.present) { map['website_name'] = Variable(websiteName.value); @@ -240,6 +239,159 @@ class BangCompanion extends UpdateCompanion { } } +class BangSync extends Table with TableInfo { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + BangSync(this.attachedDatabase, [this._alias]); + late final GeneratedColumnWithTypeConverter group = + GeneratedColumn('group', aliasedName, false, + type: DriftSqlType.int, + requiredDuringInsert: false, + $customConstraints: 'PRIMARY KEY NOT NULL') + .withConverter(BangSync.$convertergroup); + late final GeneratedColumn lastSync = GeneratedColumn( + 'last_sync', aliasedName, false, + type: DriftSqlType.dateTime, + requiredDuringInsert: true, + $customConstraints: 'NOT NULL'); + @override + List get $columns => [group, lastSync]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'bang_sync'; + @override + Set get $primaryKey => {group}; + @override + BangSyncData map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return BangSyncData( + group: BangSync.$convertergroup.fromSql(attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}group'])!), + lastSync: attachedDatabase.typeMapping + .read(DriftSqlType.dateTime, data['${effectivePrefix}last_sync'])!, + ); + } + + @override + BangSync createAlias(String alias) { + return BangSync(attachedDatabase, alias); + } + + static JsonTypeConverter2 $convertergroup = + const EnumIndexConverter(BangGroup.values); + @override + bool get dontWriteConstraints => true; +} + +class BangSyncData extends DataClass implements Insertable { + final BangGroup group; + final DateTime lastSync; + const BangSyncData({required this.group, required this.lastSync}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + { + map['group'] = Variable(BangSync.$convertergroup.toSql(group)); + } + map['last_sync'] = Variable(lastSync); + return map; + } + + factory BangSyncData.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return BangSyncData( + group: BangSync.$convertergroup + .fromJson(serializer.fromJson(json['group'])), + lastSync: serializer.fromJson(json['last_sync']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'group': serializer.toJson(BangSync.$convertergroup.toJson(group)), + 'last_sync': serializer.toJson(lastSync), + }; + } + + BangSyncData copyWith({BangGroup? group, DateTime? lastSync}) => BangSyncData( + group: group ?? this.group, + lastSync: lastSync ?? this.lastSync, + ); + @override + String toString() { + return (StringBuffer('BangSyncData(') + ..write('group: $group, ') + ..write('lastSync: $lastSync') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash(group, lastSync); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is BangSyncData && + other.group == this.group && + other.lastSync == this.lastSync); +} + +class BangSyncCompanion extends UpdateCompanion { + final Value group; + final Value lastSync; + const BangSyncCompanion({ + this.group = const Value.absent(), + this.lastSync = const Value.absent(), + }); + BangSyncCompanion.insert({ + this.group = const Value.absent(), + required DateTime lastSync, + }) : lastSync = Value(lastSync); + static Insertable custom({ + Expression? group, + Expression? lastSync, + }) { + return RawValuesInsertable({ + if (group != null) 'group': group, + if (lastSync != null) 'last_sync': lastSync, + }); + } + + BangSyncCompanion copyWith( + {Value? group, Value? lastSync}) { + return BangSyncCompanion( + group: group ?? this.group, + lastSync: lastSync ?? this.lastSync, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (group.present) { + map['group'] = Variable(BangSync.$convertergroup.toSql(group.value)); + } + if (lastSync.present) { + map['last_sync'] = Variable(lastSync.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('BangSyncCompanion(') + ..write('group: $group, ') + ..write('lastSync: $lastSync') + ..write(')')) + .toString(); + } +} + class BangFrequency extends Table with TableInfo { @override @@ -812,7 +964,7 @@ class BangDataView extends ViewInfo @override Map get createViewStatements => { SqlDialect.sqlite: - 'CREATE VIEW bang_data_view WITH BangData AS SELECT b.*, bf.frequency, bf.last_used, bi.icon_data FROM bang AS b LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" LEFT JOIN bang_icon AS bi ON b."trigger" = bi."trigger"', + 'CREATE VIEW bang_data_view AS SELECT b.*, bf.frequency, bf.last_used, bi.icon_data FROM bang AS b LEFT JOIN bang_frequency AS bf ON b."trigger" = bf."trigger" LEFT JOIN bang_icon AS bi ON b."trigger" = bi."trigger"', }; @override BangDataView get asDslTable => this; @@ -846,9 +998,9 @@ class BangDataView extends ViewInfo late final GeneratedColumn trigger = GeneratedColumn( 'trigger', aliasedName, false, type: DriftSqlType.string); - late final GeneratedColumnWithTypeConverter group = - GeneratedColumn('group', aliasedName, true, type: DriftSqlType.int) - .withConverter(BangTable.$convertergroupn); + late final GeneratedColumnWithTypeConverter group = + GeneratedColumn('group', aliasedName, false, type: DriftSqlType.int) + .withConverter(BangTable.$convertergroup); late final GeneratedColumn websiteName = GeneratedColumn( 'website_name', aliasedName, false, type: DriftSqlType.string); @@ -892,6 +1044,7 @@ abstract class _$BangDatabase extends GeneratedDatabase { _$BangDatabase(QueryExecutor e) : super(e); _$BangDatabaseManager get managers => _$BangDatabaseManager(this); late final BangTable bang = BangTable(this); + late final BangSync bangSync = BangSync(this); late final BangFrequency bangFrequency = BangFrequency(this); late final BangIcon bangIcon = BangIcon(this); late final BangFts bangFts = BangFts(this); @@ -906,6 +1059,7 @@ abstract class _$BangDatabase extends GeneratedDatabase { 'CREATE TRIGGER bang_after_update AFTER UPDATE ON bang BEGIN INSERT INTO bang_fts (bang_fts, "rowid", "trigger", website_name) VALUES (\'delete\', old."rowid", old."trigger", old.website_name);INSERT INTO bang_fts ("rowid", "trigger", website_name) VALUES (new."rowid", new."trigger", new.website_name);END', 'bang_after_update'); late final BangDao bangDao = BangDao(this as BangDatabase); + late final SyncDao syncDao = SyncDao(this as BangDatabase); Future optimizeFtsIndex() { return customInsert( 'INSERT INTO bang_fts (bang_fts) VALUES (\'optimize\')', @@ -946,6 +1100,7 @@ abstract class _$BangDatabase extends GeneratedDatabase { @override List get allSchemaEntities => [ bang, + bangSync, bangFrequency, bangIcon, bangFts, @@ -996,6 +1151,91 @@ abstract class _$BangDatabase extends GeneratedDatabase { ); } +typedef $BangSyncInsertCompanionBuilder = BangSyncCompanion Function({ + Value group, + required DateTime lastSync, +}); +typedef $BangSyncUpdateCompanionBuilder = BangSyncCompanion Function({ + Value group, + Value lastSync, +}); + +class $BangSyncTableManager extends RootTableManager< + _$BangDatabase, + BangSync, + BangSyncData, + $BangSyncFilterComposer, + $BangSyncOrderingComposer, + $BangSyncProcessedTableManager, + $BangSyncInsertCompanionBuilder, + $BangSyncUpdateCompanionBuilder> { + $BangSyncTableManager(_$BangDatabase db, BangSync table) + : super(TableManagerState( + db: db, + table: table, + filteringComposer: $BangSyncFilterComposer(ComposerState(db, table)), + orderingComposer: $BangSyncOrderingComposer(ComposerState(db, table)), + getChildManagerBuilder: (p) => $BangSyncProcessedTableManager(p), + getUpdateCompanionBuilder: ({ + Value group = const Value.absent(), + Value lastSync = const Value.absent(), + }) => + BangSyncCompanion( + group: group, + lastSync: lastSync, + ), + getInsertCompanionBuilder: ({ + Value group = const Value.absent(), + required DateTime lastSync, + }) => + BangSyncCompanion.insert( + group: group, + lastSync: lastSync, + ), + )); +} + +class $BangSyncProcessedTableManager extends ProcessedTableManager< + _$BangDatabase, + BangSync, + BangSyncData, + $BangSyncFilterComposer, + $BangSyncOrderingComposer, + $BangSyncProcessedTableManager, + $BangSyncInsertCompanionBuilder, + $BangSyncUpdateCompanionBuilder> { + $BangSyncProcessedTableManager(super.$state); +} + +class $BangSyncFilterComposer extends FilterComposer<_$BangDatabase, BangSync> { + $BangSyncFilterComposer(super.$state); + ColumnWithTypeConverterFilters get group => + $state.composableBuilder( + column: $state.table.group, + builder: (column, joinBuilders) => ColumnWithTypeConverterFilters( + column, + joinBuilders: joinBuilders)); + + ColumnFilters get lastSync => $state.composableBuilder( + column: $state.table.lastSync, + builder: (column, joinBuilders) => + ColumnFilters(column, joinBuilders: joinBuilders)); +} + +class $BangSyncOrderingComposer + extends OrderingComposer<_$BangDatabase, BangSync> { + $BangSyncOrderingComposer(super.$state); + ColumnOrderings get group => $state.composableBuilder( + column: $state.table.group, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); + + ColumnOrderings get lastSync => $state.composableBuilder( + column: $state.table.lastSync, + builder: (column, joinBuilders) => + ColumnOrderings(column, joinBuilders: joinBuilders)); +} + typedef $BangFrequencyInsertCompanionBuilder = BangFrequencyCompanion Function({ required String trigger, required int frequency, @@ -1301,6 +1541,8 @@ class $BangFtsOrderingComposer class _$BangDatabaseManager { final _$BangDatabase _db; _$BangDatabaseManager(this._db); + $BangSyncTableManager get bangSync => + $BangSyncTableManager(_db, _db.bangSync); $BangFrequencyTableManager get bangFrequency => $BangFrequencyTableManager(_db, _db.bangFrequency); $BangIconTableManager get bangIcon => diff --git a/app/lib/features/bangs/data/models/bang.dart b/app/lib/features/bangs/data/models/bang.dart index 94459ed1..30ed033c 100644 --- a/app/lib/features/bangs/data/models/bang.dart +++ b/app/lib/features/bangs/data/models/bang.dart @@ -8,6 +8,7 @@ import 'package:json_annotation/json_annotation.dart'; part 'bang.g.dart'; enum BangGroup { + general, assistant, kagi, } diff --git a/app/lib/features/bangs/domain/providers.dart b/app/lib/features/bangs/domain/providers.dart index 0a3923cb..6c4330e8 100644 --- a/app/lib/features/bangs/domain/providers.dart +++ b/app/lib/features/bangs/domain/providers.dart @@ -1,40 +1,60 @@ +import 'package:bang_navigator/features/bangs/data/models/bang.dart'; import 'package:bang_navigator/features/bangs/data/models/bang_data.dart'; -import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart'; +import 'package:bang_navigator/features/bangs/domain/repositories/bang_data.dart'; +import 'package:bang_navigator/features/bangs/domain/repositories/sync.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'providers.g.dart'; @Riverpod() -Stream bang(BangRef ref, String? trigger) { - final repository = ref.watch(bangRepositoryProvider.notifier); +Stream bangData(BangDataRef ref, String? trigger) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchBang(trigger); } @Riverpod(keepAlive: true) -Stream kagiSearchBang(KagiSearchBangRef ref) { - final repository = ref.watch(bangRepositoryProvider.notifier); +Stream kagiSearchBangData(KagiSearchBangDataRef ref) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchBang('kagi'); } @Riverpod() -Stream> allBangs(AllBangsRef ref) { - final repository = ref.watch(bangRepositoryProvider.notifier); +Stream> bangDataList(BangDataListRef ref) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchBangs(); } @Riverpod() -Stream> frequentBangs(FrequentBangsRef ref) { - final repository = ref.watch(bangRepositoryProvider.notifier); +Stream> frequentBangDataList(FrequentBangDataListRef ref) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.watchFrequentBangs(); } @Riverpod() -Future ensureIconAvailable( - EnsureIconAvailableRef ref, +Future bangDataEnsureIcon( + BangDataEnsureIconRef ref, BangData bang, ) { - final repository = ref.watch(bangRepositoryProvider.notifier); + final repository = ref.watch(bangDataRepositoryProvider.notifier); return repository.ensureIconAvailable(bang).then( (value) => value.value, ); } + +@Riverpod() +Stream lastSyncOfGroup( + LastSyncOfGroupRef ref, + BangGroup group, +) { + final repository = ref.watch(bangSyncRepositoryProvider.notifier); + return repository.watchLastSyncOfGroup(group); +} + +@Riverpod() +Stream bangCountOfGroup( + BangCountOfGroupRef ref, + BangGroup group, +) { + final repository = ref.watch(bangDataRepositoryProvider.notifier); + return repository.watchBangCount(group); +} diff --git a/app/lib/features/bangs/domain/providers.g.dart b/app/lib/features/bangs/domain/providers.g.dart index d5baf458..786c6b87 100644 --- a/app/lib/features/bangs/domain/providers.g.dart +++ b/app/lib/features/bangs/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$bangHash() => r'e3d44af52e27831b568f292d38b447a443acec95'; +String _$bangDataHash() => r'd0e96885458be8893c6b19735b8792bdd1b9bcb8'; /// Copied from Dart SDK class _SystemHash { @@ -29,27 +29,27 @@ class _SystemHash { } } -/// See also [bang]. -@ProviderFor(bang) -const bangProvider = BangFamily(); +/// See also [bangData]. +@ProviderFor(bangData) +const bangDataProvider = BangDataFamily(); -/// See also [bang]. -class BangFamily extends Family> { - /// See also [bang]. - const BangFamily(); +/// See also [bangData]. +class BangDataFamily extends Family> { + /// See also [bangData]. + const BangDataFamily(); - /// See also [bang]. - BangProvider call( + /// See also [bangData]. + BangDataProvider call( String? trigger, ) { - return BangProvider( + return BangDataProvider( trigger, ); } @override - BangProvider getProviderOverride( - covariant BangProvider provider, + BangDataProvider getProviderOverride( + covariant BangDataProvider provider, ) { return call( provider.trigger, @@ -68,29 +68,31 @@ class BangFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'bangProvider'; + String? get name => r'bangDataProvider'; } -/// See also [bang]. -class BangProvider extends AutoDisposeStreamProvider { - /// See also [bang]. - BangProvider( +/// See also [bangData]. +class BangDataProvider extends AutoDisposeStreamProvider { + /// See also [bangData]. + BangDataProvider( String? trigger, ) : this._internal( - (ref) => bang( - ref as BangRef, + (ref) => bangData( + ref as BangDataRef, trigger, ), - from: bangProvider, - name: r'bangProvider', + from: bangDataProvider, + name: r'bangDataProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$bangHash, - dependencies: BangFamily._dependencies, - allTransitiveDependencies: BangFamily._allTransitiveDependencies, + const bool.fromEnvironment('dart.vm.product') + ? null + : _$bangDataHash, + dependencies: BangDataFamily._dependencies, + allTransitiveDependencies: BangDataFamily._allTransitiveDependencies, trigger: trigger, ); - BangProvider._internal( + BangDataProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -104,12 +106,12 @@ class BangProvider extends AutoDisposeStreamProvider { @override Override overrideWith( - Stream Function(BangRef provider) create, + Stream Function(BangDataRef provider) create, ) { return ProviderOverride( origin: this, - override: BangProvider._internal( - (ref) => create(ref as BangRef), + override: BangDataProvider._internal( + (ref) => create(ref as BangDataRef), from: from, name: null, dependencies: null, @@ -122,12 +124,12 @@ class BangProvider extends AutoDisposeStreamProvider { @override AutoDisposeStreamProviderElement createElement() { - return _BangProviderElement(this); + return _BangDataProviderElement(this); } @override bool operator ==(Object other) { - return other is BangProvider && other.trigger == trigger; + return other is BangDataProvider && other.trigger == trigger; } @override @@ -139,88 +141,90 @@ class BangProvider extends AutoDisposeStreamProvider { } } -mixin BangRef on AutoDisposeStreamProviderRef { +mixin BangDataRef on AutoDisposeStreamProviderRef { /// The parameter `trigger` of this provider. String? get trigger; } -class _BangProviderElement extends AutoDisposeStreamProviderElement - with BangRef { - _BangProviderElement(super.provider); +class _BangDataProviderElement + extends AutoDisposeStreamProviderElement with BangDataRef { + _BangDataProviderElement(super.provider); @override - String? get trigger => (origin as BangProvider).trigger; + String? get trigger => (origin as BangDataProvider).trigger; } -String _$kagiSearchBangHash() => r'ec830f26cf1d23e1e75747e1523cb1868e77d40b'; +String _$kagiSearchBangDataHash() => + r'4dbeaab0541d7c8214d097523d139eede2e3f975'; -/// See also [kagiSearchBang]. -@ProviderFor(kagiSearchBang) -final kagiSearchBangProvider = StreamProvider.internal( - kagiSearchBang, - name: r'kagiSearchBangProvider', +/// See also [kagiSearchBangData]. +@ProviderFor(kagiSearchBangData) +final kagiSearchBangDataProvider = StreamProvider.internal( + kagiSearchBangData, + name: r'kagiSearchBangDataProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$kagiSearchBangHash, + : _$kagiSearchBangDataHash, dependencies: null, allTransitiveDependencies: null, ); -typedef KagiSearchBangRef = StreamProviderRef; -String _$allBangsHash() => r'212201f81e21fd0c1b5fb192359fb6ff8269a2a6'; +typedef KagiSearchBangDataRef = StreamProviderRef; +String _$bangDataListHash() => r'f5544ce7b3cc0f3e0fc3db25a99456c3cd080c1c'; -/// See also [allBangs]. -@ProviderFor(allBangs) -final allBangsProvider = AutoDisposeStreamProvider>.internal( - allBangs, - name: r'allBangsProvider', +/// See also [bangDataList]. +@ProviderFor(bangDataList) +final bangDataListProvider = AutoDisposeStreamProvider>.internal( + bangDataList, + name: r'bangDataListProvider', debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') ? null : _$allBangsHash, + const bool.fromEnvironment('dart.vm.product') ? null : _$bangDataListHash, dependencies: null, allTransitiveDependencies: null, ); -typedef AllBangsRef = AutoDisposeStreamProviderRef>; -String _$frequentBangsHash() => r'12867819c6e2bbc5f2b6342aacc286f62d3a1803'; +typedef BangDataListRef = AutoDisposeStreamProviderRef>; +String _$frequentBangDataListHash() => + r'adf8d0b72adfa8faffadd7f1a294ce3945e22fec'; -/// See also [frequentBangs]. -@ProviderFor(frequentBangs) -final frequentBangsProvider = +/// See also [frequentBangDataList]. +@ProviderFor(frequentBangDataList) +final frequentBangDataListProvider = AutoDisposeStreamProvider>.internal( - frequentBangs, - name: r'frequentBangsProvider', + frequentBangDataList, + name: r'frequentBangDataListProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$frequentBangsHash, + : _$frequentBangDataListHash, dependencies: null, allTransitiveDependencies: null, ); -typedef FrequentBangsRef = AutoDisposeStreamProviderRef>; -String _$ensureIconAvailableHash() => - r'2e112c88da8cf9bf12bdc5e5d525d59fe97a9419'; +typedef FrequentBangDataListRef = AutoDisposeStreamProviderRef>; +String _$bangDataEnsureIconHash() => + r'69b59b582a7b165b8076a1478d0d433ec0846ccb'; -/// See also [ensureIconAvailable]. -@ProviderFor(ensureIconAvailable) -const ensureIconAvailableProvider = EnsureIconAvailableFamily(); +/// See also [bangDataEnsureIcon]. +@ProviderFor(bangDataEnsureIcon) +const bangDataEnsureIconProvider = BangDataEnsureIconFamily(); -/// See also [ensureIconAvailable]. -class EnsureIconAvailableFamily extends Family> { - /// See also [ensureIconAvailable]. - const EnsureIconAvailableFamily(); +/// See also [bangDataEnsureIcon]. +class BangDataEnsureIconFamily extends Family> { + /// See also [bangDataEnsureIcon]. + const BangDataEnsureIconFamily(); - /// See also [ensureIconAvailable]. - EnsureIconAvailableProvider call( + /// See also [bangDataEnsureIcon]. + BangDataEnsureIconProvider call( BangData bang, ) { - return EnsureIconAvailableProvider( + return BangDataEnsureIconProvider( bang, ); } @override - EnsureIconAvailableProvider getProviderOverride( - covariant EnsureIconAvailableProvider provider, + BangDataEnsureIconProvider getProviderOverride( + covariant BangDataEnsureIconProvider provider, ) { return call( provider.bang, @@ -239,32 +243,32 @@ class EnsureIconAvailableFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'ensureIconAvailableProvider'; + String? get name => r'bangDataEnsureIconProvider'; } -/// See also [ensureIconAvailable]. -class EnsureIconAvailableProvider extends AutoDisposeFutureProvider { - /// See also [ensureIconAvailable]. - EnsureIconAvailableProvider( +/// See also [bangDataEnsureIcon]. +class BangDataEnsureIconProvider extends AutoDisposeFutureProvider { + /// See also [bangDataEnsureIcon]. + BangDataEnsureIconProvider( BangData bang, ) : this._internal( - (ref) => ensureIconAvailable( - ref as EnsureIconAvailableRef, + (ref) => bangDataEnsureIcon( + ref as BangDataEnsureIconRef, bang, ), - from: ensureIconAvailableProvider, - name: r'ensureIconAvailableProvider', + from: bangDataEnsureIconProvider, + name: r'bangDataEnsureIconProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$ensureIconAvailableHash, - dependencies: EnsureIconAvailableFamily._dependencies, + : _$bangDataEnsureIconHash, + dependencies: BangDataEnsureIconFamily._dependencies, allTransitiveDependencies: - EnsureIconAvailableFamily._allTransitiveDependencies, + BangDataEnsureIconFamily._allTransitiveDependencies, bang: bang, ); - EnsureIconAvailableProvider._internal( + BangDataEnsureIconProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -278,12 +282,12 @@ class EnsureIconAvailableProvider extends AutoDisposeFutureProvider { @override Override overrideWith( - FutureOr Function(EnsureIconAvailableRef provider) create, + FutureOr Function(BangDataEnsureIconRef provider) create, ) { return ProviderOverride( origin: this, - override: EnsureIconAvailableProvider._internal( - (ref) => create(ref as EnsureIconAvailableRef), + override: BangDataEnsureIconProvider._internal( + (ref) => create(ref as BangDataEnsureIconRef), from: from, name: null, dependencies: null, @@ -296,12 +300,12 @@ class EnsureIconAvailableProvider extends AutoDisposeFutureProvider { @override AutoDisposeFutureProviderElement createElement() { - return _EnsureIconAvailableProviderElement(this); + return _BangDataEnsureIconProviderElement(this); } @override bool operator ==(Object other) { - return other is EnsureIconAvailableProvider && other.bang == bang; + return other is BangDataEnsureIconProvider && other.bang == bang; } @override @@ -313,18 +317,275 @@ class EnsureIconAvailableProvider extends AutoDisposeFutureProvider { } } -mixin EnsureIconAvailableRef on AutoDisposeFutureProviderRef { +mixin BangDataEnsureIconRef on AutoDisposeFutureProviderRef { /// The parameter `bang` of this provider. BangData get bang; } -class _EnsureIconAvailableProviderElement +class _BangDataEnsureIconProviderElement extends AutoDisposeFutureProviderElement - with EnsureIconAvailableRef { - _EnsureIconAvailableProviderElement(super.provider); + with BangDataEnsureIconRef { + _BangDataEnsureIconProviderElement(super.provider); @override - BangData get bang => (origin as EnsureIconAvailableProvider).bang; + BangData get bang => (origin as BangDataEnsureIconProvider).bang; +} + +String _$lastSyncOfGroupHash() => r'4906dc24970eccda89d94697f98cf70e241ea4ec'; + +/// See also [lastSyncOfGroup]. +@ProviderFor(lastSyncOfGroup) +const lastSyncOfGroupProvider = LastSyncOfGroupFamily(); + +/// See also [lastSyncOfGroup]. +class LastSyncOfGroupFamily extends Family> { + /// See also [lastSyncOfGroup]. + const LastSyncOfGroupFamily(); + + /// See also [lastSyncOfGroup]. + LastSyncOfGroupProvider call( + BangGroup group, + ) { + return LastSyncOfGroupProvider( + group, + ); + } + + @override + LastSyncOfGroupProvider getProviderOverride( + covariant LastSyncOfGroupProvider provider, + ) { + return call( + provider.group, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'lastSyncOfGroupProvider'; +} + +/// See also [lastSyncOfGroup]. +class LastSyncOfGroupProvider extends AutoDisposeStreamProvider { + /// See also [lastSyncOfGroup]. + LastSyncOfGroupProvider( + BangGroup group, + ) : this._internal( + (ref) => lastSyncOfGroup( + ref as LastSyncOfGroupRef, + group, + ), + from: lastSyncOfGroupProvider, + name: r'lastSyncOfGroupProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$lastSyncOfGroupHash, + dependencies: LastSyncOfGroupFamily._dependencies, + allTransitiveDependencies: + LastSyncOfGroupFamily._allTransitiveDependencies, + group: group, + ); + + LastSyncOfGroupProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.group, + }) : super.internal(); + + final BangGroup group; + + @override + Override overrideWith( + Stream Function(LastSyncOfGroupRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: LastSyncOfGroupProvider._internal( + (ref) => create(ref as LastSyncOfGroupRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + group: group, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _LastSyncOfGroupProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is LastSyncOfGroupProvider && other.group == group; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, group.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin LastSyncOfGroupRef on AutoDisposeStreamProviderRef { + /// The parameter `group` of this provider. + BangGroup get group; +} + +class _LastSyncOfGroupProviderElement + extends AutoDisposeStreamProviderElement + with LastSyncOfGroupRef { + _LastSyncOfGroupProviderElement(super.provider); + + @override + BangGroup get group => (origin as LastSyncOfGroupProvider).group; +} + +String _$bangCountOfGroupHash() => r'10b363679230c0abf6a6ba7d0be2d0367a7ed215'; + +/// See also [bangCountOfGroup]. +@ProviderFor(bangCountOfGroup) +const bangCountOfGroupProvider = BangCountOfGroupFamily(); + +/// See also [bangCountOfGroup]. +class BangCountOfGroupFamily extends Family> { + /// See also [bangCountOfGroup]. + const BangCountOfGroupFamily(); + + /// See also [bangCountOfGroup]. + BangCountOfGroupProvider call( + BangGroup group, + ) { + return BangCountOfGroupProvider( + group, + ); + } + + @override + BangCountOfGroupProvider getProviderOverride( + covariant BangCountOfGroupProvider provider, + ) { + return call( + provider.group, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'bangCountOfGroupProvider'; +} + +/// See also [bangCountOfGroup]. +class BangCountOfGroupProvider extends AutoDisposeStreamProvider { + /// See also [bangCountOfGroup]. + BangCountOfGroupProvider( + BangGroup group, + ) : this._internal( + (ref) => bangCountOfGroup( + ref as BangCountOfGroupRef, + group, + ), + from: bangCountOfGroupProvider, + name: r'bangCountOfGroupProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$bangCountOfGroupHash, + dependencies: BangCountOfGroupFamily._dependencies, + allTransitiveDependencies: + BangCountOfGroupFamily._allTransitiveDependencies, + group: group, + ); + + BangCountOfGroupProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.group, + }) : super.internal(); + + final BangGroup group; + + @override + Override overrideWith( + Stream Function(BangCountOfGroupRef provider) create, + ) { + return ProviderOverride( + origin: this, + override: BangCountOfGroupProvider._internal( + (ref) => create(ref as BangCountOfGroupRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + group: group, + ), + ); + } + + @override + AutoDisposeStreamProviderElement createElement() { + return _BangCountOfGroupProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is BangCountOfGroupProvider && other.group == group; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, group.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin BangCountOfGroupRef on AutoDisposeStreamProviderRef { + /// The parameter `group` of this provider. + BangGroup get group; +} + +class _BangCountOfGroupProviderElement + extends AutoDisposeStreamProviderElement with BangCountOfGroupRef { + _BangCountOfGroupProviderElement(super.provider); + + @override + BangGroup get group => (origin as BangCountOfGroupProvider).group; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/bangs/domain/repositories/bang.dart b/app/lib/features/bangs/domain/repositories/bang.dart deleted file mode 100644 index e49c111d..00000000 --- a/app/lib/features/bangs/domain/repositories/bang.dart +++ /dev/null @@ -1,154 +0,0 @@ -import 'package:bang_navigator/domain/services/generic_website.dart'; -import 'package:bang_navigator/features/bangs/data/database/database.dart'; -import 'package:bang_navigator/features/bangs/data/models/bang.dart'; -import 'package:bang_navigator/features/bangs/data/models/bang_data.dart'; -import 'package:bang_navigator/features/bangs/data/providers.dart'; -import 'package:bang_navigator/features/bangs/data/services/bang_source.dart'; -import 'package:bang_navigator/utils/image_helper.dart'; -import 'package:drift/isolate.dart'; -import 'package:exceptions/exceptions.dart'; -import 'package:riverpod/riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'bang.g.dart'; - -Future> _syncBangGroupStatic( - BangSource source, - BangDatabase db, - Uri url, - BangGroup? group, -) async { - final result = await source.getBangs(url, group); - - return result.flatMapAsync( - (value) async { - final sourcedBangMap = - Map.fromEntries(value.map((e) => MapEntry(e.trigger, e))); - final localBangMap = - await db.bangDao.getBangs(groups: [group]).get().then( - (bangs) => - Map.fromEntries(bangs.map((e) => MapEntry(e.trigger, e))), - ); - - final sourcedBangTriggers = sourcedBangMap.keys.toSet(); - final localBangTriggers = localBangMap.keys.toSet(); - - final removedBangs = localBangTriggers.difference(sourcedBangTriggers); - final addedBangs = sourcedBangTriggers.difference(localBangTriggers).map( - (e) => sourcedBangMap[e]!, - ); - - final changedBangs = sourcedBangTriggers - .intersection(localBangTriggers) - .where((e) => sourcedBangMap[e] != localBangMap[e]) - .map( - (e) => sourcedBangMap[e]!, - ); - - await db.bangDao.deleteBangs(removedBangs); - await db.bangDao.insertBangs(addedBangs); - await db.bangDao.replaceBangs(changedBangs); - - await db.optimizeFtsIndex(); - }, - ); -} - -@Riverpod(keepAlive: true) -class BangRepository extends _$BangRepository { - late BangDatabase _db; - - @override - void build() { - _db = ref.watch(bangDatabaseProvider); - } - - Stream watchBang(String? trigger) { - if (trigger != null) { - return _db.bangDao.getBangData(trigger).watchSingleOrNull(); - } else { - return Stream.value(null); - } - } - - Stream> watchBangs({Iterable? groups}) { - return _db.bangDao.getBangDataList(groups: groups).watch(); - } - - Stream> watchFrequentBangs({Iterable? groups}) { - return _db.bangDao.getFrequentBangDataList(groups: groups).watch(); - } - - Future increaseFrequency(String trigger) { - return _db.bangDao.increaseFrequency(trigger); - } - - Future> ensureIconAvailable(BangData bang) async { - if (bang.iconData != null) { - return Result.success(bang); - } - - final result = await ref - .read(genericWebsiteServiceProvider.notifier) - .getFaviconBytes(Uri.parse(bang.getUrl('').origin)); - - return result.flatMapAsync( - (faviconBytes) async { - if (faviconBytes != null && await isImageValid(faviconBytes)) { - await _db.bangDao.upsertIcon(bang.trigger, faviconBytes); - return bang.copyWith.iconData(faviconBytes); - } - - return bang; - }, - ); - } - - Future> syncBangGroup( - Uri url, - BangGroup? group, - ) => - _syncBangGroupStatic( - ref.read(bangSourceProvider.notifier), - _db, - url, - group, - ); - - Future syncAllBangGroups() async { - await _db.computeWithDatabase( - connect: BangDatabase.new, - computation: (db) async { - final ref = ProviderContainer(); - final source = ref.read(bangSourceProvider.notifier); - - await _syncBangGroupStatic( - source, - db, - Uri.parse( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/bangs.json', - ), - null, - ); - - await _syncBangGroupStatic( - source, - db, - Uri.parse( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/kagi_bangs.json', - ), - BangGroup.kagi, - ); - - await _syncBangGroupStatic( - source, - db, - Uri.parse( - 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/assistant_bangs.json', - ), - BangGroup.assistant, - ); - }, - ); - } -} diff --git a/app/lib/features/bangs/domain/repositories/bang_data.dart b/app/lib/features/bangs/domain/repositories/bang_data.dart new file mode 100644 index 00000000..7c61605c --- /dev/null +++ b/app/lib/features/bangs/domain/repositories/bang_data.dart @@ -0,0 +1,74 @@ +import 'package:bang_navigator/domain/services/generic_website.dart'; +import 'package:bang_navigator/features/bangs/data/database/database.dart'; +import 'package:bang_navigator/features/bangs/data/models/bang.dart'; +import 'package:bang_navigator/features/bangs/data/models/bang_data.dart'; +import 'package:bang_navigator/features/bangs/data/providers.dart'; +import 'package:bang_navigator/utils/image_helper.dart'; +import 'package:drift/drift.dart'; +import 'package:exceptions/exceptions.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'bang_data.g.dart'; + +@Riverpod(keepAlive: true) +class BangDataRepository extends _$BangDataRepository { + late BangDatabase _db; + + @override + void build() { + _db = ref.watch(bangDatabaseProvider); + } + + Stream watchBang(String? trigger) { + if (trigger != null) { + return _db.bangDao.getBangData(trigger).watchSingleOrNull(); + } else { + return Stream.value(null); + } + } + + Stream watchBangCount(BangGroup group) { + return _db.bangDao.getBangCount(groups: [group]).watchSingle(); + } + + Stream> watchBangs({Iterable? groups}) { + return _db.bangDao.getBangDataList(groups: groups).watch(); + } + + Stream> watchFrequentBangs({Iterable? groups}) { + return _db.bangDao.getFrequentBangDataList(groups: groups).watch(); + } + + Future increaseFrequency(String trigger) { + return _db.bangDao.increaseBangFrequency(trigger); + } + + Future> ensureIconAvailable(BangData bang) async { + if (bang.iconData != null) { + return Result.success(bang); + } + + final result = await ref + .read(genericWebsiteServiceProvider.notifier) + .getFaviconBytes(Uri.parse(bang.getUrl('').origin)); + + return result.flatMapAsync( + (faviconBytes) async { + if (faviconBytes != null && await isImageValid(faviconBytes)) { + await _db.bangDao.upsertBangIcon(bang.trigger, faviconBytes); + return bang.copyWith.iconData(faviconBytes); + } + + return bang; + }, + ); + } + + Future clearIconData() { + return _db.bangIcon.deleteAll(); + } + + Future clearFrequency() { + return _db.bangFrequency.deleteAll(); + } +} diff --git a/app/lib/features/bangs/domain/repositories/bang_data.g.dart b/app/lib/features/bangs/domain/repositories/bang_data.g.dart new file mode 100644 index 00000000..4f2e52f0 --- /dev/null +++ b/app/lib/features/bangs/domain/repositories/bang_data.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'bang_data.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$bangDataRepositoryHash() => + r'6ac3a59546f6d7e22f3c37aacd43a3579978eb50'; + +/// See also [BangDataRepository]. +@ProviderFor(BangDataRepository) +final bangDataRepositoryProvider = + NotifierProvider.internal( + BangDataRepository.new, + name: r'bangDataRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$bangDataRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$BangDataRepository = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/bangs/domain/repositories/sync.dart b/app/lib/features/bangs/domain/repositories/sync.dart new file mode 100644 index 00000000..040d9885 --- /dev/null +++ b/app/lib/features/bangs/domain/repositories/sync.dart @@ -0,0 +1,133 @@ +import 'package:bang_navigator/features/bangs/data/database/database.dart'; +import 'package:bang_navigator/features/bangs/data/models/bang.dart'; +import 'package:bang_navigator/features/bangs/data/providers.dart'; +import 'package:bang_navigator/features/bangs/data/services/bang_source.dart'; +import 'package:drift/isolate.dart'; +import 'package:exceptions/exceptions.dart'; +import 'package:riverpod/riverpod.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'sync.g.dart'; + +@Riverpod(keepAlive: true) +class BangSyncRepository extends _$BangSyncRepository { + late BangDatabase _db; + + BangSyncRepository(); + + static Future> _fetchAndSync({ + required BangSource sourceRepository, + required BangDatabase db, + required Uri url, + required BangGroup group, + required Duration? syncInterval, + }) async { + if (syncInterval != null) { + final lastSync = + await db.syncDao.lastSyncOfGroup(group).getSingleOrNull(); + + if (lastSync != null && + lastSync.difference(DateTime.now()) < syncInterval) { + return Result.success(null); + } + } + + final result = await sourceRepository.getBangs(url, group); + return result.flatMapAsync( + (remoteBangs) async { + await db.syncDao.syncBangs( + group: group, + remoteBangs: remoteBangs, + syncTime: DateTime.now(), + ); + await db.optimizeFtsIndex(); + }, + ); + } + + Future> _syncBangGroup( + BangGroup group, + Uri url, + Duration? syncInterval, + ) async { + try { + return Result.success( + await _db.computeWithDatabase( + connect: BangDatabase.new, + computation: (db) async { + final ref = ProviderContainer(); + final result = await _fetchAndSync( + sourceRepository: ref.read(bangSourceProvider.notifier), + db: db, + url: url, + 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, + ), + ); + } + } + + Stream watchLastSyncOfGroup(BangGroup group) { + return _db.syncDao.lastSyncOfGroup(group).watchSingleOrNull(); + } + + Future> syncGeneralBangs({Duration? syncInterval}) => + _syncBangGroup( + BangGroup.general, + Uri.parse( + 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/bangs.json', + ), + syncInterval, + ); + + Future> syncKagiBangs({Duration? syncInterval}) => + _syncBangGroup( + BangGroup.kagi, + Uri.parse( + 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/kagi_bangs.json', + ), + syncInterval, + ); + + Future> syncAssistantBangs({Duration? syncInterval}) => + _syncBangGroup( + BangGroup.assistant, + Uri.parse( + 'https://raw.githubusercontent.com/kagisearch/bangs/main/data/assistant_bangs.json', + ), + syncInterval, + ); + + Future>> syncAllBangGroups({ + Duration? syncInterval, + }) async { + //Run isolated operations + final generalSync = syncGeneralBangs(syncInterval: syncInterval); + final assistantSync = syncAssistantBangs(syncInterval: syncInterval); + final kagiSync = syncKagiBangs(syncInterval: syncInterval); + + return { + BangGroup.general: await generalSync, + BangGroup.assistant: await assistantSync, + BangGroup.kagi: await kagiSync, + }; + } + + @override + void build() { + _db = ref.watch(bangDatabaseProvider); + } +} diff --git a/app/lib/features/bangs/domain/repositories/bang.g.dart b/app/lib/features/bangs/domain/repositories/sync.g.dart similarity index 55% rename from app/lib/features/bangs/domain/repositories/bang.g.dart rename to app/lib/features/bangs/domain/repositories/sync.g.dart index 75b6b327..3da6ecdf 100644 --- a/app/lib/features/bangs/domain/repositories/bang.g.dart +++ b/app/lib/features/bangs/domain/repositories/sync.g.dart @@ -1,25 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'bang.dart'; +part of 'sync.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$bangRepositoryHash() => r'd405550e2ad11c57f3644f18390348efb71bdf0f'; +String _$bangSyncRepositoryHash() => + r'79b9c561638e96eb404ceeecb4b50dec31f0c610'; -/// See also [BangRepository]. -@ProviderFor(BangRepository) -final bangRepositoryProvider = NotifierProvider.internal( - BangRepository.new, - name: r'bangRepositoryProvider', +/// See also [BangSyncRepository]. +@ProviderFor(BangSyncRepository) +final bangSyncRepositoryProvider = + NotifierProvider.internal( + BangSyncRepository.new, + name: r'bangSyncRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$bangRepositoryHash, + : _$bangSyncRepositoryHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$BangRepository = Notifier; +typedef _$BangSyncRepository = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/bangs/presentation/screens/bang.dart b/app/lib/features/bangs/presentation/screens/bang.dart index 8d52e957..e7c259d5 100644 --- a/app/lib/features/bangs/presentation/screens/bang.dart +++ b/app/lib/features/bangs/presentation/screens/bang.dart @@ -12,7 +12,7 @@ class BangScreen extends HookConsumerWidget { const BangScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final bangsAsync = ref.watch(allBangsProvider); + final bangsAsync = ref.watch(bangDataListProvider); return Scaffold( appBar: AppBar( diff --git a/app/lib/features/bangs/presentation/widgets/bang_icon.dart b/app/lib/features/bangs/presentation/widgets/bang_icon.dart index c6202220..45e8850b 100644 --- a/app/lib/features/bangs/presentation/widgets/bang_icon.dart +++ b/app/lib/features/bangs/presentation/widgets/bang_icon.dart @@ -13,7 +13,7 @@ class BangIcon extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final bangAsync = ref.watch(ensureIconAvailableProvider(bangData)); + final bangAsync = ref.watch(bangDataEnsureIconProvider(bangData)); final bang = bangAsync.valueOrNull ?? bangData; return Skeletonizer( diff --git a/app/lib/features/search_browser/domain/providers.dart b/app/lib/features/search_browser/domain/providers.dart index 815ba32c..d9bedfce 100644 --- a/app/lib/features/search_browser/domain/providers.dart +++ b/app/lib/features/search_browser/domain/providers.dart @@ -3,7 +3,7 @@ import 'dart:async'; import 'package:bang_navigator/features/bangs/data/models/bang_data.dart'; -import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart'; +import 'package:bang_navigator/features/bangs/domain/repositories/bang_data.dart'; import 'package:bang_navigator/features/search_browser/domain/entities/sheet.dart'; import 'package:bang_navigator/features/search_browser/domain/services/create_tab.dart'; import 'package:flutter/widgets.dart'; @@ -86,7 +86,7 @@ class SelectedBangTrigger extends _$SelectedBangTrigger { @Riverpod() Stream selectedBangData(SelectedBangDataRef ref) { - final repository = ref.watch(bangRepositoryProvider.notifier); + final repository = ref.watch(bangDataRepositoryProvider.notifier); final selectedBangTrigger = ref.watch(selectedBangTriggerProvider); return repository.watchBang(selectedBangTrigger); } diff --git a/app/lib/features/search_browser/domain/providers.g.dart b/app/lib/features/search_browser/domain/providers.g.dart index 1bf33c81..ee1a07ee 100644 --- a/app/lib/features/search_browser/domain/providers.g.dart +++ b/app/lib/features/search_browser/domain/providers.g.dart @@ -6,7 +6,7 @@ part of 'providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$selectedBangDataHash() => r'6d99a08f4a02027cc007ee1df30e5ac7923ca5f5'; +String _$selectedBangDataHash() => r'619555a048e9d38602ceffa42dae1ba7d68ced89'; /// See also [selectedBangData]. @ProviderFor(selectedBangData) diff --git a/app/lib/features/search_browser/presentation/widgets/bang_chips.dart b/app/lib/features/search_browser/presentation/widgets/bang_chips.dart index b3f5dc7d..3e4f3630 100644 --- a/app/lib/features/search_browser/presentation/widgets/bang_chips.dart +++ b/app/lib/features/search_browser/presentation/widgets/bang_chips.dart @@ -22,7 +22,7 @@ class BangChips extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final frequentBangsAsync = ref.watch(frequentBangsProvider); + final frequentBangsAsync = ref.watch(frequentBangDataListProvider); return frequentBangsAsync.when( data: (bangs) { diff --git a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart b/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart index 291cbf3f..d1324dfc 100644 --- a/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart +++ b/app/lib/features/search_browser/presentation/widgets/tabs/search_tab.dart @@ -1,5 +1,5 @@ import 'package:bang_navigator/features/bangs/domain/providers.dart'; -import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart'; +import 'package:bang_navigator/features/bangs/domain/repositories/bang_data.dart'; import 'package:bang_navigator/features/bangs/presentation/widgets/bang_icon.dart'; import 'package:bang_navigator/features/kagi/domain/repositories/autosuggest.dart'; import 'package:bang_navigator/features/search_browser/domain/providers.dart'; @@ -109,7 +109,7 @@ class SearchTab extends HookConsumerWidget { final focusNode = useFocusNode(); final selectedBangAsync = ref.watch(selectedBangDataProvider); - final defaultSearchBangAsync = ref.watch(kagiSearchBangProvider); + final defaultSearchBangAsync = ref.watch(kagiSearchBangDataProvider); final activeBang = selectedBangAsync.valueOrNull ?? defaultSearchBangAsync.valueOrNull; @@ -231,7 +231,7 @@ class SearchTab extends HookConsumerWidget { if (activeBang != null && (formKey.currentState?.validate() == true)) { await ref - .read(bangRepositoryProvider.notifier) + .read(bangDataRepositoryProvider.notifier) .increaseFrequency(activeBang.trigger); onSubmit(activeBang.getUrl(textController.text)); @@ -266,7 +266,7 @@ class SearchTab extends HookConsumerWidget { if (activeBang != null && (formKey.currentState?.validate() == true)) { await ref - .read(bangRepositoryProvider.notifier) + .read(bangDataRepositoryProvider.notifier) .increaseFrequency(activeBang.trigger); onSubmit(activeBang.getUrl(textController.text)); diff --git a/app/lib/features/web_view/presentation/widgets/web_view.dart b/app/lib/features/web_view/presentation/widgets/web_view.dart index 637d7cc5..578eaf07 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -158,7 +158,7 @@ class _WebViewState extends ConsumerState { if (selectedText != null && selectedText.isNotEmpty) { final searchBang = - await ref.read(kagiSearchBangProvider.future); + await ref.read(kagiSearchBangDataProvider.future); await ref .read(switchNewTabControllerProvider.notifier) diff --git a/app/lib/main.dart b/app/lib/main.dart index 2e530d0c..caec13bf 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -1,7 +1,7 @@ import 'package:bang_navigator/core/logger.dart'; import 'package:bang_navigator/core/providers.dart'; import 'package:bang_navigator/features/about/data/repositories/package_info_repository.dart'; -import 'package:bang_navigator/features/bangs/domain/repositories/bang.dart'; +import 'package:bang_navigator/features/bangs/domain/repositories/sync.dart'; import 'package:bang_navigator/features/search_browser/domain/services/session.dart'; import 'package:bang_navigator/features/settings/data/repositories/settings_repository.dart'; import 'package:bang_navigator/presentation/hooks/on_initialization.dart'; @@ -42,8 +42,8 @@ void main() async { await ref.read(packageInfoProvider.future); await ref - .read(bangRepositoryProvider.notifier) - .syncAllBangGroups(); + .read(bangSyncRepositoryProvider.notifier) + .syncAllBangGroups(syncInterval: const Duration(days: 7)); final settings = await ref.read(settingsRepositoryProvider.future);