intermediate
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/domain/entities/equatable_image.dart';
|
||||
import 'package:lensai/extensions/image.dart';
|
||||
import 'package:lensai/features/geckoview/utils/image_helper.dart';
|
||||
|
||||
class BrowserIcon with FastEquatable {
|
||||
final EquatableImage image;
|
||||
|
||||
final Color? dominantColor;
|
||||
final IconSource source;
|
||||
|
||||
static Future<BrowserIcon> fromBytes(
|
||||
Uint8List bytes, {
|
||||
required Color? dominantColor,
|
||||
required IconSource source,
|
||||
}) async {
|
||||
final image =
|
||||
await tryDecodeImage(bytes).then((image) => image!.toEquatable());
|
||||
|
||||
return BrowserIcon._(
|
||||
image: image,
|
||||
dominantColor: dominantColor,
|
||||
source: source,
|
||||
);
|
||||
}
|
||||
|
||||
BrowserIcon._({
|
||||
required this.image,
|
||||
required this.dominantColor,
|
||||
required this.source,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
image,
|
||||
dominantColor,
|
||||
source,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class HistoryItem with FastEquatable {
|
||||
final Uri url;
|
||||
final String title;
|
||||
|
||||
HistoryItem({required this.url, required this.title});
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [url, title];
|
||||
}
|
||||
|
||||
class HistoryState with FastEquatable {
|
||||
final List<HistoryItem> items;
|
||||
final int currentIndex;
|
||||
|
||||
final bool canGoBack;
|
||||
final bool canGoForward;
|
||||
|
||||
HistoryState({
|
||||
required this.items,
|
||||
required this.currentIndex,
|
||||
required this.canGoBack,
|
||||
required this.canGoForward,
|
||||
});
|
||||
|
||||
factory HistoryState.$default() => HistoryState(
|
||||
items: const [],
|
||||
currentIndex: 0,
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters =>
|
||||
[items, currentIndex, canGoBack, canGoForward];
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class ReaderableState with FastEquatable {
|
||||
final bool readerable;
|
||||
|
||||
final bool active;
|
||||
|
||||
ReaderableState({
|
||||
required this.readerable,
|
||||
required this.active,
|
||||
});
|
||||
|
||||
factory ReaderableState.$default() => ReaderableState(
|
||||
readerable: false,
|
||||
active: false,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [readerable, active];
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class SecurityState with FastEquatable {
|
||||
final bool secure;
|
||||
final String host;
|
||||
final String issuer;
|
||||
|
||||
SecurityState({
|
||||
required this.secure,
|
||||
required this.host,
|
||||
required this.issuer,
|
||||
});
|
||||
|
||||
factory SecurityState.$default() => SecurityState(
|
||||
secure: false,
|
||||
host: "",
|
||||
issuer: "",
|
||||
);
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [secure, host, issuer];
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:lensai/domain/entities/equatable_image.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/history_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/security_state.dart';
|
||||
|
||||
part 'tab_state.g.dart';
|
||||
|
||||
@CopyWith()
|
||||
class TabState with FastEquatable {
|
||||
@CopyWithField(immutable: true)
|
||||
final String id;
|
||||
|
||||
final String? contextId;
|
||||
|
||||
final Uri url;
|
||||
final String title;
|
||||
|
||||
final EquatableImage? icon;
|
||||
final EquatableImage? thumbnail;
|
||||
|
||||
final int progress;
|
||||
|
||||
final bool isPrivate;
|
||||
final bool isFullScreen;
|
||||
final bool isLoading;
|
||||
|
||||
final SecurityState securityInfoState;
|
||||
final HistoryState historyState;
|
||||
final ReaderableState readerableState;
|
||||
|
||||
TabState({
|
||||
required this.id,
|
||||
required this.contextId,
|
||||
required this.url,
|
||||
required this.title,
|
||||
required this.icon,
|
||||
required this.thumbnail,
|
||||
required this.progress,
|
||||
required this.isPrivate,
|
||||
required this.isFullScreen,
|
||||
required this.isLoading,
|
||||
required this.securityInfoState,
|
||||
required this.historyState,
|
||||
required this.readerableState,
|
||||
});
|
||||
|
||||
factory TabState.$default(String tabId) => TabState(
|
||||
id: tabId,
|
||||
contextId: null,
|
||||
url: Uri.parse('about:blank'),
|
||||
title: "",
|
||||
icon: null,
|
||||
thumbnail: null,
|
||||
progress: 0,
|
||||
isPrivate: false,
|
||||
isFullScreen: false,
|
||||
isLoading: false,
|
||||
securityInfoState: SecurityState.$default(),
|
||||
historyState: HistoryState.$default(),
|
||||
readerableState: ReaderableState.$default(),
|
||||
);
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
id,
|
||||
contextId,
|
||||
url,
|
||||
title,
|
||||
icon,
|
||||
thumbnail,
|
||||
progress,
|
||||
isPrivate,
|
||||
isFullScreen,
|
||||
isLoading,
|
||||
securityInfoState,
|
||||
historyState,
|
||||
readerableState,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_state.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$TabStateCWProxy {
|
||||
TabState contextId(String? contextId);
|
||||
|
||||
TabState url(Uri url);
|
||||
|
||||
TabState title(String title);
|
||||
|
||||
TabState icon(EquatableImage? icon);
|
||||
|
||||
TabState thumbnail(EquatableImage? thumbnail);
|
||||
|
||||
TabState progress(int progress);
|
||||
|
||||
TabState isPrivate(bool isPrivate);
|
||||
|
||||
TabState isFullScreen(bool isFullScreen);
|
||||
|
||||
TabState isLoading(bool isLoading);
|
||||
|
||||
TabState securityInfoState(SecurityState securityInfoState);
|
||||
|
||||
TabState historyState(HistoryState historyState);
|
||||
|
||||
TabState readerableState(ReaderableState readerableState);
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// TabState(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
TabState call({
|
||||
String? contextId,
|
||||
Uri? url,
|
||||
String? title,
|
||||
EquatableImage? icon,
|
||||
EquatableImage? thumbnail,
|
||||
int? progress,
|
||||
bool? isPrivate,
|
||||
bool? isFullScreen,
|
||||
bool? isLoading,
|
||||
SecurityState? securityInfoState,
|
||||
HistoryState? historyState,
|
||||
ReaderableState? readerableState,
|
||||
});
|
||||
}
|
||||
|
||||
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfTabState.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfTabState.copyWith.fieldName(...)`
|
||||
class _$TabStateCWProxyImpl implements _$TabStateCWProxy {
|
||||
const _$TabStateCWProxyImpl(this._value);
|
||||
|
||||
final TabState _value;
|
||||
|
||||
@override
|
||||
TabState contextId(String? contextId) => this(contextId: contextId);
|
||||
|
||||
@override
|
||||
TabState url(Uri url) => this(url: url);
|
||||
|
||||
@override
|
||||
TabState title(String title) => this(title: title);
|
||||
|
||||
@override
|
||||
TabState icon(EquatableImage? icon) => this(icon: icon);
|
||||
|
||||
@override
|
||||
TabState thumbnail(EquatableImage? thumbnail) => this(thumbnail: thumbnail);
|
||||
|
||||
@override
|
||||
TabState progress(int progress) => this(progress: progress);
|
||||
|
||||
@override
|
||||
TabState isPrivate(bool isPrivate) => this(isPrivate: isPrivate);
|
||||
|
||||
@override
|
||||
TabState isFullScreen(bool isFullScreen) => this(isFullScreen: isFullScreen);
|
||||
|
||||
@override
|
||||
TabState isLoading(bool isLoading) => this(isLoading: isLoading);
|
||||
|
||||
@override
|
||||
TabState securityInfoState(SecurityState securityInfoState) =>
|
||||
this(securityInfoState: securityInfoState);
|
||||
|
||||
@override
|
||||
TabState historyState(HistoryState historyState) =>
|
||||
this(historyState: historyState);
|
||||
|
||||
@override
|
||||
TabState readerableState(ReaderableState readerableState) =>
|
||||
this(readerableState: readerableState);
|
||||
|
||||
@override
|
||||
|
||||
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `TabState(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
|
||||
///
|
||||
/// Usage
|
||||
/// ```dart
|
||||
/// TabState(...).copyWith(id: 12, name: "My name")
|
||||
/// ````
|
||||
TabState call({
|
||||
Object? contextId = const $CopyWithPlaceholder(),
|
||||
Object? url = const $CopyWithPlaceholder(),
|
||||
Object? title = const $CopyWithPlaceholder(),
|
||||
Object? icon = const $CopyWithPlaceholder(),
|
||||
Object? thumbnail = const $CopyWithPlaceholder(),
|
||||
Object? progress = const $CopyWithPlaceholder(),
|
||||
Object? isPrivate = const $CopyWithPlaceholder(),
|
||||
Object? isFullScreen = const $CopyWithPlaceholder(),
|
||||
Object? isLoading = const $CopyWithPlaceholder(),
|
||||
Object? securityInfoState = const $CopyWithPlaceholder(),
|
||||
Object? historyState = const $CopyWithPlaceholder(),
|
||||
Object? readerableState = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return TabState(
|
||||
id: _value.id,
|
||||
contextId: contextId == const $CopyWithPlaceholder()
|
||||
? _value.contextId
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: contextId as String?,
|
||||
url: url == const $CopyWithPlaceholder() || url == null
|
||||
? _value.url
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: url as Uri,
|
||||
title: title == const $CopyWithPlaceholder() || title == null
|
||||
? _value.title
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: title as String,
|
||||
icon: icon == const $CopyWithPlaceholder()
|
||||
? _value.icon
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: icon as EquatableImage?,
|
||||
thumbnail: thumbnail == const $CopyWithPlaceholder()
|
||||
? _value.thumbnail
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: thumbnail as EquatableImage?,
|
||||
progress: progress == const $CopyWithPlaceholder() || progress == null
|
||||
? _value.progress
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: progress as int,
|
||||
isPrivate: isPrivate == const $CopyWithPlaceholder() || isPrivate == null
|
||||
? _value.isPrivate
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: isPrivate as bool,
|
||||
isFullScreen:
|
||||
isFullScreen == const $CopyWithPlaceholder() || isFullScreen == null
|
||||
? _value.isFullScreen
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: isFullScreen as bool,
|
||||
isLoading: isLoading == const $CopyWithPlaceholder() || isLoading == null
|
||||
? _value.isLoading
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: isLoading as bool,
|
||||
securityInfoState: securityInfoState == const $CopyWithPlaceholder() ||
|
||||
securityInfoState == null
|
||||
? _value.securityInfoState
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: securityInfoState as SecurityState,
|
||||
historyState:
|
||||
historyState == const $CopyWithPlaceholder() || historyState == null
|
||||
? _value.historyState
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: historyState as HistoryState,
|
||||
readerableState: readerableState == const $CopyWithPlaceholder() ||
|
||||
readerableState == null
|
||||
? _value.readerableState
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: readerableState as ReaderableState,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $TabStateCopyWith on TabState {
|
||||
/// Returns a callable class that can be used as follows: `instanceOfTabState.copyWith(...)` or like so:`instanceOfTabState.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$TabStateCWProxy get copyWith => _$TabStateCWProxyImpl(this);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab_session.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
GeckoEventService eventService(EventServiceRef ref) {
|
||||
final service = GeckoEventService.setUp();
|
||||
|
||||
ref.onDispose(() {
|
||||
service.dispose();
|
||||
});
|
||||
|
||||
return service;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
TabState? tabState(TabStateRef ref, String? tabId) {
|
||||
if (tabId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ref.watch(
|
||||
tabRepositoryProvider.select((tabs) => tabs[tabId]),
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
TabState? selectedTabState(SelectedTabStateRef ref) {
|
||||
final tabId = ref.watch(selectedTabControllerProvider);
|
||||
return ref.watch(tabStateProvider(tabId));
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
TabSession selectedTabSessionNotifier(SelectedTabSessionNotifierRef ref) {
|
||||
return ref.watch(tabSessionProvider(null).notifier);
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$eventServiceHash() => r'5aa357fdf0d217677a9a66ecb50417ac18929cad';
|
||||
|
||||
/// See also [eventService].
|
||||
@ProviderFor(eventService)
|
||||
final eventServiceProvider = Provider<GeckoEventService>.internal(
|
||||
eventService,
|
||||
name: r'eventServiceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product') ? null : _$eventServiceHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef EventServiceRef = ProviderRef<GeckoEventService>;
|
||||
String _$tabStateHash() => r'6985f5472625a9cf44c8bbc61e61b7577eb60e71';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [tabState].
|
||||
@ProviderFor(tabState)
|
||||
const tabStateProvider = TabStateFamily();
|
||||
|
||||
/// See also [tabState].
|
||||
class TabStateFamily extends Family<TabState?> {
|
||||
/// See also [tabState].
|
||||
const TabStateFamily();
|
||||
|
||||
/// See also [tabState].
|
||||
TabStateProvider call(
|
||||
String? tabId,
|
||||
) {
|
||||
return TabStateProvider(
|
||||
tabId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TabStateProvider getProviderOverride(
|
||||
covariant TabStateProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.tabId,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'tabStateProvider';
|
||||
}
|
||||
|
||||
/// See also [tabState].
|
||||
class TabStateProvider extends AutoDisposeProvider<TabState?> {
|
||||
/// See also [tabState].
|
||||
TabStateProvider(
|
||||
String? tabId,
|
||||
) : this._internal(
|
||||
(ref) => tabState(
|
||||
ref as TabStateRef,
|
||||
tabId,
|
||||
),
|
||||
from: tabStateProvider,
|
||||
name: r'tabStateProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabStateHash,
|
||||
dependencies: TabStateFamily._dependencies,
|
||||
allTransitiveDependencies: TabStateFamily._allTransitiveDependencies,
|
||||
tabId: tabId,
|
||||
);
|
||||
|
||||
TabStateProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.tabId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? tabId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
TabState? Function(TabStateRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TabStateProvider._internal(
|
||||
(ref) => create(ref as TabStateRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
tabId: tabId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeProviderElement<TabState?> createElement() {
|
||||
return _TabStateProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TabStateProvider && other.tabId == tabId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, tabId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin TabStateRef on AutoDisposeProviderRef<TabState?> {
|
||||
/// The parameter `tabId` of this provider.
|
||||
String? get tabId;
|
||||
}
|
||||
|
||||
class _TabStateProviderElement extends AutoDisposeProviderElement<TabState?>
|
||||
with TabStateRef {
|
||||
_TabStateProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get tabId => (origin as TabStateProvider).tabId;
|
||||
}
|
||||
|
||||
String _$selectedTabStateHash() => r'd5240ade9010f5c9f00d7a4f419e1b776f75fac1';
|
||||
|
||||
/// See also [selectedTabState].
|
||||
@ProviderFor(selectedTabState)
|
||||
final selectedTabStateProvider = AutoDisposeProvider<TabState?>.internal(
|
||||
selectedTabState,
|
||||
name: r'selectedTabStateProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTabStateHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
|
||||
String _$selectedTabSessionNotifierHash() =>
|
||||
r'1b0fe1afa87c0b09fc8407ceb91946fffaef5214';
|
||||
|
||||
/// See also [selectedTabSessionNotifier].
|
||||
@ProviderFor(selectedTabSessionNotifier)
|
||||
final selectedTabSessionNotifierProvider =
|
||||
AutoDisposeProvider<TabSession>.internal(
|
||||
selectedTabSessionNotifier,
|
||||
name: r'selectedTabSessionNotifierProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTabSessionNotifierHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<TabSession>;
|
||||
// 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,33 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'selected_tab.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SelectedTabController extends _$SelectedTabController {
|
||||
final _tabsService = GeckoTabService();
|
||||
|
||||
Future<void> selectTab(String tabId) {
|
||||
return _tabsService.selectTab(tabId: tabId);
|
||||
}
|
||||
|
||||
@override
|
||||
String? build() {
|
||||
final eventSerivce = ref.watch(eventServiceProvider);
|
||||
|
||||
final selectedTabSub = eventSerivce.selectedTabEvents.listen(
|
||||
(tabId) {
|
||||
state = tabId;
|
||||
},
|
||||
);
|
||||
|
||||
ref.onDispose(() {
|
||||
unawaited(selectedTabSub.cancel());
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'selected_tab.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$selectedTabControllerHash() =>
|
||||
r'b5d79d4d5c2dc08d504e601fa29c8b83d51c9716';
|
||||
|
||||
/// See also [SelectedTabController].
|
||||
@ProviderFor(SelectedTabController)
|
||||
final selectedTabControllerProvider =
|
||||
NotifierProvider<SelectedTabController, String?>.internal(
|
||||
SelectedTabController.new,
|
||||
name: r'selectedTabControllerProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTabControllerHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SelectedTabController = 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,165 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:lensai/extensions/image.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/history_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/security_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
|
||||
import 'package:lensai/features/geckoview/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/utils/image_helper.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabRepository extends _$TabRepository {
|
||||
final _tabsService = GeckoTabService();
|
||||
|
||||
void _onTabListChange(List<String> tabs) {
|
||||
state = {
|
||||
for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId),
|
||||
};
|
||||
}
|
||||
|
||||
void _onTabContentStateChange(TabContentState contentState) {
|
||||
final current =
|
||||
state[contentState.id] ?? TabState.$default(contentState.id);
|
||||
|
||||
state = {...state}..[contentState.id] = current.copyWith(
|
||||
contextId: contentState.contextId,
|
||||
url: Uri.parse(contentState.url),
|
||||
title: contentState.title,
|
||||
progress: contentState.progress,
|
||||
isPrivate: contentState.isPrivate,
|
||||
isFullScreen: contentState.isFullScreen,
|
||||
isLoading: contentState.isLoading,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onIconChange(IconEvent event) async {
|
||||
final IconEvent(:tabId, :bytes) = event;
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
|
||||
final image = (bytes != null)
|
||||
? (await tryDecodeImage(bytes)
|
||||
.then((image) async => image?.toEquatable()))
|
||||
: null;
|
||||
|
||||
state = {...state}..[tabId] = current.copyWith.icon(image);
|
||||
}
|
||||
|
||||
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
|
||||
final ThumbnailEvent(:tabId, :bytes) = event;
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
|
||||
final image = (bytes != null)
|
||||
? (await tryDecodeImage(bytes)
|
||||
.then((image) async => image?.toEquatable()))
|
||||
: null;
|
||||
|
||||
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
|
||||
}
|
||||
|
||||
void _onSecurityInfoStateChange(SecurityInfoEvent event) {
|
||||
final SecurityInfoEvent(:tabId, :securityInfo) = event;
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
|
||||
state = {...state}..[tabId] = current.copyWith.securityInfoState(
|
||||
SecurityState(
|
||||
secure: securityInfo.secure,
|
||||
host: securityInfo.host,
|
||||
issuer: securityInfo.issuer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onHistoryStateChange(HistoryEvent event) {
|
||||
final HistoryEvent(:tabId, :history) = event;
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
|
||||
state = {...state}..[tabId] = current.copyWith.historyState(
|
||||
HistoryState(
|
||||
items: history.items.nonNulls
|
||||
.map(
|
||||
(item) =>
|
||||
HistoryItem(url: Uri.parse(item.url), title: item.title),
|
||||
)
|
||||
.toList(),
|
||||
currentIndex: history.currentIndex,
|
||||
canGoBack: history.canGoBack,
|
||||
canGoForward: history.canGoForward,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _onReaderableStateChange(ReaderableEvent event) {
|
||||
final ReaderableEvent(:tabId, :readerable) = event;
|
||||
|
||||
final current = state[tabId] ?? TabState.$default(tabId);
|
||||
|
||||
state = {...state}..[tabId] = current.copyWith.readerableState(
|
||||
ReaderableState(
|
||||
readerable: readerable.readerable,
|
||||
active: readerable.active,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> addTab({
|
||||
Uri? url,
|
||||
bool selectTab = true,
|
||||
bool startLoading = true,
|
||||
String? parentId,
|
||||
LoadUrlFlags flags = LoadUrlFlags.NONE,
|
||||
String? contextId,
|
||||
Source source = Internal.newTab,
|
||||
bool private = false,
|
||||
HistoryMetadataKey? historyMetadata,
|
||||
Map<String, String>? additionalHeaders,
|
||||
}) {
|
||||
return _tabsService.addTab(
|
||||
url: url,
|
||||
selectTab: selectTab,
|
||||
startLoading: startLoading,
|
||||
parentId: parentId,
|
||||
flags: flags,
|
||||
contextId: contextId,
|
||||
source: source,
|
||||
private: private,
|
||||
historyMetadata: historyMetadata,
|
||||
additionalHeaders: additionalHeaders,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> closeTabs(List<String> tabIds) {
|
||||
return _tabsService.removeTabs(ids: tabIds);
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, TabState> build() {
|
||||
final eventService = ref.watch(eventServiceProvider);
|
||||
|
||||
final subscriptions = [
|
||||
eventService.tabListEvents.listen(_onTabListChange),
|
||||
eventService.tabContentEvents.listen(_onTabContentStateChange),
|
||||
eventService.iconEvents.listen(_onIconChange),
|
||||
eventService.thumbnailEvents.listen(_onThumbnailChange),
|
||||
eventService.securityInfoEvents.listen(_onSecurityInfoStateChange),
|
||||
eventService.historyEvents.listen(_onHistoryStateChange),
|
||||
eventService.readerableEvents.listen(_onReaderableStateChange),
|
||||
];
|
||||
|
||||
ref.onDispose(() {
|
||||
for (final sub in subscriptions) {
|
||||
unawaited(sub.cancel());
|
||||
}
|
||||
});
|
||||
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabRepositoryHash() => r'64724c0215624753c5c6ff8d6827b890ad9a6f2c';
|
||||
|
||||
/// See also [TabRepository].
|
||||
@ProviderFor(TabRepository)
|
||||
final tabRepositoryProvider =
|
||||
NotifierProvider<TabRepository, Map<String, TabState>>.internal(
|
||||
TabRepository.new,
|
||||
name: r'tabRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TabRepository = Notifier<Map<String, TabState>>;
|
||||
// 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,28 @@
|
||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_session.g.dart';
|
||||
|
||||
@Riverpod()
|
||||
class TabSession extends _$TabSession {
|
||||
late GeckoSessionService _sessionService;
|
||||
|
||||
Future<void> loadUrl({
|
||||
required Uri url,
|
||||
LoadUrlFlags flags = LoadUrlFlags.NONE,
|
||||
Map<String, String>? additionalHeaders,
|
||||
}) {
|
||||
return _sessionService.loadUrl(
|
||||
url: url,
|
||||
flags: flags,
|
||||
additionalHeaders: additionalHeaders,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void build(String? tabId) {
|
||||
_sessionService = (tabId != null)
|
||||
? GeckoSessionService(tabId: tabId)
|
||||
: GeckoSessionService.forActiveTab();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_session.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabSessionHash() => r'4aab49dcb544cfc9e7a61b498d2ccf2c28ef729b';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _$TabSession extends BuildlessAutoDisposeNotifier<void> {
|
||||
late final String? tabId;
|
||||
|
||||
void build(
|
||||
String? tabId,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [TabSession].
|
||||
@ProviderFor(TabSession)
|
||||
const tabSessionProvider = TabSessionFamily();
|
||||
|
||||
/// See also [TabSession].
|
||||
class TabSessionFamily extends Family<void> {
|
||||
/// See also [TabSession].
|
||||
const TabSessionFamily();
|
||||
|
||||
/// See also [TabSession].
|
||||
TabSessionProvider call(
|
||||
String? tabId,
|
||||
) {
|
||||
return TabSessionProvider(
|
||||
tabId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TabSessionProvider getProviderOverride(
|
||||
covariant TabSessionProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.tabId,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'tabSessionProvider';
|
||||
}
|
||||
|
||||
/// See also [TabSession].
|
||||
class TabSessionProvider
|
||||
extends AutoDisposeNotifierProviderImpl<TabSession, void> {
|
||||
/// See also [TabSession].
|
||||
TabSessionProvider(
|
||||
String? tabId,
|
||||
) : this._internal(
|
||||
() => TabSession()..tabId = tabId,
|
||||
from: tabSessionProvider,
|
||||
name: r'tabSessionProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabSessionHash,
|
||||
dependencies: TabSessionFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
TabSessionFamily._allTransitiveDependencies,
|
||||
tabId: tabId,
|
||||
);
|
||||
|
||||
TabSessionProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.tabId,
|
||||
}) : super.internal();
|
||||
|
||||
final String? tabId;
|
||||
|
||||
@override
|
||||
void runNotifierBuild(
|
||||
covariant TabSession notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
tabId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(TabSession Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TabSessionProvider._internal(
|
||||
() => create()..tabId = tabId,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
tabId: tabId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeNotifierProviderElement<TabSession, void> createElement() {
|
||||
return _TabSessionProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TabSessionProvider && other.tabId == tabId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, tabId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin TabSessionRef on AutoDisposeNotifierProviderRef<void> {
|
||||
/// The parameter `tabId` of this provider.
|
||||
String? get tabId;
|
||||
}
|
||||
|
||||
class _TabSessionProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<TabSession, void>
|
||||
with TabSessionRef {
|
||||
_TabSessionProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String? get tabId => (origin as TabSessionProvider).tabId;
|
||||
}
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
@@ -0,0 +1,51 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart';
|
||||
|
||||
part 'tab.g.dart';
|
||||
|
||||
@DriftAccessor()
|
||||
class TabLinkDao extends DatabaseAccessor<TabDatabase> with _$TabLinkDaoMixin {
|
||||
TabLinkDao(super.db);
|
||||
|
||||
Selectable<String> topicTabIds(String? topicId) {
|
||||
final query = (selectOnly(db.tabLink)
|
||||
..addColumns([db.tabLink.id])
|
||||
..where(
|
||||
(topicId == null)
|
||||
? db.tabLink.topicId.isNull()
|
||||
: db.tabLink.topicId.equals(topicId),
|
||||
)
|
||||
..orderBy([OrderingTerm.asc(db.tabLink.id)]));
|
||||
|
||||
return query.map((row) => row.read(db.tabLink.id)!);
|
||||
}
|
||||
|
||||
Future<void> upsertTabLink(
|
||||
String tabId, {
|
||||
required DateTime timestamp,
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
}) {
|
||||
return db.tabLink.insertOne(
|
||||
TabLinkCompanion.insert(
|
||||
id: tabId,
|
||||
timestamp: timestamp,
|
||||
topicId: topicId,
|
||||
),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> touchTabLink(
|
||||
String id, {
|
||||
required DateTime timestamp,
|
||||
}) {
|
||||
final statement = db.tabLink.update()..where((t) => t.id.equals(id));
|
||||
return statement.write(
|
||||
TabLinkCompanion(timestamp: Value(timestamp)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> syncTabLinks({required List<String> retainTabIds}) {
|
||||
return (db.tabLink.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$TabLinkDaoMixin on DatabaseAccessor<TabDatabase> {}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/core/uuid.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.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,52 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/daos/tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/daos/topic.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/drift/converters/color.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(
|
||||
include: {'database.drift'},
|
||||
daos: [TopicDao, TabLinkDao],
|
||||
)
|
||||
class TabDatabase extends _$TabDatabase {
|
||||
@override
|
||||
final int schemaVersion = 2;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration => MigrationStrategy(
|
||||
onUpgrade: (m, from, to) async {
|
||||
// disable foreign_keys before migrations
|
||||
await customStatement('PRAGMA foreign_keys = OFF');
|
||||
|
||||
if (from < 2) {
|
||||
await transaction(() async {
|
||||
await m.renameTable(tabLink, 'tab');
|
||||
|
||||
await m.dropColumn(tabLink, 'url');
|
||||
await m.dropColumn(tabLink, 'title');
|
||||
await m.dropColumn(tabLink, 'screenshot');
|
||||
});
|
||||
}
|
||||
|
||||
// Assert that the schema is valid after migrations
|
||||
if (kDebugMode) {
|
||||
final wrongForeignKeys =
|
||||
await customSelect('PRAGMA foreign_key_check').get();
|
||||
assert(
|
||||
wrongForeignKeys.isEmpty,
|
||||
'${wrongForeignKeys.map((e) => e.data)}',
|
||||
);
|
||||
}
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
await customStatement('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
);
|
||||
|
||||
TabDatabase(super.e);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/drift/converters/color.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart';
|
||||
|
||||
CREATE TABLE topic (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
name TEXT,
|
||||
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`
|
||||
) WITH TopicData;
|
||||
|
||||
CREATE TABLE tab_link (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
topic_id TEXT REFERENCES topic (id) ON DELETE CASCADE,
|
||||
timestamp DATETIME NOT NULL
|
||||
);
|
||||
|
||||
topicsWithCount WITH TopicDataWithCount:
|
||||
SELECT
|
||||
topic.*,
|
||||
tab_agg.tab_count
|
||||
FROM topic
|
||||
LEFT JOIN (
|
||||
SELECT
|
||||
topic_id,
|
||||
COUNT(*) AS tab_count,
|
||||
MAX(timestamp) AS last_updated
|
||||
FROM tab_link
|
||||
GROUP BY topic_id
|
||||
) AS tab_agg ON topic.id = tab_agg.topic_id
|
||||
ORDER BY tab_agg.last_updated DESC NULLS FIRST;
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
// 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 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 TabLink extends Table with TableInfo<TabLink, TabLinkData> {
|
||||
@override
|
||||
final GeneratedDatabase attachedDatabase;
|
||||
final String? _alias;
|
||||
TabLink(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');
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, topicId, timestamp];
|
||||
@override
|
||||
String get aliasedName => _alias ?? actualTableName;
|
||||
@override
|
||||
String get actualTableName => $name;
|
||||
static const String $name = 'tab_link';
|
||||
@override
|
||||
Set<GeneratedColumn> get $primaryKey => {id};
|
||||
@override
|
||||
TabLinkData map(Map<String, dynamic> data, {String? tablePrefix}) {
|
||||
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
|
||||
return TabLinkData(
|
||||
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'])!,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TabLink createAlias(String alias) {
|
||||
return TabLink(attachedDatabase, alias);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get dontWriteConstraints => true;
|
||||
}
|
||||
|
||||
class TabLinkData extends DataClass implements Insertable<TabLinkData> {
|
||||
final String id;
|
||||
final String? topicId;
|
||||
final DateTime timestamp;
|
||||
const TabLinkData({required this.id, this.topicId, required this.timestamp});
|
||||
@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);
|
||||
return map;
|
||||
}
|
||||
|
||||
factory TabLinkData.fromJson(Map<String, dynamic> json,
|
||||
{ValueSerializer? serializer}) {
|
||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
||||
return TabLinkData(
|
||||
id: serializer.fromJson<String>(json['id']),
|
||||
topicId: serializer.fromJson<String?>(json['topic_id']),
|
||||
timestamp: serializer.fromJson<DateTime>(json['timestamp']),
|
||||
);
|
||||
}
|
||||
@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),
|
||||
};
|
||||
}
|
||||
|
||||
TabLinkData copyWith(
|
||||
{String? id,
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
DateTime? timestamp}) =>
|
||||
TabLinkData(
|
||||
id: id ?? this.id,
|
||||
topicId: topicId.present ? topicId.value : this.topicId,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
);
|
||||
TabLinkData copyWithCompanion(TabLinkCompanion data) {
|
||||
return TabLinkData(
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TabLinkData(')
|
||||
..write('id: $id, ')
|
||||
..write('topicId: $topicId, ')
|
||||
..write('timestamp: $timestamp')
|
||||
..write(')'))
|
||||
.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(id, topicId, timestamp);
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
(other is TabLinkData &&
|
||||
other.id == this.id &&
|
||||
other.topicId == this.topicId &&
|
||||
other.timestamp == this.timestamp);
|
||||
}
|
||||
|
||||
class TabLinkCompanion extends UpdateCompanion<TabLinkData> {
|
||||
final Value<String> id;
|
||||
final Value<String?> topicId;
|
||||
final Value<DateTime> timestamp;
|
||||
final Value<int> rowid;
|
||||
const TabLinkCompanion({
|
||||
this.id = const Value.absent(),
|
||||
this.topicId = const Value.absent(),
|
||||
this.timestamp = const Value.absent(),
|
||||
this.rowid = const Value.absent(),
|
||||
});
|
||||
TabLinkCompanion.insert({
|
||||
required String id,
|
||||
this.topicId = const Value.absent(),
|
||||
required DateTime timestamp,
|
||||
this.rowid = const Value.absent(),
|
||||
}) : id = Value(id),
|
||||
timestamp = Value(timestamp);
|
||||
static Insertable<TabLinkData> custom({
|
||||
Expression<String>? id,
|
||||
Expression<String>? topicId,
|
||||
Expression<DateTime>? timestamp,
|
||||
Expression<int>? rowid,
|
||||
}) {
|
||||
return RawValuesInsertable({
|
||||
if (id != null) 'id': id,
|
||||
if (topicId != null) 'topic_id': topicId,
|
||||
if (timestamp != null) 'timestamp': timestamp,
|
||||
if (rowid != null) 'rowid': rowid,
|
||||
});
|
||||
}
|
||||
|
||||
TabLinkCompanion copyWith(
|
||||
{Value<String>? id,
|
||||
Value<String?>? topicId,
|
||||
Value<DateTime>? timestamp,
|
||||
Value<int>? rowid}) {
|
||||
return TabLinkCompanion(
|
||||
id: id ?? this.id,
|
||||
topicId: topicId ?? this.topicId,
|
||||
timestamp: timestamp ?? this.timestamp,
|
||||
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 (rowid.present) {
|
||||
map['rowid'] = Variable<int>(rowid.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return (StringBuffer('TabLinkCompanion(')
|
||||
..write('id: $id, ')
|
||||
..write('topicId: $topicId, ')
|
||||
..write('timestamp: $timestamp, ')
|
||||
..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 TabLink tabLink = TabLink(this);
|
||||
late final TopicDao topicDao = TopicDao(this as TabDatabase);
|
||||
late final TabLinkDao tabLinkDao = TabLinkDao(this as TabDatabase);
|
||||
Selectable<TopicDataWithCount> topicsWithCount() {
|
||||
return customSelect(
|
||||
'SELECT topic.*, tab_agg.tab_count FROM topic LEFT JOIN (SELECT topic_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab_link GROUP BY topic_id) AS tab_agg ON topic.id = tab_agg.topic_id ORDER BY tab_agg.last_updated DESC NULLS FIRST',
|
||||
variables: [],
|
||||
readsFrom: {
|
||||
topic,
|
||||
tabLink,
|
||||
}).map((QueryRow row) => TopicDataWithCount(
|
||||
id: row.read<String>('id'),
|
||||
name: row.readNullable<String>('name'),
|
||||
color: Topic.$convertercolor.fromSql(row.read<int>('color')),
|
||||
tabCount: row.readNullable<int>('tab_count'),
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Iterable<TableInfo<Table, Object?>> get allTables =>
|
||||
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
|
||||
@override
|
||||
List<DatabaseSchemaEntity> get allSchemaEntities => [topic, tabLink];
|
||||
@override
|
||||
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
|
||||
[
|
||||
WritePropagation(
|
||||
on: TableUpdateQuery.onTableName('topic',
|
||||
limitUpdateKind: UpdateKind.delete),
|
||||
result: [
|
||||
TableUpdate('tab_link', 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<TabLink, List<TabLinkData>> _tabLinkRefsTable(
|
||||
_$TabDatabase db) =>
|
||||
MultiTypedResultKey.fromTable(db.tabLink,
|
||||
aliasName: $_aliasNameGenerator(db.topic.id, db.tabLink.topicId));
|
||||
|
||||
$TabLinkProcessedTableManager get tabLinkRefs {
|
||||
final manager = $TabLinkTableManager($_db, $_db.tabLink)
|
||||
.filter((f) => f.topicId.id($_item.id));
|
||||
|
||||
final cache = $_typedResult.readTableOrNull(_tabLinkRefsTable($_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 tabLinkRefs(
|
||||
ComposableFilter Function($TabLinkFilterComposer f) f) {
|
||||
final $TabLinkFilterComposer composer = $state.composerBuilder(
|
||||
composer: this,
|
||||
getCurrentColumn: (t) => t.id,
|
||||
referencedTable: $state.db.tabLink,
|
||||
getReferencedColumn: (t) => t.topicId,
|
||||
builder: (joinBuilder, parentComposers) => $TabLinkFilterComposer(
|
||||
ComposerState(
|
||||
$state.db, $state.db.tabLink, 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 tabLinkRefs})> {
|
||||
$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: ({tabLinkRefs = false}) {
|
||||
return PrefetchHooks(
|
||||
db: db,
|
||||
explicitlyWatchedTables: [if (tabLinkRefs) db.tabLink],
|
||||
addJoins: null,
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [
|
||||
if (tabLinkRefs)
|
||||
await $_getPrefetchedData(
|
||||
currentTable: table,
|
||||
referencedTable: $TopicReferences._tabLinkRefsTable(db),
|
||||
managerFromTypedResult: (p0) =>
|
||||
$TopicReferences(db, table, p0).tabLinkRefs,
|
||||
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 tabLinkRefs})>;
|
||||
typedef $TabLinkCreateCompanionBuilder = TabLinkCompanion Function({
|
||||
required String id,
|
||||
Value<String?> topicId,
|
||||
required DateTime timestamp,
|
||||
Value<int> rowid,
|
||||
});
|
||||
typedef $TabLinkUpdateCompanionBuilder = TabLinkCompanion Function({
|
||||
Value<String> id,
|
||||
Value<String?> topicId,
|
||||
Value<DateTime> timestamp,
|
||||
Value<int> rowid,
|
||||
});
|
||||
|
||||
final class $TabLinkReferences
|
||||
extends BaseReferences<_$TabDatabase, TabLink, TabLinkData> {
|
||||
$TabLinkReferences(super.$_db, super.$_table, super.$_typedResult);
|
||||
|
||||
static Topic _topicIdTable(_$TabDatabase db) => db.topic
|
||||
.createAlias($_aliasNameGenerator(db.tabLink.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 $TabLinkFilterComposer extends FilterComposer<_$TabDatabase, TabLink> {
|
||||
$TabLinkFilterComposer(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));
|
||||
|
||||
$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 $TabLinkOrderingComposer
|
||||
extends OrderingComposer<_$TabDatabase, TabLink> {
|
||||
$TabLinkOrderingComposer(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));
|
||||
|
||||
$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 $TabLinkTableManager extends RootTableManager<
|
||||
_$TabDatabase,
|
||||
TabLink,
|
||||
TabLinkData,
|
||||
$TabLinkFilterComposer,
|
||||
$TabLinkOrderingComposer,
|
||||
$TabLinkCreateCompanionBuilder,
|
||||
$TabLinkUpdateCompanionBuilder,
|
||||
(TabLinkData, $TabLinkReferences),
|
||||
TabLinkData,
|
||||
PrefetchHooks Function({bool topicId})> {
|
||||
$TabLinkTableManager(_$TabDatabase db, TabLink table)
|
||||
: super(TableManagerState(
|
||||
db: db,
|
||||
table: table,
|
||||
filteringComposer: $TabLinkFilterComposer(ComposerState(db, table)),
|
||||
orderingComposer: $TabLinkOrderingComposer(ComposerState(db, table)),
|
||||
updateCompanionCallback: ({
|
||||
Value<String> id = const Value.absent(),
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
Value<DateTime> timestamp = const Value.absent(),
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TabLinkCompanion(
|
||||
id: id,
|
||||
topicId: topicId,
|
||||
timestamp: timestamp,
|
||||
rowid: rowid,
|
||||
),
|
||||
createCompanionCallback: ({
|
||||
required String id,
|
||||
Value<String?> topicId = const Value.absent(),
|
||||
required DateTime timestamp,
|
||||
Value<int> rowid = const Value.absent(),
|
||||
}) =>
|
||||
TabLinkCompanion.insert(
|
||||
id: id,
|
||||
topicId: topicId,
|
||||
timestamp: timestamp,
|
||||
rowid: rowid,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
(e) => (e.readTable(table), $TabLinkReferences(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: $TabLinkReferences._topicIdTable(db),
|
||||
referencedColumn: $TabLinkReferences._topicIdTable(db).id,
|
||||
) as T;
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
getPrefetchedDataCallback: (items) async {
|
||||
return [];
|
||||
},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
typedef $TabLinkProcessedTableManager = ProcessedTableManager<
|
||||
_$TabDatabase,
|
||||
TabLink,
|
||||
TabLinkData,
|
||||
$TabLinkFilterComposer,
|
||||
$TabLinkOrderingComposer,
|
||||
$TabLinkCreateCompanionBuilder,
|
||||
$TabLinkUpdateCompanionBuilder,
|
||||
(TabLinkData, $TabLinkReferences),
|
||||
TabLinkData,
|
||||
PrefetchHooks Function({bool topicId})>;
|
||||
|
||||
class $TabDatabaseManager {
|
||||
final _$TabDatabase _db;
|
||||
$TabDatabaseManager(this._db);
|
||||
$TopicTableManager get topic => $TopicTableManager(_db, _db.topic);
|
||||
$TabLinkTableManager get tabLink => $TabLinkTableManager(_db, _db.tabLink);
|
||||
}
|
||||
@@ -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,16 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||
|
||||
class UriConverter extends TypeConverter<Uri, String> {
|
||||
const UriConverter();
|
||||
|
||||
@override
|
||||
Uri fromSql(String fromDb) {
|
||||
return uri_parser.tryParseUrl(fromDb, eagerParsing: true)!;
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(Uri value) {
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
|
||||
class TopicData with FastEquatable {
|
||||
final String id;
|
||||
final String? name;
|
||||
final Color color;
|
||||
|
||||
TopicData({required this.id, this.name, required this.color});
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
];
|
||||
}
|
||||
|
||||
class TopicDataWithCount extends TopicData {
|
||||
final int? tabCount;
|
||||
|
||||
TopicDataWithCount({
|
||||
required super.id,
|
||||
super.name,
|
||||
required super.color,
|
||||
required this.tabCount,
|
||||
});
|
||||
|
||||
@override
|
||||
bool get cacheHash => true;
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
...super.hashParameters,
|
||||
tabCount,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:lensai/features/geckoview/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,73 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/utils/color_palette.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'providers.g.dart';
|
||||
|
||||
@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 db = ref.watch(tabDatabaseProvider);
|
||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||
|
||||
if (selectedTopic != null) {
|
||||
return db.topicDao.getTopicData(selectedTopic).watchSingleOrNull();
|
||||
}
|
||||
|
||||
return Stream.value(null);
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Future<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
|
||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||
|
||||
final allColors = colorTypes.flattened.toList();
|
||||
final usedColors = await repository.getDistinctColors();
|
||||
|
||||
Color randomColor;
|
||||
do {
|
||||
randomColor = randomColorShade(allColors);
|
||||
} while (usedColors.contains(randomColor));
|
||||
|
||||
return randomColor;
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<TopicDataWithCount>> topicsWithCount(TopicsWithCountRef ref) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
return db.topicsWithCount().watch();
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
Stream<List<String>> topicTabIds(TopicTabIdsRef ref, String topicId) {
|
||||
final db = ref.watch(tabDatabaseProvider);
|
||||
return db.tabLinkDao.topicTabIds(topicId).watch();
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'providers.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281';
|
||||
|
||||
/// 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 _$unusedRandomTopicColorHash() =>
|
||||
r'099730b0d987cc37bcae4ef5c999cbebfa67d7da';
|
||||
|
||||
/// 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 _$topicsWithCountHash() => r'cd9792b0e0af12cd8796a3f0b76b42171a4200e6';
|
||||
|
||||
/// See also [topicsWithCount].
|
||||
@ProviderFor(topicsWithCount)
|
||||
final topicsWithCountProvider =
|
||||
AutoDisposeStreamProvider<List<TopicDataWithCount>>.internal(
|
||||
topicsWithCount,
|
||||
name: r'topicsWithCountProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$topicsWithCountHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef TopicsWithCountRef
|
||||
= AutoDisposeStreamProviderRef<List<TopicDataWithCount>>;
|
||||
String _$topicTabIdsHash() => r'43719c70e01cf9e030d75171405ab9650936ddf8';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [topicTabIds].
|
||||
@ProviderFor(topicTabIds)
|
||||
const topicTabIdsProvider = TopicTabIdsFamily();
|
||||
|
||||
/// See also [topicTabIds].
|
||||
class TopicTabIdsFamily extends Family<AsyncValue<List<String>>> {
|
||||
/// See also [topicTabIds].
|
||||
const TopicTabIdsFamily();
|
||||
|
||||
/// See also [topicTabIds].
|
||||
TopicTabIdsProvider call(
|
||||
String topicId,
|
||||
) {
|
||||
return TopicTabIdsProvider(
|
||||
topicId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TopicTabIdsProvider getProviderOverride(
|
||||
covariant TopicTabIdsProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.topicId,
|
||||
);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'topicTabIdsProvider';
|
||||
}
|
||||
|
||||
/// See also [topicTabIds].
|
||||
class TopicTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
|
||||
/// See also [topicTabIds].
|
||||
TopicTabIdsProvider(
|
||||
String topicId,
|
||||
) : this._internal(
|
||||
(ref) => topicTabIds(
|
||||
ref as TopicTabIdsRef,
|
||||
topicId,
|
||||
),
|
||||
from: topicTabIdsProvider,
|
||||
name: r'topicTabIdsProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$topicTabIdsHash,
|
||||
dependencies: TopicTabIdsFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
TopicTabIdsFamily._allTransitiveDependencies,
|
||||
topicId: topicId,
|
||||
);
|
||||
|
||||
TopicTabIdsProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.topicId,
|
||||
}) : super.internal();
|
||||
|
||||
final String topicId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
Stream<List<String>> Function(TopicTabIdsRef provider) create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: TopicTabIdsProvider._internal(
|
||||
(ref) => create(ref as TopicTabIdsRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
topicId: topicId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeStreamProviderElement<List<String>> createElement() {
|
||||
return _TopicTabIdsProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is TopicTabIdsProvider && other.topicId == topicId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, topicId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
mixin TopicTabIdsRef on AutoDisposeStreamProviderRef<List<String>> {
|
||||
/// The parameter `topicId` of this provider.
|
||||
String get topicId;
|
||||
}
|
||||
|
||||
class _TopicTabIdsProviderElement
|
||||
extends AutoDisposeStreamProviderElement<List<String>> with TopicTabIdsRef {
|
||||
_TopicTabIdsProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get topicId => (origin as TopicTabIdsProvider).topicId;
|
||||
}
|
||||
|
||||
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,27 @@
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'selected_topic.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class SelectedTopicRepository extends _$SelectedTopicRepository {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'selected_topic.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$selectedTopicRepositoryHash() =>
|
||||
r'd7ffe4ff5501aff747e5155d74b07974784f26f9';
|
||||
|
||||
/// See also [SelectedTopicRepository].
|
||||
@ProviderFor(SelectedTopicRepository)
|
||||
final selectedTopicRepositoryProvider =
|
||||
NotifierProvider<SelectedTopicRepository, String?>.internal(
|
||||
SelectedTopicRepository.new,
|
||||
name: r'selectedTopicRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$selectedTopicRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$SelectedTopicRepository = 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,53 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/selected_tab.dart';
|
||||
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'tab_link.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class TabLinkRepository extends _$TabLinkRepository {
|
||||
late TabDatabase _db;
|
||||
|
||||
@override
|
||||
void build() {
|
||||
_db = ref.watch(tabDatabaseProvider);
|
||||
|
||||
//Sync existing tabs
|
||||
ref.listen(
|
||||
tabRepositoryProvider.select((tabs) => tabs.keys.toList()),
|
||||
(previous, next) async {
|
||||
//No empty list to avoid data loss
|
||||
if (next.isNotEmpty) {
|
||||
await _db.tabLinkDao.syncTabLinks(retainTabIds: next);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
selectedTabControllerProvider,
|
||||
(previous, next) async {
|
||||
if (next != null) {
|
||||
await _db.tabLinkDao.touchTabLink(next, timestamp: DateTime.now());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> assignTab(String tabId, String? topicId) {
|
||||
return _db.tabLinkDao.upsertTabLink(
|
||||
tabId,
|
||||
timestamp: DateTime.now(),
|
||||
topicId: Value(topicId),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> closeAllTabs(String? topicId) async {
|
||||
final tabIds = await _db.tabLinkDao.topicTabIds(topicId).get();
|
||||
if (tabIds.isNotEmpty) {
|
||||
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'tab_link.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$tabLinkRepositoryHash() => r'e85d88fadb8133c3ef1e38f0176694a12a749397';
|
||||
|
||||
/// See also [TabLinkRepository].
|
||||
@ProviderFor(TabLinkRepository)
|
||||
final tabLinkRepositoryProvider =
|
||||
NotifierProvider<TabLinkRepository, void>.internal(
|
||||
TabLinkRepository.new,
|
||||
name: r'tabLinkRepositoryProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$tabLinkRepositoryHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$TabLinkRepository = 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,40 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/geckoview/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);
|
||||
}
|
||||
|
||||
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'6c456c17af4ea7d7ef1747c92d0442459d061080';
|
||||
|
||||
/// 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,210 @@
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/geckoview/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(topicRepositoryProvider);
|
||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||
|
||||
return Skeletonizer(
|
||||
enabled: topicsAsync.isLoading,
|
||||
child: topicsAsync.when(
|
||||
data: (topics) => FadingScroll(
|
||||
fadingSize: 25,
|
||||
builder: (context, controller) {
|
||||
return ListView.builder(
|
||||
controller: controller,
|
||||
itemCount: topics.length,
|
||||
itemBuilder: (context, index) {
|
||||
final topic = topics[index];
|
||||
return _TopicTile(
|
||||
topic,
|
||||
key: ValueKey(topic.id),
|
||||
isSelected: topic.id == selectedTopic,
|
||||
onEdit: (edited) async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.replaceTopic(
|
||||
id: topic.id,
|
||||
name: edited.name,
|
||||
color: edited.color,
|
||||
);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(topicRepositoryProvider.notifier)
|
||||
.deleteTopic(topic.id);
|
||||
},
|
||||
onTap: () {
|
||||
ref
|
||||
.read(selectedTopicProvider.notifier)
|
||||
.toggleTopic(topic.id);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
error: (error, stackTrace) => SizedBox.shrink(),
|
||||
loading: () => ListView.builder(
|
||||
itemCount: 3,
|
||||
itemBuilder: (context, index) => _TopicTile(
|
||||
TopicData(id: 'null', color: Colors.transparent),
|
||||
isSelected: false,
|
||||
onEdit: (_) {},
|
||||
onDelete: () {},
|
||||
onTap: () {},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+340
@@ -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/geckoview/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,73 @@
|
||||
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/geckoview/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||
|
||||
class TopicChips extends HookConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final topicsAsync = ref.watch(topicRepositoryProvider);
|
||||
final selectedTopic = ref
|
||||
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
||||
|
||||
return topicsAsync.when(
|
||||
data: (availableTopics) => SizedBox(
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 16),
|
||||
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'),
|
||||
itemBadgeCount: (topic) => topic.tabCount,
|
||||
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/geckoview/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;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
Future<Image?> tryDecodeImage(
|
||||
Uint8List bytes, {
|
||||
int? targetWidth,
|
||||
int? targetHeight,
|
||||
bool allowUpscaling = true,
|
||||
}) async {
|
||||
try {
|
||||
final codec = await instantiateImageCodec(
|
||||
bytes,
|
||||
targetWidth: targetWidth,
|
||||
targetHeight: targetHeight,
|
||||
allowUpscaling: allowUpscaling,
|
||||
);
|
||||
|
||||
final frameInfo = await codec.getNextFrame();
|
||||
final image = frameInfo.image;
|
||||
|
||||
if (image.width > 0) {
|
||||
return image;
|
||||
}
|
||||
} catch (e) {
|
||||
//swallow
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
Reference in New Issue
Block a user