Add Supa account and search changes

This commit is contained in:
Fabian Freund
2026-05-22 18:10:22 +02:00
parent 3a19865b2e
commit 51289f1266
374 changed files with 54061 additions and 5013 deletions
@@ -19,6 +19,7 @@
*/
import 'package:drift/drift.dart';
import 'package:weblibre/data/database/extensions/database_table_size.dart';
import 'package:weblibre/features/user/data/icon_cache_marker.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';
@@ -43,6 +44,14 @@ class CacheDao extends DatabaseAccessor<UserDatabase> with $CacheDaoMixin {
return query.map((row) => row.read(db.iconCache.iconData));
}
SingleOrNullSelectable<DateTime?> getCachedIconFetchDate(String origin) {
final query = selectOnly(db.iconCache)
..addColumns([db.iconCache.fetchDate])
..where(db.iconCache.origin.equals(origin));
return query.map((row) => row.read(db.iconCache.fetchDate));
}
Future<int> cacheIcon(String origin, Uint8List bytes) {
return db.iconCache.insertOne(
IconCacheCompanion.insert(
@@ -58,4 +67,15 @@ class CacheDao extends DatabaseAccessor<UserDatabase> with $CacheDaoMixin {
),
);
}
Future<void> cacheIconIfAbsent(String origin, Uint8List bytes) async {
final existing = await getCachedIcon(origin).getSingleOrNull();
if (existing == null || isMissingIconMarker(existing)) {
await cacheIcon(origin, bytes);
}
}
Future<int> cacheMissingIcon(String origin) {
return cacheIcon(origin, missingIconMarkerBytes);
}
}
@@ -0,0 +1,100 @@
import 'package:drift/drift.dart';
import 'package:weblibre/features/user/data/database/database.dart';
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
class ReservedToken {
final int id;
final Uint8List token;
const ReservedToken({required this.id, required this.token});
}
@DriftAccessor()
class SearchTokensDao extends DatabaseAccessor<UserDatabase> {
SearchTokensDao(super.attachedDatabase);
Future<void> addTokens(
List<Uint8List> tokens, {
required String issuerKeyVersion,
}) {
return batch((b) {
b.insertAll(
db.searchTokens,
tokens
.map(
(t) => SearchTokensCompanion.insert(
token: t,
issuerKeyVersion: issuerKeyVersion,
insertedAt: DateTime.now(),
),
)
.toList(),
);
});
}
/// Reserve the oldest unreserved token. Stamps `reserved_at` so the same
/// row won't be handed out twice. Caller MUST follow with either
/// [commitReserved] (server consumed it) or [releaseReserved] (we know the
/// server didn't see it).
Future<ReservedToken?> reserveOne() {
return transaction(() async {
final row =
await (select(db.searchTokens)
..where((t) => t.reservedAt.isNull())
..orderBy([(t) => OrderingTerm.asc(t.id)])
..limit(1))
.getSingleOrNull();
if (row == null) return null;
await (update(db.searchTokens)..where((t) => t.id.equals(row.id))).write(
SearchTokensCompanion(reservedAt: Value(DateTime.now())),
);
return ReservedToken(id: row.id, token: row.token);
});
}
/// Permanently delete a reserved token after a successful redemption.
Future<void> commitReserved(int id) async {
await (delete(db.searchTokens)..where((t) => t.id.equals(id))).go();
}
/// Clear the reservation so the token is handed out again. Use only when
/// the server is known not to have consumed it.
Future<void> releaseReserved(int id) async {
await (update(db.searchTokens)..where((t) => t.id.equals(id))).write(
const SearchTokensCompanion(reservedAt: Value(null)),
);
}
/// Release any reservation older than [maxAge] — covers crashes mid-flight.
/// Returns the number of rows released.
Future<int> releaseStaleReservations(Duration maxAge) async {
final cutoff = DateTime.now().subtract(maxAge);
return (update(db.searchTokens)..where(
(t) =>
t.reservedAt.isNotNull() &
t.reservedAt.isSmallerThanValue(cutoff),
))
.write(const SearchTokensCompanion(reservedAt: Value(null)));
}
Future<int> count() async {
final c = db.searchTokens.id.count();
final query = selectOnly(db.searchTokens)
..addColumns([c])
..where(db.searchTokens.reservedAt.isNull());
final row = await query.getSingle();
return row.read(c) ?? 0;
}
Stream<int> watchCount() {
final c = db.searchTokens.id.count();
final query = selectOnly(db.searchTokens)
..addColumns([c])
..where(db.searchTokens.reservedAt.isNull());
return query.map((row) => row.read(c) ?? 0).watchSingle();
}
Future<int> clear() {
return delete(db.searchTokens).go();
}
}
@@ -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 $SearchTokensDaoMixin on i0.DatabaseAccessor<i1.UserDatabase> {
SearchTokensDaoManager get managers => SearchTokensDaoManager(this);
}
class SearchTokensDaoManager {
final $SearchTokensDaoMixin _db;
SearchTokensDaoManager(this._db);
}