topics intermediate
This commit is contained in:
@@ -10,6 +10,7 @@ import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
|
||||
import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
|
||||
import 'package:lensai/features/search_browser/presentation/screens/browser.dart';
|
||||
import 'package:lensai/features/settings/presentation/screens/settings.dart';
|
||||
import 'package:lensai/features/topics/presentation/screens/topic_list.dart';
|
||||
|
||||
part 'routes.g.dart';
|
||||
|
||||
@@ -41,6 +42,10 @@ part 'routes.g.dart';
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<TopicListRoute>(
|
||||
name: 'TopicsRoute',
|
||||
path: 'topics',
|
||||
),
|
||||
],
|
||||
)
|
||||
class KagiRoute extends GoRouteData {
|
||||
@@ -97,6 +102,13 @@ class BangSearchRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class TopicListRoute extends GoRouteData {
|
||||
@override
|
||||
Widget build(BuildContext context, GoRouterState state) {
|
||||
return const TopicListScreen();
|
||||
}
|
||||
}
|
||||
|
||||
@TypedGoRoute<SettingsRoute>(
|
||||
name: 'SettingsRoute',
|
||||
path: '/settings',
|
||||
|
||||
@@ -46,6 +46,11 @@ RouteBase get $kagiRoute => GoRouteData.$route(
|
||||
),
|
||||
],
|
||||
),
|
||||
GoRouteData.$route(
|
||||
path: 'topics',
|
||||
name: 'TopicsRoute',
|
||||
factory: $TopicListRouteExtension._fromState,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -158,6 +163,23 @@ extension $BangSubCategoryRouteExtension on BangSubCategoryRoute {
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
extension $TopicListRouteExtension on TopicListRoute {
|
||||
static TopicListRoute _fromState(GoRouterState state) => TopicListRoute();
|
||||
|
||||
String get location => GoRouteData.$location(
|
||||
'/topics',
|
||||
);
|
||||
|
||||
void go(BuildContext context) => context.go(location);
|
||||
|
||||
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
|
||||
|
||||
void pushReplacement(BuildContext context) =>
|
||||
context.pushReplacement(location);
|
||||
|
||||
void replace(BuildContext context) => context.replace(location);
|
||||
}
|
||||
|
||||
RouteBase get $settingsRoute => GoRouteData.$route(
|
||||
path: '/settings',
|
||||
name: 'SettingsRoute',
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/topic_chips.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart';
|
||||
@@ -24,28 +25,36 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
builder: (context, ref, child) {
|
||||
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
|
||||
return Align(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(switchNewTabControllerProvider.notifier)
|
||||
.add(Uri.https('kagi.com'));
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
TextButton.icon(
|
||||
onPressed: () async {
|
||||
await ref
|
||||
.read(switchNewTabControllerProvider.notifier)
|
||||
.add(Uri.https('kagi.com'));
|
||||
|
||||
onClose();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('New Tab'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
ref.read(webViewRepositoryProvider.notifier).closeAllTabs();
|
||||
onClose();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Close All'),
|
||||
onClose();
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
label: const Text('New Tab'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(webViewRepositoryProvider.notifier)
|
||||
.closeAllTabs();
|
||||
onClose();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
label: const Text('Close All'),
|
||||
),
|
||||
],
|
||||
),
|
||||
TopicChips(),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -55,10 +64,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
double get minExtent => 48;
|
||||
double get minExtent => 96;
|
||||
|
||||
@override
|
||||
double get maxExtent => 48;
|
||||
double get maxExtent => 96;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||
TabDao(super.db);
|
||||
|
||||
Future<void> upsertTab(ITab tab) {
|
||||
return db.tab.insertOne(
|
||||
TabCompanion.insert(
|
||||
id: tab.id,
|
||||
timestamp: DateTime.now(),
|
||||
url: tab.url,
|
||||
topicId: Value(tab.topicId),
|
||||
title: Value(tab.title),
|
||||
screenshot: Value(tab.screenshot),
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
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(),
|
||||
}) {
|
||||
final statement = db.tab.update()..where((t) => t.id.equals(id));
|
||||
|
||||
return statement.write(
|
||||
TabCompanion(
|
||||
timestamp: Value(DateTime.now()),
|
||||
url: url,
|
||||
title: title,
|
||||
topicId: topicId,
|
||||
screenshot: screenshot,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$TabDaoMixin on DatabaseAccessor<TabDatabase> {}
|
||||
@@ -0,0 +1,53 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/core/uuid.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
|
||||
part 'topic.g.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class TopicDao extends DatabaseAccessor<TabDatabase> with _$TopicDaoMixin {
|
||||
TopicDao(super.db);
|
||||
|
||||
Future<void> addTopic({String? name, required Color color}) {
|
||||
return db.topic.insertOne(
|
||||
TopicCompanion.insert(
|
||||
id: uuid.v7(),
|
||||
name: Value(name),
|
||||
color: color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> replaceTopic(
|
||||
String id, {
|
||||
required String? name,
|
||||
required Color color,
|
||||
}) {
|
||||
return db.topic.replaceOne(
|
||||
TopicCompanion(
|
||||
id: Value(id),
|
||||
name: Value(name),
|
||||
color: Value(color),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteTopic(String id) {
|
||||
return db.topic.deleteOne(TopicCompanion.custom(id: Variable(id)));
|
||||
}
|
||||
|
||||
SingleOrNullSelectable<TopicData> getTopicData(String id) {
|
||||
return select(db.topic)..where((t) => t.id.equals(id));
|
||||
}
|
||||
|
||||
Selectable<Color> getDistinctColors() {
|
||||
final query = db.selectOnly(db.topic, distinct: true)
|
||||
..addColumns([db.topic.color])
|
||||
..where(db.topic.color.isNotNull());
|
||||
|
||||
return query
|
||||
.map((row) => row.readWithConverter<Color?, int>(db.topic.color)!);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'topic.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$TopicDaoMixin on DatabaseAccessor<TabDatabase> {}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
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';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
include: {'database.drift'},
|
||||
daos: [TopicDao, TabDao],
|
||||
)
|
||||
class TabDatabase extends _$TabDatabase {
|
||||
@override
|
||||
final int schemaVersion = 1;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
);
|
||||
|
||||
TabDatabase(super.e);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
|
||||
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
|
||||
|
||||
CREATE TABLE topic (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT,
|
||||
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`
|
||||
);
|
||||
|
||||
CREATE TABLE tab (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
topic_id TEXT REFERENCES topic (id) ON DELETE CASCADE,
|
||||
timestamp DATETIME NOT NULL,
|
||||
url TEXT NOT NULL MAPPED BY `const UriConverter()`,
|
||||
title TEXT,
|
||||
screenshot BLOB
|
||||
);
|
||||
|
||||
topics:
|
||||
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;
|
||||
@@ -0,0 +1,930 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'database.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
class Topic extends Table with TableInfo<Topic, TopicData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Topic(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL');
|
||||
late final GeneratedColumn<String> name = GeneratedColumn<String>(
|
||||
'name', aliasedName, true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '');
|
||||
late final GeneratedColumnWithTypeConverter<Color, int> color =
|
||||
GeneratedColumn<int>('color', aliasedName, false,
|
||||
type: DriftSqlType.int,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL')
|
||||
.withConverter<Color>(Topic.$convertercolor);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, name, color];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'topic';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TopicData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TopicData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||
name: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}name']),
|
||||
color: Topic.$convertercolor.fromSql(attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.int, data['${effectivePrefix}color'])!),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Topic createAlias(String alias) {
|
||||
return Topic(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static TypeConverter<Color, int> $convertercolor = const ColorConverter();
|
||||
@override
|
||||
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;
|
||||
final Value<Color> color;
|
||||
final Value<int> rowid;
|
||||
const TopicCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.name = const Value.absent(),
|
||||
this.color = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
TopicCompanion.insert({
|
||||
required String id,
|
||||
this.name = const Value.absent(),
|
||||
required Color color,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
color = Value(color);
|
||||
static Insertable<TopicData> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? name,
|
||||
Expression<int>? color,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (name != null) 'name': name,
|
||||
if (color != null) 'color': color,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
TopicCompanion copyWith(
|
||||
{Value<String>? id,
|
||||
Value<String?>? name,
|
||||
Value<Color>? color,
|
||||
Value<int>? rowid}) {
|
||||
return TopicCompanion(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
color: color ?? this.color,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
}
|
||||
if (name.present) {
|
||||
map['name'] = Variable<String>(name.value);
|
||||
}
|
||||
if (color.present) {
|
||||
map['color'] = Variable<int>(Topic.$convertercolor.toSql(color.value));
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TopicCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('name: $name, ')
|
||||
..write('color: $color, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
class Tab extends Table with TableInfo<Tab, TabData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
Tab(this.attachedDatabase, [this._alias]);
|
||||
late final GeneratedColumn<String> id = GeneratedColumn<String>(
|
||||
'id', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'PRIMARY KEY NOT NULL');
|
||||
late final GeneratedColumn<String> topicId = GeneratedColumn<String>(
|
||||
'topic_id', aliasedName, true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: 'REFERENCES topic(id)ON DELETE CASCADE');
|
||||
late final GeneratedColumn<DateTime> timestamp = GeneratedColumn<DateTime>(
|
||||
'timestamp', aliasedName, false,
|
||||
type: DriftSqlType.dateTime,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL');
|
||||
late final GeneratedColumnWithTypeConverter<Uri, String> url =
|
||||
GeneratedColumn<String>('url', aliasedName, false,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: true,
|
||||
$customConstraints: 'NOT NULL')
|
||||
.withConverter<Uri>(Tab.$converterurl);
|
||||
late final GeneratedColumn<String> title = GeneratedColumn<String>(
|
||||
'title', aliasedName, true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '');
|
||||
late final GeneratedColumn<Uint8List> screenshot = GeneratedColumn<Uint8List>(
|
||||
'screenshot', aliasedName, true,
|
||||
type: DriftSqlType.blob,
|
||||
requiredDuringInsert: false,
|
||||
$customConstraints: '');
|
||||
@override
|
||||
List<GeneratedColumn> get $columns =>
|
||||
[id, topicId, timestamp, url, title, screenshot];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'tab';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TabData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TabData(
|
||||
id: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
|
||||
topicId: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}topic_id']),
|
||||
timestamp: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.dateTime, data['${effectivePrefix}timestamp'])!,
|
||||
url: Tab.$converterurl.fromSql(attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}url'])!),
|
||||
title: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.string, data['${effectivePrefix}title']),
|
||||
screenshot: attachedDatabase.typeMapping
|
||||
.read(DriftSqlType.blob, data['${effectivePrefix}screenshot']),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Tab createAlias(String alias) {
|
||||
return Tab(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
static TypeConverter<Uri, String> $converterurl = const UriConverter();
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class TabData extends DataClass implements Insertable<TabData> {
|
||||
final String id;
|
||||
final String? topicId;
|
||||
final DateTime timestamp;
|
||||
final Uri url;
|
||||
final String? title;
|
||||
final Uint8List? screenshot;
|
||||
const TabData(
|
||||
{required this.id,
|
||||
this.topicId,
|
||||
required this.timestamp,
|
||||
required this.url,
|
||||
this.title,
|
||||
this.screenshot});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
map['id'] = Variable<String>(id);
|
||||
if (!nullToAbsent || topicId != null) {
|
||||
map['topic_id'] = Variable<String>(topicId);
|
||||
}
|
||||
map['timestamp'] = Variable<DateTime>(timestamp);
|
||||
{
|
||||
map['url'] = Variable<String>(Tab.$converterurl.toSql(url));
|
||||
}
|
||||
if (!nullToAbsent || title != null) {
|
||||
map['title'] = Variable<String>(title);
|
||||
}
|
||||
if (!nullToAbsent || screenshot != null) {
|
||||
map['screenshot'] = Variable<Uint8List>(screenshot);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
factory TabData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TabData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
topicId: serializer.fromJson<String?>(json['topic_id']),
|
||||
timestamp: serializer.fromJson<DateTime>(json['timestamp']),
|
||||
url: serializer.fromJson<Uri>(json['url']),
|
||||
title: serializer.fromJson<String?>(json['title']),
|
||||
screenshot: serializer.fromJson<Uint8List?>(json['screenshot']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return <String, dynamic>{
|
||||
'id': serializer.toJson<String>(id),
|
||||
'topic_id': serializer.toJson<String?>(topicId),
|
||||
'timestamp': serializer.toJson<DateTime>(timestamp),
|
||||
'url': serializer.toJson<Uri>(url),
|
||||
'title': serializer.toJson<String?>(title),
|
||||
'screenshot': serializer.toJson<Uint8List?>(screenshot),
|
||||
};
|
||||
}
|
||||
|
||||
TabData copyWith(
|
||||
{String? id,
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
DateTime? timestamp,
|
||||
Uri? url,
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<Uint8List?> screenshot = const Value.absent()}) =>
|
||||
TabData(
|
||||
id: id ?? this.id,
|
||||
topicId: topicId.present ? topicId.value : this.topicId,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
url: url ?? this.url,
|
||||
title: title.present ? title.value : this.title,
|
||||
screenshot: screenshot.present ? screenshot.value : this.screenshot,
|
||||
);
|
||||
TabData copyWithCompanion(TabCompanion data) {
|
||||
return TabData(
|
||||
id: data.id.present ? data.id.value : this.id,
|
||||
topicId: data.topicId.present ? data.topicId.value : this.topicId,
|
||||
timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp,
|
||||
url: data.url.present ? data.url.value : this.url,
|
||||
title: data.title.present ? data.title.value : this.title,
|
||||
screenshot:
|
||||
data.screenshot.present ? data.screenshot.value : this.screenshot,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TabData(')
|
||||
..write('id: $id, ')
|
||||
..write('topicId: $topicId, ')
|
||||
..write('timestamp: $timestamp, ')
|
||||
..write('url: $url, ')
|
||||
..write('title: $title, ')
|
||||
..write('screenshot: $screenshot')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
id, topicId, timestamp, url, title, $driftBlobEquality.hash(screenshot));
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is TabData &&
|
||||
other.id == this.id &&
|
||||
other.topicId == this.topicId &&
|
||||
other.timestamp == this.timestamp &&
|
||||
other.url == this.url &&
|
||||
other.title == this.title &&
|
||||
$driftBlobEquality.equals(other.screenshot, this.screenshot));
|
||||
}
|
||||
|
||||
class TabCompanion extends UpdateCompanion<TabData> {
|
||||
final Value<String> id;
|
||||
final Value<String?> topicId;
|
||||
final Value<DateTime> timestamp;
|
||||
final Value<Uri> url;
|
||||
final Value<String?> title;
|
||||
final Value<Uint8List?> screenshot;
|
||||
final Value<int> rowid;
|
||||
const TabCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.topicId = const Value.absent(),
|
||||
this.timestamp = const Value.absent(),
|
||||
this.url = const Value.absent(),
|
||||
this.title = const Value.absent(),
|
||||
this.screenshot = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
TabCompanion.insert({
|
||||
required String id,
|
||||
this.topicId = const Value.absent(),
|
||||
required DateTime timestamp,
|
||||
required Uri url,
|
||||
this.title = const Value.absent(),
|
||||
this.screenshot = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
timestamp = Value(timestamp),
|
||||
url = Value(url);
|
||||
static Insertable<TabData> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? topicId,
|
||||
Expression<DateTime>? timestamp,
|
||||
Expression<String>? url,
|
||||
Expression<String>? title,
|
||||
Expression<Uint8List>? screenshot,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (topicId != null) 'topic_id': topicId,
|
||||
if (timestamp != null) 'timestamp': timestamp,
|
||||
if (url != null) 'url': url,
|
||||
if (title != null) 'title': title,
|
||||
if (screenshot != null) 'screenshot': screenshot,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
TabCompanion copyWith(
|
||||
{Value<String>? id,
|
||||
Value<String?>? topicId,
|
||||
Value<DateTime>? timestamp,
|
||||
Value<Uri>? url,
|
||||
Value<String?>? title,
|
||||
Value<Uint8List?>? screenshot,
|
||||
Value<int>? rowid}) {
|
||||
return TabCompanion(
|
||||
id: id ?? this.id,
|
||||
topicId: topicId ?? this.topicId,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
url: url ?? this.url,
|
||||
title: title ?? this.title,
|
||||
screenshot: screenshot ?? this.screenshot,
|
||||
rowid: rowid ?? this.rowid,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
final map = <String, Expression>{};
|
||||
if (id.present) {
|
||||
map['id'] = Variable<String>(id.value);
|
||||
}
|
||||
if (topicId.present) {
|
||||
map['topic_id'] = Variable<String>(topicId.value);
|
||||
}
|
||||
if (timestamp.present) {
|
||||
map['timestamp'] = Variable<DateTime>(timestamp.value);
|
||||
}
|
||||
if (url.present) {
|
||||
map['url'] = Variable<String>(Tab.$converterurl.toSql(url.value));
|
||||
}
|
||||
if (title.present) {
|
||||
map['title'] = Variable<String>(title.value);
|
||||
}
|
||||
if (screenshot.present) {
|
||||
map['screenshot'] = Variable<Uint8List>(screenshot.value);
|
||||
}
|
||||
if (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TabCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('topicId: $topicId, ')
|
||||
..write('timestamp: $timestamp, ')
|
||||
..write('url: $url, ')
|
||||
..write('title: $title, ')
|
||||
..write('screenshot: $screenshot, ')
|
||||
..write('rowid: $rowid')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$TabDatabase extends GeneratedDatabase {
|
||||
_$TabDatabase(QueryExecutor e) : super(e);
|
||||
$TabDatabaseManager get managers => $TabDatabaseManager(this);
|
||||
late final Topic topic = Topic(this);
|
||||
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() {
|
||||
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',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
topic,
|
||||
tab,
|
||||
}).asyncMap(topic.mapFromRow);
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [topic, tab];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
|
||||
[
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('topic',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
result: [
|
||||
TableUpdate('tab', kind: UpdateKind.delete),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
typedef $TopicCreateCompanionBuilder = TopicCompanion Function({
|
||||
required String id,
|
||||
Value<String?> name,
|
||||
required Color color,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $TopicUpdateCompanionBuilder = TopicCompanion Function({
|
||||
Value<String> id,
|
||||
Value<String?> name,
|
||||
Value<Color> color,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
final class $TopicReferences
|
||||
extends BaseReferences<_$TabDatabase, Topic, TopicData> {
|
||||
$TopicReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static MultiTypedResultKey<Tab, List<TabData>> _tabRefsTable(
|
||||
_$TabDatabase db) =>
|
||||
MultiTypedResultKey.fromTable(db.tab,
|
||||
aliasName: $_aliasNameGenerator(db.topic.id, db.tab.topicId));
|
||||
|
||||
$TabProcessedTableManager get tabRefs {
|
||||
final manager =
|
||||
$TabTableManager($_db, $_db.tab).filter((f) => f.topicId.id($_item.id));
|
||||
|
||||
final cache = $_typedResult.readTableOrNull(_tabRefsTable($_db));
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: cache));
|
||||
}
|
||||
}
|
||||
|
||||
class $TopicFilterComposer extends FilterComposer<_$TabDatabase, Topic> {
|
||||
$TopicFilterComposer(super.$state);
|
||||
ColumnFilters<String> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<String> get name => $state.composableBuilder(
|
||||
column: $state.table.name,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnWithTypeConverterFilters<Color, Color, int> get color =>
|
||||
$state.composableBuilder(
|
||||
column: $state.table.color,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
|
||||
ComposableFilter tabRefs(ComposableFilter Function($TabFilterComposer f) f) {
|
||||
final $TabFilterComposer composer = $state.composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.id,
|
||||
referencedTable: $state.db.tab,
|
||||
getReferencedColumn: (t) => t.topicId,
|
||||
builder: (joinBuilder, parentComposers) => $TabFilterComposer(
|
||||
ComposerState(
|
||||
$state.db, $state.db.tab, joinBuilder, parentComposers)));
|
||||
return f(composer);
|
||||
}
|
||||
}
|
||||
|
||||
class $TopicOrderingComposer extends OrderingComposer<_$TabDatabase, Topic> {
|
||||
$TopicOrderingComposer(super.$state);
|
||||
ColumnOrderings<String> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<String> get name => $state.composableBuilder(
|
||||
column: $state.table.name,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<int> get color => $state.composableBuilder(
|
||||
column: $state.table.color,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
}
|
||||
|
||||
class $TopicTableManager extends RootTableManager<
|
||||
_$TabDatabase,
|
||||
Topic,
|
||||
TopicData,
|
||||
$TopicFilterComposer,
|
||||
$TopicOrderingComposer,
|
||||
$TopicCreateCompanionBuilder,
|
||||
$TopicUpdateCompanionBuilder,
|
||||
(TopicData, $TopicReferences),
|
||||
TopicData,
|
||||
PrefetchHooks Function({bool tabRefs})> {
|
||||
$TopicTableManager(_$TabDatabase db, Topic table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $TopicFilterComposer(ComposerState(db, table)),
|
||||
orderingComposer: $TopicOrderingComposer(ComposerState(db, table)),
|
||||
updateCompanionCallback: ({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String?> name = const Value.absent(),
|
||||
Value<Color> color = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TopicCompanion(
|
||||
id: id,
|
||||
name: name,
|
||||
color: color,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String id,
|
||||
Value<String?> name = const Value.absent(),
|
||||
required Color color,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TopicCompanion.insert(
|
||||
id: id,
|
||||
name: name,
|
||||
color: color,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), $TopicReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({tabRefs = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [if (tabRefs) db.tab],
|
||||
addJoins: null,
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [
|
||||
if (tabRefs)
|
||||
await $_getPrefetchedData(
|
||||
currentTable: table,
|
||||
referencedTable: $TopicReferences._tabRefsTable(db),
|
||||
managerFromTypedResult: (p0) =>
|
||||
$TopicReferences(db, table, p0).tabRefs,
|
||||
referencedItemsForCurrentItem: (item,
|
||||
referencedItems) =>
|
||||
referencedItems.where((e) => e.topicId == item.id),
|
||||
typedResults: items)
|
||||
];
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
typedef $TopicProcessedTableManager = ProcessedTableManager<
|
||||
_$TabDatabase,
|
||||
Topic,
|
||||
TopicData,
|
||||
$TopicFilterComposer,
|
||||
$TopicOrderingComposer,
|
||||
$TopicCreateCompanionBuilder,
|
||||
$TopicUpdateCompanionBuilder,
|
||||
(TopicData, $TopicReferences),
|
||||
TopicData,
|
||||
PrefetchHooks Function({bool tabRefs})>;
|
||||
typedef $TabCreateCompanionBuilder = TabCompanion Function({
|
||||
required String id,
|
||||
Value<String?> topicId,
|
||||
required DateTime timestamp,
|
||||
required Uri url,
|
||||
Value<String?> title,
|
||||
Value<Uint8List?> screenshot,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $TabUpdateCompanionBuilder = TabCompanion Function({
|
||||
Value<String> id,
|
||||
Value<String?> topicId,
|
||||
Value<DateTime> timestamp,
|
||||
Value<Uri> url,
|
||||
Value<String?> title,
|
||||
Value<Uint8List?> screenshot,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> {
|
||||
$TabReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static Topic _topicIdTable(_$TabDatabase db) =>
|
||||
db.topic.createAlias($_aliasNameGenerator(db.tab.topicId, db.topic.id));
|
||||
|
||||
$TopicProcessedTableManager? get topicId {
|
||||
if ($_item.topicId == null) return null;
|
||||
final manager = $TopicTableManager($_db, $_db.topic)
|
||||
.filter((f) => f.id($_item.topicId!));
|
||||
final item = $_typedResult.readTableOrNull(_topicIdTable($_db));
|
||||
if (item == null) return manager;
|
||||
return ProcessedTableManager(
|
||||
manager.$state.copyWith(prefetchedData: [item]));
|
||||
}
|
||||
}
|
||||
|
||||
class $TabFilterComposer extends FilterComposer<_$TabDatabase, Tab> {
|
||||
$TabFilterComposer(super.$state);
|
||||
ColumnFilters<String> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<DateTime> get timestamp => $state.composableBuilder(
|
||||
column: $state.table.timestamp,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnWithTypeConverterFilters<Uri, Uri, String> get url =>
|
||||
$state.composableBuilder(
|
||||
column: $state.table.url,
|
||||
builder: (column, joinBuilders) => ColumnWithTypeConverterFilters(
|
||||
column,
|
||||
joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<String> get title => $state.composableBuilder(
|
||||
column: $state.table.title,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnFilters<Uint8List> get screenshot => $state.composableBuilder(
|
||||
column: $state.table.screenshot,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnFilters(column, joinBuilders: joinBuilders));
|
||||
|
||||
$TopicFilterComposer get topicId {
|
||||
final $TopicFilterComposer composer = $state.composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.topicId,
|
||||
referencedTable: $state.db.topic,
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder, parentComposers) => $TopicFilterComposer(
|
||||
ComposerState(
|
||||
$state.db, $state.db.topic, joinBuilder, parentComposers)));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $TabOrderingComposer extends OrderingComposer<_$TabDatabase, Tab> {
|
||||
$TabOrderingComposer(super.$state);
|
||||
ColumnOrderings<String> get id => $state.composableBuilder(
|
||||
column: $state.table.id,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<DateTime> get timestamp => $state.composableBuilder(
|
||||
column: $state.table.timestamp,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<String> get url => $state.composableBuilder(
|
||||
column: $state.table.url,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<String> get title => $state.composableBuilder(
|
||||
column: $state.table.title,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
ColumnOrderings<Uint8List> get screenshot => $state.composableBuilder(
|
||||
column: $state.table.screenshot,
|
||||
builder: (column, joinBuilders) =>
|
||||
ColumnOrderings(column, joinBuilders: joinBuilders));
|
||||
|
||||
$TopicOrderingComposer get topicId {
|
||||
final $TopicOrderingComposer composer = $state.composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.topicId,
|
||||
referencedTable: $state.db.topic,
|
||||
getReferencedColumn: (t) => t.id,
|
||||
builder: (joinBuilder, parentComposers) => $TopicOrderingComposer(
|
||||
ComposerState(
|
||||
$state.db, $state.db.topic, joinBuilder, parentComposers)));
|
||||
return composer;
|
||||
}
|
||||
}
|
||||
|
||||
class $TabTableManager extends RootTableManager<
|
||||
_$TabDatabase,
|
||||
Tab,
|
||||
TabData,
|
||||
$TabFilterComposer,
|
||||
$TabOrderingComposer,
|
||||
$TabCreateCompanionBuilder,
|
||||
$TabUpdateCompanionBuilder,
|
||||
(TabData, $TabReferences),
|
||||
TabData,
|
||||
PrefetchHooks Function({bool topicId})> {
|
||||
$TabTableManager(_$TabDatabase db, Tab table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $TabFilterComposer(ComposerState(db, table)),
|
||||
orderingComposer: $TabOrderingComposer(ComposerState(db, table)),
|
||||
updateCompanionCallback: ({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
Value<DateTime> timestamp = const Value.absent(),
|
||||
Value<Uri> url = const Value.absent(),
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<Uint8List?> screenshot = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TabCompanion(
|
||||
id: id,
|
||||
topicId: topicId,
|
||||
timestamp: timestamp,
|
||||
url: url,
|
||||
title: title,
|
||||
screenshot: screenshot,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String id,
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
required DateTime timestamp,
|
||||
required Uri url,
|
||||
Value<String?> title = const Value.absent(),
|
||||
Value<Uint8List?> screenshot = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TabCompanion.insert(
|
||||
id: id,
|
||||
topicId: topicId,
|
||||
timestamp: timestamp,
|
||||
url: url,
|
||||
title: title,
|
||||
screenshot: screenshot,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map((e) => (e.readTable(table), $TabReferences(db, table, e)))
|
||||
.toList(),
|
||||
prefetchHooksCallback: ({topicId = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [],
|
||||
addJoins: <
|
||||
T extends TableManagerState<
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic,
|
||||
dynamic>>(state) {
|
||||
if (topicId) {
|
||||
state = state.withJoin(
|
||||
currentTable: table,
|
||||
currentColumn: table.topicId,
|
||||
referencedTable: $TabReferences._topicIdTable(db),
|
||||
referencedColumn: $TabReferences._topicIdTable(db).id,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
typedef $TabProcessedTableManager = ProcessedTableManager<
|
||||
_$TabDatabase,
|
||||
Tab,
|
||||
TabData,
|
||||
$TabFilterComposer,
|
||||
$TabOrderingComposer,
|
||||
$TabCreateCompanionBuilder,
|
||||
$TabUpdateCompanionBuilder,
|
||||
(TabData, $TabReferences),
|
||||
TabData,
|
||||
PrefetchHooks Function({bool topicId})>;
|
||||
|
||||
class $TabDatabaseManager {
|
||||
final _$TabDatabase _db;
|
||||
$TabDatabaseManager(this._db);
|
||||
$TopicTableManager get topic => $TopicTableManager(_db, _db.topic);
|
||||
$TabTableManager get tab => $TabTableManager(_db, _db.tab);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class ColorConverter extends TypeConverter<Color, int> {
|
||||
const ColorConverter();
|
||||
|
||||
@override
|
||||
Color fromSql(int fromDb) {
|
||||
return Color(fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
int toSql(Color value) {
|
||||
return value.value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class UriConverter extends TypeConverter<Uri, String> {
|
||||
@override
|
||||
Uri fromSql(String fromDb) {
|
||||
return uri_parser.tryParseUrl(fromDb, eagerParsing: true)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Uri value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:sqlite3/sqlite3.dart';
|
||||
import 'package:sqlite3_flutter_libs/sqlite3_flutter_libs.dart';
|
||||
import 'package:universal_io/io.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
TabDatabase tabDatabase(TabDatabaseRef ref) {
|
||||
return TabDatabase(
|
||||
LazyDatabase(() async {
|
||||
// put the database file, called db.sqlite here, into the documents folder
|
||||
// for your app.
|
||||
final dbFolder = await path_provider.getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'tab.db'));
|
||||
|
||||
// Also work around limitations on old Android versions
|
||||
if (Platform.isAndroid) {
|
||||
await applyWorkaroundToOpenSqlite3OnOldAndroidVersions();
|
||||
}
|
||||
|
||||
// Make sqlite3 pick a more suitable location for temporary files - the
|
||||
// one from the system may be inaccessible due to sandboxing.
|
||||
final cachebase = (await path_provider.getTemporaryDirectory()).path;
|
||||
// We can't access /tmp on Android, which sqlite3 would try by default.
|
||||
// Explicitly tell it about the correct temporary directory.
|
||||
sqlite3.tempDirectory = cachebase;
|
||||
|
||||
return NativeDatabase.createInBackground(file);
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabDatabaseHash() => r'b886b089ae5fdb9bdcc0982efe5fcbe9bc66486f';
|
||||
|
||||
/// See also [tabDatabase].
|
||||
@ProviderFor(tabDatabase)
|
||||
final tabDatabaseProvider = Provider<TabDatabase>.internal(
|
||||
tabDatabase,
|
||||
name: r'tabDatabaseProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$tabDatabaseHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef TabDatabaseRef = ProviderRef<TabDatabase>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.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) {
|
||||
state = id;
|
||||
}
|
||||
|
||||
void toggleTopic(String id) {
|
||||
if (state == id) {
|
||||
clearTopic();
|
||||
} else {
|
||||
setTopic(id);
|
||||
}
|
||||
}
|
||||
|
||||
void clearTopic() {
|
||||
state = null;
|
||||
}
|
||||
|
||||
@override
|
||||
String? build() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<TopicData?> selectedTopicData(SelectedTopicDataRef ref) {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
final selectedBangTrigger = ref.watch(selectedTopicProvider);
|
||||
return repository.watchTopic(selectedBangTrigger);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<Set<Color>> distinctTopicColors(DistinctTopicColorsRef ref) {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
return repository.getDistinctColors();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
|
||||
final allColors = colorTypes.flattened.toList();
|
||||
final usedColors = await ref.read(distinctTopicColorsProvider.future);
|
||||
|
||||
Color randomColor;
|
||||
do {
|
||||
randomColor = randomColorShade(allColors);
|
||||
} while (usedColors.contains(randomColor));
|
||||
|
||||
return randomColor;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
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';
|
||||
|
||||
/// See also [selectedTopicData].
|
||||
@ProviderFor(selectedTopicData)
|
||||
final selectedTopicDataProvider =
|
||||
AutoDisposeStreamProvider<TopicData?>.internal(
|
||||
selectedTopicData,
|
||||
name: r'selectedTopicDataProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTopicDataHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
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';
|
||||
|
||||
/// See also [unusedRandomTopicColor].
|
||||
@ProviderFor(unusedRandomTopicColor)
|
||||
final unusedRandomTopicColorProvider =
|
||||
AutoDisposeFutureProvider<Color>.internal(
|
||||
unusedRandomTopicColor,
|
||||
name: r'unusedRandomTopicColorProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$unusedRandomTopicColorHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef UnusedRandomTopicColorRef = AutoDisposeFutureProviderRef<Color>;
|
||||
String _$selectedTopicHash() => r'f4e1c0620971a0b7501ee9aff0d5b41da3130ba0';
|
||||
|
||||
/// See also [SelectedTopic].
|
||||
@ProviderFor(SelectedTopic)
|
||||
final selectedTopicProvider = NotifierProvider<SelectedTopic, String?>.internal(
|
||||
SelectedTopic.new,
|
||||
name: r'selectedTopicProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTopicHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SelectedTopic = Notifier<String?>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,15 @@
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabRepository extends _$TabRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
void build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabRepositoryHash() => r'3f564f553e9586df34e927b20fe4bc860e0814e9';
|
||||
|
||||
/// See also [TabRepository].
|
||||
@ProviderFor(TabRepository)
|
||||
final tabRepositoryProvider = NotifierProvider<TabRepository, void>.internal(
|
||||
TabRepository.new,
|
||||
name: r'tabRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TabRepository = Notifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,52 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'topic.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TopicRepository extends _$TopicRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
void build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
}
|
||||
|
||||
Future<void> addTopic({required String? name, required Color color}) {
|
||||
return _db.topicDao.addTopic(name: name, color: color);
|
||||
}
|
||||
|
||||
Future<void> replaceTopic({
|
||||
required String id,
|
||||
required String? name,
|
||||
required Color color,
|
||||
}) {
|
||||
return _db.topicDao.replaceTopic(id, name: name, color: color);
|
||||
}
|
||||
|
||||
Future<void> deleteTopic(String id) {
|
||||
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()
|
||||
.get()
|
||||
.then((colors) => colors.toSet());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'topic.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$topicRepositoryHash() => r'f0e8d3170d3706a79decfbe4700f20fcdebfae2c';
|
||||
|
||||
/// See also [TopicRepository].
|
||||
@ProviderFor(TopicRepository)
|
||||
final topicRepositoryProvider =
|
||||
NotifierProvider<TopicRepository, void>.internal(
|
||||
TopicRepository.new,
|
||||
name: r'topicRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$topicRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TopicRepository = Notifier<void>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,203 @@
|
||||
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/domain/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/topic_dialog.dart';
|
||||
import 'package:skeletonizer/skeletonizer.dart';
|
||||
|
||||
class _TopicTile extends HookWidget {
|
||||
final TopicData topic;
|
||||
final bool isSelected;
|
||||
|
||||
final void Function(TopicResult edited) onEdit;
|
||||
final void Function() onDelete;
|
||||
final void Function() onTap;
|
||||
|
||||
const _TopicTile(
|
||||
this.topic, {
|
||||
required this.isSelected,
|
||||
required this.onEdit,
|
||||
required this.onDelete,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final menuController = useMemoized(() => MenuController());
|
||||
|
||||
return ListTile(
|
||||
selected: isSelected,
|
||||
leading: CircleAvatar(backgroundColor: topic.color),
|
||||
title: Text(topic.name ?? 'New Topic'),
|
||||
trailing: MenuAnchor(
|
||||
controller: menuController,
|
||||
builder: (context, controller, child) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
if (controller.isOpen) {
|
||||
controller.close();
|
||||
} else {
|
||||
controller.open();
|
||||
}
|
||||
},
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0),
|
||||
child: Icon(Icons.more_vert),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
menuChildren: [
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final result = await showDialog<TopicResult?>(
|
||||
context: context,
|
||||
builder: (context) => TopicDialog.edit(
|
||||
name: topic.name,
|
||||
initialColor: topic.color,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
onEdit(result);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.edit),
|
||||
child: const Text('Edit'),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final result = await showDialog<bool?>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: const Text('Delete Topic'),
|
||||
content: const Text(
|
||||
'Are you sure you want to delete this topic and all attached tabs?',
|
||||
),
|
||||
actions: <Widget>[
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, false);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context, true);
|
||||
},
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == true) {
|
||||
onDelete();
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.delete),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TopicListScreen extends HookConsumerWidget {
|
||||
const TopicListScreen();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Topics'),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
final initialColor =
|
||||
await ref.read(unusedRandomTopicColorProvider.future);
|
||||
|
||||
if (context.mounted) {
|
||||
final result = await showDialog<TopicResult?>(
|
||||
context: context,
|
||||
builder: (context) => TopicDialog.create(
|
||||
initialColor: initialColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.addTopic(name: result.name, color: result.color);
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
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);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (error, stackTrace) => SizedBox.shrink(),
|
||||
loading: () => ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => _TopicTile(
|
||||
const TopicData(id: 'null', color: Colors.transparent),
|
||||
isSelected: false,
|
||||
onEdit: (_) {},
|
||||
onDelete: () {},
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,340 @@
|
||||
// The Color Picker which contains Material Design Color Palette.
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lensai/features/topics/utils/color_palette.dart';
|
||||
|
||||
class MaterialPicker extends StatefulWidget {
|
||||
const MaterialPicker({
|
||||
super.key,
|
||||
required this.pickerColor,
|
||||
required this.onColorChanged,
|
||||
this.onPrimaryChanged,
|
||||
this.enableLabel = false,
|
||||
this.portraitOnly = false,
|
||||
});
|
||||
|
||||
final Color pickerColor;
|
||||
final ValueChanged<Color> onColorChanged;
|
||||
final ValueChanged<Color>? onPrimaryChanged;
|
||||
final bool enableLabel;
|
||||
final bool portraitOnly;
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _MaterialPickerState();
|
||||
}
|
||||
|
||||
class _MaterialPickerState extends State<MaterialPicker> {
|
||||
List<Color> _currentColorType = [Colors.red, Colors.redAccent];
|
||||
Color _currentShading = Colors.transparent;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
for (final colors in colorTypes) {
|
||||
shadingTypes(colors).forEach((Map<Color, String> color) {
|
||||
if (widget.pickerColor.value == color.keys.first.value) {
|
||||
return setState(() {
|
||||
_currentColorType = colors;
|
||||
_currentShading = color.keys.first;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isPortrait =
|
||||
MediaQuery.of(context).orientation == Orientation.portrait ||
|
||||
widget.portraitOnly;
|
||||
|
||||
Widget colorList() {
|
||||
return Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(),
|
||||
child: Container(
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
width: isPortrait ? 60 : null,
|
||||
height: isPortrait ? null : 60,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
),
|
||||
],
|
||||
border: isPortrait
|
||||
? Border(
|
||||
right: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
)
|
||||
: Border(
|
||||
top: BorderSide(
|
||||
color: (Theme.of(context).brightness == Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 7))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 7)),
|
||||
...colorTypes.map((List<Color> colors) {
|
||||
final Color colorType = colors[0];
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
if (widget.onPrimaryChanged != null) {
|
||||
widget.onPrimaryChanged!.call(colorType);
|
||||
}
|
||||
setState(() => _currentColorType = colors);
|
||||
},
|
||||
child: Container(
|
||||
color: const Color(0x00000000),
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
width: 25,
|
||||
height: 25,
|
||||
decoration: BoxDecoration(
|
||||
color: colorType,
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: _currentColorType == colors
|
||||
? [
|
||||
if (colorType ==
|
||||
Theme.of(context).cardColor)
|
||||
BoxShadow(
|
||||
color:
|
||||
(Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: colorType,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border: colorType == Theme.of(context).cardColor
|
||||
? Border.all(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 5))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 5)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget shadingList() {
|
||||
return ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(dragDevices: PointerDeviceKind.values.toSet()),
|
||||
child: FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView(
|
||||
controller: controller,
|
||||
scrollDirection: isPortrait ? Axis.vertical : Axis.horizontal,
|
||||
children: [
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
...shadingTypes(_currentColorType)
|
||||
.map((Map<Color, String> colors) {
|
||||
final Color color = colors.keys.first;
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
setState(() => _currentShading = color);
|
||||
widget.onColorChanged(color);
|
||||
},
|
||||
child: Container(
|
||||
color: const Color(0x00000000),
|
||||
margin: isPortrait
|
||||
? const EdgeInsets.only(right: 10)
|
||||
: const EdgeInsets.only(bottom: 10),
|
||||
padding: isPortrait
|
||||
? const EdgeInsets.fromLTRB(0, 7, 0, 7)
|
||||
: const EdgeInsets.fromLTRB(7, 0, 7, 0),
|
||||
child: Align(
|
||||
child: AnimatedContainer(
|
||||
curve: Curves.fastOutSlowIn,
|
||||
duration: const Duration(milliseconds: 500),
|
||||
width: isPortrait
|
||||
? (_currentShading == color ? 250 : 230)
|
||||
: (_currentShading == color ? 50 : 30),
|
||||
height: isPortrait ? 50 : 220,
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
boxShadow: _currentShading == color
|
||||
? [
|
||||
if ((color == Colors.white) ||
|
||||
(color == Colors.black))
|
||||
BoxShadow(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
blurRadius: 10,
|
||||
)
|
||||
else
|
||||
BoxShadow(
|
||||
color: color,
|
||||
blurRadius: 10,
|
||||
),
|
||||
]
|
||||
: null,
|
||||
border: (color == Colors.white) ||
|
||||
(color == Colors.black)
|
||||
? Border.all(
|
||||
color: (Theme.of(context).brightness ==
|
||||
Brightness.light)
|
||||
? Colors.grey[300]!
|
||||
: Colors.black38,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: widget.enableLabel
|
||||
? isPortrait
|
||||
? Row(
|
||||
children: [
|
||||
Text(
|
||||
' ${colors.values.first}',
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Text(
|
||||
'#${color.toString().replaceFirst('Color(0xff', '').replaceFirst(')', '').toUpperCase()} ',
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: AnimatedOpacity(
|
||||
duration:
|
||||
const Duration(milliseconds: 300),
|
||||
opacity: _currentShading == color ? 1 : 0,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
alignment: Alignment.topCenter,
|
||||
child: Text(
|
||||
colors.values.first,
|
||||
style: TextStyle(
|
||||
color: useWhiteForeground(color)
|
||||
? Colors.white
|
||||
: Colors.black,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
if (isPortrait)
|
||||
const Padding(padding: EdgeInsets.only(top: 15))
|
||||
else
|
||||
const Padding(padding: EdgeInsets.only(left: 15)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (isPortrait) {
|
||||
return SizedBox(
|
||||
width: 350,
|
||||
height: 500,
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return SizedBox(
|
||||
width: 500,
|
||||
height: 300,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
colorList(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: shadingList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class TopicChips extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final topicsAsync = ref.watch(topicListProvider);
|
||||
final selectedTopic = ref
|
||||
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
||||
|
||||
return topicsAsync.when(
|
||||
data: (availableTopics) => SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
if (selectedTopic != null || availableTopics.isNotEmpty)
|
||||
Expanded(
|
||||
child: SelectableChips(
|
||||
deleteIcon: false,
|
||||
itemId: (topic) => topic.id,
|
||||
itemAvatar: (topic) => Container(
|
||||
width: 20.0,
|
||||
height: 20.0,
|
||||
decoration: BoxDecoration(
|
||||
color: topic.color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
itemLabel: (topic) => Text(topic.name ?? 'New Topic'),
|
||||
availableItems: availableTopics,
|
||||
selectedItem: selectedTopic,
|
||||
onSelected: (topic) {
|
||||
ref.read(selectedTopicProvider.notifier).setTopic(topic.id);
|
||||
},
|
||||
onDeleted: (topic) async {
|
||||
ref.read(selectedTopicProvider.notifier).clearTopic();
|
||||
},
|
||||
),
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Text(
|
||||
"Press '>' to manage Topics.",
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context.push(TopicListRoute().location);
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox(
|
||||
height: 48,
|
||||
width: double.infinity,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/material_color_picker.dart';
|
||||
|
||||
typedef TopicResult = ({String? name, Color color});
|
||||
|
||||
enum _DialogMode { create, edit }
|
||||
|
||||
class TopicDialog extends HookWidget {
|
||||
final _DialogMode _mode;
|
||||
|
||||
final String? initialName;
|
||||
final Color initialColor;
|
||||
|
||||
const TopicDialog._({
|
||||
required _DialogMode mode,
|
||||
required this.initialColor,
|
||||
this.initialName,
|
||||
}) : _mode = mode;
|
||||
|
||||
factory TopicDialog.create({required Color initialColor}) {
|
||||
return TopicDialog._(
|
||||
mode: _DialogMode.create,
|
||||
initialColor: initialColor,
|
||||
);
|
||||
}
|
||||
|
||||
factory TopicDialog.edit({
|
||||
required String? name,
|
||||
required Color initialColor,
|
||||
}) {
|
||||
return TopicDialog._(
|
||||
mode: _DialogMode.edit,
|
||||
initialColor: initialColor,
|
||||
initialName: name,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedColor = useState<Color>(initialColor);
|
||||
final textController = useTextEditingController(text: initialName);
|
||||
|
||||
return SimpleDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 20.0,
|
||||
right: 20.0,
|
||||
bottom: 24.0,
|
||||
),
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
vertical: 24.0,
|
||||
),
|
||||
title: Text(
|
||||
switch (_mode) {
|
||||
_DialogMode.create => 'New Topic',
|
||||
_DialogMode.edit => 'Edit Topic',
|
||||
},
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: TextField(
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
height: 24,
|
||||
width: 24,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: selectedColor.value,
|
||||
),
|
||||
),
|
||||
),
|
||||
label: const Text('Name'),
|
||||
),
|
||||
controller: textController,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
MaterialPicker(
|
||||
pickerColor: selectedColor.value,
|
||||
onColorChanged: (value) {
|
||||
selectedColor.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
OverflowBar(
|
||||
alignment: MainAxisAlignment.end,
|
||||
spacing: 8.0,
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop<TopicResult?>(context);
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
final name = textController.text.trim();
|
||||
Navigator.pop<TopicResult?>(
|
||||
context,
|
||||
(
|
||||
name: name.isNotEmpty ? name : null,
|
||||
color: selectedColor.value,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
switch (_mode) {
|
||||
_DialogMode.create => 'Add',
|
||||
_DialogMode.edit => 'Edit',
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
final _rnd = Random();
|
||||
|
||||
/// Check if is good condition to use white foreground color by passing
|
||||
/// the background color, and optional bias.
|
||||
///
|
||||
/// Reference:
|
||||
///
|
||||
/// Old: https://www.w3.org/TR/WCAG20-TECHS/G18.html
|
||||
///
|
||||
/// New: https://github.com/mchome/flutter_statusbarcolor/issues/40
|
||||
bool useWhiteForeground(Color backgroundColor, {double bias = 0.0}) {
|
||||
// Old:
|
||||
// return 1.05 / (color.computeLuminance() + 0.05) > 4.5;
|
||||
|
||||
// New:
|
||||
final v = sqrt(
|
||||
pow(backgroundColor.red, 2) * 0.299 +
|
||||
pow(backgroundColor.green, 2) * 0.587 +
|
||||
pow(backgroundColor.blue, 2) * 0.114,
|
||||
).round();
|
||||
return v < (130 + bias);
|
||||
}
|
||||
|
||||
const List<List<Color>> colorTypes = [
|
||||
[Colors.red, Colors.redAccent],
|
||||
[Colors.pink, Colors.pinkAccent],
|
||||
[Colors.purple, Colors.purpleAccent],
|
||||
[Colors.deepPurple, Colors.deepPurpleAccent],
|
||||
[Colors.indigo, Colors.indigoAccent],
|
||||
[Colors.blue, Colors.blueAccent],
|
||||
[Colors.lightBlue, Colors.lightBlueAccent],
|
||||
[Colors.cyan, Colors.cyanAccent],
|
||||
[Colors.teal, Colors.tealAccent],
|
||||
[Colors.green, Colors.greenAccent],
|
||||
[Colors.lightGreen, Colors.lightGreenAccent],
|
||||
[Colors.lime, Colors.limeAccent],
|
||||
[Colors.yellow, Colors.yellowAccent],
|
||||
[Colors.amber, Colors.amberAccent],
|
||||
[Colors.orange, Colors.orangeAccent],
|
||||
[Colors.deepOrange, Colors.deepOrangeAccent],
|
||||
[Colors.brown],
|
||||
[Colors.grey],
|
||||
[Colors.blueGrey],
|
||||
[Colors.black],
|
||||
];
|
||||
|
||||
List<Map<Color, String>> shadingTypes(List<Color> colors) {
|
||||
final List<Map<Color, String>> result = [];
|
||||
|
||||
for (final Color colorType in colors) {
|
||||
if (colorType == Colors.grey) {
|
||||
result.addAll(
|
||||
[50, 100, 200, 300, 350, 400, 500, 600, 700, 800, 850, 900]
|
||||
.map((int shade) => {Colors.grey[shade]!: shade.toString()})
|
||||
.toList(),
|
||||
);
|
||||
} else if (colorType == Colors.black || colorType == Colors.white) {
|
||||
result.addAll([
|
||||
{Colors.black: ''},
|
||||
{Colors.white: ''},
|
||||
]);
|
||||
} else if (colorType is MaterialAccentColor) {
|
||||
result.addAll(
|
||||
[100, 200, 400, 700]
|
||||
.map((int shade) => {colorType[shade]!: 'A$shade'})
|
||||
.toList(),
|
||||
);
|
||||
} else if (colorType is MaterialColor) {
|
||||
result.addAll(
|
||||
[50, 100, 200, 300, 400, 500, 600, 700, 800, 900]
|
||||
.map((int shade) => {colorType[shade]!: shade.toString()})
|
||||
.toList(),
|
||||
);
|
||||
} else {
|
||||
result.add({const Color(0x00000000): ''});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
Color randomColorShade(List<Color> colors) {
|
||||
final color = colors[_rnd.nextInt(colors.length)];
|
||||
final shades = shadingTypes([color]);
|
||||
|
||||
return shades[_rnd.nextInt(shades.length)].keys.first;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ abstract interface class ITab {
|
||||
String get id;
|
||||
Uri get url;
|
||||
String? get title;
|
||||
String? get topicId;
|
||||
Favicon? get favicon;
|
||||
Uint8List? get screenshot;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
@@ -7,17 +6,14 @@ import 'package:lensai/core/uuid.dart';
|
||||
import 'package:lensai/domain/entities/web_page_info.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||
|
||||
part 'web_view_page.g.dart';
|
||||
|
||||
typedef PageHistory = ({bool canGoBack, bool canGoForward});
|
||||
|
||||
@CopyWith(constructor: '_')
|
||||
class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
||||
@CopyWithField(immutable: true)
|
||||
//@CopyWithField(immutable: true)
|
||||
final Key key;
|
||||
|
||||
@override
|
||||
@CopyWithField(immutable: true)
|
||||
//@CopyWithField(immutable: true)
|
||||
final String id;
|
||||
|
||||
final InAppWebViewController? controller;
|
||||
@@ -25,35 +21,40 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
||||
// ignore: missing_field_in_equatable_props
|
||||
final SslError? sslError;
|
||||
|
||||
@override
|
||||
final String? topicId;
|
||||
|
||||
@override
|
||||
final Uint8List? screenshot;
|
||||
|
||||
final PageHistory pageHistory;
|
||||
|
||||
WebViewPage({
|
||||
String? id,
|
||||
this.controller,
|
||||
required super.url,
|
||||
this.sslError,
|
||||
super.title,
|
||||
super.favicon,
|
||||
this.screenshot,
|
||||
this.pageHistory = (canGoBack: false, canGoForward: false),
|
||||
}) : key = GlobalKey(),
|
||||
id = id ?? uuid.v7();
|
||||
|
||||
WebViewPage._({
|
||||
required this.key,
|
||||
required this.id,
|
||||
required this.controller,
|
||||
required super.url,
|
||||
required this.sslError,
|
||||
required super.title,
|
||||
required this.topicId,
|
||||
required super.favicon,
|
||||
required this.screenshot,
|
||||
required this.pageHistory,
|
||||
});
|
||||
|
||||
WebViewPage.create({
|
||||
String? id,
|
||||
this.controller,
|
||||
required super.url,
|
||||
this.sslError,
|
||||
super.title,
|
||||
this.topicId,
|
||||
super.favicon,
|
||||
this.screenshot,
|
||||
this.pageHistory = (canGoBack: false, canGoForward: false),
|
||||
}) : key = GlobalKey(),
|
||||
id = id ?? uuid.v7();
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@@ -65,6 +66,7 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
||||
super.url,
|
||||
sslError?.toString(),
|
||||
title,
|
||||
topicId,
|
||||
favicon?.toString(),
|
||||
screenshot,
|
||||
pageHistory,
|
||||
|
||||
@@ -1,127 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'web_view_page.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$WebViewPageCWProxy {
|
||||
WebViewPage controller(InAppWebViewController? controller);
|
||||
|
||||
WebViewPage url(Uri url);
|
||||
|
||||
WebViewPage sslError(SslError? sslError);
|
||||
|
||||
WebViewPage title(String? title);
|
||||
|
||||
WebViewPage favicon(Favicon? favicon);
|
||||
|
||||
WebViewPage screenshot(Uint8List? screenshot);
|
||||
|
||||
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory);
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// WebViewPage(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
WebViewPage call({
|
||||
InAppWebViewController? controller,
|
||||
Uri? url,
|
||||
SslError? sslError,
|
||||
String? title,
|
||||
Favicon? favicon,
|
||||
Uint8List? screenshot,
|
||||
({bool canGoBack, bool canGoForward})? pageHistory,
|
||||
});
|
||||
}
|
||||
|
||||
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfWebViewPage.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfWebViewPage.copyWith.fieldName(...)`
|
||||
class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy {
|
||||
const _$WebViewPageCWProxyImpl(this._value);
|
||||
|
||||
final WebViewPage _value;
|
||||
|
||||
@override
|
||||
WebViewPage controller(InAppWebViewController? controller) =>
|
||||
this(controller: controller);
|
||||
|
||||
@override
|
||||
WebViewPage url(Uri url) => this(url: url);
|
||||
|
||||
@override
|
||||
WebViewPage sslError(SslError? sslError) => this(sslError: sslError);
|
||||
|
||||
@override
|
||||
WebViewPage title(String? title) => this(title: title);
|
||||
|
||||
@override
|
||||
WebViewPage favicon(Favicon? favicon) => this(favicon: favicon);
|
||||
|
||||
@override
|
||||
WebViewPage screenshot(Uint8List? screenshot) => this(screenshot: screenshot);
|
||||
|
||||
@override
|
||||
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory) =>
|
||||
this(pageHistory: pageHistory);
|
||||
|
||||
@override
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// WebViewPage(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
WebViewPage call({
|
||||
Object? controller = const $CopyWithPlaceholder(),
|
||||
Object? url = const $CopyWithPlaceholder(),
|
||||
Object? sslError = const $CopyWithPlaceholder(),
|
||||
Object? title = const $CopyWithPlaceholder(),
|
||||
Object? favicon = const $CopyWithPlaceholder(),
|
||||
Object? screenshot = const $CopyWithPlaceholder(),
|
||||
Object? pageHistory = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return WebViewPage._(
|
||||
key: _value.key,
|
||||
id: _value.id,
|
||||
controller: controller == const $CopyWithPlaceholder()
|
||||
? _value.controller
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: controller as InAppWebViewController?,
|
||||
url: url == const $CopyWithPlaceholder() || url == null
|
||||
? _value.url
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: url as Uri,
|
||||
sslError: sslError == const $CopyWithPlaceholder()
|
||||
? _value.sslError
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: sslError as SslError?,
|
||||
title: title == const $CopyWithPlaceholder()
|
||||
? _value.title
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: title as String?,
|
||||
favicon: favicon == const $CopyWithPlaceholder()
|
||||
? _value.favicon
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: favicon as Favicon?,
|
||||
screenshot: screenshot == const $CopyWithPlaceholder()
|
||||
? _value.screenshot
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: screenshot as Uint8List?,
|
||||
pageHistory:
|
||||
pageHistory == const $CopyWithPlaceholder() || pageHistory == null
|
||||
? _value.pageHistory
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: pageHistory as ({bool canGoBack, bool canGoForward}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $WebViewPageCopyWith on WebViewPage {
|
||||
/// Returns a callable class that can be used as follows: `instanceOfWebViewPage.copyWith(...)` or like so:`instanceOfWebViewPage.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$WebViewPageCWProxy get copyWith => _$WebViewPageCWProxyImpl(this);
|
||||
}
|
||||
@@ -15,7 +15,7 @@ class SwitchNewTabController extends _$SwitchNewTabController {
|
||||
state = const AsyncLoading();
|
||||
state = await AsyncValue.guard(
|
||||
() async {
|
||||
final newTab = WebViewPage(
|
||||
final newTab = WebViewPage.create(
|
||||
url: WebUri.uri(url),
|
||||
);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ part of 'switch_new_tab.dart';
|
||||
// **************************************************************************
|
||||
|
||||
String _$switchNewTabControllerHash() =>
|
||||
r'a5b8f7fd71c8cb6cc2023b41504e5cd467c5b063';
|
||||
r'fb831015e4b99d3ae86bd2a88a3c6f4c7004783d';
|
||||
|
||||
/// See also [SwitchNewTabController].
|
||||
@ProviderFor(SwitchNewTabController)
|
||||
|
||||
@@ -91,7 +91,7 @@ class WebPageDialog extends HookConsumerWidget {
|
||||
onDismiss: onDismiss,
|
||||
),
|
||||
SimpleDialog(
|
||||
titlePadding: const EdgeInsets.fromLTRB(24.0, 12.0, 24.0, 0.0),
|
||||
titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
insetPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20.0,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
@@ -37,6 +38,109 @@ const _webViewSupportedSchemes = [
|
||||
"about",
|
||||
];
|
||||
|
||||
abstract class _WebViewPageCWProxy {
|
||||
WebViewPage controller(InAppWebViewController? controller);
|
||||
|
||||
WebViewPage url(Uri url);
|
||||
|
||||
WebViewPage sslError(SslError? sslError);
|
||||
|
||||
WebViewPage title(String? title);
|
||||
|
||||
WebViewPage topicId(String? topicId);
|
||||
|
||||
WebViewPage favicon(Favicon? favicon);
|
||||
|
||||
WebViewPage screenshot(Uint8List? screenshot);
|
||||
|
||||
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory);
|
||||
|
||||
WebViewPage call({
|
||||
InAppWebViewController? controller,
|
||||
Uri? url,
|
||||
SslError? sslError,
|
||||
String? title,
|
||||
String? topicId,
|
||||
Favicon? favicon,
|
||||
Uint8List? screenshot,
|
||||
({bool canGoBack, bool canGoForward})? pageHistory,
|
||||
});
|
||||
}
|
||||
|
||||
class _WebViewPageCWProxyImpl implements _WebViewPageCWProxy {
|
||||
const _WebViewPageCWProxyImpl(this._value);
|
||||
|
||||
final WebViewPage _value;
|
||||
|
||||
@override
|
||||
WebViewPage controller(InAppWebViewController? controller) =>
|
||||
this(controller: controller);
|
||||
|
||||
@override
|
||||
WebViewPage url(Uri url) => this(url: url);
|
||||
|
||||
@override
|
||||
WebViewPage sslError(SslError? sslError) => this(sslError: sslError);
|
||||
|
||||
@override
|
||||
WebViewPage title(String? title) => this(title: title);
|
||||
|
||||
@override
|
||||
WebViewPage topicId(String? topicId) => this(topicId: topicId);
|
||||
|
||||
@override
|
||||
WebViewPage favicon(Favicon? favicon) => this(favicon: favicon);
|
||||
|
||||
@override
|
||||
WebViewPage screenshot(Uint8List? screenshot) => this(screenshot: screenshot);
|
||||
|
||||
@override
|
||||
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory) =>
|
||||
this(pageHistory: pageHistory);
|
||||
|
||||
@override
|
||||
WebViewPage call({
|
||||
Object? controller = const $CopyWithPlaceholder(),
|
||||
Object? url = const $CopyWithPlaceholder(),
|
||||
Object? sslError = const $CopyWithPlaceholder(),
|
||||
Object? title = const $CopyWithPlaceholder(),
|
||||
Object? topicId = const $CopyWithPlaceholder(),
|
||||
Object? favicon = const $CopyWithPlaceholder(),
|
||||
Object? screenshot = const $CopyWithPlaceholder(),
|
||||
Object? pageHistory = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return WebViewPage(
|
||||
key: _value.key,
|
||||
id: _value.id,
|
||||
controller: controller == const $CopyWithPlaceholder()
|
||||
? _value.controller
|
||||
: controller as InAppWebViewController?,
|
||||
url: url == const $CopyWithPlaceholder() || url == null
|
||||
? _value.url
|
||||
: url as Uri,
|
||||
sslError: sslError == const $CopyWithPlaceholder()
|
||||
? _value.sslError
|
||||
: sslError as SslError?,
|
||||
title: title == const $CopyWithPlaceholder()
|
||||
? _value.title
|
||||
: title as String?,
|
||||
topicId: topicId == const $CopyWithPlaceholder()
|
||||
? _value.topicId
|
||||
: topicId as String?,
|
||||
favicon: favicon == const $CopyWithPlaceholder()
|
||||
? _value.favicon
|
||||
: favicon as Favicon?,
|
||||
screenshot: screenshot == const $CopyWithPlaceholder()
|
||||
? _value.screenshot
|
||||
: screenshot as Uint8List?,
|
||||
pageHistory:
|
||||
pageHistory == const $CopyWithPlaceholder() || pageHistory == null
|
||||
? _value.pageHistory
|
||||
: pageHistory as ({bool canGoBack, bool canGoForward}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebView extends StatefulHookConsumerWidget {
|
||||
final String tabId;
|
||||
|
||||
@@ -48,10 +152,6 @@ class WebView extends StatefulHookConsumerWidget {
|
||||
InAppWebViewController? get currentController =>
|
||||
_pageNotifier.value.controller;
|
||||
|
||||
void updatePage(WebViewPage Function(WebViewPage page) update) {
|
||||
_pageNotifier.value = update(_pageNotifier.value);
|
||||
}
|
||||
|
||||
WebView({required WebViewPage tab})
|
||||
: _pageNotifier = ValueNotifier(tab),
|
||||
tabId = tab.id,
|
||||
@@ -65,6 +165,13 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
Timer? _onLoadStopDebounce;
|
||||
Timer? _periodicScreenshotUpdate;
|
||||
|
||||
void updatePage(
|
||||
WebViewPage Function(_WebViewPageCWProxyImpl copyWith) update,
|
||||
) {
|
||||
final x = widget._pageNotifier.value =
|
||||
update(_WebViewPageCWProxyImpl(widget._pageNotifier.value));
|
||||
}
|
||||
|
||||
Future<bool> _downloadChat(
|
||||
DownloadStartRequest downloadStartRequest,
|
||||
BuildContext context,
|
||||
@@ -125,9 +232,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
},
|
||||
);
|
||||
|
||||
widget.updatePage(
|
||||
(page) => page.copyWith.screenshot(screenshot),
|
||||
);
|
||||
updatePage((copyWith) => copyWith.screenshot(screenshot));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -286,7 +391,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
await controller.startSafeBrowsing();
|
||||
}
|
||||
|
||||
widget.updatePage((page) => page.copyWith.controller(controller));
|
||||
updatePage((copyWith) => copyWith.controller(controller));
|
||||
},
|
||||
onReceivedServerTrustAuthRequest: (controller, challenge) async {
|
||||
final sslError = challenge.protectionSpace.sslError;
|
||||
@@ -294,7 +399,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
if (sslError != null && sslError.code != null) {
|
||||
if (challenge.protectionSpace.host ==
|
||||
await controller.getUrl().then((value) => value?.host)) {
|
||||
widget.updatePage((page) => page.copyWith.sslError(sslError));
|
||||
updatePage((copyWith) => copyWith.sslError(sslError));
|
||||
if (context.mounted) {
|
||||
ui_helper.showErrorMessage(
|
||||
context,
|
||||
@@ -309,7 +414,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
);
|
||||
}
|
||||
|
||||
widget.updatePage((page) => page.copyWith.sslError(null));
|
||||
updatePage((copyWith) => copyWith.sslError(null));
|
||||
return ServerTrustAuthResponse(
|
||||
action: ServerTrustAuthResponseAction.PROCEED,
|
||||
);
|
||||
@@ -371,8 +476,8 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
);
|
||||
|
||||
if (url != null) {
|
||||
widget.updatePage(
|
||||
(page) => page.copyWith(
|
||||
updatePage(
|
||||
(copyWith) => copyWith(
|
||||
url: url,
|
||||
// ignore: avoid_redundant_argument_values
|
||||
sslError: null,
|
||||
@@ -384,7 +489,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
},
|
||||
onLoadStop: (controller, url) async {
|
||||
if (url != null) {
|
||||
widget.updatePage((page) => page.copyWith.url(url));
|
||||
updatePage((copyWith) => copyWith.url(url));
|
||||
}
|
||||
|
||||
_onLoadStopDebounce?.cancel();
|
||||
@@ -417,7 +522,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
canGoForward: await controller.canGoForward()
|
||||
);
|
||||
|
||||
widget.updatePage((page) => page.copyWith.pageHistory(history));
|
||||
updatePage((copyWith) => copyWith.pageHistory(history));
|
||||
}
|
||||
},
|
||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||
@@ -488,7 +593,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
||||
}
|
||||
},
|
||||
onTitleChanged: (controller, title) {
|
||||
widget.updatePage((page) => page.copyWith.title(title));
|
||||
updatePage((copyWith) => copyWith.title(title));
|
||||
},
|
||||
onDownloadStartRequest: (controller, downloadStartRequest) async {
|
||||
final handled = switch (downloadStartRequest.mimeType) {
|
||||
|
||||
@@ -4,10 +4,11 @@ class SelectableChips<T, K> extends StatelessWidget {
|
||||
final List<T> availableItems;
|
||||
final T? selectedItem;
|
||||
final int maxCount;
|
||||
final bool deleteIcon;
|
||||
|
||||
final K Function(T item) itemId;
|
||||
final Widget Function(T item) itemLabel;
|
||||
final Widget Function(T item)? itemAvatar;
|
||||
final Widget? Function(T item)? itemAvatar;
|
||||
|
||||
final void Function(T item)? onSelected;
|
||||
final void Function(T item)? onDeleted;
|
||||
@@ -19,6 +20,7 @@ class SelectableChips<T, K> extends StatelessWidget {
|
||||
required this.availableItems,
|
||||
this.selectedItem,
|
||||
this.maxCount = 25,
|
||||
this.deleteIcon = true,
|
||||
this.onSelected,
|
||||
this.onDeleted,
|
||||
super.key,
|
||||
@@ -63,9 +65,11 @@ class SelectableChips<T, K> extends StatelessWidget {
|
||||
onDeleted?.call(item);
|
||||
}
|
||||
},
|
||||
onDeleted: () {
|
||||
onDeleted?.call(item);
|
||||
},
|
||||
onDeleted: deleteIcon
|
||||
? () {
|
||||
onDeleted?.call(item);
|
||||
}
|
||||
: null,
|
||||
label: itemLabel.call(item),
|
||||
avatar: itemAvatar?.call(item),
|
||||
),
|
||||
|
||||
+9
-9
@@ -326,6 +326,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.17"
|
||||
fading_scroll:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fading_scroll
|
||||
sha256: "0eeb846385950dfb03415bf76cebf087e7ec21b4fe28fd0c864ce334b6f56f02"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.0"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -387,14 +395,6 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_colorpicker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_colorpicker
|
||||
sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
flutter_hooks:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -1435,5 +1435,5 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.2"
|
||||
sdks:
|
||||
dart: ">=3.5.0-259.0.dev <4.0.0"
|
||||
dart: ">=3.5.0 <4.0.0"
|
||||
flutter: ">=3.22.0"
|
||||
|
||||
+1
-1
@@ -14,11 +14,11 @@ dependencies:
|
||||
dynamic_color: ^1.7.0
|
||||
exceptions: ^0.6.1
|
||||
expandable_page_view: ^1.0.17
|
||||
fading_scroll: ^0.9.0
|
||||
fast_equatable: ^1.1.0
|
||||
file_picker: ^8.1.2
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_colorpicker: ^1.1.0
|
||||
flutter_hooks: ^0.20.5
|
||||
flutter_inappwebview: ^6.0.0
|
||||
flutter_markdown: ^0.7.3+1
|
||||
|
||||
Reference in New Issue
Block a user