prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
/*
|
||||
* 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:drift/drift.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.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';
|
||||
|
||||
@DriftAccessor()
|
||||
class BangDao extends DatabaseAccessor<BangDatabase> with $BangDaoMixin {
|
||||
BangDao(super.db);
|
||||
|
||||
Selectable<Bang> getBangList({Iterable<BangGroup>? groups}) {
|
||||
final selectable = select(db.bang);
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
SingleSelectable<int> getBangCount({Iterable<BangGroup>? groups}) {
|
||||
return db.bang.count(
|
||||
where: groups.mapNotNull(
|
||||
(groups) =>
|
||||
(t) => t.group.isInValues(groups),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> upsertBang(Bang bang) {
|
||||
return db.bang.insertOne(bang, mode: InsertMode.insertOrReplace);
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<BangData> getBangData(
|
||||
BangGroup group,
|
||||
String trigger,
|
||||
) {
|
||||
return select(db.bangDataView)
|
||||
..where((t) => t.group.equalsValue(group) & t.trigger.equals(trigger));
|
||||
}
|
||||
|
||||
Selectable<BangData> getBangDataList({
|
||||
Iterable<String>? triggers,
|
||||
Iterable<BangGroup>? groups,
|
||||
String? domain,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
bool? orderMostFrequentFirst,
|
||||
}) {
|
||||
final selectable = select(db.bangDataView);
|
||||
if (triggers != null) {
|
||||
selectable.where((t) => t.trigger.isIn(triggers));
|
||||
}
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
if (domain != null) {
|
||||
selectable.where((t) => t.domain.equals(domain));
|
||||
}
|
||||
if (category != null) {
|
||||
selectable.where((t) => t.category.equals(category));
|
||||
|
||||
if (subCategory != null) {
|
||||
selectable.where((t) => t.subCategory.equals(subCategory));
|
||||
}
|
||||
}
|
||||
|
||||
selectable.orderBy([
|
||||
if (orderMostFrequentFirst == true) (t) => OrderingTerm.desc(t.frequency),
|
||||
(t) => OrderingTerm.asc(t.websiteName),
|
||||
]);
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
Selectable<BangData> getFrequentBangDataList({Iterable<BangGroup>? groups}) {
|
||||
final selectable = select(db.bangDataView)
|
||||
..where((t) => t.frequency.isBiggerThanValue(0));
|
||||
|
||||
if (groups != null) {
|
||||
selectable.where((t) => t.group.isInValues(groups));
|
||||
}
|
||||
|
||||
selectable.orderBy([
|
||||
(t) => OrderingTerm.desc(t.frequency),
|
||||
(t) => OrderingTerm.desc(t.lastUsed),
|
||||
]);
|
||||
|
||||
return selectable;
|
||||
}
|
||||
|
||||
Future<int> increaseBangFrequency(BangKey key) {
|
||||
return db.bangFrequency.insertOne(
|
||||
BangFrequencyCompanion.insert(
|
||||
trigger: key.trigger,
|
||||
group: key.group,
|
||||
frequency: 1,
|
||||
lastUsed: DateTime.now(),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => BangFrequencyCompanion.custom(
|
||||
frequency: old.frequency + const Constant(1),
|
||||
lastUsed: Variable(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<BangData> queryBangs(String searchString) {
|
||||
final ftsQuery = db.buildFtsQuery(searchString);
|
||||
|
||||
if (ftsQuery.isNotEmpty) {
|
||||
return db.definitionsDrift.queryBangs(query: ftsQuery);
|
||||
} else {
|
||||
return db.definitionsDrift.queryBangsBasic(
|
||||
query: db.buildLikeQuery(searchString),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<int> addSearchEntry(
|
||||
BangGroup group,
|
||||
String trigger,
|
||||
String searchQuery,
|
||||
) {
|
||||
return db.bangHistory.insertOne(
|
||||
BangHistoryCompanion.insert(
|
||||
searchQuery: searchQuery,
|
||||
trigger: trigger,
|
||||
group: group,
|
||||
searchDate: DateTime.now(),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
target: [db.bangHistory.searchQuery],
|
||||
(old) => BangHistoryCompanion(
|
||||
trigger: Value(trigger),
|
||||
group: Value(group),
|
||||
searchDate: Value(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<int> removeSearchEntry(String searchQuery) {
|
||||
return db.bangHistory.deleteWhere((t) => t.searchQuery.equals(searchQuery));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i1;
|
||||
|
||||
mixin $BangDaoMixin on i0.DatabaseAccessor<i1.BangDatabase> {
|
||||
BangDaoManager get managers => BangDaoManager(this);
|
||||
}
|
||||
|
||||
class BangDaoManager {
|
||||
final $BangDaoMixin _db;
|
||||
BangDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 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:drift/drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/sync.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class SyncDao extends DatabaseAccessor<BangDatabase> with $SyncDaoMixin {
|
||||
SyncDao(super.db);
|
||||
|
||||
SingleOrNullSelectable<DateTime?> getLastSyncOfGroup(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<void> upsertLastSyncOfGroup(BangGroup group, DateTime lastSync) {
|
||||
return db.bangSync.insertOne(
|
||||
BangSyncCompanion.insert(group: Value(group), lastSync: lastSync),
|
||||
onConflict: DoUpdate(
|
||||
(old) => BangSyncCompanion(lastSync: Value(lastSync)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> insertBangs(Iterable<Bang> bangs) {
|
||||
return db.bang.insertAll(bangs);
|
||||
}
|
||||
|
||||
Future<void> replaceBangs(Iterable<Bang> bangs) {
|
||||
return batch((batch) {
|
||||
batch.replaceAll(db.bang, bangs);
|
||||
});
|
||||
}
|
||||
|
||||
Future<int> deleteBangs(BangGroup group, Iterable<String> triggers) {
|
||||
final statement = delete(db.bang)
|
||||
..where((t) => t.group.equalsValue(group) & t.trigger.isIn(triggers));
|
||||
return statement.go();
|
||||
}
|
||||
|
||||
Future<void> syncBangs({
|
||||
required BangGroup group,
|
||||
required Iterable<Bang> 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(group, removedBangs);
|
||||
await insertBangs(addedBangs);
|
||||
await replaceBangs(changedBangs);
|
||||
await upsertLastSyncOfGroup(group, syncTime);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i1;
|
||||
|
||||
mixin $SyncDaoMixin on i0.DatabaseAccessor<i1.BangDatabase> {
|
||||
SyncDaoManager get managers => SyncDaoManager(this);
|
||||
}
|
||||
|
||||
class SyncDaoManager {
|
||||
final $SyncDaoMixin _db;
|
||||
SyncDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
/*
|
||||
* 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:drift/drift.dart';
|
||||
import 'package:drift/internal/versioned_schema.dart';
|
||||
import 'package:drift_dev/api/migrations_native.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/daos/sync.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.steps.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/search/domain/fts_tokenizer.dart';
|
||||
|
||||
@DriftDatabase(include: {'definitions.drift'}, daos: [BangDao, SyncDao])
|
||||
class BangDatabase extends $BangDatabase with PrefixQueryBuilderMixin {
|
||||
@override
|
||||
final int schemaVersion = 5;
|
||||
|
||||
@override
|
||||
final int ftsTokenLimit = 6;
|
||||
@override
|
||||
final int ftsMinTokenLength = 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// Following the advice from https://drift.simonbinder.eu/Migrations/api/#general-tips
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
await transaction(
|
||||
() => VersionedSchema.runMigrationSteps(
|
||||
migrator: m,
|
||||
from: from,
|
||||
to: to,
|
||||
steps: _upgrade,
|
||||
),
|
||||
);
|
||||
|
||||
if (kDebugMode) {
|
||||
final wrongForeignKeys = await customSelect(
|
||||
'PRAGMA foreign_key_check',
|
||||
).get();
|
||||
assert(
|
||||
wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}',
|
||||
);
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
if (kDebugMode) {
|
||||
// This check pulls in a fair amount of code that's not needed
|
||||
// anywhere else, so we recommend only doing it in debug builds.
|
||||
await validateDatabaseSchema();
|
||||
}
|
||||
|
||||
if (details.hadUpgrade && details.versionBefore != null) {
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
if (details.versionBefore! < 3) {
|
||||
await bang.deleteWhere((t) => t.group.equals(3));
|
||||
await bangTriggers.deleteWhere((t) => t.group.equals(3));
|
||||
await bangSync.deleteWhere((t) => t.group.equals(3));
|
||||
await bangFrequency.deleteWhere((t) => t.group.equals(3));
|
||||
await bangHistory.deleteWhere((t) => t.group.equals(3));
|
||||
} else if (details.versionBefore! < 5) {
|
||||
await bang.deleteWhere((t) => t.group.equals(1));
|
||||
await bangTriggers.deleteWhere((t) => t.group.equals(1));
|
||||
await bangSync.deleteWhere((t) => t.group.equals(1));
|
||||
await bangFrequency.deleteWhere((t) => t.group.equals(1));
|
||||
await bangHistory.deleteWhere((t) => t.group.equals(1));
|
||||
|
||||
await (bang.update()..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangCompanion.custom(group: bang.group - const Constant(1)),
|
||||
);
|
||||
await (bangTriggers.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangTriggersCompanion.custom(
|
||||
group: bangTriggers.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangSync.update()..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangSyncCompanion.custom(
|
||||
group: bangSync.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangFrequency.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangFrequencyCompanion.custom(
|
||||
group: bangFrequency.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
await (bangHistory.update()
|
||||
..where((t) => t.group.isBiggerThanValue(0)))
|
||||
.write(
|
||||
BangHistoryCompanion.custom(
|
||||
group: bangHistory.group - const Constant(1),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
|
||||
BangDatabase(super.e);
|
||||
|
||||
static final _upgrade = migrationSteps(
|
||||
from1To2: (m, schema) async {
|
||||
//Too many changes, we switch to a new database
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.addColumn(schema.bang, schema.bang.searxngApi);
|
||||
},
|
||||
from3To4: (m, schema) async {
|
||||
await m.alterTable(TableMigration(schema.bangHistory));
|
||||
},
|
||||
from4To5: (m, schema) async {
|
||||
await m.addColumn(schema.bang, schema.bang.snapDomain);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart'
|
||||
as i1;
|
||||
import 'package:weblibre/features/bangs/data/database/daos/bang.dart' as i2;
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart' as i3;
|
||||
import 'package:weblibre/features/bangs/data/database/daos/sync.dart' as i4;
|
||||
import 'package:drift/internal/modular.dart' as i5;
|
||||
import 'package:sqlite3/common.dart' as i6;
|
||||
|
||||
abstract class $BangDatabase extends i0.GeneratedDatabase {
|
||||
$BangDatabase(i0.QueryExecutor e) : super(e);
|
||||
$BangDatabaseManager get managers => $BangDatabaseManager(this);
|
||||
late final i1.BangTable bang = i1.BangTable(this);
|
||||
late final i1.BangTriggers bangTriggers = i1.BangTriggers(this);
|
||||
late final i1.BangSync bangSync = i1.BangSync(this);
|
||||
late final i1.BangFrequency bangFrequency = i1.BangFrequency(this);
|
||||
late final i1.BangHistory bangHistory = i1.BangHistory(this);
|
||||
late final i1.BangFts bangFts = i1.BangFts(this);
|
||||
late final i1.BangTriggersFts bangTriggersFts = i1.BangTriggersFts(this);
|
||||
late final i1.BangDataView bangDataView = i1.BangDataView(this);
|
||||
late final i2.BangDao bangDao = i2.BangDao(this as i3.BangDatabase);
|
||||
late final i4.SyncDao syncDao = i4.SyncDao(this as i3.BangDatabase);
|
||||
i1.DefinitionsDrift get definitionsDrift => i5.ReadDatabaseContainer(
|
||||
this,
|
||||
).accessor<i1.DefinitionsDrift>(i1.DefinitionsDrift.new);
|
||||
@override
|
||||
Iterable<i0.TableInfo<i0.Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<i0.TableInfo<i0.Table, Object?>>();
|
||||
@override
|
||||
List<i0.DatabaseSchemaEntity> get allSchemaEntities => [
|
||||
bang,
|
||||
bangTriggers,
|
||||
i1.idxBangTriggersLookup,
|
||||
i1.bangTriggersAfterInsert,
|
||||
i1.bangTriggersAfterUpdate,
|
||||
bangSync,
|
||||
bangFrequency,
|
||||
bangHistory,
|
||||
bangFts,
|
||||
bangTriggersFts,
|
||||
bangDataView,
|
||||
i1.bangAfterInsert,
|
||||
i1.bangAfterDelete,
|
||||
i1.bangAfterUpdate,
|
||||
i1.bangTriggersAfterInsertFts,
|
||||
i1.bangTriggersAfterDeleteFts,
|
||||
i1.bangTriggersAfterUpdateFts,
|
||||
];
|
||||
@override
|
||||
i0.StreamQueryUpdateRules
|
||||
get streamUpdateRules => const i0.StreamQueryUpdateRules([
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [
|
||||
i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.delete),
|
||||
i0.TableUpdate('bang_triggers', kind: i0.UpdateKind.insert),
|
||||
],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_frequency', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_history', kind: i0.UpdateKind.delete)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.insert,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.delete,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
i0.WritePropagation(
|
||||
on: i0.TableUpdateQuery.onTableName(
|
||||
'bang_triggers',
|
||||
limitUpdateKind: i0.UpdateKind.update,
|
||||
),
|
||||
result: [i0.TableUpdate('bang_triggers_fts', kind: i0.UpdateKind.insert)],
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
class $BangDatabaseManager {
|
||||
final $BangDatabase _db;
|
||||
$BangDatabaseManager(this._db);
|
||||
i1.$BangTableTableManager get bang =>
|
||||
i1.$BangTableTableManager(_db, _db.bang);
|
||||
i1.$BangTriggersTableManager get bangTriggers =>
|
||||
i1.$BangTriggersTableManager(_db, _db.bangTriggers);
|
||||
i1.$BangSyncTableManager get bangSync =>
|
||||
i1.$BangSyncTableManager(_db, _db.bangSync);
|
||||
i1.$BangFrequencyTableManager get bangFrequency =>
|
||||
i1.$BangFrequencyTableManager(_db, _db.bangFrequency);
|
||||
i1.$BangHistoryTableManager get bangHistory =>
|
||||
i1.$BangHistoryTableManager(_db, _db.bangHistory);
|
||||
i1.$BangFtsTableManager get bangFts =>
|
||||
i1.$BangFtsTableManager(_db, _db.bangFts);
|
||||
i1.$BangTriggersFtsTableManager get bangTriggersFts =>
|
||||
i1.$BangTriggersFtsTableManager(_db, _db.bangTriggersFts);
|
||||
}
|
||||
|
||||
extension DefineFunctions on i6.CommonDatabase {
|
||||
void defineFunctions({
|
||||
required String Function(int, String?) lexoRankNext,
|
||||
required String Function(int, String?) lexoRankPrevious,
|
||||
required String Function(String?, String?) lexoRankReorderAfter,
|
||||
required String Function(String?, String?) lexoRankReorderBefore,
|
||||
}) {
|
||||
createFunction(
|
||||
functionName: 'lexo_rank_next',
|
||||
argumentCount: const i6.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as int;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankNext(arg0, arg1);
|
||||
},
|
||||
);
|
||||
createFunction(
|
||||
functionName: 'lexo_rank_previous',
|
||||
argumentCount: const i6.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as int;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankPrevious(arg0, arg1);
|
||||
},
|
||||
);
|
||||
createFunction(
|
||||
functionName: 'lexo_rank_reorder_after',
|
||||
argumentCount: const i6.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as String?;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankReorderAfter(arg0, arg1);
|
||||
},
|
||||
);
|
||||
createFunction(
|
||||
functionName: 'lexo_rank_reorder_before',
|
||||
argumentCount: const i6.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as String?;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankReorderBefore(arg0, arg1);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,295 @@
|
||||
import 'package:weblibre/features/bangs/data/database/drift/converters/bang_format.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/drift/converters/trigger_list.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.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/search_history_entry.dart';
|
||||
|
||||
CREATE TABLE bang (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
website_name TEXT NOT NULL,
|
||||
domain TEXT NOT NULL,
|
||||
url_template TEXT NOT NULL,
|
||||
category TEXT,
|
||||
sub_category TEXT,
|
||||
format TEXT MAPPED BY `const BangFormatConverter()`,
|
||||
additional_triggers TEXT MAPPED BY `const TriggerListConverter()`,
|
||||
searxng_api BOOL NOT NULL DEFAULT FALSE,
|
||||
snap_domain TEXT,
|
||||
PRIMARY KEY ("trigger", "group")
|
||||
) WITH Bang;
|
||||
|
||||
CREATE TABLE bang_triggers (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
additional_trigger TEXT NOT NULL,
|
||||
PRIMARY KEY ("trigger", "group", additional_trigger),
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_bang_triggers_lookup ON bang_triggers (additional_trigger, "group");
|
||||
|
||||
-- Trigger to populate bang_triggers when inserting a new bang
|
||||
CREATE TRIGGER bang_triggers_after_insert AFTER INSERT ON bang
|
||||
WHEN new.additional_triggers IS NOT NULL
|
||||
BEGIN
|
||||
INSERT INTO bang_triggers("trigger", "group", additional_trigger)
|
||||
SELECT
|
||||
new."trigger",
|
||||
new."group",
|
||||
json_each.value
|
||||
FROM json_each(new.additional_triggers);
|
||||
END;
|
||||
|
||||
-- Trigger to update bang_triggers when updating a bang
|
||||
CREATE TRIGGER bang_triggers_after_update AFTER UPDATE ON bang BEGIN
|
||||
-- Delete old additional triggers
|
||||
DELETE FROM bang_triggers
|
||||
WHERE "trigger" = old."trigger" AND "group" = old."group";
|
||||
|
||||
-- Insert new additional triggers if they exist
|
||||
INSERT INTO bang_triggers("trigger", "group", additional_trigger)
|
||||
SELECT
|
||||
new."trigger",
|
||||
new."group",
|
||||
json_each.value
|
||||
FROM json_each(new.additional_triggers)
|
||||
WHERE new.additional_triggers IS NOT NULL;
|
||||
END;
|
||||
|
||||
CREATE TABLE bang_sync (
|
||||
"group" ENUM(BangGroup) PRIMARY KEY NOT NULL,
|
||||
last_sync DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE bang_frequency (
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
frequency INTEGER NOT NULL,
|
||||
last_used DATETIME NOT NULL,
|
||||
PRIMARY KEY ("trigger", "group"),
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE bang_history (
|
||||
search_query TEXT UNIQUE NOT NULL,
|
||||
"trigger" TEXT NOT NULL,
|
||||
"group" ENUM(BangGroup) NOT NULL,
|
||||
search_date DATETIME NOT NULL,
|
||||
FOREIGN KEY ("trigger", "group") REFERENCES bang ("trigger", "group") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE bang_fts
|
||||
USING fts5(
|
||||
trigger,
|
||||
website_name,
|
||||
content=bang,
|
||||
prefix='2 3'
|
||||
);
|
||||
|
||||
CREATE VIRTUAL TABLE bang_triggers_fts
|
||||
USING fts5(
|
||||
additional_trigger,
|
||||
content=bang_triggers,
|
||||
prefix='2 3'
|
||||
);
|
||||
|
||||
CREATE VIEW bang_data_view WITH BangData AS
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used
|
||||
FROM
|
||||
bang b
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group";
|
||||
|
||||
-- Triggers to keep the FTS index up to date.
|
||||
CREATE TRIGGER bang_after_insert AFTER INSERT ON bang BEGIN
|
||||
INSERT INTO
|
||||
bang_fts(rowid, "trigger", website_name)
|
||||
VALUES (new.rowid, new."trigger", new.website_name);
|
||||
END;
|
||||
CREATE TRIGGER bang_after_delete AFTER DELETE ON bang BEGIN
|
||||
INSERT INTO
|
||||
bang_fts(bang_fts, rowid, "trigger", website_name)
|
||||
VALUES('delete', old.rowid, old."trigger", old.website_name);
|
||||
END;
|
||||
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;
|
||||
|
||||
CREATE TRIGGER bang_triggers_after_insert_fts AFTER INSERT ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(rowid, additional_trigger)
|
||||
VALUES (new.rowid, new.additional_trigger);
|
||||
END;
|
||||
CREATE TRIGGER bang_triggers_after_delete_fts AFTER DELETE ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(bang_triggers_fts, rowid, additional_trigger)
|
||||
VALUES('delete', old.rowid, old.additional_trigger);
|
||||
END;
|
||||
CREATE TRIGGER bang_triggers_after_update_fts AFTER UPDATE ON bang_triggers BEGIN
|
||||
INSERT INTO
|
||||
bang_triggers_fts(bang_triggers_fts, rowid, additional_trigger)
|
||||
VALUES('delete', old.rowid, old.additional_trigger);
|
||||
INSERT INTO
|
||||
bang_triggers_fts(rowid, additional_trigger)
|
||||
VALUES (new.rowid, new.additional_trigger);
|
||||
END;
|
||||
|
||||
optimizeBangFtsIndex:
|
||||
INSERT INTO bang_fts(bang_fts) VALUES ('optimize');
|
||||
|
||||
optimizeTriggerFtsIndex:
|
||||
INSERT INTO bang_triggers_fts(bang_triggers_fts) VALUES ('optimize');
|
||||
|
||||
queryBangs WITH BangData:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
10.0 AS "trigger",
|
||||
8.0 AS additional_trigger,
|
||||
5.0 AS website_name
|
||||
),
|
||||
bang_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||
FROM
|
||||
bang_fts(:query) fts
|
||||
INNER JOIN
|
||||
bang b ON b.rowid = fts.rowid
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
),
|
||||
trigger_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_triggers_fts, weights.additional_trigger) AS weighted_rank
|
||||
FROM
|
||||
bang_triggers_fts(:query) tfts
|
||||
INNER JOIN
|
||||
bang_triggers bt ON bt.rowid = tfts.rowid
|
||||
INNER JOIN
|
||||
bang b ON b."trigger" = bt."trigger" AND b."group" = bt."group"
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
),
|
||||
combined_results AS (
|
||||
SELECT * FROM bang_results
|
||||
UNION ALL
|
||||
SELECT * FROM trigger_results
|
||||
)
|
||||
SELECT
|
||||
*,
|
||||
MIN(weighted_rank) AS weighted_rank
|
||||
FROM combined_results
|
||||
GROUP BY "trigger", "group"
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
frequency NULLS LAST;
|
||||
|
||||
queryBangsBasic WITH BangData:
|
||||
WITH weights AS (
|
||||
SELECT
|
||||
10.0 AS "trigger",
|
||||
8.0 AS additional_trigger,
|
||||
5.0 AS website_name
|
||||
),
|
||||
bang_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_fts, weights."trigger", weights.website_name) AS weighted_rank
|
||||
FROM
|
||||
bang_fts fts
|
||||
INNER JOIN
|
||||
bang b ON b.rowid = fts.rowid
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
fts."trigger" LIKE :query OR
|
||||
fts.website_name LIKE :query
|
||||
),
|
||||
trigger_results AS (
|
||||
SELECT
|
||||
b.*,
|
||||
bf.frequency,
|
||||
bf.last_used,
|
||||
bm25(bang_triggers_fts, weights.additional_trigger) AS weighted_rank
|
||||
FROM
|
||||
bang_triggers_fts tfts
|
||||
INNER JOIN
|
||||
bang_triggers bt ON bt.rowid = tfts.rowid
|
||||
INNER JOIN
|
||||
bang b ON b."trigger" = bt."trigger" AND b."group" = bt."group"
|
||||
LEFT JOIN
|
||||
bang_frequency bf ON b."trigger" = bf."trigger" AND b."group" = bf."group"
|
||||
CROSS JOIN weights
|
||||
WHERE
|
||||
tfts.additional_trigger LIKE :query
|
||||
),
|
||||
combined_results AS (
|
||||
SELECT * FROM bang_results
|
||||
UNION ALL
|
||||
SELECT * FROM trigger_results
|
||||
)
|
||||
SELECT
|
||||
*,
|
||||
MIN(weighted_rank) AS weighted_rank
|
||||
FROM combined_results
|
||||
GROUP BY "trigger", "group"
|
||||
ORDER BY
|
||||
weighted_rank ASC,
|
||||
frequency NULLS LAST;
|
||||
|
||||
categoriesJson:
|
||||
WITH categories AS (
|
||||
SELECT
|
||||
b.category,
|
||||
json_group_array(
|
||||
DISTINCT b.sub_category
|
||||
ORDER BY b.sub_category
|
||||
) AS sub_categories
|
||||
FROM
|
||||
bang b
|
||||
WHERE
|
||||
b.category IS NOT NULL AND
|
||||
b.sub_category IS NOT NULL
|
||||
GROUP BY b.category
|
||||
ORDER BY b.category
|
||||
)
|
||||
SELECT
|
||||
json_group_object(
|
||||
c.category,
|
||||
json(c.sub_categories)
|
||||
) AS categories_json
|
||||
FROM categories c;
|
||||
|
||||
searchHistoryEntries WITH SearchHistoryEntry:
|
||||
SELECT *
|
||||
FROM bang_history
|
||||
ORDER BY search_date DESC
|
||||
LIMIT :limit;
|
||||
|
||||
evictHistoryEntries:
|
||||
DELETE FROM bang_history
|
||||
WHERE rowid IN (
|
||||
SELECT rowid
|
||||
FROM bang_history
|
||||
ORDER BY search_date DESC
|
||||
LIMIT -1 OFFSET :limit
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* 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:weblibre/features/bangs/data/models/bang.dart';
|
||||
|
||||
class BangFormatConverter extends TypeConverter<Set<BangFormat>?, String?> {
|
||||
const BangFormatConverter();
|
||||
|
||||
@override
|
||||
Set<BangFormat>? fromSql(String? fromDb) {
|
||||
if (fromDb == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Bang.decodeFormat(jsonDecode(fromDb) as List);
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(Set<BangFormat>? value) {
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return jsonEncode(Bang.encodeFormat(value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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:nullability/nullability.dart';
|
||||
|
||||
class TriggerListConverter extends TypeConverter<Set<String>?, String?> {
|
||||
const TriggerListConverter();
|
||||
|
||||
@override
|
||||
Set<String>? fromSql(String? fromDb) {
|
||||
return fromDb.mapNotNull(
|
||||
(value) => (jsonDecode(value) as List).cast<String>().toSet(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String? toSql(Set<String>? value) {
|
||||
return value.mapNotNull((value) => jsonEncode(value.toList()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:drift/drift.dart' show Expression, Insertable, Value;
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/extensions/uri.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/definitions.drift.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
part 'bang.g.dart';
|
||||
|
||||
enum BangFormat {
|
||||
///When the bang is invoked with no query, opens the base path of the URL (/)
|
||||
///instead of any path given in the template (g., /search)
|
||||
@JsonValue('open_base_path')
|
||||
openBasePath,
|
||||
|
||||
///URL encode the search terms. Some sites do not work with this, so it can
|
||||
///be disabled by omitting this.
|
||||
@JsonValue('url_encode_placeholder')
|
||||
urlEncodePlaceholder,
|
||||
|
||||
///URL encodes spaces as +, instead of %20. Some sites only work correctly
|
||||
///with one or the other.
|
||||
@JsonValue('url_encode_space_to_plus')
|
||||
urlEncodeSpaceToPlus,
|
||||
|
||||
///When the bang is invoked with no query, open the snap domain (ad) instead of any path given in the template
|
||||
@JsonValue('open_snap_domain')
|
||||
openSnapDomain,
|
||||
}
|
||||
|
||||
@JsonSerializable()
|
||||
@CopyWith()
|
||||
class Bang with FastEquatable implements Insertable<Bang> {
|
||||
static const _templateQueryPlaceholder = '{{{s}}}';
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
final BangGroup? group;
|
||||
|
||||
///The name of the website associated with the bang.
|
||||
@JsonKey(name: 's')
|
||||
final String websiteName;
|
||||
|
||||
///The domain name of the websit
|
||||
@JsonKey(name: 'd')
|
||||
final String domain;
|
||||
|
||||
///The specific trigger word or phrase used to invoke the bang.
|
||||
@JsonKey(name: 't')
|
||||
final String trigger;
|
||||
|
||||
///The URL template to use when the bang is invoked, where `{{{s}}}` is replaced by the user's query.
|
||||
@JsonKey(name: 'u')
|
||||
final String urlTemplate;
|
||||
|
||||
///The category of the website, if applicable
|
||||
@JsonKey(name: 'c')
|
||||
final String? category;
|
||||
|
||||
///The subcategory of the website, if applicable
|
||||
@JsonKey(name: 'sc')
|
||||
final String? subCategory;
|
||||
|
||||
///The format flags indicating how the query should be processed.
|
||||
@JsonKey(name: 'fmt')
|
||||
final Set<BangFormat>? format;
|
||||
|
||||
///Additional triggers that invoke this bang
|
||||
@JsonKey(name: 'ts')
|
||||
final Set<String>? additionalTriggers;
|
||||
|
||||
///Additional triggers that invoke this bang
|
||||
@JsonKey(name: 'ad')
|
||||
final String? snapDomain;
|
||||
|
||||
@JsonKey(defaultValue: false)
|
||||
final bool searxngApi;
|
||||
|
||||
String formatQuery(String input) {
|
||||
return (format == null ||
|
||||
format!.contains(BangFormat.urlEncodePlaceholder) == true)
|
||||
? (format == null ||
|
||||
format?.contains(BangFormat.urlEncodeSpaceToPlus) == true)
|
||||
? Uri.encodeQueryComponent(input)
|
||||
: Uri.encodeComponent(input)
|
||||
: input;
|
||||
}
|
||||
|
||||
Uri getDefaultUrl() {
|
||||
return getTemplateUrl('');
|
||||
}
|
||||
|
||||
Uri getTemplateUrl(String? query) {
|
||||
final queryEmpty = query.isEmpty;
|
||||
|
||||
if (queryEmpty && format?.contains(BangFormat.openSnapDomain) == true) {
|
||||
if (snapDomain.isNotEmpty) {
|
||||
return Uri.parse(snapDomain!);
|
||||
}
|
||||
}
|
||||
|
||||
final url = (!queryEmpty)
|
||||
? urlTemplate.replaceAll(_templateQueryPlaceholder, formatQuery(query!))
|
||||
: urlTemplate;
|
||||
|
||||
var template = Uri.parse(url);
|
||||
if (!template.hasScheme || template.origin.isEmpty) {
|
||||
template = Uri.https(
|
||||
domain,
|
||||
).replace(path: template.path, query: template.query);
|
||||
}
|
||||
|
||||
if (queryEmpty && format?.contains(BangFormat.openBasePath) == true) {
|
||||
template = template.base;
|
||||
}
|
||||
|
||||
return template;
|
||||
}
|
||||
|
||||
static Set<BangFormat> decodeFormat(Iterable input) {
|
||||
return input.map((e) => $enumDecode(_$BangFormatEnumMap, e)).toSet();
|
||||
}
|
||||
|
||||
static List<String> encodeFormat(Iterable<BangFormat> format) {
|
||||
return format.map((e) => _$BangFormatEnumMap[e]!).toList();
|
||||
}
|
||||
|
||||
Bang({
|
||||
required this.websiteName,
|
||||
required this.domain,
|
||||
required this.trigger,
|
||||
required this.urlTemplate,
|
||||
required this.searxngApi,
|
||||
this.group,
|
||||
this.category,
|
||||
this.subCategory,
|
||||
this.format,
|
||||
this.additionalTriggers,
|
||||
this.snapDomain,
|
||||
});
|
||||
|
||||
factory Bang.fromJson(Map<String, dynamic> json) => _$BangFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$BangToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
group,
|
||||
websiteName,
|
||||
domain,
|
||||
trigger,
|
||||
urlTemplate,
|
||||
category,
|
||||
subCategory,
|
||||
format,
|
||||
additionalTriggers,
|
||||
snapDomain,
|
||||
searxngApi,
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, Expression<Object>> toColumns(bool nullToAbsent) {
|
||||
return BangCompanion(
|
||||
trigger: Value(trigger),
|
||||
websiteName: Value(websiteName),
|
||||
domain: Value(domain),
|
||||
urlTemplate: Value(urlTemplate),
|
||||
group: Value.absentIfNull(group),
|
||||
category: Value.absentIfNull(category),
|
||||
subCategory: Value.absentIfNull(subCategory),
|
||||
format: Value.absentIfNull(format),
|
||||
additionalTriggers: Value.absentIfNull(additionalTriggers),
|
||||
snapDomain: Value.absentIfNull(snapDomain),
|
||||
).toColumns(nullToAbsent);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bang.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$BangCWProxy {
|
||||
Bang websiteName(String websiteName);
|
||||
|
||||
Bang domain(String domain);
|
||||
|
||||
Bang trigger(String trigger);
|
||||
|
||||
Bang urlTemplate(String urlTemplate);
|
||||
|
||||
Bang searxngApi(bool searxngApi);
|
||||
|
||||
Bang group(BangGroup? group);
|
||||
|
||||
Bang category(String? category);
|
||||
|
||||
Bang subCategory(String? subCategory);
|
||||
|
||||
Bang format(Set<BangFormat>? format);
|
||||
|
||||
Bang additionalTriggers(Set<String>? additionalTriggers);
|
||||
|
||||
Bang snapDomain(String? snapDomain);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Bang(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Bang(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Bang call({
|
||||
String websiteName,
|
||||
String domain,
|
||||
String trigger,
|
||||
String urlTemplate,
|
||||
bool searxngApi,
|
||||
BangGroup? group,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
Set<BangFormat>? format,
|
||||
Set<String>? additionalTriggers,
|
||||
String? snapDomain,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfBang.copyWith(...)` or call `instanceOfBang.copyWith.fieldName(value)` for a single field.
|
||||
class _$BangCWProxyImpl implements _$BangCWProxy {
|
||||
const _$BangCWProxyImpl(this._value);
|
||||
|
||||
final Bang _value;
|
||||
|
||||
@override
|
||||
Bang websiteName(String websiteName) => call(websiteName: websiteName);
|
||||
|
||||
@override
|
||||
Bang domain(String domain) => call(domain: domain);
|
||||
|
||||
@override
|
||||
Bang trigger(String trigger) => call(trigger: trigger);
|
||||
|
||||
@override
|
||||
Bang urlTemplate(String urlTemplate) => call(urlTemplate: urlTemplate);
|
||||
|
||||
@override
|
||||
Bang searxngApi(bool searxngApi) => call(searxngApi: searxngApi);
|
||||
|
||||
@override
|
||||
Bang group(BangGroup? group) => call(group: group);
|
||||
|
||||
@override
|
||||
Bang category(String? category) => call(category: category);
|
||||
|
||||
@override
|
||||
Bang subCategory(String? subCategory) => call(subCategory: subCategory);
|
||||
|
||||
@override
|
||||
Bang format(Set<BangFormat>? format) => call(format: format);
|
||||
|
||||
@override
|
||||
Bang additionalTriggers(Set<String>? additionalTriggers) =>
|
||||
call(additionalTriggers: additionalTriggers);
|
||||
|
||||
@override
|
||||
Bang snapDomain(String? snapDomain) => call(snapDomain: snapDomain);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `Bang(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// Bang(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
Bang call({
|
||||
Object? websiteName = const $CopyWithPlaceholder(),
|
||||
Object? domain = const $CopyWithPlaceholder(),
|
||||
Object? trigger = const $CopyWithPlaceholder(),
|
||||
Object? urlTemplate = const $CopyWithPlaceholder(),
|
||||
Object? searxngApi = const $CopyWithPlaceholder(),
|
||||
Object? group = const $CopyWithPlaceholder(),
|
||||
Object? category = const $CopyWithPlaceholder(),
|
||||
Object? subCategory = const $CopyWithPlaceholder(),
|
||||
Object? format = const $CopyWithPlaceholder(),
|
||||
Object? additionalTriggers = const $CopyWithPlaceholder(),
|
||||
Object? snapDomain = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return Bang(
|
||||
websiteName:
|
||||
websiteName == const $CopyWithPlaceholder() || websiteName == null
|
||||
? _value.websiteName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: websiteName as String,
|
||||
domain: domain == const $CopyWithPlaceholder() || domain == null
|
||||
? _value.domain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: domain as String,
|
||||
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
|
||||
? _value.trigger
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: trigger as String,
|
||||
urlTemplate:
|
||||
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
|
||||
? _value.urlTemplate
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: urlTemplate as String,
|
||||
searxngApi:
|
||||
searxngApi == const $CopyWithPlaceholder() || searxngApi == null
|
||||
? _value.searxngApi
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: searxngApi as bool,
|
||||
group: group == const $CopyWithPlaceholder()
|
||||
? _value.group
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: group as BangGroup?,
|
||||
category: category == const $CopyWithPlaceholder()
|
||||
? _value.category
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: category as String?,
|
||||
subCategory: subCategory == const $CopyWithPlaceholder()
|
||||
? _value.subCategory
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: subCategory as String?,
|
||||
format: format == const $CopyWithPlaceholder()
|
||||
? _value.format
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: format as Set<BangFormat>?,
|
||||
additionalTriggers: additionalTriggers == const $CopyWithPlaceholder()
|
||||
? _value.additionalTriggers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: additionalTriggers as Set<String>?,
|
||||
snapDomain: snapDomain == const $CopyWithPlaceholder()
|
||||
? _value.snapDomain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: snapDomain as String?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $BangCopyWith on Bang {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfBang.copyWith(...)` or `instanceOfBang.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$BangCWProxy get copyWith => _$BangCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
Bang _$BangFromJson(Map<String, dynamic> json) => Bang(
|
||||
websiteName: json['s'] as String,
|
||||
domain: json['d'] as String,
|
||||
trigger: json['t'] as String,
|
||||
urlTemplate: json['u'] as String,
|
||||
searxngApi: json['searxngApi'] as bool? ?? false,
|
||||
category: json['c'] as String?,
|
||||
subCategory: json['sc'] as String?,
|
||||
format: (json['fmt'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$BangFormatEnumMap, e))
|
||||
.toSet(),
|
||||
additionalTriggers: (json['ts'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toSet(),
|
||||
snapDomain: json['ad'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$BangToJson(Bang instance) => <String, dynamic>{
|
||||
's': instance.websiteName,
|
||||
'd': instance.domain,
|
||||
't': instance.trigger,
|
||||
'u': instance.urlTemplate,
|
||||
'c': instance.category,
|
||||
'sc': instance.subCategory,
|
||||
'fmt': instance.format?.map((e) => _$BangFormatEnumMap[e]!).toList(),
|
||||
'ts': instance.additionalTriggers?.toList(),
|
||||
'ad': instance.snapDomain,
|
||||
'searxngApi': instance.searxngApi,
|
||||
};
|
||||
|
||||
const _$BangFormatEnumMap = {
|
||||
BangFormat.openBasePath: 'open_base_path',
|
||||
BangFormat.urlEncodePlaceholder: 'url_encode_placeholder',
|
||||
BangFormat.urlEncodeSpaceToPlus: 'url_encode_space_to_plus',
|
||||
BangFormat.openSnapDomain: 'open_snap_domain',
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.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/geckoview/domain/entities/browser_icon.dart';
|
||||
|
||||
part 'bang_data.g.dart';
|
||||
|
||||
@CopyWith(constructor: '_copyWith')
|
||||
class BangData extends Bang {
|
||||
final int frequency;
|
||||
final DateTime? lastUsed;
|
||||
|
||||
final BrowserIcon? icon;
|
||||
|
||||
@override
|
||||
BangGroup get group => super.group!;
|
||||
|
||||
BangData({
|
||||
required super.websiteName,
|
||||
required super.domain,
|
||||
required super.trigger,
|
||||
required super.urlTemplate,
|
||||
required super.group,
|
||||
required super.searxngApi,
|
||||
super.category,
|
||||
super.subCategory,
|
||||
super.format,
|
||||
super.additionalTriggers,
|
||||
super.snapDomain,
|
||||
int? frequency,
|
||||
this.lastUsed,
|
||||
this.icon,
|
||||
}) : frequency = frequency ?? 0;
|
||||
|
||||
//For some reasons including super.group breaks generation of copywith, so we have this one for now
|
||||
BangData._copyWith({
|
||||
required super.websiteName,
|
||||
required super.domain,
|
||||
required super.trigger,
|
||||
required super.urlTemplate,
|
||||
required super.searxngApi,
|
||||
super.category,
|
||||
super.subCategory,
|
||||
super.format,
|
||||
super.additionalTriggers,
|
||||
super.snapDomain,
|
||||
int? frequency,
|
||||
this.lastUsed,
|
||||
this.icon,
|
||||
}) : frequency = frequency ?? 0;
|
||||
|
||||
BangKey toKey() => BangKey(group: group, trigger: trigger);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
...super.hashParameters,
|
||||
frequency,
|
||||
lastUsed,
|
||||
icon,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'bang_data.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$BangDataCWProxy {
|
||||
BangData websiteName(String websiteName);
|
||||
|
||||
BangData domain(String domain);
|
||||
|
||||
BangData trigger(String trigger);
|
||||
|
||||
BangData urlTemplate(String urlTemplate);
|
||||
|
||||
BangData searxngApi(bool searxngApi);
|
||||
|
||||
BangData category(String? category);
|
||||
|
||||
BangData subCategory(String? subCategory);
|
||||
|
||||
BangData format(Set<BangFormat>? format);
|
||||
|
||||
BangData additionalTriggers(Set<String>? additionalTriggers);
|
||||
|
||||
BangData snapDomain(String? snapDomain);
|
||||
|
||||
BangData frequency(int? frequency);
|
||||
|
||||
BangData lastUsed(DateTime? lastUsed);
|
||||
|
||||
BangData icon(BrowserIcon? icon);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BangData(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BangData(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BangData call({
|
||||
String websiteName,
|
||||
String domain,
|
||||
String trigger,
|
||||
String urlTemplate,
|
||||
bool searxngApi,
|
||||
String? category,
|
||||
String? subCategory,
|
||||
Set<BangFormat>? format,
|
||||
Set<String>? additionalTriggers,
|
||||
String? snapDomain,
|
||||
int? frequency,
|
||||
DateTime? lastUsed,
|
||||
BrowserIcon? icon,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfBangData.copyWith(...)` or call `instanceOfBangData.copyWith.fieldName(value)` for a single field.
|
||||
class _$BangDataCWProxyImpl implements _$BangDataCWProxy {
|
||||
const _$BangDataCWProxyImpl(this._value);
|
||||
|
||||
final BangData _value;
|
||||
|
||||
@override
|
||||
BangData websiteName(String websiteName) => call(websiteName: websiteName);
|
||||
|
||||
@override
|
||||
BangData domain(String domain) => call(domain: domain);
|
||||
|
||||
@override
|
||||
BangData trigger(String trigger) => call(trigger: trigger);
|
||||
|
||||
@override
|
||||
BangData urlTemplate(String urlTemplate) => call(urlTemplate: urlTemplate);
|
||||
|
||||
@override
|
||||
BangData searxngApi(bool searxngApi) => call(searxngApi: searxngApi);
|
||||
|
||||
@override
|
||||
BangData category(String? category) => call(category: category);
|
||||
|
||||
@override
|
||||
BangData subCategory(String? subCategory) => call(subCategory: subCategory);
|
||||
|
||||
@override
|
||||
BangData format(Set<BangFormat>? format) => call(format: format);
|
||||
|
||||
@override
|
||||
BangData additionalTriggers(Set<String>? additionalTriggers) =>
|
||||
call(additionalTriggers: additionalTriggers);
|
||||
|
||||
@override
|
||||
BangData snapDomain(String? snapDomain) => call(snapDomain: snapDomain);
|
||||
|
||||
@override
|
||||
BangData frequency(int? frequency) => call(frequency: frequency);
|
||||
|
||||
@override
|
||||
BangData lastUsed(DateTime? lastUsed) => call(lastUsed: lastUsed);
|
||||
|
||||
@override
|
||||
BangData icon(BrowserIcon? icon) => call(icon: icon);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `BangData(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// BangData(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
BangData call({
|
||||
Object? websiteName = const $CopyWithPlaceholder(),
|
||||
Object? domain = const $CopyWithPlaceholder(),
|
||||
Object? trigger = const $CopyWithPlaceholder(),
|
||||
Object? urlTemplate = const $CopyWithPlaceholder(),
|
||||
Object? searxngApi = const $CopyWithPlaceholder(),
|
||||
Object? category = const $CopyWithPlaceholder(),
|
||||
Object? subCategory = const $CopyWithPlaceholder(),
|
||||
Object? format = const $CopyWithPlaceholder(),
|
||||
Object? additionalTriggers = const $CopyWithPlaceholder(),
|
||||
Object? snapDomain = const $CopyWithPlaceholder(),
|
||||
Object? frequency = const $CopyWithPlaceholder(),
|
||||
Object? lastUsed = const $CopyWithPlaceholder(),
|
||||
Object? icon = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return BangData._copyWith(
|
||||
websiteName:
|
||||
websiteName == const $CopyWithPlaceholder() || websiteName == null
|
||||
? _value.websiteName
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: websiteName as String,
|
||||
domain: domain == const $CopyWithPlaceholder() || domain == null
|
||||
? _value.domain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: domain as String,
|
||||
trigger: trigger == const $CopyWithPlaceholder() || trigger == null
|
||||
? _value.trigger
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: trigger as String,
|
||||
urlTemplate:
|
||||
urlTemplate == const $CopyWithPlaceholder() || urlTemplate == null
|
||||
? _value.urlTemplate
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: urlTemplate as String,
|
||||
searxngApi:
|
||||
searxngApi == const $CopyWithPlaceholder() || searxngApi == null
|
||||
? _value.searxngApi
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: searxngApi as bool,
|
||||
category: category == const $CopyWithPlaceholder()
|
||||
? _value.category
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: category as String?,
|
||||
subCategory: subCategory == const $CopyWithPlaceholder()
|
||||
? _value.subCategory
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: subCategory as String?,
|
||||
format: format == const $CopyWithPlaceholder()
|
||||
? _value.format
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: format as Set<BangFormat>?,
|
||||
additionalTriggers: additionalTriggers == const $CopyWithPlaceholder()
|
||||
? _value.additionalTriggers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: additionalTriggers as Set<String>?,
|
||||
snapDomain: snapDomain == const $CopyWithPlaceholder()
|
||||
? _value.snapDomain
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: snapDomain as String?,
|
||||
frequency: frequency == const $CopyWithPlaceholder()
|
||||
? _value.frequency
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: frequency as int?,
|
||||
lastUsed: lastUsed == const $CopyWithPlaceholder()
|
||||
? _value.lastUsed
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: lastUsed as DateTime?,
|
||||
icon: icon == const $CopyWithPlaceholder()
|
||||
? _value.icon
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: icon as BrowserIcon?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $BangDataCopyWith on BangData {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfBangData.copyWith(...)` or `instanceOfBangData.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$BangDataCWProxy get copyWith => _$BangDataCWProxyImpl(this);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
enum BangGroup {
|
||||
general(
|
||||
remote:
|
||||
'https://raw.githubusercontent.com/FaFre/bangs/main/data/bangs.json',
|
||||
bundled: 'assets/bangs/bangs.json',
|
||||
),
|
||||
kagi(
|
||||
remote:
|
||||
'https://raw.githubusercontent.com/FaFre/bangs/main/data/kagi_bangs.json',
|
||||
bundled: 'assets/bangs/kagi_bangs.json',
|
||||
),
|
||||
user(remote: null, bundled: null);
|
||||
|
||||
final String? bundled;
|
||||
final String? remote;
|
||||
|
||||
const BangGroup({required this.bundled, required this.remote});
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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:json_annotation/json_annotation.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:weblibre/core/logger.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
class BangKey {
|
||||
final String trigger;
|
||||
final BangGroup group;
|
||||
|
||||
const BangKey({required this.group, required this.trigger});
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '${group.name}::$trigger';
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is BangKey &&
|
||||
runtimeType == other.runtimeType &&
|
||||
trigger == other.trigger &&
|
||||
group == other.group;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(trigger, group);
|
||||
|
||||
static BangKey? tryFromString(String key) {
|
||||
try {
|
||||
var [group, trigger] = key.split('::');
|
||||
|
||||
//Migrate to schema v5
|
||||
if (group == 'assistant') {
|
||||
group = BangGroup.kagi.name;
|
||||
}
|
||||
|
||||
return BangKey(
|
||||
group: BangGroup.values.firstWhere((g) => g.name == group),
|
||||
trigger: trigger,
|
||||
);
|
||||
} catch (e, s) {
|
||||
logger.w(
|
||||
'Failed to parse BangKey from string: "$key"',
|
||||
error: e,
|
||||
stackTrace: s,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BangKeyConverter implements JsonConverter<BangKey?, String?> {
|
||||
const BangKeyConverter();
|
||||
|
||||
@override
|
||||
BangKey? fromJson(String? json) {
|
||||
return json.mapNotNull((json) => BangKey.tryFromString(json));
|
||||
}
|
||||
|
||||
@override
|
||||
String? toJson(BangKey? object) {
|
||||
return object?.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class SearchHistoryEntry with FastEquatable {
|
||||
final String searchQuery;
|
||||
final String trigger;
|
||||
final DateTime searchDate;
|
||||
|
||||
SearchHistoryEntry({
|
||||
required this.searchQuery,
|
||||
required this.trigger,
|
||||
required this.searchDate,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [searchQuery, trigger, searchDate];
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
import 'package:weblibre/core/database_registry.dart';
|
||||
import 'package:weblibre/core/filesystem.dart';
|
||||
import 'package:weblibre/features/bangs/data/database/database.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
BangDatabase bangDatabase(Ref ref) {
|
||||
final db = BangDatabase(
|
||||
LazyDatabase(() async {
|
||||
final file = File(p.join(filesystem.profileDatabasesDir.path, 'bang.db'));
|
||||
|
||||
// Also work around limitations on old Android versions
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
}),
|
||||
);
|
||||
|
||||
DatabaseRegistry.instance.register('bang', db);
|
||||
|
||||
ref.onDispose(() async {
|
||||
await db.close();
|
||||
});
|
||||
|
||||
return db;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(bangDatabase)
|
||||
final bangDatabaseProvider = BangDatabaseProvider._();
|
||||
|
||||
final class BangDatabaseProvider
|
||||
extends $FunctionalProvider<BangDatabase, BangDatabase, BangDatabase>
|
||||
with $Provider<BangDatabase> {
|
||||
BangDatabaseProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangDatabaseProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDatabaseHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<BangDatabase> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
BangDatabase create(Ref ref) {
|
||||
return bangDatabase(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(BangDatabase value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<BangDatabase>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDatabaseHash() => r'0369d508def140a32c08c0551cefed57b1ca4b26';
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* 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:exceptions/exceptions.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart' show rootBundle;
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/core/http_error_handler.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang.dart';
|
||||
import 'package:weblibre/features/bangs/data/models/bang_group.dart';
|
||||
|
||||
part 'data_source.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BangDataSourceService extends _$BangDataSourceService {
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
Future<Result<List<Bang>>> fetchRemoteBangs(Uri url, BangGroup group) {
|
||||
return Result.fromAsync(() async {
|
||||
return await compute((args) async {
|
||||
final client = http.Client();
|
||||
try {
|
||||
final url = Uri.parse(args[0]);
|
||||
final response = await client
|
||||
.get(url)
|
||||
.timeout(const Duration(seconds: 30));
|
||||
|
||||
return jsonDecode(utf8.decode(response.bodyBytes)) as List;
|
||||
} finally {
|
||||
client.close();
|
||||
}
|
||||
}, [url.toString()]).then(
|
||||
(json) => json.map((e) {
|
||||
final bang = Bang.fromJson(e as Map<String, dynamic>);
|
||||
return bang.copyWith.group(group);
|
||||
}).toList(),
|
||||
);
|
||||
}, exceptionHandler: handleHttpError);
|
||||
}
|
||||
|
||||
Future<DateTime> getBundledBangDate(String path) async {
|
||||
final content = await rootBundle.loadString(path);
|
||||
return DateTime.parse(content.trim()).toLocal();
|
||||
}
|
||||
|
||||
Future<Result<List<Bang>>> getBundledBangs(String path, BangGroup? group) {
|
||||
return Result.fromAsync(() async {
|
||||
final content = await rootBundle.loadString(path);
|
||||
final json = jsonDecode(content) as List;
|
||||
|
||||
return json.map((e) {
|
||||
var bang = Bang.fromJson(e as Map<String, dynamic>);
|
||||
|
||||
if (group != null) {
|
||||
bang = bang.copyWith.group(group);
|
||||
}
|
||||
|
||||
return bang;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'data_source.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(BangDataSourceService)
|
||||
final bangDataSourceServiceProvider = BangDataSourceServiceProvider._();
|
||||
|
||||
final class BangDataSourceServiceProvider
|
||||
extends $NotifierProvider<BangDataSourceService, void> {
|
||||
BangDataSourceServiceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'bangDataSourceServiceProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$bangDataSourceServiceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BangDataSourceService create() => BangDataSourceService();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(void value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<void>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$bangDataSourceServiceHash() =>
|
||||
r'b1bd96bbd834de0f71af86d7f791d367d0a837be';
|
||||
|
||||
abstract class _$BangDataSourceService 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user