prepare for multiple apps
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 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/data/database/extensions/database_table_size.dart';
|
||||
import 'package:weblibre/features/user/data/database/daos/cache.drift.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class CacheDao extends DatabaseAccessor<UserDatabase> with $CacheDaoMixin {
|
||||
CacheDao(super.attachedDatabase);
|
||||
|
||||
SingleSelectable<double> getIconCacheSize() {
|
||||
return db.tableSize(db.iconCache);
|
||||
}
|
||||
|
||||
Future<int> clearIconCache() {
|
||||
return db.iconCache.deleteAll();
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<Uint8List?> getCachedIcon(String origin) {
|
||||
final query = selectOnly(db.iconCache)
|
||||
..addColumns([db.iconCache.iconData])
|
||||
..where(db.iconCache.origin.equals(origin));
|
||||
|
||||
return query.map((row) => row.read(db.iconCache.iconData));
|
||||
}
|
||||
|
||||
Future<int> cacheIcon(String origin, Uint8List bytes) {
|
||||
return db.iconCache.insertOne(
|
||||
IconCacheCompanion.insert(
|
||||
origin: origin,
|
||||
iconData: bytes,
|
||||
fetchDate: DateTime.now(),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => IconCacheCompanion(
|
||||
iconData: Value(bytes),
|
||||
fetchDate: Value(DateTime.now()),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/user/data/database/database.dart' as i1;
|
||||
|
||||
mixin $CacheDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
|
||||
CacheDaoManager get managers => CacheDaoManager(this);
|
||||
}
|
||||
|
||||
class CacheDaoManager {
|
||||
final $CacheDaoMixin _db;
|
||||
CacheDaoManager(this._db);
|
||||
}
|
||||
@@ -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 'package:drift/drift.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class OnboardingDao extends DatabaseAccessor<UserDatabase> {
|
||||
OnboardingDao(super.attachedDatabase);
|
||||
|
||||
SingleOrNullSelectable<int?> getLastRevision() {
|
||||
final maxRevision = db.onboarding.revision.max();
|
||||
|
||||
final query = selectOnly(db.onboarding)..addColumns([maxRevision]);
|
||||
|
||||
return query.map((row) => row.read(maxRevision));
|
||||
}
|
||||
|
||||
Future<void> pushRevision(int revision, DateTime completionDate) {
|
||||
return db.onboarding.insertOne(
|
||||
OnboardingCompanion.insert(
|
||||
revision: revision,
|
||||
completionDate: completionDate,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/user/data/database/database.dart' as i1;
|
||||
|
||||
mixin $OnboardingDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
|
||||
OnboardingDaoManager get managers => OnboardingDaoManager(this);
|
||||
}
|
||||
|
||||
class OnboardingDaoManager {
|
||||
final $OnboardingDaoMixin _db;
|
||||
OnboardingDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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';
|
||||
import 'package:weblibre/features/user/data/database/daos/setting.drift.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class SettingDao extends DatabaseAccessor<UserDatabase> with $SettingDaoMixin {
|
||||
SettingDao(super.attachedDatabase);
|
||||
|
||||
Future<int> updateSetting(String key, String? partitionKey, Object? value) {
|
||||
final normalizedValue = (value is Iterable) ? jsonEncode(value) : value;
|
||||
|
||||
final driftvalue = normalizedValue.mapNotNull(
|
||||
(normalizedValue) => DriftAny(normalizedValue),
|
||||
);
|
||||
|
||||
return db.setting.insertOne(
|
||||
SettingCompanion.insert(
|
||||
key: key,
|
||||
partitionKey: Value(partitionKey),
|
||||
value: Value(driftvalue),
|
||||
),
|
||||
onConflict: DoUpdate(
|
||||
(old) => SettingCompanion(
|
||||
partitionKey: Value(partitionKey),
|
||||
value: Value(driftvalue),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Selectable<MapEntry<String, DriftAny?>> getAllSettingsOfPartitionKey(
|
||||
String? partitionKey,
|
||||
) {
|
||||
final query = db.setting.select()
|
||||
..where((r) => r.partitionKey.equalsNullable(partitionKey));
|
||||
|
||||
return query.map((row) => MapEntry(row.key, row.value));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/user/data/database/database.dart' as i1;
|
||||
|
||||
mixin $SettingDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
|
||||
SettingDaoManager get managers => SettingDaoManager(this);
|
||||
}
|
||||
|
||||
class SettingDaoManager {
|
||||
final $SettingDaoMixin _db;
|
||||
SettingDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
/*
|
||||
* 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/user/data/database/daos/toolbar_button_config.drift.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class ToolbarButtonConfigDao extends DatabaseAccessor<UserDatabase>
|
||||
with $ToolbarButtonConfigDaoMixin {
|
||||
ToolbarButtonConfigDao(super.attachedDatabase);
|
||||
|
||||
Selectable<ToolbarButtonConfig> selectAll() =>
|
||||
db.toolbarButtonConfigs.select()
|
||||
..orderBy([(t) => OrderingTerm.asc(t.orderKey)]);
|
||||
|
||||
Stream<List<ToolbarButtonConfig>> watchAll() => selectAll().watch();
|
||||
|
||||
Future<List<ToolbarButtonConfig>> getAll() => selectAll().get();
|
||||
|
||||
Future<void> upsert(ToolbarButtonConfig config) =>
|
||||
into(db.toolbarButtonConfigs).insertOnConflictUpdate(config);
|
||||
|
||||
Future<void> assignOrderKey(String buttonId, {required String orderKey}) =>
|
||||
(update(db.toolbarButtonConfigs)
|
||||
..where((t) => t.buttonId.equals(buttonId)))
|
||||
.write(ToolbarButtonConfigsCompanion(orderKey: Value(orderKey)));
|
||||
|
||||
Future<void> assignVisibility(String buttonId, {required bool visible}) =>
|
||||
(update(db.toolbarButtonConfigs)
|
||||
..where((t) => t.buttonId.equals(buttonId)))
|
||||
.write(ToolbarButtonConfigsCompanion(isVisible: Value(visible)));
|
||||
|
||||
Future<void> assignFallback(String buttonId, String? fallbackId) =>
|
||||
(update(db.toolbarButtonConfigs)
|
||||
..where((t) => t.buttonId.equals(buttonId)))
|
||||
.write(ToolbarButtonConfigsCompanion(fallbackId: Value(fallbackId)));
|
||||
|
||||
Future<void> replaceAll(List<ToolbarButtonConfig> configs) =>
|
||||
transaction(() async {
|
||||
await delete(db.toolbarButtonConfigs).go();
|
||||
await _insertWithDeferredFallbacks(configs);
|
||||
});
|
||||
|
||||
Future<void> seedMissing(
|
||||
List<({String buttonId, bool defaultVisible, String? defaultFallback})>
|
||||
defaults,
|
||||
) async {
|
||||
final existing = await getAll();
|
||||
final existingIds = {for (final r in existing) r.buttonId};
|
||||
|
||||
final missing = defaults
|
||||
.where((d) => !existingIds.contains(d.buttonId))
|
||||
.toList();
|
||||
if (missing.isEmpty) return;
|
||||
|
||||
await transaction(() async {
|
||||
final inserted = <ToolbarButtonConfig>[];
|
||||
for (final def in missing) {
|
||||
final orderKey = await generateTrailingOrderKey().getSingle();
|
||||
inserted.add(
|
||||
ToolbarButtonConfig(
|
||||
buttonId: def.buttonId,
|
||||
orderKey: orderKey,
|
||||
isVisible: def.defaultVisible,
|
||||
fallbackId: def.defaultFallback,
|
||||
),
|
||||
);
|
||||
await into(db.toolbarButtonConfigs).insert(
|
||||
ToolbarButtonConfig(
|
||||
buttonId: def.buttonId,
|
||||
orderKey: orderKey,
|
||||
isVisible: def.defaultVisible,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _assignFallbacks(inserted);
|
||||
});
|
||||
}
|
||||
|
||||
SingleSelectable<String> generateLeadingOrderKey({int bucket = 0}) =>
|
||||
db.definitionsDrift.toolbarLeadingOrderKey(bucket: bucket);
|
||||
|
||||
SingleSelectable<String> generateTrailingOrderKey({int bucket = 0}) =>
|
||||
db.definitionsDrift.toolbarTrailingOrderKey(bucket: bucket);
|
||||
|
||||
SingleOrNullSelectable<String> generateOrderKeyAfterButtonId(
|
||||
String buttonId,
|
||||
) => db.definitionsDrift.toolbarOrderKeyAfterButton(buttonId: buttonId);
|
||||
|
||||
SingleSelectable<String> generateOrderKeyBeforeButtonId(String buttonId) =>
|
||||
db.definitionsDrift.toolbarOrderKeyBeforeButton(buttonId: buttonId);
|
||||
|
||||
Future<void> _insertWithDeferredFallbacks(
|
||||
List<ToolbarButtonConfig> configs,
|
||||
) async {
|
||||
for (final config in configs) {
|
||||
await into(db.toolbarButtonConfigs).insert(
|
||||
ToolbarButtonConfig(
|
||||
buttonId: config.buttonId,
|
||||
orderKey: config.orderKey,
|
||||
isVisible: config.isVisible,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
await _assignFallbacks(configs);
|
||||
}
|
||||
|
||||
Future<void> _assignFallbacks(List<ToolbarButtonConfig> configs) async {
|
||||
for (final config in configs) {
|
||||
if (config.fallbackId == null) continue;
|
||||
await assignFallback(config.buttonId, config.fallbackId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/user/data/database/database.dart' as i1;
|
||||
|
||||
mixin $ToolbarButtonConfigDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
|
||||
ToolbarButtonConfigDaoManager get managers =>
|
||||
ToolbarButtonConfigDaoManager(this);
|
||||
}
|
||||
|
||||
class ToolbarButtonConfigDaoManager {
|
||||
final $ToolbarButtonConfigDaoMixin _db;
|
||||
ToolbarButtonConfigDaoManager(this._db);
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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/user/data/database/daos/cache.dart';
|
||||
import 'package:weblibre/features/user/data/database/daos/onboarding.dart';
|
||||
import 'package:weblibre/features/user/data/database/daos/setting.dart';
|
||||
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.drift.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.steps.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
include: {'definitions.drift'},
|
||||
daos: [SettingDao, CacheDao, OnboardingDao, ToolbarButtonConfigDao],
|
||||
)
|
||||
class UserDatabase extends $UserDatabase {
|
||||
@override
|
||||
final int schemaVersion = 3;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
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();
|
||||
}
|
||||
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
|
||||
await onAfterOpen?.call(this);
|
||||
},
|
||||
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');
|
||||
},
|
||||
);
|
||||
|
||||
UserDatabase(super.e, {this.onAfterOpen});
|
||||
|
||||
final Future<void> Function(UserDatabase db)? onAfterOpen;
|
||||
|
||||
static final _upgrade = migrationSteps(
|
||||
from1To2: (m, schema) async {
|
||||
await m.createTable(schema.riverpod);
|
||||
},
|
||||
from2To3: (m, schema) async {
|
||||
await m.createTable(schema.toolbarButtonConfigs);
|
||||
await m.createIndex(schema.idxToolbarOrderKey);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// dart format width=80
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart' as i0;
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart'
|
||||
as i1;
|
||||
import 'package:weblibre/features/user/data/database/daos/setting.dart' as i2;
|
||||
import 'package:weblibre/features/user/data/database/database.dart' as i3;
|
||||
import 'package:weblibre/features/user/data/database/daos/cache.dart' as i4;
|
||||
import 'package:weblibre/features/user/data/database/daos/onboarding.dart'
|
||||
as i5;
|
||||
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart'
|
||||
as i6;
|
||||
import 'package:drift/internal/modular.dart' as i7;
|
||||
import 'package:sqlite3/common.dart' as i8;
|
||||
|
||||
abstract class $UserDatabase extends i0.GeneratedDatabase {
|
||||
$UserDatabase(i0.QueryExecutor e) : super(e);
|
||||
$UserDatabaseManager get managers => $UserDatabaseManager(this);
|
||||
late final i1.Setting setting = i1.Setting(this);
|
||||
late final i1.IconCache iconCache = i1.IconCache(this);
|
||||
late final i1.Onboarding onboarding = i1.Onboarding(this);
|
||||
late final i1.Riverpod riverpod = i1.Riverpod(this);
|
||||
late final i1.ToolbarButtonConfigs toolbarButtonConfigs =
|
||||
i1.ToolbarButtonConfigs(this);
|
||||
late final i2.SettingDao settingDao = i2.SettingDao(this as i3.UserDatabase);
|
||||
late final i4.CacheDao cacheDao = i4.CacheDao(this as i3.UserDatabase);
|
||||
late final i5.OnboardingDao onboardingDao = i5.OnboardingDao(
|
||||
this as i3.UserDatabase,
|
||||
);
|
||||
late final i6.ToolbarButtonConfigDao toolbarButtonConfigDao =
|
||||
i6.ToolbarButtonConfigDao(this as i3.UserDatabase);
|
||||
i1.DefinitionsDrift get definitionsDrift => i7.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 => [
|
||||
setting,
|
||||
iconCache,
|
||||
onboarding,
|
||||
riverpod,
|
||||
toolbarButtonConfigs,
|
||||
i1.idxToolbarOrderKey,
|
||||
];
|
||||
}
|
||||
|
||||
class $UserDatabaseManager {
|
||||
final $UserDatabase _db;
|
||||
$UserDatabaseManager(this._db);
|
||||
i1.$SettingTableManager get setting =>
|
||||
i1.$SettingTableManager(_db, _db.setting);
|
||||
i1.$IconCacheTableManager get iconCache =>
|
||||
i1.$IconCacheTableManager(_db, _db.iconCache);
|
||||
i1.$OnboardingTableManager get onboarding =>
|
||||
i1.$OnboardingTableManager(_db, _db.onboarding);
|
||||
i1.$RiverpodTableManager get riverpod =>
|
||||
i1.$RiverpodTableManager(_db, _db.riverpod);
|
||||
i1.$ToolbarButtonConfigsTableManager get toolbarButtonConfigs =>
|
||||
i1.$ToolbarButtonConfigsTableManager(_db, _db.toolbarButtonConfigs);
|
||||
}
|
||||
|
||||
extension DefineFunctions on i8.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 i8.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 i8.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 i8.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 i8.AllowedArgumentCount(2),
|
||||
function: (args) {
|
||||
final arg0 = args[0] as String?;
|
||||
final arg1 = args[1] as String?;
|
||||
return lexoRankReorderBefore(arg0, arg1);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
// dart format width=80
|
||||
import 'package:drift/internal/versioned_schema.dart' as i0;
|
||||
import 'package:drift/drift.dart' as i1;
|
||||
import 'dart:typed_data' as i2;
|
||||
import 'package:drift/drift.dart'; // GENERATED BY drift_dev, DO NOT MODIFY.
|
||||
|
||||
// ignore_for_file: type=lint,unused_import
|
||||
//
|
||||
final class Schema2 extends i0.VersionedSchema {
|
||||
Schema2({required super.database}) : super(version: 2);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
setting,
|
||||
iconCache,
|
||||
onboarding,
|
||||
riverpod,
|
||||
];
|
||||
late final Shape0 setting = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'setting',
|
||||
withoutRowId: false,
|
||||
isStrict: true,
|
||||
tableConstraints: [],
|
||||
columns: [_column_0, _column_1, _column_2],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape1 iconCache = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'icon_cache',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_3, _column_4, _column_5],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 onboarding = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'onboarding',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_6, _column_7],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 riverpod = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'riverpod',
|
||||
withoutRowId: true,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_0, _column_8, _column_9, _column_10],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
}
|
||||
|
||||
class Shape0 extends i0.VersionedTable {
|
||||
Shape0({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get key =>
|
||||
columnsByName['key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get partitionKey =>
|
||||
columnsByName['partition_key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<i1.DriftAny> get value =>
|
||||
columnsByName['value']! as i1.GeneratedColumn<i1.DriftAny>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_0(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_1(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'partition_key',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<i1.DriftAny> _column_2(String aliasedName) =>
|
||||
i1.GeneratedColumn<i1.DriftAny>(
|
||||
'value',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.any,
|
||||
$customConstraints: '',
|
||||
);
|
||||
|
||||
class Shape1 extends i0.VersionedTable {
|
||||
Shape1({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get origin =>
|
||||
columnsByName['origin']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<i2.Uint8List> get iconData =>
|
||||
columnsByName['icon_data']! as i1.GeneratedColumn<i2.Uint8List>;
|
||||
i1.GeneratedColumn<int> get fetchDate =>
|
||||
columnsByName['fetch_date']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_3(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'origin',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<i2.Uint8List> _column_4(String aliasedName) =>
|
||||
i1.GeneratedColumn<i2.Uint8List>(
|
||||
'icon_data',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.blob,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_5(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'fetch_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
class Shape2 extends i0.VersionedTable {
|
||||
Shape2({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<int> get revision =>
|
||||
columnsByName['revision']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<int> get completionDate =>
|
||||
columnsByName['completion_date']! as i1.GeneratedColumn<int>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<int> _column_6(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'revision',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_7(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'completion_date',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
|
||||
class Shape3 extends i0.VersionedTable {
|
||||
Shape3({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get key =>
|
||||
columnsByName['key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get json =>
|
||||
columnsByName['json']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get expireAt =>
|
||||
columnsByName['expireAt']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get destroyKey =>
|
||||
columnsByName['destroyKey']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_8(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'json',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_9(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'expireAt',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: '',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_10(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'destroyKey',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: '',
|
||||
);
|
||||
|
||||
final class Schema3 extends i0.VersionedSchema {
|
||||
Schema3({required super.database}) : super(version: 3);
|
||||
@override
|
||||
late final List<i1.DatabaseSchemaEntity> entities = [
|
||||
setting,
|
||||
iconCache,
|
||||
onboarding,
|
||||
riverpod,
|
||||
toolbarButtonConfigs,
|
||||
idxToolbarOrderKey,
|
||||
];
|
||||
late final Shape0 setting = Shape0(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'setting',
|
||||
withoutRowId: false,
|
||||
isStrict: true,
|
||||
tableConstraints: [],
|
||||
columns: [_column_0, _column_1, _column_2],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape1 iconCache = Shape1(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'icon_cache',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_3, _column_4, _column_5],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape2 onboarding = Shape2(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'onboarding',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_6, _column_7],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape3 riverpod = Shape3(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'riverpod',
|
||||
withoutRowId: true,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_0, _column_8, _column_9, _column_10],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
late final Shape4 toolbarButtonConfigs = Shape4(
|
||||
source: i0.VersionedTable(
|
||||
entityName: 'toolbar_button_configs',
|
||||
withoutRowId: false,
|
||||
isStrict: false,
|
||||
tableConstraints: [],
|
||||
columns: [_column_11, _column_12, _column_13, _column_14],
|
||||
attachedDatabase: database,
|
||||
),
|
||||
alias: null,
|
||||
);
|
||||
final i1.Index idxToolbarOrderKey = i1.Index(
|
||||
'idx_toolbar_order_key',
|
||||
'CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs (order_key)',
|
||||
);
|
||||
}
|
||||
|
||||
class Shape4 extends i0.VersionedTable {
|
||||
Shape4({required super.source, required super.alias}) : super.aliased();
|
||||
i1.GeneratedColumn<String> get buttonId =>
|
||||
columnsByName['button_id']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<String> get orderKey =>
|
||||
columnsByName['order_key']! as i1.GeneratedColumn<String>;
|
||||
i1.GeneratedColumn<int> get isVisible =>
|
||||
columnsByName['is_visible']! as i1.GeneratedColumn<int>;
|
||||
i1.GeneratedColumn<String> get fallbackId =>
|
||||
columnsByName['fallback_id']! as i1.GeneratedColumn<String>;
|
||||
}
|
||||
|
||||
i1.GeneratedColumn<String> _column_11(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'button_id',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL PRIMARY KEY',
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_12(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'order_key',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints: 'NOT NULL',
|
||||
);
|
||||
i1.GeneratedColumn<int> _column_13(String aliasedName) =>
|
||||
i1.GeneratedColumn<int>(
|
||||
'is_visible',
|
||||
aliasedName,
|
||||
false,
|
||||
type: i1.DriftSqlType.int,
|
||||
$customConstraints: 'NOT NULL DEFAULT TRUE',
|
||||
defaultValue: const i1.CustomExpression('TRUE'),
|
||||
);
|
||||
i1.GeneratedColumn<String> _column_14(String aliasedName) =>
|
||||
i1.GeneratedColumn<String>(
|
||||
'fallback_id',
|
||||
aliasedName,
|
||||
true,
|
||||
type: i1.DriftSqlType.string,
|
||||
$customConstraints:
|
||||
'REFERENCES toolbar_button_configs(button_id)ON DELETE SET NULL',
|
||||
);
|
||||
i0.MigrationStepWithVersion migrationSteps({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
}) {
|
||||
return (currentVersion, database) async {
|
||||
switch (currentVersion) {
|
||||
case 1:
|
||||
final schema = Schema2(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from1To2(migrator, schema);
|
||||
return 2;
|
||||
case 2:
|
||||
final schema = Schema3(database: database);
|
||||
final migrator = i1.Migrator(database, schema);
|
||||
await from2To3(migrator, schema);
|
||||
return 3;
|
||||
default:
|
||||
throw ArgumentError.value('Unknown migration from $currentVersion');
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
i1.OnUpgrade stepByStep({
|
||||
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
|
||||
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
|
||||
}) => i0.VersionedSchema.stepByStepHelper(
|
||||
step: migrationSteps(from1To2: from1To2, from2To3: from2To3),
|
||||
);
|
||||
@@ -0,0 +1,87 @@
|
||||
CREATE TABLE setting (
|
||||
"key" TEXT PRIMARY KEY NOT NULL,
|
||||
partition_key TEXT,
|
||||
"value" ANY
|
||||
) STRICT;
|
||||
|
||||
CREATE TABLE icon_cache (
|
||||
origin TEXT PRIMARY KEY NOT NULL,
|
||||
icon_data BLOB NOT NULL,
|
||||
fetch_date DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE onboarding (
|
||||
revision INTEGER NOT NULL,
|
||||
completion_date DATETIME NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE riverpod (
|
||||
"key" TEXT PRIMARY KEY NOT NULL,
|
||||
json TEXT NOT NULL,
|
||||
expireAt DATETIME,
|
||||
destroyKey TEXT
|
||||
) WITHOUT ROWID;
|
||||
|
||||
CREATE TABLE toolbar_button_configs (
|
||||
button_id TEXT NOT NULL PRIMARY KEY,
|
||||
order_key TEXT NOT NULL,
|
||||
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
fallback_id TEXT REFERENCES toolbar_button_configs(button_id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX idx_toolbar_order_key ON toolbar_button_configs(order_key);
|
||||
|
||||
toolbarLeadingOrderKey(:bucket AS INTEGER):
|
||||
SELECT lexo_rank_previous(
|
||||
:bucket,
|
||||
(
|
||||
SELECT order_key
|
||||
FROM toolbar_button_configs
|
||||
ORDER BY order_key
|
||||
LIMIT 1
|
||||
)
|
||||
);
|
||||
|
||||
toolbarTrailingOrderKey(:bucket AS INTEGER):
|
||||
SELECT lexo_rank_next(
|
||||
:bucket,
|
||||
(
|
||||
SELECT order_key
|
||||
FROM toolbar_button_configs
|
||||
ORDER BY order_key DESC
|
||||
LIMIT 1
|
||||
)
|
||||
);
|
||||
|
||||
toolbarOrderKeyAfterButton(:button_id AS TEXT):
|
||||
WITH ordered_table AS (
|
||||
SELECT
|
||||
button_id,
|
||||
order_key,
|
||||
LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key
|
||||
FROM toolbar_button_configs
|
||||
)
|
||||
SELECT lexo_rank_reorder_after(order_key, next_order_key)
|
||||
FROM ordered_table
|
||||
WHERE button_id = :button_id;
|
||||
|
||||
toolbarOrderKeyBeforeButton(:button_id AS TEXT):
|
||||
WITH ordered_table AS (
|
||||
SELECT
|
||||
button_id,
|
||||
order_key,
|
||||
LAG(order_key) OVER (ORDER BY order_key) AS prev_order_key
|
||||
FROM toolbar_button_configs
|
||||
)
|
||||
SELECT lexo_rank_reorder_before(order_key, prev_order_key)
|
||||
FROM ordered_table
|
||||
WHERE button_id = :button_id;
|
||||
|
||||
evictCacheEntries:
|
||||
DELETE FROM icon_cache
|
||||
WHERE rowid IN (
|
||||
SELECT rowid
|
||||
FROM icon_cache
|
||||
ORDER BY fetch_date DESC
|
||||
LIMIT -1 OFFSET :limit
|
||||
);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 Fabian Freund.
|
||||
*
|
||||
* This file is part of WebLibre
|
||||
* (see https://weblibre.eu).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:nullability/nullability.dart';
|
||||
import 'package:riverpod/experimental/persist.dart';
|
||||
import 'package:weblibre/features/user/data/database/database.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
final class RiverpodStorage extends Storage<String, String> {
|
||||
final UserDatabase _db;
|
||||
|
||||
RiverpodStorage(this._db);
|
||||
|
||||
@override
|
||||
Future<void> delete(String key) {
|
||||
return _db.riverpod.deleteWhere((x) => x.key.equals(key));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteOutOfDate() {
|
||||
return _db.riverpod.deleteWhere(
|
||||
(x) => x.expireAt.isSmallerThanValue(DateTime.now()),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PersistedData<String>?> read(String key) async {
|
||||
final query = _db.riverpod.select()..where((x) => x.key.equals(key));
|
||||
|
||||
final data = await query.getSingleOrNull();
|
||||
|
||||
return data.mapNotNull(
|
||||
(data) => PersistedData(
|
||||
data.json,
|
||||
destroyKey: data.destroyKey,
|
||||
expireAt: data.expireAt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> write(String key, String value, StorageOptions options) {
|
||||
return _db.riverpod.insertOne(
|
||||
RiverpodCompanion.insert(
|
||||
key: key,
|
||||
json: value,
|
||||
destroyKey: Value(options.destroyKey),
|
||||
expireAt: Value(
|
||||
options.cacheTime.duration.mapNotNull(
|
||||
(duration) => DateTime.now().add(duration),
|
||||
),
|
||||
),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user