improved tab & topic handling

This commit is contained in:
Fabian Freund
2024-08-28 16:00:39 +02:00
parent 95dd254484
commit 0a7e84236f
43 changed files with 2035 additions and 847 deletions
@@ -8,11 +8,42 @@ part 'tab.g.dart';
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
TabDao(super.db);
Future<void> upsertTab(ITab tab) {
Stream<List<TabData>> watchTabs() {
return (db.tab.select()..orderBy([(u) => OrderingTerm.asc(u.id)])).watch();
}
Stream<TabData?> watchTab(String tabId) {
return (db.tab.select()..where((t) => t.id.equals(tabId)))
.watchSingleOrNull();
}
Stream<bool> watchTabExisiting(String tabId) {
final existsStatement =
existsQuery(db.tab.select()..where((t) => t.id.equals(tabId)));
return selectExpressions([existsStatement])
.map((row) => row.read(existsStatement)!)
.watchSingle();
}
Stream<List<String>> watchTopicTabs(String? topicId) {
final query = (selectOnly(db.tab)
..addColumns([db.tab.id])
..where(
(topicId == null)
? db.tab.topicId.isNull()
: db.tab.topicId.equals(topicId),
)
..orderBy([OrderingTerm.asc(db.tab.id)]));
return query.map((row) => row.read(db.tab.id)!).watch();
}
Future<void> upsertTab(ITab tab, DateTime timestamp) {
return db.tab.insertOne(
TabCompanion.insert(
id: tab.id,
timestamp: DateTime.now(),
timestamp: timestamp,
url: tab.url,
topicId: Value(tab.topicId),
title: Value(tab.title),
@@ -24,6 +55,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
Future<void> updateTab(
String id, {
required DateTime timestamp,
Value<Uri> url = const Value.absent(),
Value<String?> title = const Value.absent(),
Value<String?> topicId = const Value.absent(),
@@ -33,7 +65,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
return statement.write(
TabCompanion(
timestamp: Value(DateTime.now()),
timestamp: Value(timestamp),
url: url,
title: title,
topicId: topicId,
@@ -41,4 +73,18 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
),
);
}
Future<void> deleteTab(String id) {
return db.tab.deleteOne(TabCompanion.custom(id: Variable(id)));
}
Future<void> deleteTopicTabs(String? topicId) {
return (db.tab.delete()
..where(
(t) => (topicId == null)
? t.topicId.isNull()
: t.topicId.equals(topicId),
))
.go();
}
}
@@ -3,6 +3,7 @@ import 'dart:ui';
import 'package:drift/drift.dart';
import 'package:lensai/core/uuid.dart';
import 'package:lensai/features/topics/data/database/database.dart';
import 'package:lensai/features/topics/data/models/topic_data.dart';
part 'topic.g.dart';
@@ -5,6 +5,7 @@ import 'package:lensai/features/topics/data/database/daos/tab.dart';
import 'package:lensai/features/topics/data/database/daos/topic.dart';
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
import 'package:lensai/features/topics/data/models/topic_data.dart';
part 'database.g.dart';
@@ -1,11 +1,12 @@
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
import 'package:lensai/features/topics/data/models/topic_data.dart';
CREATE TABLE topic (
id TEXT PRIMARY KEY NOT NULL,
name TEXT,
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`
);
) WITH TopicData;
CREATE TABLE tab (
id TEXT PRIMARY KEY NOT NULL,
@@ -16,12 +17,18 @@ CREATE TABLE tab (
screenshot BLOB
);
topics:
SELECT topic.*
topicsWithCount WITH TopicDataWithCount:
SELECT
topic.*,
tab_agg.tab_count
FROM topic
LEFT JOIN (
SELECT topic_id, MAX(timestamp) AS last_updated
SELECT
topic_id,
COUNT(*) AS tab_count,
MAX(timestamp) AS last_updated
FROM tab
GROUP BY topic_id
) AS tab_max ON topic.id = tab_max.topic_id
ORDER BY tab_max.last_updated DESC NULLS FIRST;
) AS tab_agg ON topic.id = tab_agg.topic_id
ORDER BY tab_agg.last_updated DESC NULLS FIRST;
@@ -56,81 +56,6 @@ class Topic extends Table with TableInfo<Topic, TopicData> {
bool get dontWriteConstraints => true;
}
class TopicData extends DataClass implements Insertable<TopicData> {
final String id;
final String? name;
final Color color;
const TopicData({required this.id, this.name, required this.color});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
if (!nullToAbsent || name != null) {
map['name'] = Variable<String>(name);
}
{
map['color'] = Variable<int>(Topic.$convertercolor.toSql(color));
}
return map;
}
factory TopicData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TopicData(
id: serializer.fromJson<String>(json['id']),
name: serializer.fromJson<String?>(json['name']),
color: serializer.fromJson<Color>(json['color']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'name': serializer.toJson<String?>(name),
'color': serializer.toJson<Color>(color),
};
}
TopicData copyWith(
{String? id,
Value<String?> name = const Value.absent(),
Color? color}) =>
TopicData(
id: id ?? this.id,
name: name.present ? name.value : this.name,
color: color ?? this.color,
);
TopicData copyWithCompanion(TopicCompanion data) {
return TopicData(
id: data.id.present ? data.id.value : this.id,
name: data.name.present ? data.name.value : this.name,
color: data.color.present ? data.color.value : this.color,
);
}
@override
String toString() {
return (StringBuffer('TopicData(')
..write('id: $id, ')
..write('name: $name, ')
..write('color: $color')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, name, color);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TopicData &&
other.id == this.id &&
other.name == this.name &&
other.color == this.color);
}
class TopicCompanion extends UpdateCompanion<TopicData> {
final Value<String> id;
final Value<String?> name;
@@ -511,14 +436,19 @@ abstract class _$TabDatabase extends GeneratedDatabase {
late final Tab tab = Tab(this);
late final TopicDao topicDao = TopicDao(this as TabDatabase);
late final TabDao tabDao = TabDao(this as TabDatabase);
Selectable<TopicData> topics() {
Selectable<TopicDataWithCount> topicsWithCount() {
return customSelect(
'SELECT topic.* FROM topic LEFT JOIN (SELECT topic_id, MAX(timestamp) AS last_updated FROM tab GROUP BY topic_id) AS tab_max ON topic.id = tab_max.topic_id ORDER BY tab_max.last_updated DESC NULLS FIRST',
'SELECT topic.*, tab_agg.tab_count FROM topic LEFT JOIN (SELECT topic_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY topic_id) AS tab_agg ON topic.id = tab_agg.topic_id ORDER BY tab_agg.last_updated DESC NULLS FIRST',
variables: [],
readsFrom: {
topic,
tab,
}).asyncMap(topic.mapFromRow);
}).map((QueryRow row) => TopicDataWithCount(
id: row.read<String>('id'),
name: row.readNullable<String>('name'),
color: Topic.$convertercolor.fromSql(row.read<int>('color')),
tabCount: row.readNullable<int>('tab_count'),
));
}
@override
@@ -2,6 +2,8 @@ import 'package:drift/drift.dart';
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
class UriConverter extends TypeConverter<Uri, String> {
const UriConverter();
@override
Uri fromSql(String fromDb) {
return uri_parser.tryParseUrl(fromDb, eagerParsing: true)!;
@@ -0,0 +1,41 @@
import 'dart:ui';
import 'package:fast_equatable/fast_equatable.dart';
class TopicData with FastEquatable {
final String id;
final String? name;
final Color color;
TopicData({required this.id, this.name, required this.color});
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
id,
name,
color,
];
}
class TopicDataWithCount extends TopicData {
final int? tabCount;
TopicDataWithCount({
required super.id,
super.name,
required super.color,
required this.tabCount,
});
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
...super.hashParameters,
tabCount,
];
}
@@ -35,3 +35,15 @@ TabDatabase tabDatabase(TabDatabaseRef ref) {
}),
);
}
@Riverpod()
Stream<bool> isTabExisting(IsTabExistingRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.watchTabExisiting(tabId);
}
@Riverpod()
Stream<TabData?> tabData(TabDataRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.watchTab(tabId);
}
@@ -20,5 +20,280 @@ final tabDatabaseProvider = Provider<TabDatabase>.internal(
);
typedef TabDatabaseRef = ProviderRef<TabDatabase>;
String _$isTabExistingHash() => r'98fc7bf073ba8f73c20df6ba7076672ff497246f';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [isTabExisting].
@ProviderFor(isTabExisting)
const isTabExistingProvider = IsTabExistingFamily();
/// See also [isTabExisting].
class IsTabExistingFamily extends Family<AsyncValue<bool>> {
/// See also [isTabExisting].
const IsTabExistingFamily();
/// See also [isTabExisting].
IsTabExistingProvider call(
String tabId,
) {
return IsTabExistingProvider(
tabId,
);
}
@override
IsTabExistingProvider getProviderOverride(
covariant IsTabExistingProvider provider,
) {
return call(
provider.tabId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'isTabExistingProvider';
}
/// See also [isTabExisting].
class IsTabExistingProvider extends AutoDisposeStreamProvider<bool> {
/// See also [isTabExisting].
IsTabExistingProvider(
String tabId,
) : this._internal(
(ref) => isTabExisting(
ref as IsTabExistingRef,
tabId,
),
from: isTabExistingProvider,
name: r'isTabExistingProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$isTabExistingHash,
dependencies: IsTabExistingFamily._dependencies,
allTransitiveDependencies:
IsTabExistingFamily._allTransitiveDependencies,
tabId: tabId,
);
IsTabExistingProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<bool> Function(IsTabExistingRef provider) create,
) {
return ProviderOverride(
origin: this,
override: IsTabExistingProvider._internal(
(ref) => create(ref as IsTabExistingRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<bool> createElement() {
return _IsTabExistingProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is IsTabExistingProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin IsTabExistingRef on AutoDisposeStreamProviderRef<bool> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _IsTabExistingProviderElement
extends AutoDisposeStreamProviderElement<bool> with IsTabExistingRef {
_IsTabExistingProviderElement(super.provider);
@override
String get tabId => (origin as IsTabExistingProvider).tabId;
}
String _$tabDataHash() => r'8b799386305d0c31103bc48a5a467df41d57d065';
/// See also [tabData].
@ProviderFor(tabData)
const tabDataProvider = TabDataFamily();
/// See also [tabData].
class TabDataFamily extends Family<AsyncValue<TabData?>> {
/// See also [tabData].
const TabDataFamily();
/// See also [tabData].
TabDataProvider call(
String tabId,
) {
return TabDataProvider(
tabId,
);
}
@override
TabDataProvider getProviderOverride(
covariant TabDataProvider provider,
) {
return call(
provider.tabId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabDataProvider';
}
/// See also [tabData].
class TabDataProvider extends AutoDisposeStreamProvider<TabData?> {
/// See also [tabData].
TabDataProvider(
String tabId,
) : this._internal(
(ref) => tabData(
ref as TabDataRef,
tabId,
),
from: tabDataProvider,
name: r'tabDataProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabDataHash,
dependencies: TabDataFamily._dependencies,
allTransitiveDependencies: TabDataFamily._allTransitiveDependencies,
tabId: tabId,
);
TabDataProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<TabData?> Function(TabDataRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TabDataProvider._internal(
(ref) => create(ref as TabDataRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<TabData?> createElement() {
return _TabDataProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabDataProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin TabDataRef on AutoDisposeStreamProviderRef<TabData?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabDataProviderElement extends AutoDisposeStreamProviderElement<TabData?>
with TabDataRef {
_TabDataProviderElement(super.provider);
@override
String get tabId => (origin as TabDataProvider).tabId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member