one step closer

This commit is contained in:
Fabian Freund
2024-10-22 07:27:37 +02:00
parent c4d93e8fe7
commit 8d351e55c0
92 changed files with 3829 additions and 2694 deletions
+1
View File
@@ -24,6 +24,7 @@ analyzer:
plugins: plugins:
- custom_lint - custom_lint
exclude: exclude:
- "**.drift"
- "**.g.dart" - "**.g.dart"
- "**.swagger.dart" - "**.swagger.dart"
- "**.freezed.dart" - "**.freezed.dart"
+2 -1
View File
@@ -39,7 +39,8 @@
android:label="Lensai" android:label="Lensai"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/launcher_icon" android:icon="@mipmap/launcher_icon"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true"
android:enableOnBackInvokedCallback="true">
<activity android:name="eu.lensai.flutter_mozilla_components.NotificationActivity" android:theme="@style/Theme.AppCompat.Translucent" /> <activity android:name="eu.lensai.flutter_mozilla_components.NotificationActivity" android:theme="@style/Theme.AppCompat.Translucent" />
<activity <activity
@@ -1,5 +1,20 @@
package me.movenext.bang_navigator package me.movenext.bang_navigator
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterFragmentActivity() class MainActivity: FlutterFragmentActivity() {
private val CHANNEL = "me.movenext.flutter_mozilla_components/trim_memory"
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
channel.invokeMethod("onTrimMemory", level)
}
}
+4
View File
@@ -11,6 +11,10 @@ targets:
dialect: sqlite dialect: sqlite
options: options:
version: "3.45" version: "3.45"
known_functions:
lexo_rank_next: "text (int, text null)"
lexo_rank_previous: "text (int, text null)"
lexo_rank_reorder_after: "text (text null, text null)"
modules: modules:
- json1 - json1
- fts5 - fts5
+6 -6
View File
@@ -9,7 +9,7 @@ import 'package:lensai/features/chat_archive/presentation/screens/detail.dart';
import 'package:lensai/features/chat_archive/presentation/screens/list.dart'; import 'package:lensai/features/chat_archive/presentation/screens/list.dart';
import 'package:lensai/features/chat_archive/presentation/screens/search.dart'; import 'package:lensai/features/chat_archive/presentation/screens/search.dart';
import 'package:lensai/features/geckoview/features/browser/screens/browser.dart'; import 'package:lensai/features/geckoview/features/browser/screens/browser.dart';
import 'package:lensai/features/geckoview/features/topics/presentation/screens/topic_list.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
import 'package:lensai/features/settings/presentation/screens/settings.dart'; import 'package:lensai/features/settings/presentation/screens/settings.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@@ -42,9 +42,9 @@ part 'routes.g.dart';
), ),
], ],
), ),
TypedGoRoute<TopicListRoute>( TypedGoRoute<ContainerListRoute>(
name: 'TopicsRoute', name: 'ContainerListRoute',
path: 'topics', path: 'containers',
), ),
], ],
) )
@@ -102,10 +102,10 @@ class BangSearchRoute extends GoRouteData {
} }
} }
class TopicListRoute extends GoRouteData { class ContainerListRoute extends GoRouteData {
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return const TopicListScreen(); return const ContainerListScreen();
} }
} }
+7 -6
View File
@@ -47,9 +47,9 @@ RouteBase get $browserRoute => GoRouteData.$route(
], ],
), ),
GoRouteData.$route( GoRouteData.$route(
path: 'topics', path: 'containers',
name: 'TopicsRoute', name: 'ContainerListRoute',
factory: $TopicListRouteExtension._fromState, factory: $ContainerListRouteExtension._fromState,
), ),
], ],
); );
@@ -163,11 +163,12 @@ extension $BangSubCategoryRouteExtension on BangSubCategoryRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $TopicListRouteExtension on TopicListRoute { extension $ContainerListRouteExtension on ContainerListRoute {
static TopicListRoute _fromState(GoRouterState state) => TopicListRoute(); static ContainerListRoute _fromState(GoRouterState state) =>
ContainerListRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/topics', '/containers',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -0,0 +1,48 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:flutter/widgets.dart';
import 'package:json_annotation/json_annotation.dart';
class IconDataJsonConverter
implements JsonConverter<IconData, Map<String, dynamic>> {
const IconDataJsonConverter();
@override
IconData fromJson(Map<String, dynamic> json) {
return IconData(
json['codePoint'] as int,
fontFamily: json['fontFamily'] as String,
fontPackage: json['fontPackage'] as String,
);
}
@override
Map<String, dynamic> toJson(IconData iconData) {
return <String, dynamic>{
'codePoint': iconData.codePoint,
'fontFamily': iconData.fontFamily,
'fontPackage': iconData.fontPackage,
};
}
}
class IconDataTypeConverter implements TypeConverter<IconData, String> {
const IconDataTypeConverter();
@override
IconData fromSql(String fromDb) {
final json = jsonDecode(fromDb) as Map<String, dynamic>;
return const IconDataJsonConverter().fromJson(json);
}
@override
String toSql(IconData value) {
assert(
value.fontFamily != null,
'Font family must be provided to identify icon',
);
return jsonEncode(const IconDataJsonConverter().toJson(value));
}
}
@@ -0,0 +1,58 @@
import 'package:lexo_rank/lexo_rank.dart';
import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart';
import 'package:sqlite3/common.dart';
String _nextRankOrMiddle(List<Object?> args) {
final parsedBucket = LexoRankBucket.resolve(args[0]! as int);
if (args[1] != null) {
final parsedRank = LexoRank.parse(args[1]! as String);
return parsedRank.genNext().value;
} else {
return LexoRank.middle(bucket: parsedBucket).value;
}
}
String _previousRankOrMiddle(List<Object?> args) {
final parsedBucket = LexoRankBucket.resolve(args[0]! as int);
if (args[1] != null) {
final parsedRank = LexoRank.parse(args[1]! as String);
return parsedRank.genPrev().value;
} else {
return LexoRank.middle(bucket: parsedBucket).value;
}
}
String _reorderAfter(List<Object?> args) {
final first = (args[0] != null) ? LexoRank.parse(args[0]! as String) : null;
final last = (args[1] != null) ? LexoRank.parse(args[1]! as String) : null;
if (first == null) {
throw Exception('Tab not found');
} else if (last == null) {
return first.genNext().value;
} else {
return first.genBetween(last).value;
}
}
void registerLexorankFunctions(CommonDatabase database) {
database.createFunction(
functionName: 'lexo_rank_next',
argumentCount: const AllowedArgumentCount(2),
function: _nextRankOrMiddle,
);
database.createFunction(
functionName: 'lexo_rank_previous',
argumentCount: const AllowedArgumentCount(2),
function: _previousRankOrMiddle,
);
database.createFunction(
functionName: 'lexo_rank_reorder_after',
argumentCount: const AllowedArgumentCount(2),
function: _reorderAfter,
);
}
@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/core/logger.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -10,8 +11,6 @@ part 'providers.g.dart';
GeckoEventService eventService(EventServiceRef ref) { GeckoEventService eventService(EventServiceRef ref) {
final service = GeckoEventService.setUp(); final service = GeckoEventService.setUp();
unawaited(GeckoTabService().syncEvents());
ref.onDispose(() { ref.onDispose(() {
service.dispose(); service.dispose();
}); });
@@ -19,6 +18,41 @@ GeckoEventService eventService(EventServiceRef ref) {
return service; return service;
} }
@Riverpod(keepAlive: true)
class EngineReadyState extends _$EngineReadyState {
@override
bool build() {
final eventService = ref.watch(eventServiceProvider);
final currentState =
eventService.engineReadyStateEvents.valueOrNull ?? false;
if (!currentState) {
unawaited(
eventService.engineReadyStateEvents
.firstWhere((value) => value == true)
.timeout(
const Duration(seconds: 5),
onTimeout: () {
logger.w('Waiting for engine ready state timed out');
return true;
},
).whenComplete(() => state = true),
);
}
final sub = eventService.engineReadyStateEvents.listen((value) {
state = value;
});
ref.onDispose(() async {
await sub.cancel();
});
return currentState;
}
}
@Riverpod() @Riverpod()
TabSession selectedTabSessionNotifier(SelectedTabSessionNotifierRef ref) { TabSession selectedTabSessionNotifier(SelectedTabSessionNotifierRef ref) {
return ref.watch(tabSessionProvider(null).notifier); return ref.watch(tabSessionProvider(null).notifier);
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$eventServiceHash() => r'c36c6d895629002945c7b4eafba8389771e2eb2d'; String _$eventServiceHash() => r'5aa357fdf0d217677a9a66ecb50417ac18929cad';
/// See also [eventService]. /// See also [eventService].
@ProviderFor(eventService) @ProviderFor(eventService)
@@ -37,5 +37,21 @@ final selectedTabSessionNotifierProvider =
); );
typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<TabSession>; typedef SelectedTabSessionNotifierRef = AutoDisposeProviderRef<TabSession>;
String _$engineReadyStateHash() => r'c682333e2e07cf0635aa7ae793a2088ca648c950';
/// See also [EngineReadyState].
@ProviderFor(EngineReadyState)
final engineReadyStateProvider =
NotifierProvider<EngineReadyState, bool>.internal(
EngineReadyState.new,
name: r'engineReadyStateProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$engineReadyStateHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$EngineReadyState = Notifier<bool>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -1,8 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'selected_tab.g.dart'; part 'selected_tab.g.dart';
@@ -13,21 +14,29 @@ class SelectedTab extends _$SelectedTab {
@override @override
String? build() { String? build() {
_db = ref.watch(tabDatabaseProvider);
final eventSerivce = ref.watch(eventServiceProvider); final eventSerivce = ref.watch(eventServiceProvider);
final selectedTabSub = eventSerivce.selectedTabEvents.listen( _db = ref.watch(tabDatabaseProvider);
(tabId) {
state = tabId; ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(onSelectedTabChange: true);
}
}, },
); );
ref.listenSelf((previous, next) async { final selectedTabSub = eventSerivce.selectedTabEvents.listen(
if (next != null) { (tabId) async {
await _db.tabLinkDao.touchTabLink(next, timestamp: DateTime.now()); state = tabId;
}
}); if (tabId != null) {
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
}
},
);
ref.onDispose(() { ref.onDispose(() {
unawaited(selectedTabSub.cancel()); unawaited(selectedTabSub.cancel());
@@ -6,7 +6,7 @@ part of 'selected_tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$selectedTabHash() => r'd57003aa20d8a6f711918821ae355e5537a55685'; String _$selectedTabHash() => r'e49fb1c3938a97f384673e158742e56479aa8a8f';
/// See also [SelectedTab]. /// See also [SelectedTab].
@ProviderFor(SelectedTab) @ProviderFor(SelectedTab)
@@ -0,0 +1,53 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab_list.g.dart';
@Riverpod(keepAlive: true)
class TabList extends _$TabList {
late TabDatabase _db;
List<String> build() {
final eventService = ref.watch(eventServiceProvider);
_db = ref.watch(tabDatabaseProvider);
ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async {
if (next) {
await GeckoTabService().syncEvents(onTabListChange: true);
}
},
);
final tabListSub = eventService.tabListEvents.listen(
(tabs) async {
if (!const DeepCollectionEquality().equals(state, tabs)) {
//Make sure we only syncing empty lists when there has been tabs
//before as security measurement
final syncTabs = tabs.isNotEmpty || state.isNotEmpty;
state = tabs;
if (syncTabs) {
await _db.tabDao.syncTabs(retainTabIds: tabs);
}
}
},
);
ref.onDispose(() {
unawaited(tabListSub.cancel());
});
return [];
}
}
@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_list.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$tabListHash() => r'fd97d4692b1c74e91c5ebd4a75e6ff45ceb82ef9';
/// See also [TabList].
@ProviderFor(TabList)
final tabListProvider = NotifierProvider<TabList, List<String>>.internal(
TabList.new,
name: r'tabListProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$tabListHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$TabList = Notifier<List<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
@@ -33,6 +33,10 @@ class TabSession extends _$TabSession {
return _sessionService.goForward(); return _sessionService.goForward();
} }
Future<void> exitFullscreen() {
return _sessionService.exitFullscreen();
}
Future<Uint8List?> requestScreenshot() { Future<Uint8List?> requestScreenshot() {
return _sessionService.requestScreenshot(); return _sessionService.requestScreenshot();
} }
@@ -6,7 +6,7 @@ part of 'tab_session.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabSessionHash() => r'25afbead5c1110ca865e48a5009da70eb20d3a28'; String _$tabSessionHash() => r'29b20f40af01e599ef80e198aec5f093d74efc6a';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -1,6 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/image.dart'; import 'package:lensai/extensions/image.dart';
import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart'; import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart';
@@ -10,33 +9,25 @@ 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/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_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:lensai/features/geckoview/utils/image_helper.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:synchronized/synchronized.dart';
part 'tab_state.g.dart'; part 'tab_state.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class TabStates extends _$TabStates { class TabStates extends _$TabStates {
final _tabsService = GeckoTabService(); final _lock = Lock();
late TabDatabase _db;
void _onTabListChange(List<String> tabs) {
state = {
for (final tabId in tabs) tabId: state[tabId] ?? TabState.$default(tabId),
};
}
void _onTabContentStateChange(TabContentState contentState) { void _onTabContentStateChange(TabContentState contentState) {
final current = final current =
state[contentState.id] ?? TabState.$default(contentState.id); state[contentState.id] ?? TabState.$default(contentState.id);
state = {...state}..[contentState.id] = current.copyWith( state = {...state}..[contentState.id] = current.copyWith(
contextId: contentState.contextId, contextId: contentState.contextId,
url: Uri.parse(contentState.url), url: Uri.parse(contentState.url),
title: contentState.title, title: (contentState.title.isNotEmpty)
? contentState.title
: current.title,
progress: contentState.progress, progress: contentState.progress,
isPrivate: contentState.isPrivate, isPrivate: contentState.isPrivate,
isFullScreen: contentState.isFullScreen, isFullScreen: contentState.isFullScreen,
@@ -47,26 +38,24 @@ class TabStates extends _$TabStates {
Future<void> _onIconChange(IconEvent event) async { Future<void> _onIconChange(IconEvent event) async {
final IconEvent(:tabId, :bytes) = event; final IconEvent(:tabId, :bytes) = event;
final current = state[tabId] ?? TabState.$default(tabId);
final image = (bytes != null) final image = (bytes != null)
? (await tryDecodeImage(bytes) ? (await tryDecodeImage(bytes)
.then((image) async => image?.toEquatable())) .then((image) async => image?.toEquatable()))
: null; : null;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.icon(image); state = {...state}..[tabId] = current.copyWith.icon(image);
} }
Future<void> _onThumbnailChange(ThumbnailEvent event) async { Future<void> _onThumbnailChange(ThumbnailEvent event) async {
final ThumbnailEvent(:tabId, :bytes) = event; final ThumbnailEvent(:tabId, :bytes) = event;
final current = state[tabId] ?? TabState.$default(tabId);
final image = (bytes != null) final image = (bytes != null)
? (await tryDecodeImage(bytes) ? (await tryDecodeImage(bytes)
.then((image) async => image?.toEquatable())) .then((image) async => image?.toEquatable()))
: null; : null;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.thumbnail(image); state = {...state}..[tabId] = current.copyWith.thumbnail(image);
} }
@@ -74,7 +63,6 @@ class TabStates extends _$TabStates {
final SecurityInfoEvent(:tabId, :securityInfo) = event; final SecurityInfoEvent(:tabId, :securityInfo) = event;
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.securityInfoState( state = {...state}..[tabId] = current.copyWith.securityInfoState(
SecurityState( SecurityState(
secure: securityInfo.secure, secure: securityInfo.secure,
@@ -88,7 +76,6 @@ class TabStates extends _$TabStates {
final HistoryEvent(:tabId, :history) = event; final HistoryEvent(:tabId, :history) = event;
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.historyState( state = {...state}..[tabId] = current.copyWith.historyState(
HistoryState( HistoryState(
items: history.items.nonNulls items: history.items.nonNulls
@@ -108,7 +95,6 @@ class TabStates extends _$TabStates {
final ReaderableEvent(:tabId, :readerable) = event; final ReaderableEvent(:tabId, :readerable) = event;
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.readerableState( state = {...state}..[tabId] = current.copyWith.readerableState(
ReaderableState( ReaderableState(
readerable: readerable.readerable, readerable: readerable.readerable,
@@ -117,13 +103,13 @@ class TabStates extends _$TabStates {
); );
} }
void _onFindResults(FindResultsEvent event) { void _onFindResultsChange(FindResultsEvent event) {
final FindResultsEvent(:tabId, :results) = event; final FindResultsEvent(:tabId, :results) = event;
if (results.isNotEmpty) { if (results.isNotEmpty) {
final current = state[tabId] ?? TabState.$default(tabId);
final result = results.last; final result = results.last;
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.findResultState( state = {...state}..[tabId] = current.copyWith.findResultState(
FindResultState( FindResultState(
activeMatchOrdinal: result.activeMatchOrdinal, activeMatchOrdinal: result.activeMatchOrdinal,
@@ -134,64 +120,61 @@ class TabStates extends _$TabStates {
} }
} }
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);
}
Future<void> closeTab(String tabIds) {
return _tabsService.removeTab(tabId: tabIds);
}
@override @override
Map<String, TabState> build() { Map<String, TabState> build() {
final eventService = ref.watch(eventServiceProvider); final eventService = ref.watch(eventServiceProvider);
_db = ref.watch(tabDatabaseProvider);
final subscriptions = [ final subscriptions = [
eventService.tabListEvents.listen(_onTabListChange), eventService.tabContentEvents.listen(
eventService.tabContentEvents.listen(_onTabContentStateChange), (event) async {
eventService.iconEvents.listen(_onIconChange), await _lock.synchronized(() => _onTabContentStateChange(event));
eventService.thumbnailEvents.listen(_onThumbnailChange), },
eventService.securityInfoEvents.listen(_onSecurityInfoStateChange), ),
eventService.historyEvents.listen(_onHistoryStateChange), eventService.iconEvents.listen(
eventService.readerableEvents.listen(_onReaderableStateChange), (event) async {
eventService.findResultsEvent.listen(_onFindResults), await _lock.synchronized(() => _onIconChange(event));
},
),
eventService.thumbnailEvents.listen(
(event) async {
await _lock.synchronized(() => _onThumbnailChange(event));
},
),
eventService.securityInfoEvents.listen(
(event) async {
await _lock.synchronized(() => _onSecurityInfoStateChange(event));
},
),
eventService.historyEvents.listen(
(event) async {
await _lock.synchronized(() => _onHistoryStateChange(event));
},
),
eventService.readerableEvents.listen(
(event) async {
await _lock.synchronized(() => _onReaderableStateChange(event));
},
),
eventService.findResultsEvent.listen(
(event) async {
await _lock.synchronized(() => _onFindResultsChange(event));
},
),
]; ];
ref.listenSelf( ref.listen(
fireImmediately: true,
engineReadyStateProvider,
(previous, next) async { (previous, next) async {
if (!const DeepCollectionEquality() if (next) {
.equals(previous?.keys.toSet(), next.keys.toSet())) { await GeckoTabService().syncEvents(
final ids = next.keys.toList(); onTabContentStateChange: true,
if (ids.isNotEmpty) { onIconChange: true,
await _db.tabLinkDao.syncTabLinks(retainTabIds: ids); onThumbnailChange: true,
} onSecurityInfoStateChange: true,
onHistoryStateChange: true,
onFindResults: true,
);
} }
}, },
); );
@@ -169,7 +169,7 @@ final selectedTabStateProvider = AutoDisposeProvider<TabState?>.internal(
); );
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>; typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
String _$tabStatesHash() => r'f3ae09e49150955dcb29ec89f0f3fe63155a1d6e'; String _$tabStatesHash() => r'dcd293279a901eeaab7728979e39e5ca3bee8c66';
/// See also [TabStates]. /// See also [TabStates].
@ProviderFor(TabStates) @ProviderFor(TabStates)
@@ -1,6 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab.g.dart'; part 'tab.g.dart';
@@ -9,6 +14,8 @@ part 'tab.g.dart';
class TabRepository extends _$TabRepository { class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService(); final _tabsService = GeckoTabService();
late TabDatabase _db;
Future<String> addTab({ Future<String> addTab({
Uri? url, Uri? url,
bool selectTab = true, bool selectTab = true,
@@ -48,5 +55,20 @@ class TabRepository extends _$TabRepository {
} }
@override @override
void build() {} void build() {
final eventSerivce = ref.watch(eventServiceProvider);
_db = ref.watch(tabDatabaseProvider);
final tabAddedSub = eventSerivce.tabAddedStream.listen(
(tabId) async {
final containerId = ref.read(selectedContainerProvider);
await _db.tabDao.upsertTab(tabId, containerId: Value(containerId));
},
);
ref.onDispose(() {
unawaited(tabAddedSub.cancel());
});
}
} }
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabRepositoryHash() => r'0abf38ef42f29015d81ccd11eb5990b741ebc616'; String _$tabRepositoryHash() => r'05040223b83cd5ff80afaf89407043a10e4777cb';
/// See also [TabRepository]. /// See also [TabRepository].
@ProviderFor(TabRepository) @ProviderFor(TabRepository)
@@ -2,11 +2,10 @@
import 'dart:async'; import 'dart:async';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart'; import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/features/topics/data/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -85,27 +84,15 @@ class ShowFindInPage extends _$ShowFindInPage {
} }
@Riverpod() @Riverpod()
Stream<List<String>> availableTabIds(AvailableTabIdsRef ref, String? topicId) { List<String> availableTabIds(
final db = ref.watch(tabDatabaseProvider); AvailableTabIdsRef ref,
final openTabs = ref String? containerId,
.watch( ) {
tabStatesProvider.select( final containerTabs = ref.watch(
(states) => containerTabIdsProvider(containerId).select((value) => value.valueOrNull),
EquatableCollection(states.keys.toSet(), immutable: false), );
), final tabStates = ref.watch(tabListProvider);
)
.collection;
if (topicId != null) { return containerTabs?.where((tabId) => tabStates.contains(tabId)).toList() ??
return db.tabLinkDao.topicTabIds(topicId).watch().map( [];
(tabIds) =>
tabIds.where((tabId) => openTabs.contains(tabId)).toList(),
);
} else {
return db.tabLinkDao.allTabIds().watch().map(
(assignedTabIds) => openTabs
.where((tabId) => !assignedTabIds.contains(tabId))
.toList(),
);
}
} }
@@ -156,23 +156,23 @@ class _SelectedBangDataProviderElement
String? get domain => (origin as SelectedBangDataProvider).domain; String? get domain => (origin as SelectedBangDataProvider).domain;
} }
String _$availableTabIdsHash() => r'bea231cdf6211fc876b8031b5d72999eae5d7b15'; String _$availableTabIdsHash() => r'427ea2e58c6bc27e2f16cffb850dc6621875e247';
/// See also [availableTabIds]. /// See also [availableTabIds].
@ProviderFor(availableTabIds) @ProviderFor(availableTabIds)
const availableTabIdsProvider = AvailableTabIdsFamily(); const availableTabIdsProvider = AvailableTabIdsFamily();
/// See also [availableTabIds]. /// See also [availableTabIds].
class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> { class AvailableTabIdsFamily extends Family<List<String>> {
/// See also [availableTabIds]. /// See also [availableTabIds].
const AvailableTabIdsFamily(); const AvailableTabIdsFamily();
/// See also [availableTabIds]. /// See also [availableTabIds].
AvailableTabIdsProvider call( AvailableTabIdsProvider call(
String? topicId, String? containerId,
) { ) {
return AvailableTabIdsProvider( return AvailableTabIdsProvider(
topicId, containerId,
); );
} }
@@ -181,7 +181,7 @@ class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
covariant AvailableTabIdsProvider provider, covariant AvailableTabIdsProvider provider,
) { ) {
return call( return call(
provider.topicId, provider.containerId,
); );
} }
@@ -201,14 +201,14 @@ class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
} }
/// See also [availableTabIds]. /// See also [availableTabIds].
class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> { class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
/// See also [availableTabIds]. /// See also [availableTabIds].
AvailableTabIdsProvider( AvailableTabIdsProvider(
String? topicId, String? containerId,
) : this._internal( ) : this._internal(
(ref) => availableTabIds( (ref) => availableTabIds(
ref as AvailableTabIdsRef, ref as AvailableTabIdsRef,
topicId, containerId,
), ),
from: availableTabIdsProvider, from: availableTabIdsProvider,
name: r'availableTabIdsProvider', name: r'availableTabIdsProvider',
@@ -219,7 +219,7 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: AvailableTabIdsFamily._dependencies, dependencies: AvailableTabIdsFamily._dependencies,
allTransitiveDependencies: allTransitiveDependencies:
AvailableTabIdsFamily._allTransitiveDependencies, AvailableTabIdsFamily._allTransitiveDependencies,
topicId: topicId, containerId: containerId,
); );
AvailableTabIdsProvider._internal( AvailableTabIdsProvider._internal(
@@ -229,14 +229,14 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.topicId, required this.containerId,
}) : super.internal(); }) : super.internal();
final String? topicId; final String? containerId;
@override @override
Override overrideWith( Override overrideWith(
Stream<List<String>> Function(AvailableTabIdsRef provider) create, List<String> Function(AvailableTabIdsRef provider) create,
) { ) {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
@@ -247,42 +247,41 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
topicId: topicId, containerId: containerId,
), ),
); );
} }
@override @override
AutoDisposeStreamProviderElement<List<String>> createElement() { AutoDisposeProviderElement<List<String>> createElement() {
return _AvailableTabIdsProviderElement(this); return _AvailableTabIdsProviderElement(this);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is AvailableTabIdsProvider && other.topicId == topicId; return other is AvailableTabIdsProvider && other.containerId == containerId;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, topicId.hashCode); hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
} }
mixin AvailableTabIdsRef on AutoDisposeStreamProviderRef<List<String>> { mixin AvailableTabIdsRef on AutoDisposeProviderRef<List<String>> {
/// The parameter `topicId` of this provider. /// The parameter `containerId` of this provider.
String? get topicId; String? get containerId;
} }
class _AvailableTabIdsProviderElement class _AvailableTabIdsProviderElement
extends AutoDisposeStreamProviderElement<List<String>> extends AutoDisposeProviderElement<List<String>> with AvailableTabIdsRef {
with AvailableTabIdsRef {
_AvailableTabIdsProviderElement(super.provider); _AvailableTabIdsProviderElement(super.provider);
@override @override
String? get topicId => (origin as AvailableTabIdsProvider).topicId; String? get containerId => (origin as AvailableTabIdsProvider).containerId;
} }
String _$selectedBangTriggerHash() => String _$selectedBangTriggerHash() =>
@@ -5,8 +5,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart'; import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
class TabActionDialog extends HookConsumerWidget { class TabActionDialog extends HookConsumerWidget {
final TabState initialTab; final TabState initialTab;
@@ -22,20 +22,21 @@ class TabActionDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
final tabTopicId = ref.watch( final tabContainerId = ref.watch(
tabTopicIdProvider(initialTab.id).select((value) => value.valueOrNull), tabContainerIdProvider(initialTab.id)
.select((value) => value.valueOrNull),
); );
final topics = ref final containers = ref
.watch( .watch(
topicsWithCountProvider.select( containersWithCountProvider.select(
(value) => EquatableCollection(value.valueOrNull, immutable: true), (value) => EquatableCollection(value.valueOrNull, immutable: true),
), ),
) )
.collection; .collection;
final selectedTopic = final selectedContainer = containers
topics?.firstWhereOrNull((topic) => topic.id == tabTopicId); ?.firstWhereOrNull((container) => container.id == tabContainerId);
final expansionController = useExpansionTileController(); final expansionController = useExpansionTileController();
@@ -68,22 +69,23 @@ class TabActionDialog extends HookConsumerWidget {
width: double.maxFinite, width: double.maxFinite,
child: ExpansionTile( child: ExpansionTile(
controller: expansionController, controller: expansionController,
leading: (selectedTopic != null) leading: (selectedContainer != null)
? CircleAvatar(backgroundColor: selectedTopic.color) ? CircleAvatar(backgroundColor: selectedContainer.color)
: null, : null,
title: (selectedTopic != null) title: (selectedContainer != null)
? Text(selectedTopic.name ?? 'New Topic') ? Text(selectedContainer.name ?? 'New Container')
: const Text('Assign a Topic'), : const Text('Assign a Container'),
children: topics children: containers
?.where((topic) => topic.id != tabTopicId) ?.where((container) => container.id != tabContainerId)
.map( .map(
(topic) => ListTile( (container) => ListTile(
leading: CircleAvatar(backgroundColor: topic.color), leading:
title: Text(topic.name ?? 'New Topic'), CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'),
onTap: () async { onTap: () async {
await ref await ref
.read(tabLinkRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.assignTab(initialTab.id, topic.id); .assignContainer(initialTab.id, container.id);
expansionController.collapse(); expansionController.collapse();
}, },
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:text_scroll/text_scroll.dart'; import 'package:text_scroll/text_scroll.dart';
class AppBarTitle extends StatelessWidget { class AppBarTitle extends StatelessWidget {
@@ -23,11 +24,16 @@ class AppBarTitle extends StatelessWidget {
color: Theme.of(context).colorScheme.errorContainer, color: Theme.of(context).colorScheme.errorContainer,
size: 14, size: 14,
); );
} else { } else if (!tab.isLoading) {
return const Icon( return const Icon(
MdiIcons.lock, MdiIcons.lock,
size: 14, size: 14,
); );
} else {
return const Icon(
MdiIcons.timerSand,
size: 14,
);
} }
} }
@@ -38,10 +44,18 @@ class AppBarTitle extends StatelessWidget {
onTap: onTap, onTap: onTap,
child: Row( child: Row(
children: [ children: [
RawImage( Skeletonizer(
image: tab.icon?.value, enabled: tab.icon == null,
height: 16, child: Skeleton.replace(
width: 16, replacement: const Bone.icon(
size: 16,
),
child: RawImage(
image: tab.icon?.value,
height: 16,
width: 16,
),
),
), ),
const SizedBox( const SizedBox(
width: 8, width: 8,
@@ -51,20 +65,29 @@ class AppBarTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
TextScroll( Skeletonizer(
key: ValueKey(tab.title), enabled: tab.title.isEmpty,
tab.title, child: Skeleton.replace(
style: theme.textTheme.bodyLarge replacement: const Padding(
?.copyWith(color: theme.colorScheme.onSurface), padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
// mode: TextScrollMode.bouncing, child: Bone.text(),
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)), ),
delayBefore: const Duration(milliseconds: 500), child: TextScroll(
pauseBetween: const Duration(milliseconds: 5000), key: ValueKey(tab.title),
fadedBorder: true, tab.title,
fadeBorderSide: FadeBorderSide.right, style: theme.textTheme.bodyLarge
fadedBorderWidth: 0.05, ?.copyWith(color: theme.colorScheme.onSurface),
intervalSpaces: 4, // mode: TextScrollMode.bouncing,
numberOfReps: 2, velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 5000),
fadedBorder: true,
fadeBorderSide: FadeBorderSide.right,
fadedBorderWidth: 0.05,
intervalSpaces: 4,
numberOfReps: 2,
),
),
), ),
Row( Row(
children: [ children: [
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -11,9 +12,11 @@ import 'package:lensai/features/geckoview/features/browser/domain/providers.dart
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart'; import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_chips.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
import 'package:reorderable_grid/reorderable_grid.dart';
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate { class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
static const _headerSize = 104.0; static const _headerSize = 104.0;
@@ -52,10 +55,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
), ),
TextButton.icon( TextButton.icon(
onPressed: () async { onPressed: () async {
final topic = ref.read(selectedTopicProvider); final container = ref.read(selectedContainerProvider);
await ref await ref
.read(tabLinkRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.closeAllTabs(topic); .closeAllTabs(container);
onClose(); onClose();
}, },
@@ -64,7 +67,7 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
), ),
], ],
), ),
TopicChips(), ContainerChips(),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
@@ -125,14 +128,11 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
), ),
HookConsumer( HookConsumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final topic = ref.watch(selectedTopicProvider); final container = ref.watch(selectedContainerProvider);
final availableTabs = ref final availableTabs = ref
.watch( .watch(
availableTabIdsProvider(topic).select( availableTabIdsProvider(container).select(
(value) => EquatableCollection( (value) => EquatableCollection(value, immutable: true),
value.valueOrNull ?? [],
immutable: true,
),
), ),
) )
.collection; .collection;
@@ -156,8 +156,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
availableTabs.indexWhere((webView) => webView == activeTab); availableTabs.indexWhere((webView) => webView == activeTab);
if (index > -1) { if (index > -1) {
final reversedIndex = availableTabs.length - 1 - index; final offset = (index ~/ 2) * itemHeight;
final offset = (reversedIndex ~/ 2) * itemHeight;
if (offset != sheetScrollController.offset) { if (offset != sheetScrollController.offset) {
unawaited( unawaited(
@@ -175,21 +174,15 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
[availableTabs, activeTab], [availableTabs, activeTab],
); );
return SliverPadding( final tabs = useMemoized(
padding: const EdgeInsets.symmetric(horizontal: 4.0), () => availableTabs
sliver: SliverGrid.count( .mapIndexed(
//Sync values for itemHeight calculation _calculateItemHeight (index, tabId) => ReorderableGridDelayedDragStartListener(
childAspectRatio: 0.75, key: ValueKey(tabId),
mainAxisSpacing: 8.0, index: index,
crossAxisSpacing: 8.0, child: Consumer(
crossAxisCount: 2,
children: availableTabs.reversed
.map(
(tabId) => Consumer(
key: ValueKey(tabId),
builder: (context, ref, child) { builder: (context, ref, child) {
final tab = ref.watch(tabStateProvider(tabId)); final tab = ref.watch(tabStateProvider(tabId));
return (tab != null) return (tab != null)
? TabPreview( ? TabPreview(
tab: tab, tab: tab,
@@ -207,7 +200,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
onClose(); onClose();
} }
}, },
onLongPress: () { onDoubleTap: () {
ref ref
.read( .read(
overlayDialogControllerProvider overlayDialogControllerProvider
@@ -236,8 +229,52 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
: const SizedBox.shrink(); : const SizedBox.shrink();
}, },
), ),
) ),
.toList(), )
.toList(),
[availableTabs, activeTab],
);
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
sliver: SliverReorderableGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
itemCount: tabs.length,
itemBuilder: (context, index) => tabs[index],
onReorder: (oldIndex, newIndex) async {
final containerRepository =
ref.read(containerRepositoryProvider.notifier);
final tabId = availableTabs[oldIndex];
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId);
final String key;
if (newIndex <= 0) {
key = await containerRepository
.getLeadingOrderKey(containerId);
} else if (newIndex >= availableTabs.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex - 1;
key = await containerRepository.getOrderKeyAfterTab(
availableTabs[orderAfterIndex],
containerId,
);
}
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
},
), ),
); );
}, },
@@ -6,14 +6,14 @@ class TabPreview extends StatelessWidget {
final bool isActive; final bool isActive;
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onLongPress; final VoidCallback? onDoubleTap;
final VoidCallback? onDelete; final VoidCallback? onDelete;
const TabPreview({ const TabPreview({
required this.tab, required this.tab,
required this.isActive, required this.isActive,
this.onTap, this.onTap,
this.onLongPress, this.onDoubleTap,
this.onDelete, this.onDelete,
super.key, super.key,
}); });
@@ -35,7 +35,7 @@ class TabPreview extends StatelessWidget {
child: InkWell( child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16.0)), borderRadius: const BorderRadius.all(Radius.circular(16.0)),
onTap: onTap, onTap: onTap,
onLongPress: onLongPress, onDoubleTap: onDoubleTap,
child: Column( child: Column(
children: [ children: [
Row( Row(
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
class TabsActionButton extends HookConsumerWidget { class TabsActionButton extends HookConsumerWidget {
final bool isActive; final bool isActive;
@@ -16,7 +16,7 @@ class TabsActionButton extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context); final theme = Theme.of(context);
final tabCount = ref.watch(tabStatesProvider.select((tabs) => tabs.length)); final tabCount = ref.watch(tabListProvider.select((tabs) => tabs.length));
return InkWell( return InkWell(
onTap: onTap, onTap: onTap,
File diff suppressed because it is too large Load Diff
@@ -11,6 +11,7 @@ class ReaderableScreenController extends _$ReaderableScreenController {
Future<void> toggleReaderView(bool enable) async { Future<void> toggleReaderView(bool enable) async {
state = const AsyncValue.loading(); state = const AsyncValue.loading();
final eventChange = ref.read(eventServiceProvider).readerableEvents.first; final eventChange = ref.read(eventServiceProvider).readerableEvents.first;
final toggle = _service.toggleReaderView(enable); final toggle = _service.toggleReaderView(enable);
@@ -0,0 +1,76 @@
import 'dart:ui';
import 'package:drift/drift.dart';
import 'package:lensai/core/uuid.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
part 'container.g.dart';
@DriftAccessor()
class ContainerDao extends DatabaseAccessor<TabDatabase>
with _$ContainerDaoMixin {
ContainerDao(super.db);
Future<void> addContainer({String? name, required Color color}) {
return db.container.insertOne(
ContainerCompanion.insert(
id: uuid.v7(),
name: Value(name),
color: color,
),
);
}
Future<void> replaceContainer(
String id, {
required String? name,
required Color color,
}) {
return db.container.replaceOne(
ContainerCompanion(
id: Value(id),
name: Value(name),
color: Value(color),
),
);
}
Future<void> deleteContainer(String id) {
return db.container.deleteOne(ContainerCompanion.custom(id: Variable(id)));
}
SingleOrNullSelectable<ContainerData> getContainerData(String id) {
return select(db.container)..where((t) => t.id.equals(id));
}
Selectable<Color> getDistinctColors() {
final query = db.selectOnly(db.container, distinct: true)
..addColumns([db.container.color])
..where(db.container.color.isNotNull());
return query
.map((row) => row.readWithConverter<Color?, int>(db.container.color)!);
}
SingleSelectable<String> generateLeadingOrderKey(
String? containerId, {
int bucket = 0,
}) {
return db.leadingOrderKey(bucket: bucket, containerId: containerId);
}
SingleSelectable<String> generateTrailingOrderKey(
String? containerId, {
int bucket = 0,
}) {
return db.trailingOrderKey(bucket: bucket, containerId: containerId);
}
SingleSelectable<String> generateOrderKeyAfterTabId(
String? containerId,
String tabId,
) {
return db.orderKeyAfterTab(containerId: containerId, tabId: tabId);
}
}
@@ -0,0 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'container.dart';
// ignore_for_file: type=lint
mixin _$ContainerDaoMixin on DatabaseAccessor<TabDatabase> {}
@@ -0,0 +1,128 @@
import 'dart:async';
import 'package:drift/drift.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lexo_rank/lexo_rank.dart';
part 'tab.g.dart';
@DriftAccessor()
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
TabDao(super.db);
Selectable<String> containerTabIds(String? containerId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.id])
..where(
(containerId != null)
? db.tab.containerId.equals(containerId)
: db.tab.containerId.isNull(),
)
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
return query.map((row) => row.read(db.tab.id)!);
}
Selectable<String> allTabIds() {
final query = selectOnly(db.tab)
..addColumns([db.tab.id])
..orderBy([OrderingTerm.asc(db.tab.orderKey)]);
return query.map((row) => row.read(db.tab.id)!);
}
SingleSelectable<String?> tabContainerId(String tabId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.containerId])
..where(db.tab.id.equals(tabId));
return query.map((row) => row.read(db.tab.containerId));
}
Future<String> upsertTab(
String tabId, {
Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(),
}) {
return db.transaction(() async {
final currentOrderKey = orderKey.value ??
await db.containerDao
.generateLeadingOrderKey(containerId.value)
.getSingle();
await db.tab.insertOne(
TabCompanion.insert(
id: tabId,
timestamp: DateTime.now(),
containerId: containerId,
orderKey: currentOrderKey,
),
onConflict: DoUpdate(
(old) => TabCompanion.custom(
containerId:
(containerId.present) ? Variable(containerId.value) : null,
orderKey: (orderKey.present) ? Variable(orderKey.value) : null,
),
),
);
return tabId;
});
}
Future<void> assignContainer(
String id, {
required String? containerId,
}) {
final statement = db.tab.update()..where((t) => t.id.equals(id));
return statement.write(
TabCompanion(containerId: Value(containerId)),
);
}
Future<void> assignOrderKey(
String id, {
required String orderKey,
}) {
final statement = db.tab.update()..where((t) => t.id.equals(id));
return statement.write(
TabCompanion(orderKey: Value(orderKey)),
);
}
Future<void> touchTab(
String id, {
required DateTime timestamp,
}) {
final statement = db.tab.update()..where((t) => t.id.equals(id));
return statement.write(
TabCompanion(timestamp: Value(timestamp)),
);
}
Future<void> syncTabs({required List<String> retainTabIds}) async {
return db.transaction(() async {
await (db.tab.delete()..where((t) => t.id.isNotIn(retainTabIds))).go();
var currentOrderKey =
await db.containerDao.generateLeadingOrderKey(null).getSingle();
await db.tab.insertAll(
retainTabIds.map(
(id) {
final insertable = TabCompanion.insert(
id: id,
orderKey: currentOrderKey,
timestamp: DateTime.now(),
);
currentOrderKey = LexoRank.parse(currentOrderKey).genPrev().value;
return insertable;
},
),
mode: InsertMode.insertOrIgnore,
);
});
}
}
@@ -3,4 +3,4 @@
part of 'tab.dart'; part of 'tab.dart';
// ignore_for_file: type=lint // ignore_for_file: type=lint
mixin _$TabLinkDaoMixin on DatabaseAccessor<TabDatabase> {} mixin _$TabDaoMixin on DatabaseAccessor<TabDatabase> {}
@@ -1,17 +1,17 @@
import 'dart:ui';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:lensai/features/geckoview/features/topics/data/database/daos/tab.dart'; import 'package:flutter/widgets.dart' show Color, IconData;
import 'package:lensai/features/geckoview/features/topics/data/database/daos/topic.dart'; import 'package:lensai/data/database/converters/color.dart';
import 'package:lensai/features/geckoview/features/topics/data/database/drift/converters/color.dart'; import 'package:lensai/data/database/converters/icon_data.dart';
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart'; import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
part 'database.g.dart'; part 'database.g.dart';
@DriftDatabase( @DriftDatabase(
include: {'database.drift'}, include: {'database.drift'},
daos: [TopicDao, TabLinkDao], daos: [ContainerDao, TabDao],
) )
class TabDatabase extends _$TabDatabase { class TabDatabase extends _$TabDatabase {
@override @override
@@ -25,13 +25,11 @@ class TabDatabase extends _$TabDatabase {
if (from < 2) { if (from < 2) {
await transaction(() async { await transaction(() async {
await m.renameTable(tabLink, 'tab'); // await m.dropColumn(tabLink, 'url');
// await m.dropColumn(tabLink, 'title');
// await m.dropColumn(tabLink, 'screenshot');
await m.dropColumn(tabLink, 'url'); // await m.alterTable(TableMigration(tab));
await m.dropColumn(tabLink, 'title');
await m.dropColumn(tabLink, 'screenshot');
await m.alterTable(TableMigration(tabLink));
}); });
} }
@@ -0,0 +1,69 @@
import 'package:lensai/data/database/converters/color.dart';
import 'package:lensai/data/database/converters/icon_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
CREATE TABLE container (
id TEXT PRIMARY KEY NOT NULL,
contextual_identity TEXT,
name TEXT,
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`,
icon TEXT MAPPED BY `const IconDataTypeConverter()`
) WITH ContainerData;
CREATE TABLE tab (
id TEXT PRIMARY KEY NOT NULL,
container_id TEXT REFERENCES container (id) ON DELETE CASCADE,
order_key TEXT NOT NULL,
timestamp DATETIME NOT NULL
);
containersWithCount WITH ContainerDataWithCount:
SELECT
container.*,
tab_agg.tab_count
FROM container
LEFT JOIN (
SELECT
container_id,
COUNT(*) AS tab_count,
MAX(timestamp) AS last_updated
FROM tab
GROUP BY container_id
) AS tab_agg ON container.id = tab_agg.container_id
ORDER BY tab_agg.last_updated DESC NULLS FIRST;
leadingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER):
SELECT lexo_rank_previous(
:bucket,
(
SELECT order_key
FROM tab
WHERE container_id IS :container_id
ORDER BY order_key
LIMIT 1
)
);
trailingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER):
SELECT lexo_rank_next(
:bucket,
(
SELECT order_key
FROM tab
WHERE container_id IS :container_id
ORDER BY order_key DESC
LIMIT 1
)
);
orderKeyAfterTab(:tab_id AS TEXT, :container_id AS TEXT OR NULL):
WITH ordered_table AS (
SELECT id,
order_key,
LEAD(order_key) OVER (ORDER BY order_key) AS next_order_key
FROM tab
WHERE container_id IS :container_id
)
SELECT lexo_rank_reorder_after(order_key, next_order_key)
FROM ordered_table
WHERE id = :tab_id;
@@ -0,0 +1,996 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'database.dart';
// ignore_for_file: type=lint
class Container extends Table with TableInfo<Container, ContainerData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
Container(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> contextualIdentity =
GeneratedColumn<String>('contextual_identity', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '');
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>(Container.$convertercolor);
late final GeneratedColumnWithTypeConverter<IconData?, String> icon =
GeneratedColumn<String>('icon', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '')
.withConverter<IconData?>(Container.$convertericonn);
@override
List<GeneratedColumn> get $columns =>
[id, contextualIdentity, name, color, icon];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'container';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
ContainerData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return ContainerData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
contextualIdentity: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}contextual_identity']),
name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name']),
color: Container.$convertercolor.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}color'])!),
icon: Container.$convertericonn.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}icon'])),
);
}
@override
Container createAlias(String alias) {
return Container(attachedDatabase, alias);
}
static TypeConverter<Color, int> $convertercolor = const ColorConverter();
static TypeConverter<IconData, String> $convertericon =
const IconDataTypeConverter();
static TypeConverter<IconData?, String?> $convertericonn =
NullAwareTypeConverter.wrap($convertericon);
@override
bool get dontWriteConstraints => true;
}
class ContainerCompanion extends UpdateCompanion<ContainerData> {
final Value<String> id;
final Value<String?> contextualIdentity;
final Value<String?> name;
final Value<Color> color;
final Value<IconData?> icon;
final Value<int> rowid;
const ContainerCompanion({
this.id = const Value.absent(),
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(),
this.color = const Value.absent(),
this.icon = const Value.absent(),
this.rowid = const Value.absent(),
});
ContainerCompanion.insert({
required String id,
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(),
required Color color,
this.icon = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
color = Value(color);
static Insertable<ContainerData> custom({
Expression<String>? id,
Expression<String>? contextualIdentity,
Expression<String>? name,
Expression<int>? color,
Expression<String>? icon,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (contextualIdentity != null) 'contextual_identity': contextualIdentity,
if (name != null) 'name': name,
if (color != null) 'color': color,
if (icon != null) 'icon': icon,
if (rowid != null) 'rowid': rowid,
});
}
ContainerCompanion copyWith(
{Value<String>? id,
Value<String?>? contextualIdentity,
Value<String?>? name,
Value<Color>? color,
Value<IconData?>? icon,
Value<int>? rowid}) {
return ContainerCompanion(
id: id ?? this.id,
contextualIdentity: contextualIdentity ?? this.contextualIdentity,
name: name ?? this.name,
color: color ?? this.color,
icon: icon ?? this.icon,
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 (contextualIdentity.present) {
map['contextual_identity'] = Variable<String>(contextualIdentity.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
if (color.present) {
map['color'] =
Variable<int>(Container.$convertercolor.toSql(color.value));
}
if (icon.present) {
map['icon'] =
Variable<String>(Container.$convertericonn.toSql(icon.value));
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
}
return map;
}
@override
String toString() {
return (StringBuffer('ContainerCompanion(')
..write('id: $id, ')
..write('contextualIdentity: $contextualIdentity, ')
..write('name: $name, ')
..write('color: $color, ')
..write('icon: $icon, ')
..write('rowid: $rowid')
..write(')'))
.toString();
}
}
class Tab extends Table with TableInfo<Tab, TabData> {
@override
final GeneratedDatabase attachedDatabase;
final String? _alias;
Tab(this.attachedDatabase, [this._alias]);
late final GeneratedColumn<String> id = GeneratedColumn<String>(
'id', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL');
late final GeneratedColumn<String> containerId = GeneratedColumn<String>(
'container_id', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: 'REFERENCES container(id)ON DELETE CASCADE');
late final GeneratedColumn<String> orderKey = GeneratedColumn<String>(
'order_key', aliasedName, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
late final GeneratedColumn<DateTime> timestamp = GeneratedColumn<DateTime>(
'timestamp', aliasedName, false,
type: DriftSqlType.dateTime,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL');
@override
List<GeneratedColumn> get $columns => [id, containerId, orderKey, timestamp];
@override
String get aliasedName => _alias ?? actualTableName;
@override
String get actualTableName => $name;
static const String $name = 'tab';
@override
Set<GeneratedColumn> get $primaryKey => {id};
@override
TabData map(Map<String, dynamic> data, {String? tablePrefix}) {
final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
return TabData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
containerId: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}container_id']),
orderKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}order_key'])!,
timestamp: attachedDatabase.typeMapping
.read(DriftSqlType.dateTime, data['${effectivePrefix}timestamp'])!,
);
}
@override
Tab createAlias(String alias) {
return Tab(attachedDatabase, alias);
}
@override
bool get dontWriteConstraints => true;
}
class TabData extends DataClass implements Insertable<TabData> {
final String id;
final String? containerId;
final String orderKey;
final DateTime timestamp;
const TabData(
{required this.id,
this.containerId,
required this.orderKey,
required this.timestamp});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
if (!nullToAbsent || containerId != null) {
map['container_id'] = Variable<String>(containerId);
}
map['order_key'] = Variable<String>(orderKey);
map['timestamp'] = Variable<DateTime>(timestamp);
return map;
}
factory TabData.fromJson(Map<String, dynamic> json,
{ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return TabData(
id: serializer.fromJson<String>(json['id']),
containerId: serializer.fromJson<String?>(json['container_id']),
orderKey: serializer.fromJson<String>(json['order_key']),
timestamp: serializer.fromJson<DateTime>(json['timestamp']),
);
}
@override
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'id': serializer.toJson<String>(id),
'container_id': serializer.toJson<String?>(containerId),
'order_key': serializer.toJson<String>(orderKey),
'timestamp': serializer.toJson<DateTime>(timestamp),
};
}
TabData copyWith(
{String? id,
Value<String?> containerId = const Value.absent(),
String? orderKey,
DateTime? timestamp}) =>
TabData(
id: id ?? this.id,
containerId: containerId.present ? containerId.value : this.containerId,
orderKey: orderKey ?? this.orderKey,
timestamp: timestamp ?? this.timestamp,
);
TabData copyWithCompanion(TabCompanion data) {
return TabData(
id: data.id.present ? data.id.value : this.id,
containerId:
data.containerId.present ? data.containerId.value : this.containerId,
orderKey: data.orderKey.present ? data.orderKey.value : this.orderKey,
timestamp: data.timestamp.present ? data.timestamp.value : this.timestamp,
);
}
@override
String toString() {
return (StringBuffer('TabData(')
..write('id: $id, ')
..write('containerId: $containerId, ')
..write('orderKey: $orderKey, ')
..write('timestamp: $timestamp')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(id, containerId, orderKey, timestamp);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is TabData &&
other.id == this.id &&
other.containerId == this.containerId &&
other.orderKey == this.orderKey &&
other.timestamp == this.timestamp);
}
class TabCompanion extends UpdateCompanion<TabData> {
final Value<String> id;
final Value<String?> containerId;
final Value<String> orderKey;
final Value<DateTime> timestamp;
final Value<int> rowid;
const TabCompanion({
this.id = const Value.absent(),
this.containerId = const Value.absent(),
this.orderKey = const Value.absent(),
this.timestamp = const Value.absent(),
this.rowid = const Value.absent(),
});
TabCompanion.insert({
required String id,
this.containerId = const Value.absent(),
required String orderKey,
required DateTime timestamp,
this.rowid = const Value.absent(),
}) : id = Value(id),
orderKey = Value(orderKey),
timestamp = Value(timestamp);
static Insertable<TabData> custom({
Expression<String>? id,
Expression<String>? containerId,
Expression<String>? orderKey,
Expression<DateTime>? timestamp,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (containerId != null) 'container_id': containerId,
if (orderKey != null) 'order_key': orderKey,
if (timestamp != null) 'timestamp': timestamp,
if (rowid != null) 'rowid': rowid,
});
}
TabCompanion copyWith(
{Value<String>? id,
Value<String?>? containerId,
Value<String>? orderKey,
Value<DateTime>? timestamp,
Value<int>? rowid}) {
return TabCompanion(
id: id ?? this.id,
containerId: containerId ?? this.containerId,
orderKey: orderKey ?? this.orderKey,
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 (containerId.present) {
map['container_id'] = Variable<String>(containerId.value);
}
if (orderKey.present) {
map['order_key'] = Variable<String>(orderKey.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('TabCompanion(')
..write('id: $id, ')
..write('containerId: $containerId, ')
..write('orderKey: $orderKey, ')
..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 Container container = Container(this);
late final Tab tab = Tab(this);
late final ContainerDao containerDao = ContainerDao(this as TabDatabase);
late final TabDao tabDao = TabDao(this as TabDatabase);
Selectable<ContainerDataWithCount> containersWithCount() {
return customSelect(
'SELECT container.*, tab_agg.tab_count FROM container LEFT JOIN (SELECT container_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY container_id) AS tab_agg ON container.id = tab_agg.container_id ORDER BY tab_agg.last_updated DESC NULLS FIRST',
variables: [],
readsFrom: {
container,
tab,
}).map((QueryRow row) => ContainerDataWithCount(
id: row.read<String>('id'),
contextualIdentity: row.readNullable<String>('contextual_identity'),
name: row.readNullable<String>('name'),
color: Container.$convertercolor.fromSql(row.read<int>('color')),
icon: NullAwareTypeConverter.wrapFromSql(
Container.$convertericon, row.readNullable<String>('icon')),
tabCount: row.readNullable<int>('tab_count'),
));
}
Selectable<String> leadingOrderKey(
{required int bucket, String? containerId}) {
return customSelect(
'SELECT lexo_rank_previous(?1, (SELECT order_key FROM tab WHERE container_id IS ?2 ORDER BY order_key LIMIT 1)) AS _c0',
variables: [
Variable<int>(bucket),
Variable<String>(containerId)
],
readsFrom: {
tab,
}).map((QueryRow row) => row.read<String>('_c0'));
}
Selectable<String> trailingOrderKey(
{required int bucket, String? containerId}) {
return customSelect(
'SELECT lexo_rank_next(?1, (SELECT order_key FROM tab WHERE container_id IS ?2 ORDER BY order_key DESC LIMIT 1)) AS _c0',
variables: [
Variable<int>(bucket),
Variable<String>(containerId)
],
readsFrom: {
tab,
}).map((QueryRow row) => row.read<String>('_c0'));
}
Selectable<String> orderKeyAfterTab(
{String? containerId, required String tabId}) {
return customSelect(
'WITH ordered_table AS (SELECT id, order_key, LEAD(order_key)OVER (ORDER BY order_key RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW EXCLUDE NO OTHERS) AS next_order_key FROM tab WHERE container_id IS ?1) SELECT lexo_rank_reorder_after(order_key, next_order_key) AS _c0 FROM ordered_table WHERE id = ?2',
variables: [
Variable<String>(containerId),
Variable<String>(tabId)
],
readsFrom: {
tab,
}).map((QueryRow row) => row.read<String>('_c0'));
}
@override
Iterable<TableInfo<Table, Object?>> get allTables =>
allSchemaEntities.whereType<TableInfo<Table, Object?>>();
@override
List<DatabaseSchemaEntity> get allSchemaEntities => [container, tab];
@override
StreamQueryUpdateRules get streamUpdateRules => const StreamQueryUpdateRules(
[
WritePropagation(
on: TableUpdateQuery.onTableName('container',
limitUpdateKind: UpdateKind.delete),
result: [
TableUpdate('tab', kind: UpdateKind.delete),
],
),
],
);
}
typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({
required String id,
Value<String?> contextualIdentity,
Value<String?> name,
required Color color,
Value<IconData?> icon,
Value<int> rowid,
});
typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({
Value<String> id,
Value<String?> contextualIdentity,
Value<String?> name,
Value<Color> color,
Value<IconData?> icon,
Value<int> rowid,
});
final class $ContainerReferences
extends BaseReferences<_$TabDatabase, Container, ContainerData> {
$ContainerReferences(super.$_db, super.$_table, super.$_typedResult);
static MultiTypedResultKey<Tab, List<TabData>> _tabRefsTable(
_$TabDatabase db) =>
MultiTypedResultKey.fromTable(db.tab,
aliasName: $_aliasNameGenerator(db.container.id, db.tab.containerId));
$TabProcessedTableManager get tabRefs {
final manager = $TabTableManager($_db, $_db.tab)
.filter((f) => f.containerId.id($_item.id));
final cache = $_typedResult.readTableOrNull(_tabRefsTable($_db));
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: cache));
}
}
class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> {
$ContainerFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<Color, Color, int> get color =>
$composableBuilder(
column: $table.color,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnWithTypeConverterFilters<IconData?, IconData, String> get icon =>
$composableBuilder(
column: $table.icon,
builder: (column) => ColumnWithTypeConverterFilters(column));
Expression<bool> tabRefs(Expression<bool> Function($TabFilterComposer f) f) {
final $TabFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.tab,
getReferencedColumn: (t) => t.containerId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TabFilterComposer(
$db: $db,
$table: $db.tab,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $ContainerOrderingComposer extends Composer<_$TabDatabase, Container> {
$ContainerOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get color => $composableBuilder(
column: $table.color, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get icon => $composableBuilder(
column: $table.icon, builder: (column) => ColumnOrderings(column));
}
class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> {
$ContainerAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity, builder: (column) => column);
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumnWithTypeConverter<Color, int> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumnWithTypeConverter<IconData?, String> get icon =>
$composableBuilder(column: $table.icon, builder: (column) => column);
Expression<T> tabRefs<T extends Object>(
Expression<T> Function($TabAnnotationComposer a) f) {
final $TabAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.tab,
getReferencedColumn: (t) => t.containerId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TabAnnotationComposer(
$db: $db,
$table: $db.tab,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $ContainerTableManager extends RootTableManager<
_$TabDatabase,
Container,
ContainerData,
$ContainerFilterComposer,
$ContainerOrderingComposer,
$ContainerAnnotationComposer,
$ContainerCreateCompanionBuilder,
$ContainerUpdateCompanionBuilder,
(ContainerData, $ContainerReferences),
ContainerData,
PrefetchHooks Function({bool tabRefs})> {
$ContainerTableManager(_$TabDatabase db, Container table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$ContainerFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$ContainerOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$ContainerAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> id = const Value.absent(),
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(),
Value<Color> color = const Value.absent(),
Value<IconData?> icon = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ContainerCompanion(
id: id,
contextualIdentity: contextualIdentity,
name: name,
color: color,
icon: icon,
rowid: rowid,
),
createCompanionCallback: ({
required String id,
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(),
required Color color,
Value<IconData?> icon = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ContainerCompanion.insert(
id: id,
contextualIdentity: contextualIdentity,
name: name,
color: color,
icon: icon,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) =>
(e.readTable(table), $ContainerReferences(db, table, e)))
.toList(),
prefetchHooksCallback: ({tabRefs = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [if (tabRefs) db.tab],
addJoins: null,
getPrefetchedDataCallback: (items) async {
return [
if (tabRefs)
await $_getPrefetchedData(
currentTable: table,
referencedTable: $ContainerReferences._tabRefsTable(db),
managerFromTypedResult: (p0) =>
$ContainerReferences(db, table, p0).tabRefs,
referencedItemsForCurrentItem:
(item, referencedItems) => referencedItems
.where((e) => e.containerId == item.id),
typedResults: items)
];
},
);
},
));
}
typedef $ContainerProcessedTableManager = ProcessedTableManager<
_$TabDatabase,
Container,
ContainerData,
$ContainerFilterComposer,
$ContainerOrderingComposer,
$ContainerAnnotationComposer,
$ContainerCreateCompanionBuilder,
$ContainerUpdateCompanionBuilder,
(ContainerData, $ContainerReferences),
ContainerData,
PrefetchHooks Function({bool tabRefs})>;
typedef $TabCreateCompanionBuilder = TabCompanion Function({
required String id,
Value<String?> containerId,
required String orderKey,
required DateTime timestamp,
Value<int> rowid,
});
typedef $TabUpdateCompanionBuilder = TabCompanion Function({
Value<String> id,
Value<String?> containerId,
Value<String> orderKey,
Value<DateTime> timestamp,
Value<int> rowid,
});
final class $TabReferences extends BaseReferences<_$TabDatabase, Tab, TabData> {
$TabReferences(super.$_db, super.$_table, super.$_typedResult);
static Container _containerIdTable(_$TabDatabase db) => db.container
.createAlias($_aliasNameGenerator(db.tab.containerId, db.container.id));
$ContainerProcessedTableManager? get containerId {
if ($_item.containerId == null) return null;
final manager = $ContainerTableManager($_db, $_db.container)
.filter((f) => f.id($_item.containerId!));
final item = $_typedResult.readTableOrNull(_containerIdTable($_db));
if (item == null) return manager;
return ProcessedTableManager(
manager.$state.copyWith(prefetchedData: [item]));
}
}
class $TabFilterComposer extends Composer<_$TabDatabase, Tab> {
$TabFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get orderKey => $composableBuilder(
column: $table.orderKey, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get timestamp => $composableBuilder(
column: $table.timestamp, builder: (column) => ColumnFilters(column));
$ContainerFilterComposer get containerId {
final $ContainerFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: $db.container,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$ContainerFilterComposer(
$db: $db,
$table: $db.container,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabOrderingComposer extends Composer<_$TabDatabase, Tab> {
$TabOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get orderKey => $composableBuilder(
column: $table.orderKey, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get timestamp => $composableBuilder(
column: $table.timestamp, builder: (column) => ColumnOrderings(column));
$ContainerOrderingComposer get containerId {
final $ContainerOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: $db.container,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$ContainerOrderingComposer(
$db: $db,
$table: $db.container,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabAnnotationComposer extends Composer<_$TabDatabase, Tab> {
$TabAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get orderKey =>
$composableBuilder(column: $table.orderKey, builder: (column) => column);
GeneratedColumn<DateTime> get timestamp =>
$composableBuilder(column: $table.timestamp, builder: (column) => column);
$ContainerAnnotationComposer get containerId {
final $ContainerAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.containerId,
referencedTable: $db.container,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$ContainerAnnotationComposer(
$db: $db,
$table: $db.container,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabTableManager extends RootTableManager<
_$TabDatabase,
Tab,
TabData,
$TabFilterComposer,
$TabOrderingComposer,
$TabAnnotationComposer,
$TabCreateCompanionBuilder,
$TabUpdateCompanionBuilder,
(TabData, $TabReferences),
TabData,
PrefetchHooks Function({bool containerId})> {
$TabTableManager(_$TabDatabase db, Tab table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$TabFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$TabOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$TabAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> id = const Value.absent(),
Value<String?> containerId = const Value.absent(),
Value<String> orderKey = const Value.absent(),
Value<DateTime> timestamp = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
TabCompanion(
id: id,
containerId: containerId,
orderKey: orderKey,
timestamp: timestamp,
rowid: rowid,
),
createCompanionCallback: ({
required String id,
Value<String?> containerId = const Value.absent(),
required String orderKey,
required DateTime timestamp,
Value<int> rowid = const Value.absent(),
}) =>
TabCompanion.insert(
id: id,
containerId: containerId,
orderKey: orderKey,
timestamp: timestamp,
rowid: rowid,
),
withReferenceMapper: (p0) => p0
.map((e) => (e.readTable(table), $TabReferences(db, table, e)))
.toList(),
prefetchHooksCallback: ({containerId = false}) {
return PrefetchHooks(
db: db,
explicitlyWatchedTables: [],
addJoins: <
T extends TableManagerState<
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic,
dynamic>>(state) {
if (containerId) {
state = state.withJoin(
currentTable: table,
currentColumn: table.containerId,
referencedTable: $TabReferences._containerIdTable(db),
referencedColumn: $TabReferences._containerIdTable(db).id,
) as T;
}
return state;
},
getPrefetchedDataCallback: (items) async {
return [];
},
);
},
));
}
typedef $TabProcessedTableManager = ProcessedTableManager<
_$TabDatabase,
Tab,
TabData,
$TabFilterComposer,
$TabOrderingComposer,
$TabAnnotationComposer,
$TabCreateCompanionBuilder,
$TabUpdateCompanionBuilder,
(TabData, $TabReferences),
TabData,
PrefetchHooks Function({bool containerId})>;
class $TabDatabaseManager {
final _$TabDatabase _db;
$TabDatabaseManager(this._db);
$ContainerTableManager get container =>
$ContainerTableManager(_db, _db.container);
$TabTableManager get tab => $TabTableManager(_db, _db.tab);
}
@@ -1,13 +1,20 @@
import 'dart:ui';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/widgets.dart';
class TopicData with FastEquatable { class ContainerData with FastEquatable {
final String id; final String id;
final String? contextualIdentity;
final String? name; final String? name;
final Color color; final Color color;
final IconData? icon;
TopicData({required this.id, this.name, required this.color}); ContainerData({
required this.id,
this.contextualIdentity,
this.name,
required this.color,
this.icon,
});
@override @override
bool get cacheHash => true; bool get cacheHash => true;
@@ -15,18 +22,22 @@ class TopicData with FastEquatable {
@override @override
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
id, id,
contextualIdentity,
name, name,
color, color,
icon,
]; ];
} }
class TopicDataWithCount extends TopicData { class ContainerDataWithCount extends ContainerData {
final int? tabCount; final int? tabCount;
TopicDataWithCount({ ContainerDataWithCount({
required super.id, required super.id,
super.contextualIdentity,
super.name, super.name,
required super.color, required super.color,
super.icon,
required this.tabCount, required this.tabCount,
}); });
@@ -1,6 +1,7 @@
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:lensai/features/geckoview/features/topics/data/database/database.dart'; import 'package:lensai/data/database/functions/lexo_rank_functions.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart' as path_provider; import 'package:path_provider/path_provider.dart' as path_provider;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -17,7 +18,7 @@ TabDatabase tabDatabase(TabDatabaseRef ref) {
// put the database file, called db.sqlite here, into the documents folder // put the database file, called db.sqlite here, into the documents folder
// for your app. // for your app.
final dbFolder = await path_provider.getApplicationDocumentsDirectory(); final dbFolder = await path_provider.getApplicationDocumentsDirectory();
final file = File(p.join(dbFolder.path, 'tab.db')); final file = File(p.join(dbFolder.path, 'tab2.db'));
// Also work around limitations on old Android versions // Also work around limitations on old Android versions
if (Platform.isAndroid) { if (Platform.isAndroid) {
@@ -31,7 +32,12 @@ TabDatabase tabDatabase(TabDatabaseRef ref) {
// Explicitly tell it about the correct temporary directory. // Explicitly tell it about the correct temporary directory.
sqlite3.tempDirectory = cachebase; sqlite3.tempDirectory = cachebase;
return NativeDatabase.createInBackground(file); return NativeDatabase.createInBackground(
file,
setup: (database) {
registerLexorankFunctions(database);
},
);
}), }),
); );
} }
@@ -6,7 +6,7 @@ part of 'providers.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabDatabaseHash() => r'b886b089ae5fdb9bdcc0982efe5fcbe9bc66486f'; String _$tabDatabaseHash() => r'567872961683d21156a9af463466f8ddbdd02f1f';
/// See also [tabDatabase]. /// See also [tabDatabase].
@ProviderFor(tabDatabase) @ProviderFor(tabDatabase)
@@ -0,0 +1,50 @@
import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'providers.g.dart';
@Riverpod()
Future<Color> unusedRandomContainerColor(
UnusedRandomContainerColorRef ref,
) async {
final repository = ref.watch(containerRepositoryProvider.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<ContainerDataWithCount>> containersWithCount(
ContainersWithCountRef ref,
) {
final db = ref.watch(tabDatabaseProvider);
return db.containersWithCount().watch();
}
@Riverpod()
Stream<String?> tabContainerId(TabContainerIdRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.tabContainerId(tabId).watchSingle();
}
@Riverpod()
Stream<List<String>> containerTabIds(
ContainerTabIdsRef ref,
String? containerId,
) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.containerTabIds(containerId).watch();
}
@@ -0,0 +1,322 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$unusedRandomContainerColorHash() =>
r'129cefaefa895012a6d7b35da69773e28e3289cc';
/// See also [unusedRandomContainerColor].
@ProviderFor(unusedRandomContainerColor)
final unusedRandomContainerColorProvider =
AutoDisposeFutureProvider<Color>.internal(
unusedRandomContainerColor,
name: r'unusedRandomContainerColorProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$unusedRandomContainerColorHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef UnusedRandomContainerColorRef = AutoDisposeFutureProviderRef<Color>;
String _$containersWithCountHash() =>
r'e1bab28091dd1b9ecfd7d76e6393f459604c803a';
/// See also [containersWithCount].
@ProviderFor(containersWithCount)
final containersWithCountProvider =
AutoDisposeStreamProvider<List<ContainerDataWithCount>>.internal(
containersWithCount,
name: r'containersWithCountProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$containersWithCountHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ContainersWithCountRef
= AutoDisposeStreamProviderRef<List<ContainerDataWithCount>>;
String _$tabContainerIdHash() => r'8adbfdc2ef46219569073fe9e58967f4935a45e5';
/// 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 [tabContainerId].
@ProviderFor(tabContainerId)
const tabContainerIdProvider = TabContainerIdFamily();
/// See also [tabContainerId].
class TabContainerIdFamily extends Family<AsyncValue<String?>> {
/// See also [tabContainerId].
const TabContainerIdFamily();
/// See also [tabContainerId].
TabContainerIdProvider call(
String tabId,
) {
return TabContainerIdProvider(
tabId,
);
}
@override
TabContainerIdProvider getProviderOverride(
covariant TabContainerIdProvider 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'tabContainerIdProvider';
}
/// See also [tabContainerId].
class TabContainerIdProvider extends AutoDisposeStreamProvider<String?> {
/// See also [tabContainerId].
TabContainerIdProvider(
String tabId,
) : this._internal(
(ref) => tabContainerId(
ref as TabContainerIdRef,
tabId,
),
from: tabContainerIdProvider,
name: r'tabContainerIdProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabContainerIdHash,
dependencies: TabContainerIdFamily._dependencies,
allTransitiveDependencies:
TabContainerIdFamily._allTransitiveDependencies,
tabId: tabId,
);
TabContainerIdProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<String?> Function(TabContainerIdRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TabContainerIdProvider._internal(
(ref) => create(ref as TabContainerIdRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<String?> createElement() {
return _TabContainerIdProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabContainerIdProvider && 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 TabContainerIdRef on AutoDisposeStreamProviderRef<String?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabContainerIdProviderElement
extends AutoDisposeStreamProviderElement<String?> with TabContainerIdRef {
_TabContainerIdProviderElement(super.provider);
@override
String get tabId => (origin as TabContainerIdProvider).tabId;
}
String _$containerTabIdsHash() => r'79e9bccfb17cf3ce802638c8b806c5a76fa8f732';
/// See also [containerTabIds].
@ProviderFor(containerTabIds)
const containerTabIdsProvider = ContainerTabIdsFamily();
/// See also [containerTabIds].
class ContainerTabIdsFamily extends Family<AsyncValue<List<String>>> {
/// See also [containerTabIds].
const ContainerTabIdsFamily();
/// See also [containerTabIds].
ContainerTabIdsProvider call(
String? containerId,
) {
return ContainerTabIdsProvider(
containerId,
);
}
@override
ContainerTabIdsProvider getProviderOverride(
covariant ContainerTabIdsProvider provider,
) {
return call(
provider.containerId,
);
}
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'containerTabIdsProvider';
}
/// See also [containerTabIds].
class ContainerTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
/// See also [containerTabIds].
ContainerTabIdsProvider(
String? containerId,
) : this._internal(
(ref) => containerTabIds(
ref as ContainerTabIdsRef,
containerId,
),
from: containerTabIdsProvider,
name: r'containerTabIdsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$containerTabIdsHash,
dependencies: ContainerTabIdsFamily._dependencies,
allTransitiveDependencies:
ContainerTabIdsFamily._allTransitiveDependencies,
containerId: containerId,
);
ContainerTabIdsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
}) : super.internal();
final String? containerId;
@override
Override overrideWith(
Stream<List<String>> Function(ContainerTabIdsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: ContainerTabIdsProvider._internal(
(ref) => create(ref as ContainerTabIdsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
),
);
}
@override
AutoDisposeStreamProviderElement<List<String>> createElement() {
return _ContainerTabIdsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ContainerTabIdsProvider && other.containerId == containerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin ContainerTabIdsRef on AutoDisposeStreamProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
}
class _ContainerTabIdsProviderElement
extends AutoDisposeStreamProviderElement<List<String>>
with ContainerTabIdsRef {
_ContainerTabIdsProviderElement(super.provider);
@override
String? get containerId => (origin as ContainerTabIdsProvider).containerId;
}
// 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,43 @@
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'selected_container.g.dart';
@Riverpod(keepAlive: true)
class SelectedContainer extends _$SelectedContainer {
void setContainerId(String id) {
state = id;
}
void toggleContainer(String id) {
if (state == id) {
clearContainer();
} else {
setContainerId(id);
}
}
void clearContainer() {
state = null;
}
@override
String? build() {
return null;
}
}
@Riverpod()
Stream<ContainerData?> selectedContainerData(SelectedContainerDataRef ref) {
final db = ref.watch(tabDatabaseProvider);
final selectedContainer = ref.watch(selectedContainerProvider);
if (selectedContainer != null) {
return db.containerDao
.getContainerData(selectedContainer)
.watchSingleOrNull();
}
return Stream.value(null);
}
@@ -0,0 +1,43 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'selected_container.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$selectedContainerDataHash() =>
r'dc930d1bb47b3c7989c6c119b0459f37d687af84';
/// See also [selectedContainerData].
@ProviderFor(selectedContainerData)
final selectedContainerDataProvider =
AutoDisposeStreamProvider<ContainerData?>.internal(
selectedContainerData,
name: r'selectedContainerDataProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedContainerDataHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf';
/// See also [SelectedContainer].
@ProviderFor(SelectedContainer)
final selectedContainerProvider =
NotifierProvider<SelectedContainer, String?>.internal(
SelectedContainer.new,
name: r'selectedContainerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$selectedContainerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SelectedContainer = 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,54 @@
import 'dart:ui';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'container.g.dart';
@Riverpod(keepAlive: true)
class ContainerRepository extends _$ContainerRepository {
late TabDatabase _db;
@override
void build() {
_db = ref.watch(tabDatabaseProvider);
}
Future<void> addContainer({required String? name, required Color color}) {
return _db.containerDao.addContainer(name: name, color: color);
}
Future<void> replaceContainer({
required String id,
required String? name,
required Color color,
}) {
return _db.containerDao.replaceContainer(id, name: name, color: color);
}
Future<void> deleteContainer(String id) {
return _db.containerDao.deleteContainer(id);
}
Future<Set<Color>> getDistinctColors() {
return _db.containerDao
.getDistinctColors()
.get()
.then((colors) => colors.toSet());
}
Future<String> getLeadingOrderKey(String? containerId) {
return _db.containerDao.generateLeadingOrderKey(containerId).getSingle();
}
Future<String> getTrailingOrderKey(String? containerId) {
return _db.containerDao.generateTrailingOrderKey(containerId).getSingle();
}
Future<String> getOrderKeyAfterTab(String tabId, String? containerId) {
return _db.containerDao
.generateOrderKeyAfterTabId(containerId, tabId)
.getSingle();
}
}
@@ -1,26 +1,27 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_link.dart'; part of 'container.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabLinkRepositoryHash() => r'cdc23ca6ea215925f39842b16f9abcc289b0efbb'; String _$containerRepositoryHash() =>
r'89ad15861fb213d6a3fa51232fd0ff9943f51e92';
/// See also [TabLinkRepository]. /// See also [ContainerRepository].
@ProviderFor(TabLinkRepository) @ProviderFor(ContainerRepository)
final tabLinkRepositoryProvider = final containerRepositoryProvider =
NotifierProvider<TabLinkRepository, void>.internal( NotifierProvider<ContainerRepository, void>.internal(
TabLinkRepository.new, ContainerRepository.new,
name: r'tabLinkRepositoryProvider', name: r'containerRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$tabLinkRepositoryHash, : _$containerRepositoryHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$TabLinkRepository = Notifier<void>; typedef _$ContainerRepository = Notifier<void>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,41 @@
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab.g.dart';
@Riverpod(keepAlive: true)
class TabDataRepository extends _$TabDataRepository {
late TabDatabase _db;
@override
void build() {
_db = ref.watch(tabDatabaseProvider);
}
Future<void> assignContainer(String tabId, String? containerId) {
return _db.tabDao.assignContainer(
tabId,
containerId: containerId,
);
}
Future<void> assignOrderKey(String tabId, String orderKey) {
return _db.tabDao.assignOrderKey(
tabId,
orderKey: orderKey,
);
}
Future<void> closeAllTabs(String? containerId) async {
final tabIds = await _db.tabDao.containerTabIds(containerId).get();
if (tabIds.isNotEmpty) {
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
}
}
Future<String?> containerTabId(String tabId) {
return _db.tabDao.tabContainerId(tabId).getSingle();
}
}
@@ -1,26 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'topic.dart'; part of 'tab.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$topicRepositoryHash() => r'6c456c17af4ea7d7ef1747c92d0442459d061080'; String _$tabDataRepositoryHash() => r'c067bbbf319cd95ad869655740fdf49d13159b94';
/// See also [TopicRepository]. /// See also [TabDataRepository].
@ProviderFor(TopicRepository) @ProviderFor(TabDataRepository)
final topicRepositoryProvider = final tabDataRepositoryProvider =
NotifierProvider<TopicRepository, void>.internal( NotifierProvider<TabDataRepository, void>.internal(
TopicRepository.new, TabDataRepository.new,
name: r'topicRepositoryProvider', name: r'tabDataRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$topicRepositoryHash, : _$tabDataRepositoryHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$TopicRepository = Notifier<void>; typedef _$TabDataRepository = Notifier<void>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -2,23 +2,23 @@ import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/topics/domain/repositories/topic.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_dialog.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
class _TopicTile extends HookWidget { class _ContainerTile extends HookWidget {
final TopicData topic; final ContainerData container;
final bool isSelected; final bool isSelected;
final void Function(TopicResult edited) onEdit; final void Function(ContainerResult edited) onEdit;
final void Function() onDelete; final void Function() onDelete;
final void Function() onTap; final void Function() onTap;
const _TopicTile( const _ContainerTile(
this.topic, { this.container, {
required this.isSelected, required this.isSelected,
required this.onEdit, required this.onEdit,
required this.onDelete, required this.onDelete,
@@ -32,8 +32,8 @@ class _TopicTile extends HookWidget {
return ListTile( return ListTile(
selected: isSelected, selected: isSelected,
leading: CircleAvatar(backgroundColor: topic.color), leading: CircleAvatar(backgroundColor: container.color),
title: Text(topic.name ?? 'New Topic'), title: Text(container.name ?? 'New Container'),
trailing: MenuAnchor( trailing: MenuAnchor(
controller: menuController, controller: menuController,
builder: (context, controller, child) { builder: (context, controller, child) {
@@ -57,11 +57,11 @@ class _TopicTile extends HookWidget {
menuChildren: [ menuChildren: [
MenuItemButton( MenuItemButton(
onPressed: () async { onPressed: () async {
final result = await showDialog<TopicResult?>( final result = await showDialog<ContainerResult?>(
context: context, context: context,
builder: (context) => TopicDialog.edit( builder: (context) => ContainerDialog.edit(
name: topic.name, name: container.name,
initialColor: topic.color, initialColor: container.color,
), ),
); );
@@ -78,9 +78,9 @@ class _TopicTile extends HookWidget {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Delete Topic'), title: const Text('Delete Container'),
content: const Text( content: const Text(
'Are you sure you want to delete this topic and all attached tabs?', 'Are you sure you want to delete this container and close all attached tabs?',
), ),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
@@ -114,32 +114,32 @@ class _TopicTile extends HookWidget {
} }
} }
class TopicListScreen extends HookConsumerWidget { class ContainerListScreen extends HookConsumerWidget {
const TopicListScreen(); const ContainerListScreen();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Topics'), title: const Text('Containers'),
actions: [ actions: [
IconButton( IconButton(
onPressed: () async { onPressed: () async {
final initialColor = final initialColor =
await ref.read(unusedRandomTopicColorProvider.future); await ref.read(unusedRandomContainerColorProvider.future);
if (context.mounted) { if (context.mounted) {
final result = await showDialog<TopicResult?>( final result = await showDialog<ContainerResult?>(
context: context, context: context,
builder: (context) => TopicDialog.create( builder: (context) => ContainerDialog.create(
initialColor: initialColor, initialColor: initialColor,
), ),
); );
if (result != null) { if (result != null) {
await ref await ref
.read(topicRepositoryProvider.notifier) .read(containerRepositoryProvider.notifier)
.addTopic(name: result.name, color: result.color); .addContainer(name: result.name, color: result.color);
} }
} }
}, },
@@ -149,42 +149,42 @@ class TopicListScreen extends HookConsumerWidget {
), ),
body: HookConsumer( body: HookConsumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final topicsAsync = ref.watch(topicsWithCountProvider); final containersAsync = ref.watch(containersWithCountProvider);
final selectedTopic = ref.watch(selectedTopicProvider); final selectedContainer = ref.watch(selectedContainerProvider);
return Skeletonizer( return Skeletonizer(
enabled: topicsAsync.isLoading, enabled: containersAsync.isLoading,
child: topicsAsync.when( child: containersAsync.when(
data: (topics) => FadingScroll( data: (containers) => FadingScroll(
fadingSize: 25, fadingSize: 25,
builder: (context, controller) { builder: (context, controller) {
return ListView.builder( return ListView.builder(
controller: controller, controller: controller,
itemCount: topics.length, itemCount: containers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final topic = topics[index]; final container = containers[index];
return _TopicTile( return _ContainerTile(
topic, container,
key: ValueKey(topic.id), key: ValueKey(container.id),
isSelected: topic.id == selectedTopic, isSelected: container.id == selectedContainer,
onEdit: (edited) async { onEdit: (edited) async {
await ref await ref
.read(topicRepositoryProvider.notifier) .read(containerRepositoryProvider.notifier)
.replaceTopic( .replaceContainer(
id: topic.id, id: container.id,
name: edited.name, name: edited.name,
color: edited.color, color: edited.color,
); );
}, },
onDelete: () async { onDelete: () async {
await ref await ref
.read(topicRepositoryProvider.notifier) .read(containerRepositoryProvider.notifier)
.deleteTopic(topic.id); .deleteContainer(container.id);
}, },
onTap: () { onTap: () {
ref ref
.read(selectedTopicProvider.notifier) .read(selectedContainerProvider.notifier)
.toggleTopic(topic.id); .toggleContainer(container.id);
}, },
); );
}, },
@@ -194,8 +194,8 @@ class TopicListScreen extends HookConsumerWidget {
error: (error, stackTrace) => SizedBox.shrink(), error: (error, stackTrace) => SizedBox.shrink(),
loading: () => ListView.builder( loading: () => ListView.builder(
itemCount: 3, itemCount: 3,
itemBuilder: (context, index) => _TopicTile( itemBuilder: (context, index) => _ContainerTile(
TopicData(id: 'null', color: Colors.transparent), ContainerData(id: 'null', color: Colors.transparent),
isSelected: false, isSelected: false,
onEdit: (_) {}, onEdit: (_) {},
onDelete: () {}, onDelete: () {},
@@ -0,0 +1,79 @@
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/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart';
class ContainerChips extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final containersAsync = ref.watch(containersWithCountProvider);
final selectedContainer = ref.watch(
selectedContainerDataProvider.select((value) => value.valueOrNull),
);
return containersAsync.when(
data: (availableContainers) => SizedBox(
height: 48,
child: Row(
children: [
const SizedBox(width: 16),
if (selectedContainer != null || availableContainers.isNotEmpty)
Expanded(
child: SelectableChips(
deleteIcon: false,
itemId: (container) => container.id,
itemAvatar: (container) => Container(
width: 20.0,
height: 20.0,
decoration: BoxDecoration(
color: container.color,
shape: BoxShape.circle,
),
),
itemLabel: (container) =>
Text(container.name ?? 'New Container'),
itemBadgeCount: (container) => container.tabCount,
availableItems: availableContainers,
selectedItem: selectedContainer,
onSelected: (container) {
ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
},
onDeleted: (container) async {
ref
.read(selectedContainerProvider.notifier)
.clearContainer();
},
),
)
else
Expanded(
child: Text(
"Press '>' to manage Containers.",
style: TextStyle(
color: Theme.of(context).hintColor,
fontStyle: FontStyle.italic,
),
),
),
IconButton(
onPressed: () async {
await context.push(ContainerListRoute().location);
},
icon: const Icon(Icons.chevron_right),
),
],
),
),
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox(
height: 48,
width: double.infinity,
),
);
}
}
@@ -1,35 +1,35 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:lensai/features/geckoview/features/topics/presentation/widgets/material_color_picker.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart';
typedef TopicResult = ({String? name, Color color}); typedef ContainerResult = ({String? name, Color color});
enum _DialogMode { create, edit } enum _DialogMode { create, edit }
class TopicDialog extends HookWidget { class ContainerDialog extends HookWidget {
final _DialogMode _mode; final _DialogMode _mode;
final String? initialName; final String? initialName;
final Color initialColor; final Color initialColor;
const TopicDialog._({ const ContainerDialog._({
required _DialogMode mode, required _DialogMode mode,
required this.initialColor, required this.initialColor,
this.initialName, this.initialName,
}) : _mode = mode; }) : _mode = mode;
factory TopicDialog.create({required Color initialColor}) { factory ContainerDialog.create({required Color initialColor}) {
return TopicDialog._( return ContainerDialog._(
mode: _DialogMode.create, mode: _DialogMode.create,
initialColor: initialColor, initialColor: initialColor,
); );
} }
factory TopicDialog.edit({ factory ContainerDialog.edit({
required String? name, required String? name,
required Color initialColor, required Color initialColor,
}) { }) {
return TopicDialog._( return ContainerDialog._(
mode: _DialogMode.edit, mode: _DialogMode.edit,
initialColor: initialColor, initialColor: initialColor,
initialName: name, initialName: name,
@@ -54,8 +54,8 @@ class TopicDialog extends HookWidget {
), ),
title: Text( title: Text(
switch (_mode) { switch (_mode) {
_DialogMode.create => 'New Topic', _DialogMode.create => 'New Container',
_DialogMode.edit => 'Edit Topic', _DialogMode.edit => 'Edit Container',
}, },
), ),
children: [ children: [
@@ -94,14 +94,14 @@ class TopicDialog extends HookWidget {
children: [ children: [
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop<TopicResult?>(context); Navigator.pop<ContainerResult?>(context);
}, },
child: const Text('Cancel'), child: const Text('Cancel'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
final name = textController.text.trim(); final name = textController.text.trim();
Navigator.pop<TopicResult?>( Navigator.pop<ContainerResult?>(
context, context,
( (
name: name.isNotEmpty ? name : null, name: name.isNotEmpty ? name : null,
@@ -2,7 +2,7 @@
import 'package:fading_scroll/fading_scroll.dart'; import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lensai/features/geckoview/features/topics/utils/color_palette.dart'; import 'package:lensai/features/geckoview/features/tabs/utils/color_palette.dart';
class MaterialPicker extends StatefulWidget { class MaterialPicker extends StatefulWidget {
const MaterialPicker({ const MaterialPicker({
@@ -1,63 +0,0 @@
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(db.tabLink.topicId.equals(topicId))
..orderBy([OrderingTerm.asc(db.tabLink.id)]);
return query.map((row) => row.read(db.tabLink.id)!);
}
Selectable<String> allTabIds() {
final query = selectOnly(db.tabLink)
..addColumns([db.tabLink.id])
..orderBy([OrderingTerm.asc(db.tabLink.id)]);
return query.map((row) => row.read(db.tabLink.id)!);
}
SingleOrNullSelectable<String> tabTopicId(String tabId) {
final query = selectOnly(db.tabLink)
..addColumns([db.tabLink.topicId])
..where(db.tabLink.id.equals(tabId));
return query.map((row) => row.read(db.tabLink.topicId)!);
}
Future<void> upsertTabLink(
String tabId, {
required DateTime timestamp,
required String topicId,
}) {
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();
}
}
@@ -1,54 +0,0 @@
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)!);
}
}
@@ -1,6 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'topic.dart';
// ignore_for_file: type=lint
mixin _$TopicDaoMixin on DatabaseAccessor<TabDatabase> {}
@@ -1,30 +0,0 @@
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 NOT NULL 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;
@@ -1,826 +0,0 @@
// 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, false,
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'NOT NULL 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, required this.topicId, required this.timestamp});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['id'] = Variable<String>(id);
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, String? topicId, DateTime? timestamp}) =>
TabLinkData(
id: id ?? this.id,
topicId: topicId ?? 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,
required String topicId,
required DateTime timestamp,
this.rowid = const Value.absent(),
}) : id = Value(id),
topicId = Value(topicId),
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 Composer<_$TabDatabase, Topic> {
$TopicFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnFilters(column));
ColumnWithTypeConverterFilters<Color, Color, int> get color =>
$composableBuilder(
column: $table.color,
builder: (column) => ColumnWithTypeConverterFilters(column));
Expression<bool> tabLinkRefs(
Expression<bool> Function($TabLinkFilterComposer f) f) {
final $TabLinkFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.tabLink,
getReferencedColumn: (t) => t.topicId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TabLinkFilterComposer(
$db: $db,
$table: $db.tabLink,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $TopicOrderingComposer extends Composer<_$TabDatabase, Topic> {
$TopicOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get color => $composableBuilder(
column: $table.color, builder: (column) => ColumnOrderings(column));
}
class $TopicAnnotationComposer extends Composer<_$TabDatabase, Topic> {
$TopicAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumnWithTypeConverter<Color, int> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
Expression<T> tabLinkRefs<T extends Object>(
Expression<T> Function($TabLinkAnnotationComposer a) f) {
final $TabLinkAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.id,
referencedTable: $db.tabLink,
getReferencedColumn: (t) => t.topicId,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TabLinkAnnotationComposer(
$db: $db,
$table: $db.tabLink,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return f(composer);
}
}
class $TopicTableManager extends RootTableManager<
_$TabDatabase,
Topic,
TopicData,
$TopicFilterComposer,
$TopicOrderingComposer,
$TopicAnnotationComposer,
$TopicCreateCompanionBuilder,
$TopicUpdateCompanionBuilder,
(TopicData, $TopicReferences),
TopicData,
PrefetchHooks Function({bool tabLinkRefs})> {
$TopicTableManager(_$TabDatabase db, Topic table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$TopicFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$TopicOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$TopicAnnotationComposer($db: db, $table: 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,
$TopicAnnotationComposer,
$TopicCreateCompanionBuilder,
$TopicUpdateCompanionBuilder,
(TopicData, $TopicReferences),
TopicData,
PrefetchHooks Function({bool tabLinkRefs})>;
typedef $TabLinkCreateCompanionBuilder = TabLinkCompanion Function({
required String id,
required 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 Composer<_$TabDatabase, TabLink> {
$TabLinkFilterComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<DateTime> get timestamp => $composableBuilder(
column: $table.timestamp, builder: (column) => ColumnFilters(column));
$TopicFilterComposer get topicId {
final $TopicFilterComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.topicId,
referencedTable: $db.topic,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TopicFilterComposer(
$db: $db,
$table: $db.topic,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabLinkOrderingComposer extends Composer<_$TabDatabase, TabLink> {
$TabLinkOrderingComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<DateTime> get timestamp => $composableBuilder(
column: $table.timestamp, builder: (column) => ColumnOrderings(column));
$TopicOrderingComposer get topicId {
final $TopicOrderingComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.topicId,
referencedTable: $db.topic,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TopicOrderingComposer(
$db: $db,
$table: $db.topic,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabLinkAnnotationComposer extends Composer<_$TabDatabase, TabLink> {
$TabLinkAnnotationComposer({
required super.$db,
required super.$table,
super.joinBuilder,
super.$addJoinBuilderToRootComposer,
super.$removeJoinBuilderFromRootComposer,
});
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<DateTime> get timestamp =>
$composableBuilder(column: $table.timestamp, builder: (column) => column);
$TopicAnnotationComposer get topicId {
final $TopicAnnotationComposer composer = $composerBuilder(
composer: this,
getCurrentColumn: (t) => t.topicId,
referencedTable: $db.topic,
getReferencedColumn: (t) => t.id,
builder: (joinBuilder,
{$addJoinBuilderToRootComposer,
$removeJoinBuilderFromRootComposer}) =>
$TopicAnnotationComposer(
$db: $db,
$table: $db.topic,
$addJoinBuilderToRootComposer: $addJoinBuilderToRootComposer,
joinBuilder: joinBuilder,
$removeJoinBuilderFromRootComposer:
$removeJoinBuilderFromRootComposer,
));
return composer;
}
}
class $TabLinkTableManager extends RootTableManager<
_$TabDatabase,
TabLink,
TabLinkData,
$TabLinkFilterComposer,
$TabLinkOrderingComposer,
$TabLinkAnnotationComposer,
$TabLinkCreateCompanionBuilder,
$TabLinkUpdateCompanionBuilder,
(TabLinkData, $TabLinkReferences),
TabLinkData,
PrefetchHooks Function({bool topicId})> {
$TabLinkTableManager(_$TabDatabase db, TabLink table)
: super(TableManagerState(
db: db,
table: table,
createFilteringComposer: () =>
$TabLinkFilterComposer($db: db, $table: table),
createOrderingComposer: () =>
$TabLinkOrderingComposer($db: db, $table: table),
createComputedFieldComposer: () =>
$TabLinkAnnotationComposer($db: db, $table: 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,
required String topicId,
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,
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,
$TabLinkAnnotationComposer,
$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);
}
@@ -1,43 +0,0 @@
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()
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();
}
@Riverpod()
Stream<String?> tabTopicId(TabTopicIdRef ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabLinkDao.tabTopicId(tabId).watchSingleOrNull();
}
@@ -1,320 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'providers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
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 _$tabTopicIdHash() => r'284164ac3acd9be3ba4e15eba99fc8d97602ee84';
/// See also [tabTopicId].
@ProviderFor(tabTopicId)
const tabTopicIdProvider = TabTopicIdFamily();
/// See also [tabTopicId].
class TabTopicIdFamily extends Family<AsyncValue<String?>> {
/// See also [tabTopicId].
const TabTopicIdFamily();
/// See also [tabTopicId].
TabTopicIdProvider call(
String tabId,
) {
return TabTopicIdProvider(
tabId,
);
}
@override
TabTopicIdProvider getProviderOverride(
covariant TabTopicIdProvider 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'tabTopicIdProvider';
}
/// See also [tabTopicId].
class TabTopicIdProvider extends AutoDisposeStreamProvider<String?> {
/// See also [tabTopicId].
TabTopicIdProvider(
String tabId,
) : this._internal(
(ref) => tabTopicId(
ref as TabTopicIdRef,
tabId,
),
from: tabTopicIdProvider,
name: r'tabTopicIdProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabTopicIdHash,
dependencies: TabTopicIdFamily._dependencies,
allTransitiveDependencies:
TabTopicIdFamily._allTransitiveDependencies,
tabId: tabId,
);
TabTopicIdProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<String?> Function(TabTopicIdRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TabTopicIdProvider._internal(
(ref) => create(ref as TabTopicIdRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<String?> createElement() {
return _TabTopicIdProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabTopicIdProvider && 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 TabTopicIdRef on AutoDisposeStreamProviderRef<String?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabTopicIdProviderElement
extends AutoDisposeStreamProviderElement<String?> with TabTopicIdRef {
_TabTopicIdProviderElement(super.provider);
@override
String get tabId => (origin as TabTopicIdProvider).tabId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -1,41 +0,0 @@
import 'package:lensai/features/geckoview/features/topics/data/models/topic_data.dart';
import 'package:lensai/features/geckoview/features/topics/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'selected_topic.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);
}
@@ -1,27 +0,0 @@
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;
}
}
@@ -1,27 +0,0 @@
// 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
@@ -1,42 +0,0 @@
import 'package:lensai/features/geckoview/domain/providers/tab_state.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);
}
Future<void> assignTab(String tabId, String topicId) {
return _db.tabLinkDao.upsertTabLink(
tabId,
timestamp: DateTime.now(),
topicId: topicId,
);
}
Future<void> closeAllTabs(String? topicId) async {
final List<String> tabIds;
if (topicId != null) {
tabIds = await _db.tabLinkDao.topicTabIds(topicId).get();
} else {
final openTabs = ref.read(tabStatesProvider).keys.toSet();
final assignedTabIds = await _db.tabLinkDao.allTabIds().get();
tabIds =
openTabs.where((tabId) => !assignedTabIds.contains(tabId)).toList();
}
if (tabIds.isNotEmpty) {
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
}
}
}
@@ -1,40 +0,0 @@
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());
}
}
@@ -1,73 +0,0 @@
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/providers/selected_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(topicsWithCountProvider);
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,
),
);
}
}
@@ -1 +1 @@
/home/fafre/.pub-cache/hosted/pub.dev/file_picker-8.1.2/ /home/fafre/.pub-cache/hosted/pub.dev/file_picker-8.1.3/
@@ -1 +1 @@
/home/fafre/.pub-cache/hosted/pub.dev/package_info_plus-8.0.3/ /home/fafre/.pub-cache/hosted/pub.dev/package_info_plus-8.1.0/
@@ -1 +1 @@
/home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.0.3/ /home/fafre/.pub-cache/hosted/pub.dev/share_plus-10.1.0/
@@ -1 +1 @@
/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.24/ /home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.25/
+53 -28
View File
@@ -202,10 +202,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: convert name: convert
sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
copy_with_extension: copy_with_extension:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -234,10 +234,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: crypto name: crypto
sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.5" version: "3.0.6"
csslib: csslib:
dependency: transitive dependency: transitive
description: description:
@@ -378,18 +378,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "167bb619cdddaa10ef2907609feb8a79c16dfa479d3afaf960f8e223f754bf12" sha256: aac85f20436608e01a6ffd1fdd4e746a7f33c93a2c83752e626bdfaea139b877
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.1.2" version: "8.1.3"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
name: fixnum name: fixnum
sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.0" version: "1.1.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -696,18 +696,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: http_parser name: http_parser
sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.1"
image: image:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.2.0" version: "4.3.0"
intl: intl:
dependency: transitive dependency: transitive
description: description:
@@ -772,6 +772,15 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.1" version: "3.0.1"
lexo_rank:
dependency: "direct main"
description:
path: "."
ref: HEAD
resolved-ref: "2922c07bc9d3865cc5566efe1976ce0955029512"
url: "https://github.com/FaFre/lexo_rank.git"
source: git
version: "0.1.0"
lint: lint:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -792,10 +801,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: logging name: logging
sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.2.0" version: "1.3.0"
macros: macros:
dependency: transitive dependency: transitive
description: description:
@@ -856,10 +865,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: "894f37107424311bdae3e476552229476777b8752c5a2a2369c0cb9a2d5442ef" sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.3" version: "8.1.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -936,10 +945,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.5" version: "3.1.6"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -980,6 +989,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.1.0" version: "4.1.0"
reorderable_grid:
dependency: "direct main"
description:
name: reorderable_grid
sha256: "0b9cd95ef0f070ef99f92affe9cf85a4aa127099cd1334e5940950ce58cd981d"
url: "https://pub.dev"
source: hosted
version: "1.0.10"
riverpod: riverpod:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1032,10 +1049,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: fec12c3c39f01e4df1ec6ad92b6e85503c5ca64ffd6e28d18c9ffe53fcc4cb11 sha256: "334fcdf0ef9c0df0e3b428faebcac9568f35c747d59831474b2fc56e156d244e"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.0.3" version: "10.1.0"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -1180,10 +1197,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: sqlite3_flutter_libs name: sqlite3_flutter_libs
sha256: "62bbb4073edbcdf53f40c80775f33eea01d301b7b81417e5b3fb7395416258c1" sha256: ccd29dd6cf6fb9351fa07cd6f92895809adbf0779c1d986acf5e3d53b3250e33
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.5.24" version: "0.5.25"
sqlparser: sqlparser:
dependency: transitive dependency: transitive
description: description:
@@ -1232,6 +1249,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.0" version: "1.3.0"
synchronized:
dependency: "direct main"
description:
name: synchronized
sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225"
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@@ -1276,10 +1301,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: typed_data name: typed_data
sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" version: "1.4.0"
universal_io: universal_io:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1355,10 +1380,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.2" version: "3.1.3"
uuid: uuid:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1419,10 +1444,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd sha256: "2735daae5150e8b1dfeb3eb0544b4d3af0061e9e82cef063adcd583bdae4306a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.6.0" version: "5.7.0"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
+10 -5
View File
@@ -10,14 +10,14 @@ dependencies:
animations: ^2.0.11 animations: ^2.0.11
collection: ^1.19.0 collection: ^1.19.0
copy_with_extension: ^5.0.4 copy_with_extension: ^5.0.4
crypto: ^3.0.5 crypto: ^3.0.6
drift: ^2.21.0 drift: ^2.21.0
dynamic_color: ^1.7.0 dynamic_color: ^1.7.0
exceptions: ^0.6.1 exceptions: ^0.6.1
expandable_page_view: ^1.0.17 expandable_page_view: ^1.0.17
fading_scroll: ^0.9.1 fading_scroll: ^0.9.1
fast_equatable: ^1.1.0 fast_equatable: ^1.1.0
file_picker: ^8.1.2 file_picker: ^8.1.3
flutter: flutter:
sdk: flutter sdk: flutter
flutter_hooks: ^0.20.5 flutter_hooks: ^0.20.5
@@ -36,22 +36,27 @@ dependencies:
html: ^0.15.4 html: ^0.15.4
http: ^1.2.2 http: ^1.2.2
json_annotation: ^4.9.0 json_annotation: ^4.9.0
lexo_rank:
git:
url: https://github.com/FaFre/lexo_rank.git
logger: ^2.4.0 logger: ^2.4.0
markdown: ^7.2.2 markdown: ^7.2.2
mime: ^2.0.0 mime: ^2.0.0
package_info_plus: ^8.0.3 package_info_plus: ^8.1.0
path: ^1.9.0 path: ^1.9.0
path_provider: ^2.1.4 path_provider: ^2.1.4
reorderable_grid: ^1.0.10
riverpod: ^2.5.3 riverpod: ^2.5.3
riverpod_annotation: ^2.5.3 riverpod_annotation: ^2.5.3
rxdart: ^0.28.0 rxdart: ^0.28.0
share_plus: ^10.0.3 share_plus: ^10.1.0
shared_preferences: ^2.3.2 shared_preferences: ^2.3.2
skeletonizer: ^1.4.2 skeletonizer: ^1.4.2
speech_to_text_google_dialog: speech_to_text_google_dialog:
path: ../../speech_to_text_google_dialog path: ../../speech_to_text_google_dialog
sqlite3: ^2.4.6 sqlite3: ^2.4.6
sqlite3_flutter_libs: ^0.5.24 sqlite3_flutter_libs: ^0.5.25
synchronized: ^3.3.0+3
text_scroll: ^0.2.0 text_scroll: ^0.2.0
timeago: ^3.7.0 timeago: ^3.7.0
universal_io: ^2.2.2 universal_io: ^2.2.2
@@ -3,6 +3,8 @@ version = "1.0-SNAPSHOT"
buildscript { buildscript {
ext.kotlin_version = "1.9.22" ext.kotlin_version = "1.9.22"
ext.mozillaComponentsVersion = '131.0.3'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
@@ -56,37 +58,36 @@ android {
} }
dependencies { dependencies {
implementation 'org.mozilla.components:support-utils:131.0b1' implementation "org.mozilla.components:support-utils:$mozillaComponentsVersion"
implementation 'org.mozilla.components:support-ktx:131.0b1' implementation "org.mozilla.components:support-ktx:$mozillaComponentsVersion"
implementation 'org.mozilla.components:support-webextensions:131.0b1' implementation "org.mozilla.components:support-webextensions:$mozillaComponentsVersion"
implementation 'org.mozilla.components:support-locale:131.0b1' implementation "org.mozilla.components:support-locale:$mozillaComponentsVersion"
implementation 'org.mozilla.components:lib-fetch-httpurlconnection:131.0b1' implementation "org.mozilla.components:lib-fetch-httpurlconnection:$mozillaComponentsVersion"
implementation 'org.mozilla.components:lib-crash:131.0b1' implementation "org.mozilla.components:lib-crash:$mozillaComponentsVersion"
implementation 'org.mozilla.components:lib-publicsuffixlist:131.0b1' implementation "org.mozilla.components:lib-publicsuffixlist:$mozillaComponentsVersion"
implementation 'org.mozilla.components:lib-dataprotect:131.0b1' implementation "org.mozilla.components:lib-dataprotect:$mozillaComponentsVersion"
implementation 'org.mozilla.components:concept-engine:131.0b1' implementation "org.mozilla.components:concept-engine:$mozillaComponentsVersion"
implementation 'org.mozilla.components:concept-fetch:131.0b1' implementation "org.mozilla.components:concept-fetch:$mozillaComponentsVersion"
implementation 'org.mozilla.components:browser-engine-gecko:131.0b1' implementation "org.mozilla.components:browser-engine-gecko:$mozillaComponentsVersion"
implementation 'org.mozilla.components:browser-state:131.0b1' implementation "org.mozilla.components:browser-state:$mozillaComponentsVersion"
implementation 'org.mozilla.components:browser-session-storage:131.0b1' implementation "org.mozilla.components:browser-session-storage:$mozillaComponentsVersion"
implementation 'org.mozilla.components:browser-icons:131.0b1' implementation "org.mozilla.components:browser-icons:$mozillaComponentsVersion"
//implementation 'org.mozilla.components:browser-storage-sync:131.0b1' implementation "org.mozilla.components:browser-thumbnails:$mozillaComponentsVersion"
implementation 'org.mozilla.components:browser-thumbnails:131.0b1' implementation "org.mozilla.components:feature-addons:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-addons:131.0b1' implementation "org.mozilla.components:feature-app-links:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-app-links:131.0b1' implementation "org.mozilla.components:feature-autofill:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-autofill:131.0b1' implementation "org.mozilla.components:feature-downloads:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-downloads:131.0b1' implementation "org.mozilla.components:feature-media:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-media:131.0b1' implementation "org.mozilla.components:feature-tabs:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-tabs:131.0b1' implementation "org.mozilla.components:feature-prompts:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-prompts:131.0b1' implementation "org.mozilla.components:feature-session:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-session:131.0b1' implementation "org.mozilla.components:feature-readerview:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-readerview:131.0b1' implementation "org.mozilla.components:feature-privatemode:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-privatemode:131.0b1' implementation "org.mozilla.components:feature-sitepermissions:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-sitepermissions:131.0b1' implementation "org.mozilla.components:feature-webcompat:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-webcompat:131.0b1' implementation "org.mozilla.components:feature-webnotifications:$mozillaComponentsVersion"
implementation 'org.mozilla.components:feature-webnotifications:131.0b1' implementation "org.mozilla.components:service-digitalassetlinks:$mozillaComponentsVersion"
implementation 'org.mozilla.components:service-digitalassetlinks:131.0b1' implementation "org.mozilla.components:ui-widgets:$mozillaComponentsVersion"
implementation 'org.mozilla.components:ui-widgets:131.0b1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
@@ -229,22 +229,6 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Activit
return binding.root return binding.root
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
// consumeFlow(components.store) { flow ->
// flow.mapNotNull { state -> state.findCustomTabOrSelectedTab(sessionId) }
// .ifAnyChanged { tab ->
// arrayOf(
// tab.content.loading,
// tab.content.canGoBack,
// tab.content.canGoForward,
// )
// }
// .collect {
// binding.toolbar.invalidateActions()
// }
// }
}
@CallSuper @CallSuper
override fun onBackPressed(): Boolean = override fun onBackPressed(): Boolean =
listOf(sessionFeature).any { it.onBackPressed() } listOf(sessionFeature).any { it.onBackPressed() }
@@ -31,9 +31,9 @@ class BrowserFragment(private val context: Context) : BaseBrowserFragment(), Use
override fun createEngine(components: Components): EngineView { override fun createEngine(components: Components): EngineView {
return components.engine.createView(context).apply { return components.engine.createView(context).apply {
selectionActionDelegate = DefaultSelectionActionDelegate( // selectionActionDelegate = DefaultSelectionActionDelegate(
components.selectionAction // components.selectionAction
) // )
} }
} }
@@ -88,6 +88,7 @@ open class DefaultComponents(
} }
var engineView: EngineView? = null var engineView: EngineView? = null
var engineReportedInitialized = false
val preferences: SharedPreferences = val preferences: SharedPreferences =
applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE) applicationContext.getSharedPreferences(SAMPLE_BROWSER_PREFERENCES, Context.MODE_PRIVATE)
@@ -166,6 +167,7 @@ open class DefaultComponents(
.distinctUntilChanged() .distinctUntilChanged()
.collect { tabId -> .collect { tabId ->
flutterEvents.onSelectedTabChange( flutterEvents.onSelectedTabChange(
System.currentTimeMillis(),
tabId tabId
) { _ -> } ) { _ -> }
} }
@@ -181,6 +183,7 @@ open class DefaultComponents(
.collect { tab -> .collect { tab ->
val iconBytes = tab.content.icon?.toWebPBytes() val iconBytes = tab.content.icon?.toWebPBytes()
flutterEvents.onIconChange( flutterEvents.onIconChange(
System.currentTimeMillis(),
tab.id, tab.id,
iconBytes iconBytes
) { _ -> } ) { _ -> }
@@ -195,6 +198,7 @@ open class DefaultComponents(
.debounce { 50 } .debounce { 50 }
.collect { tab -> .collect { tab ->
flutterEvents.onSecurityInfoStateChange( flutterEvents.onSecurityInfoStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
SecurityInfoState( SecurityInfoState(
tab.content.securityInfo.secure, tab.content.securityInfo.secure,
@@ -218,6 +222,7 @@ open class DefaultComponents(
.debounce { 50 } .debounce { 50 }
.collect { tab -> .collect { tab ->
flutterEvents.onReaderableStateChange( flutterEvents.onReaderableStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
ReaderableState( ReaderableState(
tab.readerState.readerable, tab.readerState.readerable,
@@ -241,6 +246,7 @@ open class DefaultComponents(
.debounce { 50 } .debounce { 50 }
.collect { tab -> .collect { tab ->
flutterEvents.onHistoryStateChange( flutterEvents.onHistoryStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
HistoryState( HistoryState(
items = tab.content.history.items.map { item -> HistoryItem( items = tab.content.history.items.map { item -> HistoryItem(
@@ -259,7 +265,7 @@ open class DefaultComponents(
flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} } flow.mapNotNull { state -> state.tabs.map {tab -> tab.id} }
.distinctUntilChanged() .distinctUntilChanged()
.collect { tabs -> .collect { tabs ->
flutterEvents.onTabListChange(tabs) { _ -> } flutterEvents.onTabListChange(System.currentTimeMillis(), tabs) { _ -> }
} }
} }
@@ -280,6 +286,7 @@ open class DefaultComponents(
.collect { tab -> .collect { tab ->
logger.info("title: ${tab.content.title} ${tab.content.url}") logger.info("title: ${tab.content.title} ${tab.content.url}")
flutterEvents.onTabContentStateChange( flutterEvents.onTabContentStateChange(
System.currentTimeMillis(),
TabContentState( TabContentState(
id = tab.id, id = tab.id,
contextId = tab.contextId, contextId = tab.contextId,
@@ -303,6 +310,7 @@ open class DefaultComponents(
.collect { tab -> .collect { tab ->
tab.content.findResults tab.content.findResults
flutterEvents.onFindResults( flutterEvents.onFindResults(
System.currentTimeMillis(),
tab.id, tab.id,
tab.content.findResults.map { result -> FindResultState( tab.content.findResults.map { result -> FindResultState(
activeMatchOrdinal = result.activeMatchOrdinal.toLong(), activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
@@ -40,7 +40,9 @@ private class NativeFragmentView(
override fun onFlutterViewAttached(flutterView: View) { override fun onFlutterViewAttached(flutterView: View) {
super.onFlutterViewAttached(flutterView) super.onFlutterViewAttached(flutterView)
flutterEvents.onFragmentReadyStateChange(true) { _ -> }
GlobalComponents.components!!.engineReportedInitialized = false;
flutterEvents.onViewReadyStateChange(System.currentTimeMillis(),true) { _ -> }
} }
override fun getView(): View { override fun getView(): View {
@@ -84,8 +84,6 @@ object GlobalComponents {
Logger.error("Failed to initialize web extension support", e) Logger.error("Failed to initialize web extension support", e)
} }
newComponents.tabsUseCases.addTab.invoke("https://google.com", selectTab = true)
_components = newComponents _components = newComponents
} }
} }
@@ -1,9 +1,21 @@
package eu.lensai.flutter_mozilla_components.api package eu.lensai.flutter_mozilla_components.api
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi
import mozilla.components.browser.state.action.SystemAction
import mozilla.components.feature.addons.logger
class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi { class GeckoBrowserApiImpl(private val showFragmentCallback: () -> Unit) : GeckoBrowserApi {
override fun showNativeFragment() { override fun showNativeFragment() {
showFragmentCallback.invoke(); showFragmentCallback.invoke();
} }
override fun onTrimMemory(level: Long) {
logger.debug("onTrimMemory: $level")
val components = GlobalComponents.components!!
components.store.dispatch(SystemAction.LowMemoryAction(level.toInt()))
components.icons.onTrimMemory(level.toInt())
}
} }
@@ -19,7 +19,13 @@ import eu.lensai.flutter_mozilla_components.pigeons.SecurityInfoState
import eu.lensai.flutter_mozilla_components.pigeons.TabContentState import eu.lensai.flutter_mozilla_components.pigeons.TabContentState
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import mozilla.components.browser.icons.BrowserIcons
import mozilla.components.browser.icons.IconRequest
import mozilla.components.browser.session.storage.RecoverableBrowserState import mozilla.components.browser.session.storage.RecoverableBrowserState
import mozilla.components.browser.state.action.TabListAction import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.selector.findTab import mozilla.components.browser.state.selector.findTab
@@ -43,6 +49,7 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
private val engine: Engine by lazy { GlobalComponents.components!!.engine } private val engine: Engine by lazy { GlobalComponents.components!!.engine }
private val state: BrowserState by lazy { GlobalComponents.components!!.store.state } private val state: BrowserState by lazy { GlobalComponents.components!!.store.state }
private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage } private val thumbnailStorage: ThumbnailStorage by lazy { GlobalComponents.components!!.thumbnailStorage }
private val icons: BrowserIcons by lazy { GlobalComponents.components!!.icons }
private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents } private val events: GeckoStateEvents by lazy { GlobalComponents.components!!.flutterEvents }
private fun restoreSource(source: SourceValue ) : SessionState.Source { private fun restoreSource(source: SourceValue ) : SessionState.Source {
@@ -115,19 +122,27 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
onFindResults: Boolean, onFindResults: Boolean,
onThumbnailChange: Boolean, onThumbnailChange: Boolean,
) { ) {
val tabs = state.tabs.map { x -> x.copy() }.toList()
val selectedTab = state.selectedTabId
if(onSelectedTabChange) { if(onSelectedTabChange) {
events.onSelectedTabChange( events.onSelectedTabChange(
state.selectedTabId System.currentTimeMillis(),
selectedTab
) { _ -> } ) { _ -> }
} }
if(onTabListChange) { if(onTabListChange) {
events.onTabListChange(state.tabs.map {tab -> tab.id}) { _ -> } events.onTabListChange(
System.currentTimeMillis(),
tabs.map {tab -> tab.id}
) { _ -> }
} }
if(onTabContentStateChange) { if(onTabContentStateChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
events.onTabContentStateChange( events.onTabContentStateChange(
System.currentTimeMillis(),
TabContentState( TabContentState(
id = tab.id, id = tab.id,
contextId = tab.contextId, contextId = tab.contextId,
@@ -143,21 +158,35 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
} }
if(onIconChange) { if(onIconChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
if(tab.content.icon != null) { if(tab.content.icon != null) {
val iconBytes = tab.content.icon?.toWebPBytes() val iconBytes = tab.content.icon?.toWebPBytes()
events.onIconChange( events.onIconChange(
System.currentTimeMillis(),
tab.id, tab.id,
iconBytes iconBytes
) { _ -> } ) { _ -> }
} else {
CoroutineScope(Dispatchers.Default).launch {
val result = icons.loadIcon(IconRequest(url = tab.content.url)).await()
val iconBytes = result.bitmap.toWebPBytes()
runOnUiThread {
events.onIconChange(
System.currentTimeMillis(),
tab.id,
iconBytes
) { _ -> }
}
}
} }
} }
} }
if(onSecurityInfoStateChange) { if(onSecurityInfoStateChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
val iconBytes = tab.content.icon?.toWebPBytes()
events.onSecurityInfoStateChange( events.onSecurityInfoStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
SecurityInfoState( SecurityInfoState(
tab.content.securityInfo.secure, tab.content.securityInfo.secure,
@@ -169,8 +198,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
} }
if(onReaderableStateChange) { if(onReaderableStateChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
events.onReaderableStateChange( events.onReaderableStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
ReaderableState( ReaderableState(
tab.readerState.readerable, tab.readerState.readerable,
@@ -181,8 +211,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
} }
if(onHistoryStateChange) { if(onHistoryStateChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
events.onHistoryStateChange( events.onHistoryStateChange(
System.currentTimeMillis(),
tab.id, tab.id,
HistoryState( HistoryState(
items = tab.content.history.items.map { item -> HistoryItem( items = tab.content.history.items.map { item -> HistoryItem(
@@ -198,8 +229,9 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
} }
if(onFindResults) { if(onFindResults) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
events.onFindResults( events.onFindResults(
System.currentTimeMillis(),
tab.id, tab.id,
tab.content.findResults.map { result -> FindResultState( tab.content.findResults.map { result -> FindResultState(
activeMatchOrdinal = result.activeMatchOrdinal.toLong(), activeMatchOrdinal = result.activeMatchOrdinal.toLong(),
@@ -211,13 +243,13 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
} }
if(onThumbnailChange) { if(onThumbnailChange) {
state.tabs.forEach { tab -> tabs.forEach { tab ->
CoroutineScope(Dispatchers.Default).launch { CoroutineScope(Dispatchers.Default).launch {
val bitmap = thumbnailStorage.loadThumbnail( val bitmap = thumbnailStorage.loadThumbnail(
ImageLoadRequest( ImageLoadRequest(
id = tab.id, id = tab.id,
//TODO: make this configurable //TODO: make this configurable
size = 600, size = 1024,
isPrivate = tab.content.private isPrivate = tab.content.private
) )
).await() ).await()
@@ -225,7 +257,7 @@ class GeckoTabsApiImpl() : GeckoTabsApi {
if(bitmap != null) { if(bitmap != null) {
val bytes = bitmap.toWebPBytes() val bytes = bitmap.toWebPBytes()
runOnUiThread { runOnUiThread {
events.onThumbnailChange(tab.id, bytes) { _ -> } events.onThumbnailChange(System.currentTimeMillis(), tab.id, bytes) { _ -> }
} }
} }
} }
@@ -4,6 +4,23 @@ import android.graphics.Bitmap
import android.os.Build import android.os.Build
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
fun Bitmap.resize(maxWidth: Int, maxHeight: Int): Bitmap {
var width = this.width
var height = this.height
val aspectRatio: Float = width.toFloat() / height.toFloat()
if (width > height) {
width = maxWidth
height = (width / aspectRatio).toInt()
} else {
height = maxHeight
width = (height * aspectRatio).toInt()
}
return Bitmap.createScaledBitmap(this, width, height, true)
}
fun Bitmap.toWebPBytes(): ByteArray { fun Bitmap.toWebPBytes(): ByteArray {
val stream = ByteArrayOutputStream() val stream = ByteArrayOutputStream()
val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val compressFormat = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
@@ -5,16 +5,22 @@ package eu.lensai.flutter_mozilla_components.middleware
import android.graphics.Bitmap import android.graphics.Bitmap
import android.util.Log import android.util.Log
import eu.lensai.flutter_mozilla_components.GlobalComponents
import eu.lensai.flutter_mozilla_components.ext.resize
import eu.lensai.flutter_mozilla_components.ext.toWebPBytes import eu.lensai.flutter_mozilla_components.ext.toWebPBytes
import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents import eu.lensai.flutter_mozilla_components.pigeons.GeckoStateEvents
import mozilla.components.browser.state.action.BrowserAction import mozilla.components.browser.state.action.BrowserAction
import mozilla.components.browser.state.action.ContentAction import mozilla.components.browser.state.action.ContentAction
import mozilla.components.browser.state.action.LastAccessAction
import mozilla.components.browser.state.action.ReaderAction
import mozilla.components.browser.state.action.TabListAction
import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.BrowserState
import mozilla.components.feature.addons.logger
import mozilla.components.lib.state.Middleware import mozilla.components.lib.state.Middleware
import mozilla.components.lib.state.MiddlewareContext import mozilla.components.lib.state.MiddlewareContext
import org.mozilla.gecko.util.ThreadUtils.runOnUiThread import org.mozilla.gecko.util.ThreadUtils.runOnUiThread
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import kotlin.reflect.typeOf
/** /**
@@ -30,13 +36,37 @@ class FlutterEventMiddleware(private val flutterEvents: GeckoStateEvents) : Midd
) { ) {
when (action) { when (action) {
is ContentAction.UpdateThumbnailAction -> { is ContentAction.UpdateThumbnailAction -> {
val bytes = action.thumbnail.toWebPBytes() val resized = action.thumbnail.resize(maxWidth = 640, maxHeight = 480);
val bytes = resized.toWebPBytes()
runOnUiThread { runOnUiThread {
flutterEvents.onThumbnailChange(action.sessionId, bytes) { _ -> } flutterEvents.onThumbnailChange(System.currentTimeMillis(), action.sessionId, bytes) { _ -> }
}
}
//UpdateReaderConnectRequiredAction seems to be the only event that is called predictable
//after a hot reload
is ReaderAction.UpdateReaderConnectRequiredAction -> {
if(!GlobalComponents.components!!.engineReportedInitialized) {
runOnUiThread {
flutterEvents.onEngineReadyStateChange(
System.currentTimeMillis(),
true
) { _ -> }
}
GlobalComponents.components!!.engineReportedInitialized = true
}
}
is TabListAction.AddTabAction -> {
runOnUiThread {
flutterEvents.onTabAdded(
System.currentTimeMillis(),
action.tab.id
) { _ -> }
} }
} }
else -> { else -> {
// no-op //logger.debug("Event fired: " + action.javaClass.name)
} }
} }
next(action) next(action)
@@ -1158,6 +1158,7 @@ private open class GeckoPigeonCodec : StandardMessageCodec() {
/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ /** Generated interface from Pigeon that represents a handler of messages from Flutter. */
interface GeckoBrowserApi { interface GeckoBrowserApi {
fun showNativeFragment() fun showNativeFragment()
fun onTrimMemory(level: Long)
companion object { companion object {
/** The codec used by GeckoBrowserApi. */ /** The codec used by GeckoBrowserApi. */
@@ -1184,6 +1185,24 @@ interface GeckoBrowserApi {
channel.setMessageHandler(null) channel.setMessageHandler(null)
} }
} }
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val levelArg = args[0] as Long
val wrapped: List<Any?> = try {
api.onTrimMemory(levelArg)
listOf(null)
} catch (exception: Throwable) {
wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
} }
} }
} }
@@ -2153,12 +2172,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
GeckoPigeonCodec() GeckoPigeonCodec()
} }
} }
fun onFragmentReadyStateChange(stateArg: Boolean, callback: (Result<Unit>) -> Unit) fun onViewReadyStateChange(timestampArg: Long, stateArg: Boolean, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(stateArg)) { channel.send(listOf(timestampArg, stateArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2170,12 +2189,46 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onTabListChange(tabIdsArg: List<String>, callback: (Result<Unit>) -> Unit) fun onEngineReadyStateChange(timestampArg: Long, stateArg: Boolean, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, stateArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
fun onTabAdded(timestampArg: Long, tabIdArg: String, callback: (Result<Unit>) -> Unit)
{
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(timestampArg, tabIdArg)) {
if (it is List<*>) {
if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
} else {
callback(Result.success(Unit))
}
} else {
callback(Result.failure(createConnectionError(channelName)))
}
}
}
fun onTabListChange(timestampArg: Long, tabIdsArg: List<String>, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(tabIdsArg)) { channel.send(listOf(timestampArg, tabIdsArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2187,12 +2240,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onSelectedTabChange(idArg: String?, callback: (Result<Unit>) -> Unit) fun onSelectedTabChange(timestampArg: Long, idArg: String?, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg)) { channel.send(listOf(timestampArg, idArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2204,12 +2257,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onTabContentStateChange(stateArg: TabContentState, callback: (Result<Unit>) -> Unit) fun onTabContentStateChange(timestampArg: Long, stateArg: TabContentState, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(stateArg)) { channel.send(listOf(timestampArg, stateArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2221,12 +2274,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onHistoryStateChange(idArg: String, stateArg: HistoryState, callback: (Result<Unit>) -> Unit) fun onHistoryStateChange(timestampArg: Long, idArg: String, stateArg: HistoryState, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, stateArg)) { channel.send(listOf(timestampArg, idArg, stateArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2238,12 +2291,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onReaderableStateChange(idArg: String, stateArg: ReaderableState, callback: (Result<Unit>) -> Unit) fun onReaderableStateChange(timestampArg: Long, idArg: String, stateArg: ReaderableState, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, stateArg)) { channel.send(listOf(timestampArg, idArg, stateArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2255,12 +2308,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onSecurityInfoStateChange(idArg: String, stateArg: SecurityInfoState, callback: (Result<Unit>) -> Unit) fun onSecurityInfoStateChange(timestampArg: Long, idArg: String, stateArg: SecurityInfoState, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, stateArg)) { channel.send(listOf(timestampArg, idArg, stateArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2272,12 +2325,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onIconChange(idArg: String, bytesArg: ByteArray?, callback: (Result<Unit>) -> Unit) fun onIconChange(timestampArg: Long, idArg: String, bytesArg: ByteArray?, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, bytesArg)) { channel.send(listOf(timestampArg, idArg, bytesArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2289,12 +2342,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onThumbnailChange(idArg: String, bytesArg: ByteArray?, callback: (Result<Unit>) -> Unit) fun onThumbnailChange(timestampArg: Long, idArg: String, bytesArg: ByteArray?, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, bytesArg)) { channel.send(listOf(timestampArg, idArg, bytesArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -2306,12 +2359,12 @@ class GeckoStateEvents(private val binaryMessenger: BinaryMessenger, private val
} }
} }
} }
fun onFindResults(idArg: String, resultsArg: List<FindResultState>, callback: (Result<Unit>) -> Unit) fun onFindResults(timestampArg: Long, idArg: String, resultsArg: List<FindResultState>, callback: (Result<Unit>) -> Unit)
{ {
val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$separatedMessageChannelSuffix" val channelName = "dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults$separatedMessageChannelSuffix"
val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec) val channel = BasicMessageChannel<Any?>(binaryMessenger, channelName, codec)
channel.send(listOf(idArg, resultsArg)) { channel.send(listOf(timestampArg, idArg, resultsArg)) {
if (it is List<*>) { if (it is List<*>) {
if (it.size > 1) { if (it.size > 1) {
callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?))) callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
@@ -1,5 +1,20 @@
package eu.lensai.flutter_mozilla_components_example package eu.lensai.flutter_mozilla_components_example
import io.flutter.embedding.android.FlutterFragmentActivity import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterFragmentActivity() class MainActivity: FlutterFragmentActivity() {
private val CHANNEL = "me.movenext.flutter_mozilla_components/trim_memory"
private lateinit var channel: MethodChannel
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
channel.invokeMethod("onTrimMemory", level)
}
}
@@ -10,4 +10,8 @@ class GeckoBrowserService {
Future<void> showNativeFragment() { Future<void> showNativeFragment() {
return _api.showNativeFragment(); return _api.showNativeFragment();
} }
Future<void> onTrimMemory(int level) {
return _api.onTrimMemory(level);
}
} }
@@ -12,22 +12,31 @@ typedef ThumbnailEvent = ({String tabId, Uint8List? bytes});
typedef FindResultsEvent = ({String tabId, List<FindResultState> results}); typedef FindResultsEvent = ({String tabId, List<FindResultState> results});
class GeckoEventService extends GeckoStateEvents { class GeckoEventService extends GeckoStateEvents {
final _lastEventTimes = <Subject, Map<dynamic, int>>{};
// Stream controllers // Stream controllers
final _fragmentStateSubject = BehaviorSubject.seeded(false); final _viewStateSubject = BehaviorSubject.seeded(false);
final _engineStateSubject = BehaviorSubject.seeded(false);
final _tabListSubject = BehaviorSubject<List<String>>(); final _tabListSubject = BehaviorSubject<List<String>>();
final _selectedTabSubject = BehaviorSubject<String?>(); final _selectedTabSubject = BehaviorSubject<String?>();
final _tabContentSubject = BehaviorSubject<TabContentState>();
final _historySubject = BehaviorSubject<HistoryEvent>(); final _tabContentSubject = ReplaySubject<TabContentState>();
final _readerableSubject = BehaviorSubject<ReaderableEvent>(); final _historySubject = ReplaySubject<HistoryEvent>();
final _securityInfoSubject = BehaviorSubject<SecurityInfoEvent>(); final _securityInfoSubject = ReplaySubject<SecurityInfoEvent>();
final _iconSubject = BehaviorSubject<IconEvent>(); final _readerableSubject = ReplaySubject<ReaderableEvent>();
final _thumbnailSubject = BehaviorSubject<ThumbnailEvent>();
final _findResultsSubject = BehaviorSubject<FindResultsEvent>(); final _iconSubject = PublishSubject<IconEvent>();
final _thumbnailSubject = PublishSubject<ThumbnailEvent>();
final _findResultsSubject = PublishSubject<FindResultsEvent>();
final _tabAddedSubject = PublishSubject<String>();
// Event streams // Event streams
Stream<bool> get fragmentReadyStateEvents => _fragmentStateSubject.stream; ValueStream<bool> get viewReadyStateEvents => _viewStateSubject.stream;
Stream<List<String>> get tabListEvents => _tabListSubject.stream; ValueStream<bool> get engineReadyStateEvents => _engineStateSubject.stream;
Stream<String?> get selectedTabEvents => _selectedTabSubject.stream; ValueStream<List<String>> get tabListEvents => _tabListSubject.stream;
ValueStream<String?> get selectedTabEvents => _selectedTabSubject.stream;
Stream<TabContentState> get tabContentEvents => _tabContentSubject.stream; Stream<TabContentState> get tabContentEvents => _tabContentSubject.stream;
Stream<HistoryEvent> get historyEvents => _historySubject.stream; Stream<HistoryEvent> get historyEvents => _historySubject.stream;
Stream<ReaderableEvent> get readerableEvents => _readerableSubject.stream; Stream<ReaderableEvent> get readerableEvents => _readerableSubject.stream;
@@ -37,55 +46,124 @@ class GeckoEventService extends GeckoStateEvents {
Stream<ThumbnailEvent> get thumbnailEvents => _thumbnailSubject.stream; Stream<ThumbnailEvent> get thumbnailEvents => _thumbnailSubject.stream;
Stream<FindResultsEvent> get findResultsEvent => _findResultsSubject.stream; Stream<FindResultsEvent> get findResultsEvent => _findResultsSubject.stream;
Stream<String> get tabAddedStream => _tabAddedSubject.stream;
void _addWhenMoreRecent<T>(
Subject<T> subject,
int timestamp,
dynamic identifier,
T value,
) {
_lastEventTimes[subject] ??= {};
if ((_lastEventTimes[subject]?[identifier] ?? 0) < timestamp) {
_lastEventTimes[subject]![identifier] = timestamp;
subject.add(value);
}
}
@override @override
void onFragmentReadyStateChange(bool state) { void onViewReadyStateChange(int timestamp, bool state) {
_fragmentStateSubject.add(state); _addWhenMoreRecent(_viewStateSubject, timestamp, null, state);
}
@override
void onEngineReadyStateChange(int timestamp, bool state) {
_addWhenMoreRecent(_engineStateSubject, timestamp, null, state);
} }
// Overridden methods // Overridden methods
@override @override
void onTabListChange(List<String?> tabIds) { void onTabListChange(int timestamp, List<String?> tabIds) {
_tabListSubject.add(tabIds.nonNulls.toList()); _addWhenMoreRecent(
_tabListSubject,
timestamp,
null,
tabIds.nonNulls.toList(),
);
} }
@override @override
void onSelectedTabChange(String? id) { void onSelectedTabChange(int timestamp, String? id) {
_selectedTabSubject.add(id); _addWhenMoreRecent(_selectedTabSubject, timestamp, id, id);
} }
@override @override
void onTabContentStateChange(TabContentState state) { void onTabContentStateChange(int timestamp, TabContentState state) {
_tabContentSubject.add(state); _addWhenMoreRecent(_tabContentSubject, timestamp, state.id, state);
} }
@override @override
void onHistoryStateChange(String id, HistoryState state) { void onHistoryStateChange(int timestamp, String id, HistoryState state) {
_historySubject.add((tabId: id, history: state)); _addWhenMoreRecent(
_historySubject,
timestamp,
id,
(tabId: id, history: state),
);
} }
@override @override
void onReaderableStateChange(String id, ReaderableState state) { void onReaderableStateChange(
_readerableSubject.add((tabId: id, readerable: state)); int timestamp,
String id,
ReaderableState state,
) {
_addWhenMoreRecent(
_readerableSubject,
timestamp,
id,
(tabId: id, readerable: state),
);
} }
@override @override
void onSecurityInfoStateChange(String id, SecurityInfoState state) { void onSecurityInfoStateChange(
_securityInfoSubject.add((tabId: id, securityInfo: state)); int timestamp,
String id,
SecurityInfoState state,
) {
_addWhenMoreRecent(
_securityInfoSubject,
timestamp,
id,
(tabId: id, securityInfo: state),
);
} }
@override @override
void onIconChange(String id, Uint8List? bytes) { void onIconChange(int timestamp, String id, Uint8List? bytes) {
_iconSubject.add((tabId: id, bytes: bytes)); _addWhenMoreRecent(_iconSubject, timestamp, id, (tabId: id, bytes: bytes));
} }
@override @override
void onThumbnailChange(String id, Uint8List? bytes) { void onThumbnailChange(int timestamp, String id, Uint8List? bytes) {
_thumbnailSubject.add((tabId: id, bytes: bytes)); _addWhenMoreRecent(
_thumbnailSubject,
timestamp,
id,
(tabId: id, bytes: bytes),
);
} }
@override @override
void onFindResults(String id, List<FindResultState?> results) { void onFindResults(int timestamp, String id, List<FindResultState?> results) {
_findResultsSubject.add((tabId: id, results: results.nonNulls.toList())); _addWhenMoreRecent(
_findResultsSubject,
timestamp,
id,
(tabId: id, results: results.nonNulls.toList()),
);
}
@override
void onTabAdded(int timestamp, String tabId) {
_addWhenMoreRecent(
_tabAddedSubject,
timestamp,
null,
tabId,
);
} }
GeckoEventService.setUp({ GeckoEventService.setUp({
@@ -109,5 +187,6 @@ class GeckoEventService extends GeckoStateEvents {
unawaited(_iconSubject.close()); unawaited(_iconSubject.close());
unawaited(_thumbnailSubject.close()); unawaited(_thumbnailSubject.close());
unawaited(_findResultsSubject.close()); unawaited(_findResultsSubject.close());
unawaited(_tabAddedSubject.close());
} }
} }
@@ -11,14 +11,14 @@ class GeckoTabService {
GeckoTabService({GeckoTabsApi? api}) : _api = api ?? _apiInstance; GeckoTabService({GeckoTabsApi? api}) : _api = api ?? _apiInstance;
Future<void> syncEvents({ Future<void> syncEvents({
bool onSelectedTabChange = true, bool onSelectedTabChange = false,
bool onTabListChange = true, bool onTabListChange = false,
bool onTabContentStateChange = true, bool onTabContentStateChange = false,
bool onIconChange = true, bool onIconChange = false,
bool onSecurityInfoStateChange = true, bool onSecurityInfoStateChange = false,
bool onHistoryStateChange = true, bool onHistoryStateChange = false,
bool onFindResults = true, bool onFindResults = false,
bool onThumbnailChange = true, bool onThumbnailChange = false,
}) { }) {
return _api.syncEvents( return _api.syncEvents(
onSelectedTabChange: onSelectedTabChange, onSelectedTabChange: onSelectedTabChange,
@@ -8,11 +8,35 @@ import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart'; import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dart';
class GeckoView extends StatelessWidget { class GeckoView extends StatefulWidget {
final FutureOr<void> Function()? preInitializationStep; final FutureOr<void> Function()? preInitializationStep;
const GeckoView({super.key, this.preInitializationStep}); const GeckoView({super.key, this.preInitializationStep});
@override
State<GeckoView> createState() => _GeckoViewState();
}
class _GeckoViewState extends State<GeckoView> {
static const platform =
MethodChannel('me.movenext.flutter_mozilla_components/trim_memory');
final browserService = GeckoBrowserService();
@override
void initState() {
super.initState();
_setupMethodCallHandler();
}
void _setupMethodCallHandler() {
platform.setMethodCallHandler((MethodCall call) async {
if (call.method == 'onTrimMemory') {
await browserService.onTrimMemory(call.arguments as int);
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return PlatformViewLink( return PlatformViewLink(
@@ -39,8 +63,14 @@ class GeckoView extends StatelessWidget {
params.onPlatformViewCreated(value); params.onPlatformViewCreated(value);
SchedulerBinding.instance.addPostFrameCallback((_) async { SchedulerBinding.instance.addPostFrameCallback((_) async {
await preInitializationStep?.call(); await widget.preInitializationStep?.call();
await GeckoBrowserService().showNativeFragment();
await Future.delayed(
//Wait for two more frames just to be sure view has been initialized
Duration(milliseconds: ((1000 / 60) * 2).toInt()),
).whenComplete(() async {
await browserService.showNativeFragment();
});
}); });
}) })
// ignore: discarded_futures // ignore: discarded_futures
@@ -1187,6 +1187,28 @@ class GeckoBrowserApi {
return; return;
} }
} }
Future<void> onTrimMemory(int level) async {
final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoBrowserApi.onTrimMemory$pigeonVar_messageChannelSuffix';
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
pigeonVar_channelName,
pigeonChannelCodec,
binaryMessenger: pigeonVar_binaryMessenger,
);
final List<Object?>? pigeonVar_replyList =
await pigeonVar_channel.send(<Object?>[level]) as List<Object?>?;
if (pigeonVar_replyList == null) {
throw _createConnectionError(pigeonVar_channelName);
} else if (pigeonVar_replyList.length > 1) {
throw PlatformException(
code: pigeonVar_replyList[0]! as String,
message: pigeonVar_replyList[1] as String?,
details: pigeonVar_replyList[2],
);
} else {
return;
}
}
} }
class GeckoEngineSettingsApi { class GeckoEngineSettingsApi {
@@ -2243,44 +2265,107 @@ class GeckoCookieApi {
abstract class GeckoStateEvents { abstract class GeckoStateEvents {
static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec(); static const MessageCodec<Object?> pigeonChannelCodec = _PigeonCodec();
void onFragmentReadyStateChange(bool state); void onViewReadyStateChange(int timestamp, bool state);
void onTabListChange(List<String> tabIds); void onEngineReadyStateChange(int timestamp, bool state);
void onSelectedTabChange(String? id); void onTabAdded(int timestamp, String tabId);
void onTabContentStateChange(TabContentState state); void onTabListChange(int timestamp, List<String> tabIds);
void onHistoryStateChange(String id, HistoryState state); void onSelectedTabChange(int timestamp, String? id);
void onReaderableStateChange(String id, ReaderableState state); void onTabContentStateChange(int timestamp, TabContentState state);
void onSecurityInfoStateChange(String id, SecurityInfoState state); void onHistoryStateChange(int timestamp, String id, HistoryState state);
void onIconChange(String id, Uint8List? bytes); void onReaderableStateChange(int timestamp, String id, ReaderableState state);
void onThumbnailChange(String id, Uint8List? bytes); void onSecurityInfoStateChange(int timestamp, String id, SecurityInfoState state);
void onFindResults(String id, List<FindResultState> results); void onIconChange(int timestamp, String id, Uint8List? bytes);
void onThumbnailChange(int timestamp, String id, Uint8List? bytes);
void onFindResults(int timestamp, String id, List<FindResultState> results);
static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) { static void setUp(GeckoStateEvents? api, {BinaryMessenger? binaryMessenger, String messageChannelSuffix = '',}) {
messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : '';
{ {
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>( final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange$messageChannelSuffix', pigeonChannelCodec, 'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger); binaryMessenger: binaryMessenger);
if (api == null) { if (api == null) {
pigeonVar_channel.setMessageHandler(null); pigeonVar_channel.setMessageHandler(null);
} else { } else {
pigeonVar_channel.setMessageHandler((Object? message) async { pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final bool? arg_state = (args[0] as bool?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null, expected non-null int.');
final bool? arg_state = (args[1] as bool?);
assert(arg_state != null, assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFragmentReadyStateChange was null, expected non-null bool.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onViewReadyStateChange was null, expected non-null bool.');
try { try {
api.onFragmentReadyStateChange(arg_state!); api.onViewReadyStateChange(arg_timestamp!, arg_state!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
{
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange was null, expected non-null int.');
final bool? arg_state = (args[1] as bool?);
assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onEngineReadyStateChange was null, expected non-null bool.');
try {
api.onEngineReadyStateChange(arg_timestamp!, arg_state!);
return wrapResponse(empty: true);
} on PlatformException catch (e) {
return wrapResponse(error: e);
} catch (e) {
return wrapResponse(error: PlatformException(code: 'error', message: e.toString()));
}
});
}
}
{
final BasicMessageChannel<Object?> pigeonVar_channel = BasicMessageChannel<Object?>(
'dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded$messageChannelSuffix', pigeonChannelCodec,
binaryMessenger: binaryMessenger);
if (api == null) {
pigeonVar_channel.setMessageHandler(null);
} else {
pigeonVar_channel.setMessageHandler((Object? message) async {
assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded was null.');
final List<Object?> args = (message as List<Object?>?)!;
final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded was null, expected non-null int.');
final String? arg_tabId = (args[1] as String?);
assert(arg_tabId != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabAdded was null, expected non-null String.');
try {
api.onTabAdded(arg_timestamp!, arg_tabId!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2301,11 +2386,14 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final List<String>? arg_tabIds = (args[0] as List<Object?>?)?.cast<String>(); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null int.');
final List<String>? arg_tabIds = (args[1] as List<Object?>?)?.cast<String>();
assert(arg_tabIds != null, assert(arg_tabIds != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List<String>.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabListChange was null, expected non-null List<String>.');
try { try {
api.onTabListChange(arg_tabIds!); api.onTabListChange(arg_timestamp!, arg_tabIds!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2326,9 +2414,12 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSelectedTabChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
try { try {
api.onSelectedTabChange(arg_id); api.onSelectedTabChange(arg_timestamp!, arg_id);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2349,11 +2440,14 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final TabContentState? arg_state = (args[0] as TabContentState?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null, expected non-null int.');
final TabContentState? arg_state = (args[1] as TabContentState?);
assert(arg_state != null, assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null, expected non-null TabContentState.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onTabContentStateChange was null, expected non-null TabContentState.');
try { try {
api.onTabContentStateChange(arg_state!); api.onTabContentStateChange(arg_timestamp!, arg_state!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2374,14 +2468,17 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null String.');
final HistoryState? arg_state = (args[1] as HistoryState?); final HistoryState? arg_state = (args[2] as HistoryState?);
assert(arg_state != null, assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null HistoryState.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onHistoryStateChange was null, expected non-null HistoryState.');
try { try {
api.onHistoryStateChange(arg_id!, arg_state!); api.onHistoryStateChange(arg_timestamp!, arg_id!, arg_state!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2402,14 +2499,17 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null String.');
final ReaderableState? arg_state = (args[1] as ReaderableState?); final ReaderableState? arg_state = (args[2] as ReaderableState?);
assert(arg_state != null, assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null ReaderableState.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onReaderableStateChange was null, expected non-null ReaderableState.');
try { try {
api.onReaderableStateChange(arg_id!, arg_state!); api.onReaderableStateChange(arg_timestamp!, arg_id!, arg_state!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2430,14 +2530,17 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null String.');
final SecurityInfoState? arg_state = (args[1] as SecurityInfoState?); final SecurityInfoState? arg_state = (args[2] as SecurityInfoState?);
assert(arg_state != null, assert(arg_state != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null SecurityInfoState.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onSecurityInfoStateChange was null, expected non-null SecurityInfoState.');
try { try {
api.onSecurityInfoStateChange(arg_id!, arg_state!); api.onSecurityInfoStateChange(arg_timestamp!, arg_id!, arg_state!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2458,12 +2561,15 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onIconChange was null, expected non-null String.');
final Uint8List? arg_bytes = (args[1] as Uint8List?); final Uint8List? arg_bytes = (args[2] as Uint8List?);
try { try {
api.onIconChange(arg_id!, arg_bytes); api.onIconChange(arg_timestamp!, arg_id!, arg_bytes);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2484,12 +2590,15 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onThumbnailChange was null, expected non-null String.');
final Uint8List? arg_bytes = (args[1] as Uint8List?); final Uint8List? arg_bytes = (args[2] as Uint8List?);
try { try {
api.onThumbnailChange(arg_id!, arg_bytes); api.onThumbnailChange(arg_timestamp!, arg_id!, arg_bytes);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -2510,14 +2619,17 @@ abstract class GeckoStateEvents {
assert(message != null, assert(message != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null.');
final List<Object?> args = (message as List<Object?>?)!; final List<Object?> args = (message as List<Object?>?)!;
final String? arg_id = (args[0] as String?); final int? arg_timestamp = (args[0] as int?);
assert(arg_timestamp != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null int.');
final String? arg_id = (args[1] as String?);
assert(arg_id != null, assert(arg_id != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null String.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null String.');
final List<FindResultState>? arg_results = (args[1] as List<Object?>?)?.cast<FindResultState>(); final List<FindResultState>? arg_results = (args[2] as List<Object?>?)?.cast<FindResultState>();
assert(arg_results != null, assert(arg_results != null,
'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null List<FindResultState>.'); 'Argument for dev.flutter.pigeon.flutter_mozilla_components.GeckoStateEvents.onFindResults was null, expected non-null List<FindResultState>.');
try { try {
api.onFindResults(arg_id!, arg_results!); api.onFindResults(arg_timestamp!, arg_id!, arg_results!);
return wrapResponse(empty: true); return wrapResponse(empty: true);
} on PlatformException catch (e) { } on PlatformException catch (e) {
return wrapResponse(error: e); return wrapResponse(error: e);
@@ -539,6 +539,7 @@ class FindResultState {
@HostApi() @HostApi()
abstract class GeckoBrowserApi { abstract class GeckoBrowserApi {
void showNativeFragment(); void showNativeFragment();
void onTrimMemory(int level);
} }
@HostApi() @HostApi()
@@ -782,19 +783,26 @@ abstract class GeckoCookieApi {
@FlutterApi() @FlutterApi()
abstract class GeckoStateEvents { abstract class GeckoStateEvents {
void onFragmentReadyStateChange(bool state); void onViewReadyStateChange(int timestamp, bool state);
void onEngineReadyStateChange(int timestamp, bool state);
void onTabListChange(List<String> tabIds); void onTabAdded(int timestamp, String tabId);
void onSelectedTabChange(String? id);
void onTabContentStateChange(TabContentState state); void onTabListChange(int timestamp, List<String> tabIds);
void onHistoryStateChange(String id, HistoryState state); void onSelectedTabChange(int timestamp, String? id);
void onReaderableStateChange(String id, ReaderableState state);
void onSecurityInfoStateChange(String id, SecurityInfoState state);
void onIconChange(String id, Uint8List? bytes);
void onThumbnailChange(String id, Uint8List? bytes);
void onFindResults(String id, List<FindResultState> results); void onTabContentStateChange(int timestamp, TabContentState state);
void onHistoryStateChange(int timestamp, String id, HistoryState state);
void onReaderableStateChange(int timestamp, String id, ReaderableState state);
void onSecurityInfoStateChange(
int timestamp,
String id,
SecurityInfoState state,
);
void onIconChange(int timestamp, String id, Uint8List? bytes);
void onThumbnailChange(int timestamp, String id, Uint8List? bytes);
void onFindResults(int timestamp, String id, List<FindResultState> results);
} }
@HostApi() @HostApi()