improved tab & topic handling
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||
import 'package:lensai/features/topics/data/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/topics/utils/color_palette.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<TopicData>> topicList(TopicListRef ref) {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
return repository.watchTopics();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SelectedTopic extends _$SelectedTopic {
|
||||
void setTopic(String id) {
|
||||
@@ -40,21 +35,22 @@ class SelectedTopic extends _$SelectedTopic {
|
||||
|
||||
@Riverpod()
|
||||
Stream<TopicData?> selectedTopicData(SelectedTopicDataRef ref) {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
final selectedBangTrigger = ref.watch(selectedTopicProvider);
|
||||
return repository.watchTopic(selectedBangTrigger);
|
||||
}
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||
|
||||
@Riverpod()
|
||||
Future<Set<Color>> distinctTopicColors(DistinctTopicColorsRef ref) {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
return repository.getDistinctColors();
|
||||
if (selectedTopic != null) {
|
||||
return db.topicDao.getTopicData(selectedTopic).watchSingleOrNull();
|
||||
}
|
||||
|
||||
return Stream.value(null);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
|
||||
final allColors = colorTypes.flattened.toList();
|
||||
final usedColors = await ref.read(distinctTopicColorsProvider.future);
|
||||
final usedColors = await repository.getDistinctColors();
|
||||
|
||||
Color randomColor;
|
||||
do {
|
||||
|
||||
@@ -6,21 +6,7 @@ part of 'providers.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$topicListHash() => r'7220aef5653bb3c2b44b016ce4f083f20896b353';
|
||||
|
||||
/// See also [topicList].
|
||||
@ProviderFor(topicList)
|
||||
final topicListProvider = AutoDisposeStreamProvider<List<TopicData>>.internal(
|
||||
topicList,
|
||||
name: r'topicListProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$topicListHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef TopicListRef = AutoDisposeStreamProviderRef<List<TopicData>>;
|
||||
String _$selectedTopicDataHash() => r'191ad088ab90ef2348a836593f92b48d095afdc8';
|
||||
String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281';
|
||||
|
||||
/// See also [selectedTopicData].
|
||||
@ProviderFor(selectedTopicData)
|
||||
@@ -36,25 +22,8 @@ final selectedTopicDataProvider =
|
||||
);
|
||||
|
||||
typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef<TopicData?>;
|
||||
String _$distinctTopicColorsHash() =>
|
||||
r'c671f349615313d3b17e93e9e46fae12889e5e1f';
|
||||
|
||||
/// See also [distinctTopicColors].
|
||||
@ProviderFor(distinctTopicColors)
|
||||
final distinctTopicColorsProvider =
|
||||
AutoDisposeFutureProvider<Set<Color>>.internal(
|
||||
distinctTopicColors,
|
||||
name: r'distinctTopicColorsProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$distinctTopicColorsHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef DistinctTopicColorsRef = AutoDisposeFutureProviderRef<Set<Color>>;
|
||||
String _$unusedRandomTopicColorHash() =>
|
||||
r'8f6907bb50a5bf2ac0320e93531d9e61616337b9';
|
||||
r'099730b0d987cc37bcae4ef5c999cbebfa67d7da';
|
||||
|
||||
/// See also [unusedRandomTopicColor].
|
||||
@ProviderFor(unusedRandomTopicColor)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/providers.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
@@ -9,7 +11,49 @@ class TabRepository extends _$TabRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
void build() {
|
||||
Stream<List<TabData>> build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
return _db.tabDao.watchTabs();
|
||||
}
|
||||
|
||||
Future<void> updateTab(
|
||||
String id, {
|
||||
Value<Uri> url = const Value.absent(),
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
Value<Uint8List?> screenshot = const Value.absent(),
|
||||
}) {
|
||||
return _db.tabDao.updateTab(
|
||||
id,
|
||||
timestamp: DateTime.now(),
|
||||
url: url,
|
||||
title: title,
|
||||
topicId: topicId,
|
||||
screenshot: screenshot,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteTab(String id) {
|
||||
return _db.tabDao.deleteTab(id);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class TopicTabRepository extends _$TopicTabRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
Stream<List<String>> build(String? topicId) {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
return _db.tabDao.watchTopicTabs(topicId);
|
||||
}
|
||||
|
||||
Future<void> addTab(ITab tab) {
|
||||
assert(tab.topicId == topicId);
|
||||
return _db.tabDao.upsertTab(tab, DateTime.now());
|
||||
}
|
||||
|
||||
Future<void> closeAllTabs() {
|
||||
return _db.tabDao.deleteTopicTabs(topicId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,12 @@ part of 'tab.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabRepositoryHash() => r'3f564f553e9586df34e927b20fe4bc860e0814e9';
|
||||
String _$tabRepositoryHash() => r'454e182a99910512be72279f68caa0af125ed165';
|
||||
|
||||
/// See also [TabRepository].
|
||||
@ProviderFor(TabRepository)
|
||||
final tabRepositoryProvider = NotifierProvider<TabRepository, void>.internal(
|
||||
final tabRepositoryProvider =
|
||||
StreamNotifierProvider<TabRepository, List<TabData>>.internal(
|
||||
TabRepository.new,
|
||||
name: r'tabRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
@@ -20,6 +21,172 @@ final tabRepositoryProvider = NotifierProvider<TabRepository, void>.internal(
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TabRepository = Notifier<void>;
|
||||
typedef _$TabRepository = StreamNotifier<List<TabData>>;
|
||||
String _$topicTabRepositoryHash() =>
|
||||
r'52ceb013e0b31046fdd0a00d031392ba84b9ecda';
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$TopicTabRepository
|
||||
extends BuildlessAutoDisposeStreamNotifier<List<String>> {
|
||||
late final String? topicId;
|
||||
|
||||
Stream<List<String>> build(
|
||||
String? topicId,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [TopicTabRepository].
|
||||
@ProviderFor(TopicTabRepository)
|
||||
const topicTabRepositoryProvider = TopicTabRepositoryFamily();
|
||||
|
||||
/// See also [TopicTabRepository].
|
||||
class TopicTabRepositoryFamily extends Family<AsyncValue<List<String>>> {
|
||||
/// See also [TopicTabRepository].
|
||||
const TopicTabRepositoryFamily();
|
||||
|
||||
/// See also [TopicTabRepository].
|
||||
TopicTabRepositoryProvider call(
|
||||
String? topicId,
|
||||
) {
|
||||
return TopicTabRepositoryProvider(
|
||||
topicId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TopicTabRepositoryProvider getProviderOverride(
|
||||
covariant TopicTabRepositoryProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.topicId,
|
||||
);
|
||||
}
|
||||
|
||||
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'topicTabRepositoryProvider';
|
||||
}
|
||||
|
||||
/// See also [TopicTabRepository].
|
||||
class TopicTabRepositoryProvider extends AutoDisposeStreamNotifierProviderImpl<
|
||||
TopicTabRepository, List<String>> {
|
||||
/// See also [TopicTabRepository].
|
||||
TopicTabRepositoryProvider(
|
||||
String? topicId,
|
||||
) : this._internal(
|
||||
() => TopicTabRepository()..topicId = topicId,
|
||||
from: topicTabRepositoryProvider,
|
||||
name: r'topicTabRepositoryProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$topicTabRepositoryHash,
|
||||
dependencies: TopicTabRepositoryFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
TopicTabRepositoryFamily._allTransitiveDependencies,
|
||||
topicId: topicId,
|
||||
);
|
||||
|
||||
TopicTabRepositoryProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.topicId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? topicId;
|
||||
|
||||
@override
|
||||
Stream<List<String>> runNotifierBuild(
|
||||
covariant TopicTabRepository notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
topicId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(TopicTabRepository Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TopicTabRepositoryProvider._internal(
|
||||
() => create()..topicId = topicId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
topicId: topicId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamNotifierProviderElement<TopicTabRepository, List<String>>
|
||||
createElement() {
|
||||
return _TopicTabRepositoryProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TopicTabRepositoryProvider && other.topicId == topicId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, topicId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin TopicTabRepositoryRef
|
||||
on AutoDisposeStreamNotifierProviderRef<List<String>> {
|
||||
/// The parameter `topicId` of this provider.
|
||||
String? get topicId;
|
||||
}
|
||||
|
||||
class _TopicTabRepositoryProviderElement
|
||||
extends AutoDisposeStreamNotifierProviderElement<TopicTabRepository,
|
||||
List<String>> with TopicTabRepositoryRef {
|
||||
_TopicTabRepositoryProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get topicId => (origin as TopicTabRepositoryProvider).topicId;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||
import 'package:lensai/features/topics/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -11,8 +12,9 @@ class TopicRepository extends _$TopicRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
void build() {
|
||||
Stream<List<TopicDataWithCount>> build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
return _db.topicsWithCount().watch();
|
||||
}
|
||||
|
||||
Future<void> addTopic({required String? name, required Color color}) {
|
||||
@@ -31,18 +33,6 @@ class TopicRepository extends _$TopicRepository {
|
||||
return _db.topicDao.deleteTopic(id);
|
||||
}
|
||||
|
||||
Stream<List<TopicData>> watchTopics() {
|
||||
return _db.topics().watch();
|
||||
}
|
||||
|
||||
Stream<TopicData?> watchTopic(String? id) {
|
||||
if (id != null) {
|
||||
return _db.topicDao.getTopicData(id).watchSingleOrNull();
|
||||
} else {
|
||||
return Stream.value(null);
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<Color>> getDistinctColors() {
|
||||
return _db.topicDao
|
||||
.getDistinctColors()
|
||||
|
||||
@@ -6,12 +6,12 @@ part of 'topic.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$topicRepositoryHash() => r'f0e8d3170d3706a79decfbe4700f20fcdebfae2c';
|
||||
String _$topicRepositoryHash() => r'3b75c5fb1a081ba6d90e2ccb781469d0aa87c415';
|
||||
|
||||
/// See also [TopicRepository].
|
||||
@ProviderFor(TopicRepository)
|
||||
final topicRepositoryProvider =
|
||||
NotifierProvider<TopicRepository, void>.internal(
|
||||
StreamNotifierProvider<TopicRepository, List<TopicDataWithCount>>.internal(
|
||||
TopicRepository.new,
|
||||
name: r'topicRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
@@ -21,6 +21,6 @@ final topicRepositoryProvider =
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TopicRepository = Notifier<void>;
|
||||
typedef _$TopicRepository = StreamNotifier<List<TopicDataWithCount>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||
import 'package:lensai/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/topic_dialog.dart';
|
||||
@@ -147,38 +148,44 @@ class TopicListScreen extends HookConsumerWidget {
|
||||
),
|
||||
body: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
final topicsAsync = ref.watch(topicRepositoryProvider);
|
||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: topicsAsync.isLoading,
|
||||
child: topicsAsync.when(
|
||||
data: (topics) => ListView.builder(
|
||||
itemCount: topics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = topics[index];
|
||||
return _TopicTile(
|
||||
topic,
|
||||
key: ValueKey(topic.id),
|
||||
isSelected: topic.id == selectedTopic,
|
||||
onEdit: (edited) async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.replaceTopic(
|
||||
id: topic.id,
|
||||
name: edited.name,
|
||||
color: edited.color,
|
||||
);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.deleteTopic(topic.id);
|
||||
},
|
||||
onTap: () {
|
||||
ref
|
||||
.read(selectedTopicProvider.notifier)
|
||||
.toggleTopic(topic.id);
|
||||
data: (topics) => FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView.builder(
|
||||
controller: controller,
|
||||
itemCount: topics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = topics[index];
|
||||
return _TopicTile(
|
||||
topic,
|
||||
key: ValueKey(topic.id),
|
||||
isSelected: topic.id == selectedTopic,
|
||||
onEdit: (edited) async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.replaceTopic(
|
||||
id: topic.id,
|
||||
name: edited.name,
|
||||
color: edited.color,
|
||||
);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.deleteTopic(topic.id);
|
||||
},
|
||||
onTap: () {
|
||||
ref
|
||||
.read(selectedTopicProvider.notifier)
|
||||
.toggleTopic(topic.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -187,7 +194,7 @@ class TopicListScreen extends HookConsumerWidget {
|
||||
loading: () => ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => _TopicTile(
|
||||
const TopicData(id: 'null', color: Colors.transparent),
|
||||
TopicData(id: 'null', color: Colors.transparent),
|
||||
isSelected: false,
|
||||
onEdit: (_) {},
|
||||
onDelete: () {},
|
||||
|
||||
@@ -3,12 +3,13 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/core/routing/routes.dart';
|
||||
import 'package:lensai/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class TopicChips extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
final topicsAsync = ref.watch(topicRepositoryProvider);
|
||||
final selectedTopic = ref
|
||||
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
||||
|
||||
@@ -17,6 +18,7 @@ class TopicChips extends HookConsumerWidget {
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
if (selectedTopic != null || availableTopics.isNotEmpty)
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
@@ -31,6 +33,7 @@ class TopicChips extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
itemLabel: (topic) => Text(topic.name ?? 'New Topic'),
|
||||
itemBadgeCount: (topic) => topic.tabCount,
|
||||
availableItems: availableTopics,
|
||||
selectedItem: selectedTopic,
|
||||
onSelected: (topic) {
|
||||
|
||||
Reference in New Issue
Block a user