diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index cdff5034..44157fb7 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -21,6 +21,8 @@ linter: - collection_methods_unrelated_type analyzer: + errors: + overridden_fields: ignore plugins: - custom_lint exclude: diff --git a/app/assets/preferences/settings.json b/app/assets/preferences/settings.json index 62202204..c5819468 100644 --- a/app/assets/preferences/settings.json +++ b/app/assets/preferences/settings.json @@ -1,34 +1,4 @@ { - "system": { - "incognito_mode": { - "preferences": { - "privacy.sanitize.sanitizeOnShutdown": true, - "privacy.clearOnShutdown.cache": true, - "privacy.clearOnShutdown_v2.cache": true, - "privacy.clearOnShutdown.downloads": true, - "privacy.clearOnShutdown.formdata": true, - "privacy.clearOnShutdown.history": true, - "privacy.clearOnShutdown_v2.historyFormDataAndDownloads": true, - "privacy.clearOnShutdown.cookies": true, - "privacy.clearOnShutdown.offlineApps": true, - "privacy.clearOnShutdown.sessions": true, - "privacy.clearOnShutdown_v2.cookiesAndStorage": true, - "privacy.clearSiteData.cache": true, - "privacy.clearSiteData.cookiesAndStorage": false, - "privacy.clearSiteData.historyFormDataAndDownloads": true, - "privacy.cpd.cache": true, - "privacy.clearHistory.cache": true, - "privacy.cpd.formdata": true, - "privacy.cpd.history": true, - "privacy.clearHistory.historyFormDataAndDownloads": true, - "privacy.cpd.cookies": false, - "privacy.cpd.sessions": true, - "privacy.cpd.offlineApps": false, - "privacy.clearHistory.cookiesAndStorage": false, - "privacy.sanitize.timeSpan": 0 - } - } - }, "user": { "Safe Browsing": { "description": "Google Safe Browsing is a service from Google that warns users when they attempt to navigate to a dangerous website or download dangerous files.", @@ -114,6 +84,11 @@ "value": true, "title": "Force WebRTC inside the proxy" }, + "media.peerconnection.enabled": { + "value": false, + "title": "Disable WebRTC PeerConnection", + "description": "Prevent exposing local IP Address" + }, "media.peerconnection.ice.default_address_only": { "value": true, "title": "Force a single network interface for ICE candidates generation", @@ -121,30 +96,6 @@ } } }, - "Disk Avoidance": { - "description": "Control what data to persist to disk", - "preferences": { - "browser.cache.disk.enable": { - "value": false, - "title": "Disable disk cache", - "description": "Don't cache data to disk. Might impact performance negatively", - "requireUserOptIn": true - }, - "browser.privatebrowsing.forceMediaMemoryCache": { - "value": true, - "title": "Set media cache in Private Browsing to in-memory" - }, - "media.memory_cache_max_size": { - "value": 65536, - "title": "Increase in-memory media cache maximum size" - }, - "browser.sessionstore.privacy_level": { - "value": 2, - "title": "Disable storing extra session data", - "description": "Extra session data such as form content, cookies and POST data" - } - } - }, "Secure Connections": { "description": "SSL/TLS / OCSP / CERTS / HPKP", "preferences": { @@ -202,6 +153,135 @@ } } }, + "Privacy": { + "preferences": { + "network.http.referer.XOriginTrimmingPolicy": { + "value": 2, + "title": "Trim cross-origin referrers", + "description": "Trims down referrers to just the scheme, hostname and port" + }, + "network.http.referer.XOriginPolicy": { + "value": 2, + "title": "Only send cross-origin referer if host matches" + }, + "network.http.referer.disallowCrossSiteRelaxingDefault": { + "value": true, + "title": "Disallow relaxing referrer policy for cross-site requests" + }, + "network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation": { + "value": true, + "title": "Disallow relaxing referrer policy for cross-site requests", + "description": "On top navigation" + }, + "general.useragent.updates.enabled": { + "value": false, + "title": "Disable automatic User Agent updates", + "description": "This preference controls whether Firefox automatically updates its User-Agent string. When set to false, it prevents Firefox from automatically updating the browser's User-Agent string, which is a text identifier that tells websites which browser and version you're using." + }, + "privacy.query_stripping.enabled": { + "value": true, + "title": "Strip known tracking query parameters from URLs" + }, + "dom.private-attribution.submission.enabled": { + "value": false, + "title": "Disable Private Attribution Submission Control", + "shouldBeDefault": true + }, + "privacy.trackingprotection.enabled": { + "value": true, + "title": "Enable Tracking Protection" + }, + "privacy.trackingprotection.socialtracking.enabled": { + "value": true, + "title": "Enable Social Tracking Protection" + }, + "privacy.trackingprotection.cryptomining.enabled": { + "value": true, + "title": "Enable Cryptomining Protection" + }, + "privacy.trackingprotection.fingerprinting.enabled": { + "value": true, + "title": "Enable Fingerprinting Tracking Protection" + } + } + }, + "Resist Fingerprinting": { + "description": "Advanced Fingerprinting protection. This might break websites. For best results make sure to enable \"Privacy\" hardenings.", + "preferences": { + "privacy.resistFingerprinting": { + "value": true, + "title": "Resist Fingerprinting" + }, + "privacy.resistFingerprinting.letterboxing": { + "value": true, + "title": "Enable Letterboxing" + }, + "webgl.disabled": { + "value": true, + "title": "Disable WebGL (Web Graphics Library)" + }, + "privacy.partition.network_state.ocsp_cache": { + "value": true, + "title": "Enable OCSP cache" + } + } + }, + "Attack Surface Reduction": { + "preferences": { + "pdfjs.enableScripting": { + "value": false, + "title": "Disable PDFJS scripting" + }, + "mathml.disabled": { + "value": true, + "title": "Disable MathML (Mathematical Markup Language)" + }, + "gfx.font_rendering.graphite.enabled": { + "value": false, + "title": "Disable graphite" + }, + "javascript.options.ion": { + "value": false, + "title": "Disable Ion" + }, + "javascript.options.baselinejit": { + "value": false, + "title": "Disable baseline JIT" + }, + "javascript.options.jit_trustedprincipals": { + "value": false, + "title": "Disable JIT trustedprincipals" + }, + "javascript.options.asmjs": { + "value": false, + "title": "Disable asm.js" + } + } + }, + "Disk Avoidance": { + "description": "Control what data to persist to disk", + "preferences": { + "browser.cache.disk.enable": { + "value": false, + "title": "Disable disk cache", + "description": "Don't cache data to disk. Might impact performance negatively", + "requireUserOptIn": true + }, + "browser.privatebrowsing.forceMediaMemoryCache": { + "value": true, + "title": "Set media cache in Private Browsing to in-memory" + }, + "media.memory_cache_max_size": { + "value": 65536, + "title": "Increase in-memory media cache maximum size" + }, + "browser.sessionstore.privacy_level": { + "value": 2, + "title": "Disable storing extra session data", + "description": "Extra session data such as form content, cookies and POST data" + } + } + }, "Miscelaneous": { "preferences": { "permissions.manager.defaultsUrl": { @@ -212,48 +292,13 @@ "value": true, "title": "Use Punycode in Internationalized Domain Names to eliminate possible spoofing" }, - "extensions.enabledScopes": { - "value": 5, - "title": "Limit allowed extension directories", - "description": "Only allow profile and application directories", - "shouldBeDefault": true - }, "extensions.postDownloadThirdPartyPrompt": { "value": false, "title": "Disable bypassing 3rd party extension install prompts" }, - "pdfjs.enableScripting": { + "layout.css.visited_links_enabled": { "value": false, - "title": "Disable PDFJS scripting" - } - } - }, - "Privacy": { - "preferences": { - "privacy.globalprivacycontrol.enabled": { - "value": true, - "title": "Enable Global Privacy Control (GPC)", - "shouldBeDefault": true - }, - "browser.contentblocking.category": { - "value": "strict", - "title": "Enable ETP Strict Mode", - "description": "ETP Strict Mode enables Total Cookie Protection (TCP)" - }, - "network.http.referer.XOriginTrimmingPolicy": { - "value": 2, - "title": "Trim cross-origin referrers", - "description": "Trims down referrers to just the scheme, hostname and port" - }, - "general.useragent.updates.enabled": { - "value": false, - "title": "Disable automatic User Agent updates", - "description": "This preference controls whether Firefox automatically updates its User-Agent string. When set to false, it prevents Firefox from automatically updating the browser's User-Agent string, which is a text identifier that tells websites which browser and version you're using." - }, - "dom.private-attribution.submission.enabled": { - "value": false, - "title": "Disable Private Attribution Submission Control", - "shouldBeDefault": true + "title": "Disable coloring of visited links" } } } diff --git a/app/lib/core/routing/routes.dart b/app/lib/core/routing/routes.dart index 4619c5d0..2aee79e2 100644 --- a/app/lib/core/routing/routes.dart +++ b/app/lib/core/routing/routes.dart @@ -11,11 +11,16 @@ import 'package:lensai/features/chat_archive/presentation/screens/list.dart'; import 'package:lensai/features/chat_archive/presentation/screens/search.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_edit.dart'; import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart'; import 'package:lensai/features/search/presentation/screens/search.dart'; -import 'package:lensai/features/settings/presentation/screens/browser_hardening.dart'; -import 'package:lensai/features/settings/presentation/screens/browser_hardening_group.dart'; +import 'package:lensai/features/settings/presentation/screens/bang_settings.dart'; +import 'package:lensai/features/settings/presentation/screens/general_settings.dart'; import 'package:lensai/features/settings/presentation/screens/settings.dart'; +import 'package:lensai/features/settings/presentation/screens/web_engine_hardening.dart'; +import 'package:lensai/features/settings/presentation/screens/web_engine_hardening_group.dart'; +import 'package:lensai/features/settings/presentation/screens/web_engine_settings.dart'; import 'package:lensai/features/user/presentation/screens/auth.dart'; part 'routes.g.dart'; @@ -63,6 +68,16 @@ part 'routes.g.dart'; TypedGoRoute( name: 'ContainerListRoute', path: 'containers', + routes: [ + TypedGoRoute( + name: 'ContainerCreateRoute', + path: 'create', + ), + TypedGoRoute( + name: 'ContainerEditRoute', + path: 'edit', + ), + ], ), ], ) @@ -179,17 +194,49 @@ class ContainerListRoute extends GoRouteData { } } +class ContainerEditRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return ContainerEditScreen.edit( + initialContainer: state.extra! as ContainerData, + ); + } +} + +class ContainerCreateRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return ContainerEditScreen.create( + initialContainer: state.extra! as ContainerData, + ); + } +} + @TypedGoRoute( name: 'SettingsRoute', path: '/settings', routes: [ - TypedGoRoute( - name: 'BrowserHardeningRoute', - path: 'hardening', + TypedGoRoute( + name: 'GeneralSettingsRoute', + path: 'general', + ), + TypedGoRoute( + name: 'BangSettingsRoute', + path: 'bang', + ), + TypedGoRoute( + name: 'WebEngineSettingsRoute', + path: 'web_engine', routes: [ - TypedGoRoute( - name: 'BrowserHardeningGroupRoute', - path: 'group/:group', + TypedGoRoute( + name: 'WebEngineHardeningRoute', + path: 'hardening', + routes: [ + TypedGoRoute( + name: 'WebEngineHardeningGroupRoute', + path: 'group/:group', + ), + ], ), ], ), @@ -202,21 +249,42 @@ class SettingsRoute extends GoRouteData { } } -class BrowserHardeningRoute extends GoRouteData { +class GeneralSettingsRoute extends GoRouteData { @override Widget build(BuildContext context, GoRouterState state) { - return const BrowserHardeningScreen(); + return const GeneralSettingsScreen(); } } -class BrowserHardeningGroupRoute extends GoRouteData { +class BangSettingsRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const BangSettingsScreen(); + } +} + +class WebEngineSettingsRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const WebEngineSettingsScreen(); + } +} + +class WebEngineHardeningRoute extends GoRouteData { + @override + Widget build(BuildContext context, GoRouterState state) { + return const WebEngineHardeningScreen(); + } +} + +class WebEngineHardeningGroupRoute extends GoRouteData { final String group; - const BrowserHardeningGroupRoute({required this.group}); + const WebEngineHardeningGroupRoute({required this.group}); @override Widget build(BuildContext context, GoRouterState state) { - return BrowserHardeningGroupScreen(groupName: group); + return WebEngineHardeningGroupScreen(groupName: group); } } diff --git a/app/lib/core/routing/routes.g.dart b/app/lib/core/routing/routes.g.dart index 91b3a7c0..a8a7e9bd 100644 --- a/app/lib/core/routing/routes.g.dart +++ b/app/lib/core/routing/routes.g.dart @@ -65,6 +65,18 @@ RouteBase get $browserRoute => GoRouteData.$route( path: 'containers', name: 'ContainerListRoute', factory: $ContainerListRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'create', + name: 'ContainerCreateRoute', + factory: $ContainerCreateRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'edit', + name: 'ContainerEditRoute', + factory: $ContainerEditRouteExtension._fromState, + ), + ], ), ], ); @@ -255,20 +267,73 @@ extension $ContainerListRouteExtension on ContainerListRoute { void replace(BuildContext context) => context.replace(location); } +extension $ContainerCreateRouteExtension on ContainerCreateRoute { + static ContainerCreateRoute _fromState(GoRouterState state) => + ContainerCreateRoute(); + + String get location => GoRouteData.$location( + '/containers/create', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $ContainerEditRouteExtension on ContainerEditRoute { + static ContainerEditRoute _fromState(GoRouterState state) => + ContainerEditRoute(); + + String get location => GoRouteData.$location( + '/containers/edit', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + RouteBase get $settingsRoute => GoRouteData.$route( path: '/settings', name: 'SettingsRoute', factory: $SettingsRouteExtension._fromState, routes: [ GoRouteData.$route( - path: 'hardening', - name: 'BrowserHardeningRoute', - factory: $BrowserHardeningRouteExtension._fromState, + path: 'general', + name: 'GeneralSettingsRoute', + factory: $GeneralSettingsRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'bang', + name: 'BangSettingsRoute', + factory: $BangSettingsRouteExtension._fromState, + ), + GoRouteData.$route( + path: 'web_engine', + name: 'WebEngineSettingsRoute', + factory: $WebEngineSettingsRouteExtension._fromState, routes: [ GoRouteData.$route( - path: 'group/:group', - name: 'BrowserHardeningGroupRoute', - factory: $BrowserHardeningGroupRouteExtension._fromState, + path: 'hardening', + name: 'WebEngineHardeningRoute', + factory: $WebEngineHardeningRouteExtension._fromState, + routes: [ + GoRouteData.$route( + path: 'group/:group', + name: 'WebEngineHardeningGroupRoute', + factory: $WebEngineHardeningGroupRouteExtension._fromState, + ), + ], ), ], ), @@ -292,12 +357,12 @@ extension $SettingsRouteExtension on SettingsRoute { void replace(BuildContext context) => context.replace(location); } -extension $BrowserHardeningRouteExtension on BrowserHardeningRoute { - static BrowserHardeningRoute _fromState(GoRouterState state) => - BrowserHardeningRoute(); +extension $GeneralSettingsRouteExtension on GeneralSettingsRoute { + static GeneralSettingsRoute _fromState(GoRouterState state) => + GeneralSettingsRoute(); String get location => GoRouteData.$location( - '/settings/hardening', + '/settings/general', ); void go(BuildContext context) => context.go(location); @@ -310,14 +375,69 @@ extension $BrowserHardeningRouteExtension on BrowserHardeningRoute { void replace(BuildContext context) => context.replace(location); } -extension $BrowserHardeningGroupRouteExtension on BrowserHardeningGroupRoute { - static BrowserHardeningGroupRoute _fromState(GoRouterState state) => - BrowserHardeningGroupRoute( +extension $BangSettingsRouteExtension on BangSettingsRoute { + static BangSettingsRoute _fromState(GoRouterState state) => + BangSettingsRoute(); + + String get location => GoRouteData.$location( + '/settings/bang', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $WebEngineSettingsRouteExtension on WebEngineSettingsRoute { + static WebEngineSettingsRoute _fromState(GoRouterState state) => + WebEngineSettingsRoute(); + + String get location => GoRouteData.$location( + '/settings/web_engine', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $WebEngineHardeningRouteExtension on WebEngineHardeningRoute { + static WebEngineHardeningRoute _fromState(GoRouterState state) => + WebEngineHardeningRoute(); + + String get location => GoRouteData.$location( + '/settings/web_engine/hardening', + ); + + void go(BuildContext context) => context.go(location); + + Future push(BuildContext context) => context.push(location); + + void pushReplacement(BuildContext context) => + context.pushReplacement(location); + + void replace(BuildContext context) => context.replace(location); +} + +extension $WebEngineHardeningGroupRouteExtension + on WebEngineHardeningGroupRoute { + static WebEngineHardeningGroupRoute _fromState(GoRouterState state) => + WebEngineHardeningGroupRoute( group: state.pathParameters['group']!, ); String get location => GoRouteData.$location( - '/settings/hardening/group/${Uri.encodeComponent(group)}', + '/settings/web_engine/hardening/group/${Uri.encodeComponent(group)}', ); void go(BuildContext context) => context.go(location); diff --git a/app/lib/data/database/converters/color.dart b/app/lib/data/database/converters/color.dart index 74a2df06..3c5cea64 100644 --- a/app/lib/data/database/converters/color.dart +++ b/app/lib/data/database/converters/color.dart @@ -1,6 +1,21 @@ import 'dart:ui'; import 'package:drift/drift.dart'; +import 'package:json_annotation/json_annotation.dart'; + +class ColorJsonConverter implements JsonConverter { + const ColorJsonConverter(); + + @override + Color fromJson(int from) { + return Color(from); + } + + @override + int toJson(Color value) { + return value.value; + } +} class ColorConverter extends TypeConverter { const ColorConverter(); diff --git a/app/lib/data/database/functions/lexo_rank_functions.dart b/app/lib/data/database/functions/lexo_rank_functions.dart index 76e3c9d3..da668e9c 100644 --- a/app/lib/data/database/functions/lexo_rank_functions.dart +++ b/app/lib/data/database/functions/lexo_rank_functions.dart @@ -1,3 +1,4 @@ +import 'package:lensai/extensions/nullable.dart'; import 'package:lexo_rank/lexo_rank.dart'; import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart'; import 'package:sqlite3/common.dart'; @@ -27,8 +28,8 @@ String _previousRankOrMiddle(List args) { } String _reorderAfter(List 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; + final first = args[0].mapNotNull((arg) => LexoRank.parse(arg as String)); + final last = args[1].mapNotNull((arg) => LexoRank.parse(arg as String)); if (first == null) { throw Exception('Tab not found'); diff --git a/app/lib/domain/services/generic_website.dart b/app/lib/domain/services/generic_website.dart index 13c23c12..cead5471 100644 --- a/app/lib/domain/services/generic_website.dart +++ b/app/lib/domain/services/generic_website.dart @@ -10,6 +10,7 @@ import 'package:html/parser.dart' as html_parser; import 'package:http/http.dart' as http; import 'package:lensai/core/http_error_handler.dart'; import 'package:lensai/data/models/web_page_info.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart'; import 'package:lensai/utils/lru_cache.dart'; @@ -267,7 +268,7 @@ class GenericWebsiteService extends _$GenericWebsiteService { url.origin, await BrowserIcon.fromBytes( result.image, - dominantColor: (result.color != null) ? Color(result.color!) : null, + dominantColor: result.color.mapNotNull((color) => Color(color)), source: result.source, ), ); diff --git a/app/lib/domain/services/generic_website.g.dart b/app/lib/domain/services/generic_website.g.dart index abb68817..3a5035e9 100644 --- a/app/lib/domain/services/generic_website.g.dart +++ b/app/lib/domain/services/generic_website.g.dart @@ -7,7 +7,7 @@ part of 'generic_website.dart'; // ************************************************************************** String _$genericWebsiteServiceHash() => - r'8eab8ab1448d4826dc0cda0ff6b47143735ba176'; + r'27430ba17a04b0fa3cca20abf00f9d1c15fa843a'; /// See also [GenericWebsiteService]. @ProviderFor(GenericWebsiteService) diff --git a/app/lib/extensions/nullable.dart b/app/lib/extensions/nullable.dart new file mode 100644 index 00000000..9666ff26 --- /dev/null +++ b/app/lib/extensions/nullable.dart @@ -0,0 +1,7 @@ +extension NullableX on T? { + @pragma('vm:prefer-inline') + R? mapNotNull(R? Function(T) callback) { + // ignore: null_check_on_nullable_type_parameter + return (this != null) ? callback(this!) : null; + } +} diff --git a/app/lib/features/bangs/data/database/daos/bang.dart b/app/lib/features/bangs/data/database/daos/bang.dart index a98b4c64..224e19ab 100644 --- a/app/lib/features/bangs/data/database/daos/bang.dart +++ b/app/lib/features/bangs/data/database/daos/bang.dart @@ -1,4 +1,5 @@ import 'package:drift/drift.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; @@ -20,7 +21,7 @@ class BangDao extends DatabaseAccessor with _$BangDaoMixin { SingleSelectable getBangCount({Iterable? groups}) { return db.bang.count( - where: (groups != null) ? (t) => t.group.isInValues(groups) : null, + where: groups.mapNotNull((groups) => (t) => t.group.isInValues(groups)), ); } diff --git a/app/lib/features/bangs/domain/repositories/data.dart b/app/lib/features/bangs/domain/repositories/data.dart index b91ae862..ae40e303 100644 --- a/app/lib/features/bangs/domain/repositories/data.dart +++ b/app/lib/features/bangs/domain/repositories/data.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'package:drift/drift.dart'; import 'package:exceptions/exceptions.dart'; import 'package:lensai/domain/services/generic_website.dart'; -import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/models/search_history_entry.dart'; @@ -14,23 +13,27 @@ part 'data.g.dart'; @Riverpod(keepAlive: true) class BangDataRepository extends _$BangDataRepository { - late BangDatabase _db; - @override - void build() { - _db = ref.watch(bangDatabaseProvider); - } + void build() {} Stream watchBang(String? trigger) { if (trigger != null) { - return _db.bangDao.getBangData(trigger).watchSingleOrNull(); + return ref + .read(bangDatabaseProvider) + .bangDao + .getBangData(trigger) + .watchSingleOrNull(); } else { return Stream.value(null); } } Stream>> watchCategories() { - return _db.categoriesJson().watchSingle().map((json) { + return ref + .read(bangDatabaseProvider) + .categoriesJson() + .watchSingle() + .map((json) { final decoded = jsonDecode(json) as Map; return decoded.map( (key, value) => MapEntry(key, (value as List).cast()), @@ -39,7 +42,10 @@ class BangDataRepository extends _$BangDataRepository { } Stream watchBangCount(BangGroup group) { - return _db.bangDao.getBangCount(groups: [group]).watchSingle(); + return ref + .read(bangDatabaseProvider) + .bangDao + .getBangCount(groups: [group]).watchSingle(); } Stream> watchBangs({ @@ -48,7 +54,9 @@ class BangDataRepository extends _$BangDataRepository { ({String category, String? subCategory})? categoryFilter, bool? orderMostFrequentFirst, }) { - return _db.bangDao + return ref + .read(bangDatabaseProvider) + .bangDao .getBangDataList( groups: groups, domain: domain, @@ -60,15 +68,25 @@ class BangDataRepository extends _$BangDataRepository { } Stream> watchFrequentBangs({Iterable? groups}) { - return _db.bangDao.getFrequentBangDataList(groups: groups).watch(); + return ref + .read(bangDatabaseProvider) + .bangDao + .getFrequentBangDataList(groups: groups) + .watch(); } Stream> watchSearchHistory({required int limit}) { - return _db.searchHistoryEntries(limit: limit).watch(); + return ref + .read(bangDatabaseProvider) + .searchHistoryEntries(limit: limit) + .watch(); } Future increaseFrequency(String trigger) { - return _db.bangDao.increaseBangFrequency(trigger); + return ref + .read(bangDatabaseProvider) + .bangDao + .increaseBangFrequency(trigger); } Future addSearchEntry( @@ -76,17 +94,21 @@ class BangDataRepository extends _$BangDataRepository { String searchQuery, { required int maxEntryCount, }) { + final db = ref.read(bangDatabaseProvider); //Pack in a transaction to bundle rebuilds of watch() queries - return _db.transaction( + return db.transaction( () async { - await _db.bangDao.addSearchEntry(trigger, searchQuery); - await _db.evictHistoryEntries(limit: maxEntryCount); + await db.bangDao.addSearchEntry(trigger, searchQuery); + await db.evictHistoryEntries(limit: maxEntryCount); }, ); } Future removeSearchEntry(String searchQuery) { - return _db.bangDao.removeSearchEntry(searchQuery); + return ref + .read(bangDatabaseProvider) + .bangDao + .removeSearchEntry(searchQuery); } Future> ensureIconAvailable(BangData bang) async { @@ -110,10 +132,13 @@ class BangDataRepository extends _$BangDataRepository { } Future resetFrequencies() { - return _db.bangFrequency.deleteAll(); + return ref.read(bangDatabaseProvider).bangFrequency.deleteAll(); } Future resetFrequency(String trigger) { - return _db.bangFrequency.deleteWhere((t) => t.trigger.equals(trigger)); + return ref + .read(bangDatabaseProvider) + .bangFrequency + .deleteWhere((t) => t.trigger.equals(trigger)); } } diff --git a/app/lib/features/bangs/domain/repositories/data.g.dart b/app/lib/features/bangs/domain/repositories/data.g.dart index 2b150e45..a7452919 100644 --- a/app/lib/features/bangs/domain/repositories/data.g.dart +++ b/app/lib/features/bangs/domain/repositories/data.g.dart @@ -7,7 +7,7 @@ part of 'data.dart'; // ************************************************************************** String _$bangDataRepositoryHash() => - r'b51786552a812ddd54d1418450fe03ff89351e28'; + r'2c009519cf02df4d3cbb6768dbe55e9eb696e126'; /// See also [BangDataRepository]. @ProviderFor(BangDataRepository) diff --git a/app/lib/features/bangs/domain/repositories/search.dart b/app/lib/features/bangs/domain/repositories/search.dart index a1241fdf..e65699f2 100644 --- a/app/lib/features/bangs/domain/repositories/search.dart +++ b/app/lib/features/bangs/domain/repositories/search.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:lensai/features/bangs/data/database/database.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/providers.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart'; @@ -11,17 +10,20 @@ part 'search.g.dart'; @Riverpod() class BangSearch extends _$BangSearch { late StreamController> _streamController; - late BangDatabase _db; Future search(String input) async { if (input.isNotEmpty) { - await _db.bangDao.queryBangs(input).get().then(_streamController.add); + await ref + .read(bangDatabaseProvider) + .bangDao + .queryBangs(input) + .get() + .then(_streamController.add); } } @override Stream> build() { - _db = ref.watch(bangDatabaseProvider); _streamController = StreamController(); ref.onDispose(() async { diff --git a/app/lib/features/bangs/domain/repositories/search.g.dart b/app/lib/features/bangs/domain/repositories/search.g.dart index a0dadc4c..82d09180 100644 --- a/app/lib/features/bangs/domain/repositories/search.g.dart +++ b/app/lib/features/bangs/domain/repositories/search.g.dart @@ -6,7 +6,7 @@ part of 'search.dart'; // RiverpodGenerator // ************************************************************************** -String _$bangSearchHash() => r'abd71f10288ac1f559e66fc11035f9f2b027c353'; +String _$bangSearchHash() => r'632ddd09a1198394884cb5c27a2bde7850c0515b'; /// See also [BangSearch]. @ProviderFor(BangSearch) diff --git a/app/lib/features/bangs/domain/repositories/sync.dart b/app/lib/features/bangs/domain/repositories/sync.dart index 285c5b61..407d660f 100644 --- a/app/lib/features/bangs/domain/repositories/sync.dart +++ b/app/lib/features/bangs/domain/repositories/sync.dart @@ -11,10 +11,6 @@ part 'sync.g.dart'; @Riverpod(keepAlive: true) class BangSyncRepository extends _$BangSyncRepository { - late BangDatabase _db; - - BangSyncRepository(); - static Future> _fetchAndSync({ required BangSourceService sourceService, required BangDatabase db, @@ -51,22 +47,22 @@ class BangSyncRepository extends _$BangSyncRepository { ) async { try { return Result.success( - await _db.computeWithDatabase( - connect: BangDatabase.new, - computation: (db) async { - final ref = ProviderContainer(); - final result = await _fetchAndSync( - sourceService: ref.read(bangSourceServiceProvider.notifier), - db: db, - url: Uri.parse(group.url), - group: group, - syncInterval: syncInterval, - ); + await ref.read(bangDatabaseProvider).computeWithDatabase( + connect: BangDatabase.new, + computation: (db) async { + final ref = ProviderContainer(); + final result = await _fetchAndSync( + sourceService: ref.read(bangSourceServiceProvider.notifier), + db: db, + url: Uri.parse(group.url), + group: group, + syncInterval: syncInterval, + ); - //Throw if necessary - return result.value; - }, - ), + //Throw if necessary + return result.value; + }, + ), ); } catch (e) { return Result.failure( @@ -80,7 +76,11 @@ class BangSyncRepository extends _$BangSyncRepository { } Stream watchLastSyncOfGroup(BangGroup group) { - return _db.syncDao.lastSyncOfGroup(group).watchSingleOrNull(); + return ref + .read(bangDatabaseProvider) + .syncDao + .lastSyncOfGroup(group) + .watchSingleOrNull(); } Future>> syncBangGroups({ @@ -100,7 +100,5 @@ class BangSyncRepository extends _$BangSyncRepository { } @override - void build() { - _db = ref.watch(bangDatabaseProvider); - } + void build() {} } diff --git a/app/lib/features/bangs/domain/repositories/sync.g.dart b/app/lib/features/bangs/domain/repositories/sync.g.dart index 2a37a931..48cbdc99 100644 --- a/app/lib/features/bangs/domain/repositories/sync.g.dart +++ b/app/lib/features/bangs/domain/repositories/sync.g.dart @@ -7,7 +7,7 @@ part of 'sync.dart'; // ************************************************************************** String _$bangSyncRepositoryHash() => - r'86933b05d01629c67ed0443df91e991c40136423'; + r'a46813f953e550585f432b11c364d87791819c34'; /// See also [BangSyncRepository]. @ProviderFor(BangSyncRepository) diff --git a/app/lib/features/bangs/presentation/screens/list.dart b/app/lib/features/bangs/presentation/screens/list.dart index c8a63914..25474aa0 100644 --- a/app/lib/features/bangs/presentation/screens/list.dart +++ b/app/lib/features/bangs/presentation/screens/list.dart @@ -2,6 +2,7 @@ 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/extensions/nullable.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; @@ -21,9 +22,8 @@ class BangListScreen extends HookConsumerWidget { final bangsAsync = ref.watch( bangDataListProvider( filter: ( - categoryFilter: (category != null) - ? (category: category!, subCategory: subCategory) - : null, + categoryFilter: category.mapNotNull( + (category) => (category: category, subCategory: subCategory)), domain: null, groups: null, orderMostFrequentFirst: null, diff --git a/app/lib/features/bangs/presentation/screens/search.dart b/app/lib/features/bangs/presentation/screens/search.dart index eec5818f..6286a224 100644 --- a/app/lib/features/bangs/presentation/screens/search.dart +++ b/app/lib/features/bangs/presentation/screens/search.dart @@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/domain/repositories/search.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; import 'package:lensai/features/geckoview/features/browser/domain/providers.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; @@ -20,9 +20,7 @@ class BangSearchScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final resultsAsync = ref.watch(bangSearchProvider); - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select((value) => value.incognitoMode), - ); + final incognitoEnabled = ref.watch(incognitoModeEnabledProvider); final textEditingController = useTextEditingController(text: initialSearchText); diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart index 6b44faf1..dc691498 100644 --- a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.dart @@ -1,5 +1,4 @@ import 'package:exceptions/exceptions.dart'; -import 'package:lensai/features/chat/features/chat_store/data/database/database.dart'; import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart'; import 'package:lensai/features/chat/features/chat_store/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -8,16 +7,15 @@ part 'chat_metadata.g.dart'; @Riverpod() class ChatMetadataRepository extends _$ChatMetadataRepository { - late ChatDatabase _db; - Future> updateMetadata(ChatMetadata metadata) { return Result.fromAsync(() { - return _db.metadataDao.updateChatMetadata(chatId, metadata); + return ref + .read(chatDatabaseProvider) + .metadataDao + .updateChatMetadata(chatId, metadata); }); } @override - void build(String chatId) { - _db = ref.watch(chatDatabaseProvider); - } + void build(String chatId) {} } diff --git a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart index e7e58fcf..71e0d410 100644 --- a/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart +++ b/app/lib/features/chat/features/chat_store/domain/repositories/chat_metadata.g.dart @@ -7,7 +7,7 @@ part of 'chat_metadata.dart'; // ************************************************************************** String _$chatMetadataRepositoryHash() => - r'72a0226ed7e87f452f4dfe44594bb30663c2db45'; + r'bd6b28b9d034ee82f95e974e4bf8f553546e835a'; /// Copied from Dart SDK class _SystemHash { diff --git a/app/lib/features/chat_archive/domain/entities/chat_entity.dart b/app/lib/features/chat_archive/domain/entities/chat_entity.dart index 58d0ca6a..91a3a261 100644 --- a/app/lib/features/chat_archive/domain/entities/chat_entity.dart +++ b/app/lib/features/chat_archive/domain/entities/chat_entity.dart @@ -1,3 +1,5 @@ +import 'package:lensai/extensions/nullable.dart'; + class ChatEntity { static final _namePattern = RegExp(r"^(.*?) - (.*?)\.md$"); @@ -14,7 +16,7 @@ class ChatEntity { return ChatEntity._( fileName, name: match?.group(1), - dateTime: (match != null) ? DateTime.tryParse(match.group(2)!) : null, + dateTime: match.mapNotNull((match) => DateTime.tryParse(match.group(2)!)), ); } diff --git a/app/lib/features/chat_archive/domain/repositories/search.dart b/app/lib/features/chat_archive/domain/repositories/search.dart index 0abad769..295f5012 100644 --- a/app/lib/features/chat_archive/domain/repositories/search.dart +++ b/app/lib/features/chat_archive/domain/repositories/search.dart @@ -17,7 +17,6 @@ part 'search.g.dart'; class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { late Completer _populatedCompleter; late StreamController> _streamController; - late ChatSearchDatabase _searchDatabase; Future _readChat(ChatEntity chat) async { final contentResult = await ref @@ -64,7 +63,9 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { }) async { if (input.isNotEmpty) { await _populatedCompleter.future; - await _searchDatabase.searchDao + await ref + .read(chatSearchDatabaseProvider) + .searchDao .queryChats( searchString: input, snippetLength: snippetLength, @@ -81,11 +82,12 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { Stream> build() async* { _populatedCompleter = Completer(); _streamController = StreamController(); - _searchDatabase = ref.watch(chatSearchDatabaseProvider); + + final searchDatabase = ref.watch(chatSearchDatabaseProvider); // populate with initial chats - await _searchDatabase.searchDao.deleteAllChats(); - await _searchDatabase.searchDao.indexChats(await _availableChats()); + await searchDatabase.searchDao.deleteAllChats(); + await searchDatabase.searchDao.indexChats(await _availableChats()); _populatedCompleter.complete(); final changeStreamSubscription = @@ -98,11 +100,11 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { if (chat.name != null) { final companion = await _readChat(chat); if (companion != null) { - await _searchDatabase.searchDao.upsertChat(companion); + await searchDatabase.searchDao.upsertChat(companion); } } case ChangeType.REMOVE: - await _searchDatabase.searchDao.deleteChat(chat.fileName); + await searchDatabase.searchDao.deleteChat(chat.fileName); } }); diff --git a/app/lib/features/chat_archive/domain/repositories/search.g.dart b/app/lib/features/chat_archive/domain/repositories/search.g.dart index a96b13b9..da8e8236 100644 --- a/app/lib/features/chat_archive/domain/repositories/search.g.dart +++ b/app/lib/features/chat_archive/domain/repositories/search.g.dart @@ -7,7 +7,7 @@ part of 'search.dart'; // ************************************************************************** String _$chatArchiveSearchRepositoryHash() => - r'd7174cfb877c61ee4e9580f44d7ffd11bf3b4b60'; + r'6762958d43d2efc95f5d9848bfcae41436564f2b'; /// See also [ChatArchiveSearchRepository]. @ProviderFor(ChatArchiveSearchRepository) diff --git a/app/lib/features/chat_archive/presentation/screens/list.dart b/app/lib/features/chat_archive/presentation/screens/list.dart index c4c8a94e..2100ff23 100644 --- a/app/lib/features/chat_archive/presentation/screens/list.dart +++ b/app/lib/features/chat_archive/presentation/screens/list.dart @@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/extensions/date_time.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/chat_archive/domain/repositories/archive.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:skeletonizer/skeletonizer.dart'; @@ -41,9 +42,10 @@ class ChatArchiveListScreen extends HookConsumerWidget { return ListTile( title: Text(chat.toString()), - subtitle: (chat.dateTime != null) - ? Text(chat.dateTime!.formatWithMinutePrecision()) - : null, + subtitle: chat.dateTime.mapNotNull( + (chatTime) => + Text(chatTime.formatWithMinutePrecision()), + ), onTap: () async { await context.push( ChatArchiveDetailRoute(fileName: chat.fileName) diff --git a/app/lib/features/chat_archive/presentation/screens/search.dart b/app/lib/features/chat_archive/presentation/screens/search.dart index 4305e3bc..183b970f 100644 --- a/app/lib/features/chat_archive/presentation/screens/search.dart +++ b/app/lib/features/chat_archive/presentation/screens/search.dart @@ -10,7 +10,7 @@ import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/extensions/date_time.dart'; import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart'; import 'package:lensai/features/chat_archive/domain/repositories/search.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/hooks/listenable_callback.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; @@ -20,9 +20,7 @@ class ChatArchiveSearchScreen extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final resultsAsync = ref.watch(chatArchiveSearchRepositoryProvider); - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select((value) => value.incognitoMode), - ); + final incognitoEnabled = ref.watch(incognitoModeEnabledProvider); final textEditingController = useTextEditingController(); diff --git a/app/lib/features/geckoview/domain/entities/tab_state.dart b/app/lib/features/geckoview/domain/entities/tab_state.dart index 066c8321..d4847e02 100644 --- a/app/lib/features/geckoview/domain/entities/tab_state.dart +++ b/app/lib/features/geckoview/domain/entities/tab_state.dart @@ -3,6 +3,7 @@ import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/domain/entities/equatable_image.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart'; import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart'; import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; @@ -24,13 +25,13 @@ class TabState extends WebPageInfo with FastEquatable { final EquatableImage? icon; @override - BrowserIcon? get favicon => (icon != null) - ? BrowserIcon( - image: icon!, + BrowserIcon? get favicon => icon.mapNotNull( + (icon) => BrowserIcon( + image: icon, dominantColor: null, source: IconSource.memory, - ) - : null; + ), + ); final EquatableImage? thumbnail; diff --git a/app/lib/features/geckoview/domain/providers/tab_state.dart b/app/lib/features/geckoview/domain/providers/tab_state.dart index 62f08597..b91ecc2c 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/extensions/image.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart'; import 'package:lensai/features/geckoview/domain/entities/history_state.dart'; import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; @@ -39,10 +40,10 @@ class TabStates extends _$TabStates { Future _onIconChange(IconChangeEvent event) async { final IconChangeEvent(:tabId, :bytes) = event; - final image = (bytes != null) - ? (await tryDecodeImage(bytes) - .then((image) async => image?.toEquatable())) - : null; + final image = await bytes.mapNotNull( + (bytes) => + tryDecodeImage(bytes).then((image) async => image?.toEquatable()), + ); final current = state[tabId] ?? TabState.$default(tabId); state = {...state}..[tabId] = current.copyWith.icon(image); @@ -51,10 +52,10 @@ class TabStates extends _$TabStates { Future _onThumbnailChange(ThumbnailEvent event) async { final ThumbnailEvent(:tabId, :bytes) = event; - final image = (bytes != null) - ? (await tryDecodeImage(bytes) - .then((image) async => image?.toEquatable())) - : null; + final image = await bytes.mapNotNull( + (bytes) => + tryDecodeImage(bytes).then((image) async => image?.toEquatable()), + ); final current = state[tabId] ?? TabState.$default(tabId); state = {...state}..[tabId] = current.copyWith.thumbnail(image); diff --git a/app/lib/features/geckoview/domain/providers/tab_state.g.dart b/app/lib/features/geckoview/domain/providers/tab_state.g.dart index 90578176..de40ce7a 100644 --- a/app/lib/features/geckoview/domain/providers/tab_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/tab_state.g.dart @@ -173,7 +173,7 @@ final selectedTabStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SelectedTabStateRef = AutoDisposeProviderRef; -String _$tabStatesHash() => r'77f07f330dfac8dc8d28d18737203c7c122f3b74'; +String _$tabStatesHash() => r'296d85ec016b22236b5830fce6b01cda81fbc02d'; /// See also [TabStates]. @ProviderFor(TabStates) diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart index b315ae05..d622550b 100644 --- a/app/lib/features/geckoview/domain/providers/web_extensions_state.dart +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/domain/entities/equatable_image.dart'; import 'package:lensai/extensions/image.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/entities/web_extension_state.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart'; @@ -33,12 +34,10 @@ class WebExtensionsState extends _$WebExtensionsState { title: data.title, enabled: data.enabled ?? current.enabled, badgeText: data.badgeText, - badgeTextColor: (data.badgeTextColor != null) - ? Color(data.badgeTextColor!) - : null, - badgeBackgroundColor: (data.badgeBackgroundColor != null) - ? Color(data.badgeBackgroundColor!) - : null, + badgeTextColor: + data.badgeTextColor.mapNotNull((color) => Color(color)), + badgeBackgroundColor: + data.badgeBackgroundColor.mapNotNull((color) => Color(color)), ); } else { if (state.containsKey(extensionId)) { diff --git a/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart index 581d5c42..bdd4a44e 100644 --- a/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart +++ b/app/lib/features/geckoview/domain/providers/web_extensions_state.g.dart @@ -7,7 +7,7 @@ part of 'web_extensions_state.dart'; // ************************************************************************** String _$webExtensionsStateHash() => - r'bbaba8d27980046275858d2707bc66c6e349a537'; + r'4e6dc46e49ff353e1c3cc4616008685c0e21501d'; /// Copied from Dart SDK class _SystemHash { diff --git a/app/lib/features/geckoview/domain/repositories/tab.dart b/app/lib/features/geckoview/domain/repositories/tab.dart index 520a6d15..eb4aae64 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.dart @@ -2,14 +2,15 @@ import 'dart:async'; import 'package:drift/drift.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/extensions/nullable.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/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_list.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.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:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:lensai/utils/debouncer.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -19,31 +20,59 @@ part 'tab.g.dart'; class TabRepository extends _$TabRepository { final _tabsService = GeckoTabService(); - late TabDatabase _db; - Future 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? additionalHeaders, - }) { - return _tabsService.addTab( - url: url, - selectTab: selectTab, - startLoading: startLoading, - parentId: parentId, - flags: flags, - contextId: contextId, - source: source, - private: private, - historyMetadata: historyMetadata, - additionalHeaders: additionalHeaders, + }) async { + final selectedContainer = + await ref.read(selectedContainerProvider.notifier).fetchData(); + + return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional( + () { + return _tabsService.addTab( + url: url, + selectTab: selectTab, + startLoading: startLoading, + parentId: parentId, + flags: flags, + contextId: selectedContainer?.metadata.contextualIdentity, + source: source, + private: private, + historyMetadata: historyMetadata, + additionalHeaders: additionalHeaders, + ); + }, + containerId: Value(selectedContainer?.id), + ); + } + + Future duplicateTab({ + required String? selectTabId, + String? containerId, + bool selectTab = true, + }) async { + final containerData = await containerId.mapNotNull( + (containerId) => ref + .read(containerRepositoryProvider.notifier) + .getContainerData(containerId), + ); + + return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional( + () { + return _tabsService.duplicateTab( + selectTabId: selectTabId, + newContextId: containerData?.metadata.contextualIdentity, + selectNewTab: selectTab, + ); + }, + containerId: Value(containerData?.id), ); } @@ -67,18 +96,19 @@ class TabRepository extends _$TabRepository { final tabStateDebouncer = Debouncer(const Duration(seconds: 3)); Map? debounceStartValue; - _db = ref.watch(tabDatabaseProvider); + final db = ref.watch(tabDatabaseProvider); final tabAddedSub = eventSerivce.tabAddedStream.listen( (tabId) async { final containerId = ref.read(selectedContainerProvider); - await _db.tabDao.upsertTab(tabId, containerId: Value(containerId)); + await db.tabDao + .upsertUnassignedTab(tabId, containerId: Value(containerId)); }, ); final tabContentSub = tabContentService.tabContentStream.listen((content) async { - await _db.tabDao.updateTabContent( + await db.tabDao.updateTabContent( content.tabId, isProbablyReaderable: content.isProbablyReaderable, extractedContentMarkdown: content.extractedContentMarkdown, @@ -92,7 +122,7 @@ class TabRepository extends _$TabRepository { selectedTabProvider, (previous, tabId) async { if (tabId != null) { - await _db.tabDao.touchTab(tabId, timestamp: DateTime.now()); + await db.tabDao.touchTab(tabId, timestamp: DateTime.now()); } }, ); @@ -104,7 +134,7 @@ class TabRepository extends _$TabRepository { final syncTabs = next.isNotEmpty || (previous?.isNotEmpty ?? false); if (syncTabs) { - await _db.tabDao.syncTabs(retainTabIds: next); + await db.tabDao.syncTabs(retainTabIds: next); } }, ); @@ -120,7 +150,7 @@ class TabRepository extends _$TabRepository { } tabStateDebouncer.eventOccured(() async { - await _db.tabDao.updateTabs(debounceStartValue, next); + await db.tabDao.updateTabs(debounceStartValue, next); }); }, ); diff --git a/app/lib/features/geckoview/domain/repositories/tab.g.dart b/app/lib/features/geckoview/domain/repositories/tab.g.dart index 5f315035..889ecae9 100644 --- a/app/lib/features/geckoview/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabRepositoryHash() => r'b9caf89fe372b79f37c4a3ebff800ccc2d9289d2'; +String _$tabRepositoryHash() => r'85af29fa18ea5a4a67fbffbb9478c801dafddac2'; /// See also [TabRepository]. @ProviderFor(TabRepository) diff --git a/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.dart b/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.dart new file mode 100644 index 00000000..b8694ec6 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.dart @@ -0,0 +1,34 @@ +import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; +import 'package:lensai/features/user/data/models/general_settings.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'delete_browser_data.g.dart'; + +@Riverpod() +class DeleteBrowserDataService extends _$DeleteBrowserDataService { + final _service = GeckoDeleteBrowserDataService(); + + Future deleteData(Set? types) async { + if (types != null) { + for (final type in types) { + switch (type) { + case DeleteBrowsingDataType.tabs: + await _service.deleteTabs(); + case DeleteBrowsingDataType.history: + await _service.deleteBrowsingHistory(); + case DeleteBrowsingDataType.cookies: + await _service.deleteCookiesAndSiteData(); + case DeleteBrowsingDataType.cache: + await _service.deleteCachedFiles(); + case DeleteBrowsingDataType.permissions: + await _service.deleteSitePermissions(); + case DeleteBrowsingDataType.downloads: + await _service.deleteDownloads(); + } + } + } + } + + @override + void build() {} +} diff --git a/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.g.dart b/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.g.dart new file mode 100644 index 00000000..28eaf360 --- /dev/null +++ b/app/lib/features/geckoview/features/browser/domain/services/delete_browser_data.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'delete_browser_data.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$deleteBrowserDataServiceHash() => + r'f5fdbac17c14693fceac913591750ca9285b496c'; + +/// See also [DeleteBrowserDataService]. +@ProviderFor(DeleteBrowserDataService) +final deleteBrowserDataServiceProvider = + AutoDisposeNotifierProvider.internal( + DeleteBrowserDataService.new, + name: r'deleteBrowserDataServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$deleteBrowserDataServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DeleteBrowserDataService = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart index 644ee79a..9a1abc78 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.dart @@ -1,10 +1,90 @@ +import 'package:flutter/material.dart' show ThemeMode; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; -import 'package:riverpod/riverpod.dart'; +import 'package:lensai/features/user/domain/repositories/engine_settings.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'engine_settings.g.dart'; @Riverpod(keepAlive: true) -GeckoEngineSettingsService engineSettingsService(Ref ref) { - return GeckoEngineSettingsService(); +class EngineSettingsReplicationService + extends _$EngineSettingsReplicationService { + final _service = GeckoEngineSettingsService(); + + @override + void build() { + var initialSettingsSent = false; + + ref.listen( + generalSettingsRepositoryProvider + .select((settings) => settings.themeMode), + (previous, next) async { + final theme = switch (next) { + ThemeMode.system => ColorScheme.system, + ThemeMode.light => ColorScheme.light, + ThemeMode.dark => ColorScheme.dark, + }; + + await _service.preferredColorScheme(theme); + }, + ); + + ref.listen( + engineSettingsRepositoryProvider, + (previous, next) async { + if (initialSettingsSent && previous != null) { + if (previous.javascriptEnabled != next.javascriptEnabled) { + await _service.javascriptEnabled(next.javascriptEnabled); + } + if (previous.trackingProtectionPolicy != + next.trackingProtectionPolicy) { + await _service + .trackingProtectionPolicy(next.trackingProtectionPolicy); + } + if (previous.httpsOnlyMode != next.httpsOnlyMode) { + await _service.httpsOnlyMode(next.httpsOnlyMode); + } + if (previous.globalPrivacyControlEnabled != + next.globalPrivacyControlEnabled) { + await _service + .globalPrivacyControlEnabled(next.globalPrivacyControlEnabled); + } + if (previous.preferredColorScheme != next.preferredColorScheme) { + await _service.preferredColorScheme(next.preferredColorScheme); + } + if (previous.cookieBannerHandlingMode != + next.cookieBannerHandlingMode) { + await _service + .cookieBannerHandlingMode(next.cookieBannerHandlingMode); + } + if (previous.cookieBannerHandlingModePrivateBrowsing != + next.cookieBannerHandlingModePrivateBrowsing) { + await _service.cookieBannerHandlingModePrivateBrowsing( + next.cookieBannerHandlingModePrivateBrowsing, + ); + } + if (previous.cookieBannerHandlingGlobalRules != + next.cookieBannerHandlingGlobalRules) { + await _service.cookieBannerHandlingGlobalRules( + next.cookieBannerHandlingGlobalRules, + ); + } + if (previous.cookieBannerHandlingGlobalRulesSubFrames != + next.cookieBannerHandlingGlobalRulesSubFrames) { + await _service.cookieBannerHandlingGlobalRulesSubFrames( + next.cookieBannerHandlingGlobalRulesSubFrames, + ); + } + if (previous.webContentIsolationStrategy != + next.webContentIsolationStrategy) { + await _service + .webContentIsolationStrategy(next.webContentIsolationStrategy); + } + } else { + await _service.setDefaultSettings(next); + initialSettingsSent = true; + } + }, + ); + } } diff --git a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart index 95183cff..67a29725 100644 --- a/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart +++ b/app/lib/features/geckoview/features/browser/domain/services/engine_settings.g.dart @@ -6,24 +6,22 @@ part of 'engine_settings.dart'; // RiverpodGenerator // ************************************************************************** -String _$engineSettingsServiceHash() => - r'051d7b6f65eb5dcb07405b4af1bdadd8dd74fdc0'; +String _$engineSettingsReplicationServiceHash() => + r'0729260eeb34b58470622ac9a3a6abb257730a61'; -/// See also [engineSettingsService]. -@ProviderFor(engineSettingsService) -final engineSettingsServiceProvider = - Provider.internal( - engineSettingsService, - name: r'engineSettingsServiceProvider', +/// See also [EngineSettingsReplicationService]. +@ProviderFor(EngineSettingsReplicationService) +final engineSettingsReplicationServiceProvider = + NotifierProvider.internal( + EngineSettingsReplicationService.new, + name: r'engineSettingsReplicationServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$engineSettingsServiceHash, + : _$engineSettingsReplicationServiceHash, dependencies: null, allTransitiveDependencies: null, ); -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -typedef EngineSettingsServiceRef = ProviderRef; +typedef _$EngineSettingsReplicationService = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart index 46ad5750..1c10783e 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/tab_action.dart @@ -1,103 +1,104 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hooks_riverpod/hooks_riverpod.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/providers/tab_state.dart'; -import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; -import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; +// import 'package:collection/collection.dart'; +// import 'package:flutter/material.dart'; +// import 'package:flutter_hooks/flutter_hooks.dart'; +// import 'package:hooks_riverpod/hooks_riverpod.dart'; +// import 'package:lensai/data/models/equatable_iterable.dart'; +// import 'package:lensai/extensions/nullable.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/features/tabs/domain/providers.dart'; +// import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; -class TabActionDialog extends HookConsumerWidget { - final TabState initialTab; +// class TabActionDialog extends HookConsumerWidget { +// final TabState initialTab; - final void Function()? onDismiss; +// final void Function()? onDismiss; - const TabActionDialog({ - required this.initialTab, - this.onDismiss, - super.key, - }); +// const TabActionDialog({ +// required this.initialTab, +// this.onDismiss, +// super.key, +// }); - @override - Widget build(BuildContext context, WidgetRef ref) { - final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; - final tabContainerId = ref.watch( - tabContainerIdProvider(initialTab.id) - .select((value) => value.valueOrNull), - ); +// @override +// Widget build(BuildContext context, WidgetRef ref) { +// final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; +// final tabContainerId = ref.watch( +// tabContainerIdProvider(initialTab.id) +// .select((value) => value.valueOrNull), +// ); - final containers = ref - .watch( - containersWithCountProvider.select( - (value) => EquatableCollection(value.valueOrNull, immutable: true), - ), - ) - .collection; +// final containers = ref +// .watch( +// containersWithCountProvider.select( +// (value) => EquatableCollection(value.valueOrNull, immutable: true), +// ), +// ) +// .collection; - final selectedContainer = containers - ?.firstWhereOrNull((container) => container.id == tabContainerId); +// final selectedContainer = containers +// ?.firstWhereOrNull((container) => container.id == tabContainerId); - final expansionController = useExpansionTileController(); +// final expansionController = useExpansionTileController(); - return Stack( - children: [ - ModalBarrier( - color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, - onDismiss: onDismiss, - ), - SimpleDialog( - titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), - contentPadding: EdgeInsets.zero, - insetPadding: const EdgeInsets.symmetric( - horizontal: 20.0, - vertical: 24.0, - ), - title: ListTile( - leading: RawImage( - image: tabState.icon?.value, - width: 24, - height: 24, - ), - contentPadding: EdgeInsets.zero, - title: Text(tabState.title), - subtitle: Text(tabState.url.authority), - ), - children: [ - SizedBox( - //We need this to stretch the dialog, then padding from dialog is applied - width: double.maxFinite, - child: ExpansionTile( - controller: expansionController, - leading: (selectedContainer != null) - ? CircleAvatar(backgroundColor: selectedContainer.color) - : null, - title: (selectedContainer != null) - ? Text(selectedContainer.name ?? 'New Container') - : const Text('Assign a Container'), - children: containers - ?.where((container) => container.id != tabContainerId) - .map( - (container) => ListTile( - leading: - CircleAvatar(backgroundColor: container.color), - title: Text(container.name ?? 'New Container'), - onTap: () async { - await ref - .read(tabDataRepositoryProvider.notifier) - .assignContainer(initialTab.id, container.id); +// return Stack( +// children: [ +// ModalBarrier( +// color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, +// onDismiss: onDismiss, +// ), +// SimpleDialog( +// titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), +// contentPadding: EdgeInsets.zero, +// insetPadding: const EdgeInsets.symmetric( +// horizontal: 20.0, +// vertical: 24.0, +// ), +// title: ListTile( +// leading: RawImage( +// image: tabState.icon?.value, +// width: 24, +// height: 24, +// ), +// contentPadding: EdgeInsets.zero, +// title: Text(tabState.title), +// subtitle: Text(tabState.url.authority), +// ), +// children: [ +// SizedBox( +// //We need this to stretch the dialog, then padding from dialog is applied +// width: double.maxFinite, +// child: ExpansionTile( +// controller: expansionController, +// leading: selectedContainer?.color.mapNotNull( +// (color) => CircleAvatar(backgroundColor: color), +// ), +// title: (selectedContainer != null) +// ? Text(selectedContainer.name ?? 'New Container') +// : const Text('Assign a Container'), +// children: containers +// ?.where((container) => container.id != tabContainerId) +// .map( +// (container) => ListTile( +// leading: +// CircleAvatar(backgroundColor: container.color), +// title: Text(container.name ?? 'New Container'), +// onTap: () async { +// await ref +// .read(tabDataRepositoryProvider.notifier) +// .assignContainer(initialTab.id, container.id); - expansionController.collapse(); - }, - ), - ) - .toList() ?? - [], - ), - ), - ], - ), - ], - ); - } -} +// expansionController.collapse(); +// }, +// ), +// ) +// .toList() ?? +// [], +// ), +// ), +// ], +// ), +// ], +// ); +// } +// } diff --git a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart index fc71b993..ba55516c 100644 --- a/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart +++ b/app/lib/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart @@ -22,7 +22,7 @@ import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/utils/ui_helper.dart' as ui_helper; @@ -40,9 +40,7 @@ class WebPageDialog extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select((value) => value.incognitoMode), - ); + final incognitoEnabled = ref.watch(incognitoModeEnabledProvider); final availableBangsAsync = ref.watch( bangDataListProvider( diff --git a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart index 0c2ec0be..5a0fd3dd 100644 --- a/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart +++ b/app/lib/features/geckoview/features/browser/presentation/screens/browser.dart @@ -17,7 +17,6 @@ import 'package:lensai/features/geckoview/domain/providers/web_extensions_state. import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/geckoview/features/browser/domain/services/create_tab.dart'; -import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart'; import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart'; @@ -31,7 +30,6 @@ import 'package:lensai/features/geckoview/features/readerview/presentation/widge import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; import 'package:lensai/presentation/hooks/draggable_scrollable_controller.dart'; import 'package:lensai/presentation/hooks/menu_controller.dart'; import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; @@ -77,15 +75,8 @@ class BrowserScreen extends HookConsumerWidget { }, ); - ref.listen( - settingsRepositoryProvider.select((value) => value.enableJavascript), - (previous, next) async { - await ref.read(engineSettingsServiceProvider).javaScriptEnabled(next); - }, - ); - // ref.listen( - // settingsRepositoryProvider + // generalSettingsRepositoryProvider // .select((value) => value.valueOrNull?.kagiSession), // (previous, next) async { // if (next != null && next.isNotEmpty) { @@ -598,7 +589,7 @@ class BrowserScreen extends HookConsumerWidget { MenuItemButton( onPressed: () async { await context - .push(BrowserHardeningRoute().location); + .push(WebEngineHardeningRoute().location); }, leadingIcon: const Icon(Icons.info), child: const Text('Pref'), @@ -648,7 +639,8 @@ class BrowserScreen extends HookConsumerWidget { : null; final tabCount = ref.read( - tabListProvider.select((tabs) => tabs.length)); + tabListProvider.select((tabs) => tabs.length), + ); //Don't do anything if a child route is active if (GoRouterState.of(context).topRoute?.path != diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart index 8f7c58dd..f1b7f6b7 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/browser_view.dart @@ -8,13 +8,21 @@ import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart'; import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/services/delete_browser_data.dart'; +import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart'; import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; +import 'package:lensai/features/user/domain/services/local_authentication.dart'; class BrowserView extends StatefulHookConsumerWidget { final Duration screenshotPeriod; + final FutureOr Function()? postInitializationStep; - const BrowserView({this.screenshotPeriod = const Duration(seconds: 10)}); + const BrowserView({ + this.screenshotPeriod = const Duration(seconds: 10), + this.postInitializationStep, + }); @override ConsumerState createState() => _BrowserViewState(); @@ -63,6 +71,15 @@ class _BrowserViewState extends ConsumerState return GeckoView( preInitializationStep: () async { + await ref + .read(generalSettingsRepositoryProvider.notifier) + .fetch() + .then((settings) { + ref + .read(deleteBrowserDataServiceProvider.notifier) + .deleteData(settings.deleteBrowsingDataOnQuit); + }); + await ref .read(eventServiceProvider) .viewReadyStateEvents @@ -77,6 +94,9 @@ class _BrowserViewState extends ConsumerState }, ); }, + postInitializationStep: () async { + await widget.postInitializationStep?.call(); + }, ); } @@ -109,6 +129,11 @@ class _BrowserViewState extends ConsumerState cacheRepositoryProvider, (previous, next) {}, ); + + ref.listenManual( + engineSettingsReplicationServiceProvider, + (previous, next) {}, + ); } @override @@ -124,6 +149,10 @@ class _BrowserViewState extends ConsumerState _periodicScreenshotUpdate?.cancel(); _timerPaused = true; } + + ref + .read(localAuthenticationServiceProvider.notifier) + .evictCacheOnBackground(); case AppLifecycleState.resumed: if (_timerPaused) { _periodicScreenshotUpdate = diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart index 346681ec..89c60624 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/landing/action.dart @@ -13,7 +13,7 @@ class LandingAction extends HookConsumerWidget { final sessionTokenAvailable = false; // ref.watch( - // settingsRepositoryProvider.select( + // generalSettingsRepositoryProvider.select( // (value) => value.valueOrNull?.kagiSession?.isNotEmpty ?? false, // ), // ); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart index 2636ea33..aa9a7462 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart @@ -2,6 +2,7 @@ import 'package:expandable_page_view/expandable_page_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart'; @@ -24,9 +25,8 @@ class CreateTabSheetWidget extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final sharedContent = useMemoized( - () => (parameter.content != null) - ? SharedContent.parse(parameter.content!) - : null, + () => parameter.content + .mapNotNull((content) => SharedContent.parse(content)), [parameter], ); diff --git a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart index d49b50c4..55b2a891 100644 --- a/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart +++ b/app/lib/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart @@ -56,15 +56,15 @@ class _TabDraggable extends HookConsumerWidget { onClose(); } }, - onDoubleTap: () { - ref.read(overlayDialogControllerProvider.notifier).show( - TabActionDialog( - initialTab: tab, - onDismiss: - ref.read(overlayDialogControllerProvider.notifier).dismiss, - ), - ); - }, + // onDoubleTap: () { + // ref.read(overlayDialogControllerProvider.notifier).show( + // TabActionDialog( + // initialTab: tab, + // onDismiss: + // ref.read(overlayDialogControllerProvider.notifier).dismiss, + // ), + // ); + // }, onDelete: () async { await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id); }, @@ -178,8 +178,8 @@ class _Tab extends HookConsumerWidget { return ContainerChips( selectedContainer: selectedContainer, - onSelected: (container) { - ref + onSelected: (container) async { + await ref .read(selectedContainerProvider.notifier) .setContainerId(container.id); }, diff --git a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart index cf1326dc..217f3cd0 100644 --- a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart +++ b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.dart @@ -40,7 +40,7 @@ class PreferenceSetting with FastEquatable { bool get isActive => value == actualValue; - final String title; + final String? title; final String? description; final bool requireUserOptIn; diff --git a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart index ca70e1cd..1dee4e96 100644 --- a/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart +++ b/app/lib/features/geckoview/features/preferences/data/models/preference_setting.g.dart @@ -73,7 +73,7 @@ extension $PreferenceSettingGroupCopyWith on PreferenceSettingGroup { abstract class _$PreferenceSettingCWProxy { PreferenceSetting value(Object value); - PreferenceSetting title(String title); + PreferenceSetting title(String? title); PreferenceSetting description(String? description); @@ -91,7 +91,7 @@ abstract class _$PreferenceSettingCWProxy { /// ```` PreferenceSetting call({ Object value, - String title, + String? title, String? description, Object? actualValue, bool requireUserOptIn, @@ -109,7 +109,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy { PreferenceSetting value(Object value) => this(value: value); @override - PreferenceSetting title(String title) => this(title: title); + PreferenceSetting title(String? title) => this(title: title); @override PreferenceSetting description(String? description) => @@ -151,7 +151,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy { title: title == const $CopyWithPlaceholder() ? _value.title // ignore: cast_nullable_to_non_nullable - : title as String, + : title as String?, description: description == const $CopyWithPlaceholder() ? _value.description // ignore: cast_nullable_to_non_nullable @@ -186,7 +186,7 @@ extension $PreferenceSettingCopyWith on PreferenceSetting { PreferenceSetting _$PreferenceSettingFromJson(Map json) => PreferenceSetting( value: json['value'] as Object, - title: json['title'] as String, + title: json['title'] as String?, description: json['description'] as String?, requireUserOptIn: json['requireUserOptIn'] as bool? ?? false, shouldBeDefault: json['shouldBeDefault'] as bool? ?? false, diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart index 9a190742..09953823 100644 --- a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.dart @@ -5,6 +5,7 @@ import 'package:collection/collection.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:lensai/features/geckoview/features/preferences/data/models/preference_setting.dart'; +import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; @@ -12,40 +13,29 @@ import 'package:rxdart/rxdart.dart'; part 'preference_settings.g.dart'; @Riverpod(keepAlive: true) -Future> _preferenceSettingGroups( - Ref ref, -) async { - final content = await rootBundle +Future> _preferenceSettingContent(Ref ref) async { + return await rootBundle .loadString('assets/preferences/settings.json') .then(jsonDecode) as Map; +} - final userContent = content['user'] as Map; - - return userContent.map( - (key, value) => MapEntry( - key, - PreferenceSettingGroup( - // ignore: avoid_dynamic_calls - description: value['description'] as String?, - // ignore: avoid_dynamic_calls - settings: (value['preferences'] as Map).map( - (key, value) => MapEntry( - key, - PreferenceSetting.fromJson(value as Map), - ), - ), - ), - ), - ); +@Riverpod(keepAlive: true) +Future> _preferenceSettingGroups( + Ref ref, + PreferencePartition partition, +) async { + final content = await ref.read(_preferenceSettingContentProvider.future); + return deserializePreferenceSettingGroups(partition, content); } @Riverpod(keepAlive: true) Future _preferenceSettingGroup( Ref ref, + PreferencePartition partition, String groupName, ) { return ref.watch( - _preferenceSettingGroupsProvider.selectAsync( + _preferenceSettingGroupsProvider(partition).selectAsync( (groups) => groups[groupName] ?? (throw Exception('Unknown setting group')), ), @@ -90,13 +80,16 @@ class _PreferenceRepository extends _$PreferenceRepository { } @Riverpod() -class PreferenceSettingsGeneralRepository - extends _$PreferenceSettingsGeneralRepository { - late Map _statelessGroups; +class UnifiedPreferenceSettingsRepository + extends _$UnifiedPreferenceSettingsRepository { + Map? _statelessGroups; Future apply() async { + _statelessGroups = + await ref.read(_preferenceSettingGroupsProvider(partition).future); + final prefs = { - for (final group in _statelessGroups.values) + for (final group in _statelessGroups!.values) ...Map.fromEntries( group.settings.entries .where((e) => !e.value.requireUserOptIn) @@ -108,7 +101,10 @@ class PreferenceSettingsGeneralRepository } Future reset() async { - final prefs = _statelessGroups.values + _statelessGroups = + await ref.read(_preferenceSettingGroupsProvider(partition).future); + + final prefs = _statelessGroups!.values .map((group) => group.settings.keys) .flattened .toList(); @@ -117,13 +113,16 @@ class PreferenceSettingsGeneralRepository } @override - Stream> build() async* { - _statelessGroups = await ref.watch(_preferenceSettingGroupsProvider.future); + Stream> build( + PreferencePartition partition, + ) async* { + _statelessGroups = + await ref.watch(_preferenceSettingGroupsProvider(partition).future); final prefStream = ref.watch(_preferenceRepositoryProvider); yield* prefStream.map( - (prefs) => _statelessGroups.map( + (prefs) => _statelessGroups!.map( (groupName, group) => MapEntry( groupName, group.copyWith.settings( @@ -143,18 +142,21 @@ class PreferenceSettingsGeneralRepository @Riverpod() class PreferenceSettingsGroupRepository extends _$PreferenceSettingsGroupRepository { - late PreferenceSettingGroup _statelessSettingGroup; + PreferenceSettingGroup? _statelessSettingGroup; Future apply({List? filter}) async { + _statelessSettingGroup ??= await ref + .read(_preferenceSettingGroupProvider(partition, groupName).future); + final prefs = Map.fromEntries( filter?.map( (e) => MapEntry( e, - _statelessSettingGroup.settings[e]?.value ?? + _statelessSettingGroup!.settings[e]?.value ?? (throw Exception('Preference not part of group')), ), ) ?? - _statelessSettingGroup.settings.entries + _statelessSettingGroup!.settings.entries .where((e) => !e.value.requireUserOptIn) .map((e) => MapEntry(e.key, e.value.value)), ); @@ -163,28 +165,34 @@ class PreferenceSettingsGroupRepository } Future reset({List? filter}) async { + _statelessSettingGroup ??= await ref + .read(_preferenceSettingGroupProvider(partition, groupName).future); + if (filter != null && filter.any( - (value) => !_statelessSettingGroup.settings.containsKey(value), + (value) => !_statelessSettingGroup!.settings.containsKey(value), )) { throw Exception('Preference not part of group'); } await ref .read(_preferenceRepositoryProvider.notifier) - .resetPrefs(filter ?? _statelessSettingGroup.settings.keys.toList()); + .resetPrefs(filter ?? _statelessSettingGroup!.settings.keys.toList()); } @override - Stream build(String groupName) async* { - _statelessSettingGroup = - await ref.watch(_preferenceSettingGroupProvider(groupName).future); + Stream build( + PreferencePartition partition, + String groupName, + ) async* { + _statelessSettingGroup = await ref + .watch(_preferenceSettingGroupProvider(partition, groupName).future); final prefStream = ref.watch(_preferenceRepositoryProvider); yield* prefStream.map( - (prefs) => _statelessSettingGroup.copyWith.settings( - _statelessSettingGroup.settings.map( + (prefs) => _statelessSettingGroup!.copyWith.settings( + _statelessSettingGroup!.settings.map( (key, value) => MapEntry(key, value.copyWith.actualValue(prefs[key])), ), ), diff --git a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart index 5b34e468..18798b30 100644 --- a/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart +++ b/app/lib/features/geckoview/features/preferences/data/repositories/preference_settings.g.dart @@ -6,28 +6,27 @@ part of 'preference_settings.dart'; // RiverpodGenerator // ************************************************************************** -String _$preferenceSettingGroupsHash() => - r'fa9286248cc71c88c888a518441721dc99b7e0c2'; +String _$preferenceSettingContentHash() => + r'a8a61fdf8800cb7adb365c100409ddce09c574b1'; -/// See also [_preferenceSettingGroups]. -@ProviderFor(_preferenceSettingGroups) -final _preferenceSettingGroupsProvider = - FutureProvider>.internal( - _preferenceSettingGroups, - name: r'_preferenceSettingGroupsProvider', +/// See also [_preferenceSettingContent]. +@ProviderFor(_preferenceSettingContent) +final _preferenceSettingContentProvider = + FutureProvider>.internal( + _preferenceSettingContent, + name: r'_preferenceSettingContentProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$preferenceSettingGroupsHash, + : _$preferenceSettingContentHash, dependencies: null, allTransitiveDependencies: null, ); @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef _PreferenceSettingGroupsRef - = FutureProviderRef>; -String _$preferenceSettingGroupHash() => - r'960169ab9f6985a5b73a63663e671a8eac7cd776'; +typedef _PreferenceSettingContentRef = FutureProviderRef>; +String _$preferenceSettingGroupsHash() => + r'd267dacf82a11568c9cf0cc864f434db35f93394'; /// Copied from Dart SDK class _SystemHash { @@ -50,6 +49,145 @@ class _SystemHash { } } +/// See also [_preferenceSettingGroups]. +@ProviderFor(_preferenceSettingGroups) +const _preferenceSettingGroupsProvider = _PreferenceSettingGroupsFamily(); + +/// See also [_preferenceSettingGroups]. +class _PreferenceSettingGroupsFamily + extends Family>> { + /// See also [_preferenceSettingGroups]. + const _PreferenceSettingGroupsFamily(); + + /// See also [_preferenceSettingGroups]. + _PreferenceSettingGroupsProvider call( + PreferencePartition partition, + ) { + return _PreferenceSettingGroupsProvider( + partition, + ); + } + + @override + _PreferenceSettingGroupsProvider getProviderOverride( + covariant _PreferenceSettingGroupsProvider provider, + ) { + return call( + provider.partition, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_preferenceSettingGroupsProvider'; +} + +/// See also [_preferenceSettingGroups]. +class _PreferenceSettingGroupsProvider + extends FutureProvider> { + /// See also [_preferenceSettingGroups]. + _PreferenceSettingGroupsProvider( + PreferencePartition partition, + ) : this._internal( + (ref) => _preferenceSettingGroups( + ref as _PreferenceSettingGroupsRef, + partition, + ), + from: _preferenceSettingGroupsProvider, + name: r'_preferenceSettingGroupsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$preferenceSettingGroupsHash, + dependencies: _PreferenceSettingGroupsFamily._dependencies, + allTransitiveDependencies: + _PreferenceSettingGroupsFamily._allTransitiveDependencies, + partition: partition, + ); + + _PreferenceSettingGroupsProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.partition, + }) : super.internal(); + + final PreferencePartition partition; + + @override + Override overrideWith( + FutureOr> Function( + _PreferenceSettingGroupsRef provider) + create, + ) { + return ProviderOverride( + origin: this, + override: _PreferenceSettingGroupsProvider._internal( + (ref) => create(ref as _PreferenceSettingGroupsRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + partition: partition, + ), + ); + } + + @override + FutureProviderElement> createElement() { + return _PreferenceSettingGroupsProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is _PreferenceSettingGroupsProvider && + other.partition == partition; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, partition.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin _PreferenceSettingGroupsRef + on FutureProviderRef> { + /// The parameter `partition` of this provider. + PreferencePartition get partition; +} + +class _PreferenceSettingGroupsProviderElement + extends FutureProviderElement> + with _PreferenceSettingGroupsRef { + _PreferenceSettingGroupsProviderElement(super.provider); + + @override + PreferencePartition get partition => + (origin as _PreferenceSettingGroupsProvider).partition; +} + +String _$preferenceSettingGroupHash() => + r'64d80a37928d7e5aea7d3d567dde8ade7292cd5f'; + /// See also [_preferenceSettingGroup]. @ProviderFor(_preferenceSettingGroup) const _preferenceSettingGroupProvider = _PreferenceSettingGroupFamily(); @@ -62,9 +200,11 @@ class _PreferenceSettingGroupFamily /// See also [_preferenceSettingGroup]. _PreferenceSettingGroupProvider call( + PreferencePartition partition, String groupName, ) { return _PreferenceSettingGroupProvider( + partition, groupName, ); } @@ -74,6 +214,7 @@ class _PreferenceSettingGroupFamily covariant _PreferenceSettingGroupProvider provider, ) { return call( + provider.partition, provider.groupName, ); } @@ -98,10 +239,12 @@ class _PreferenceSettingGroupProvider extends FutureProvider { /// See also [_preferenceSettingGroup]. _PreferenceSettingGroupProvider( + PreferencePartition partition, String groupName, ) : this._internal( (ref) => _preferenceSettingGroup( ref as _PreferenceSettingGroupRef, + partition, groupName, ), from: _preferenceSettingGroupProvider, @@ -113,6 +256,7 @@ class _PreferenceSettingGroupProvider dependencies: _PreferenceSettingGroupFamily._dependencies, allTransitiveDependencies: _PreferenceSettingGroupFamily._allTransitiveDependencies, + partition: partition, groupName: groupName, ); @@ -123,9 +267,11 @@ class _PreferenceSettingGroupProvider required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, + required this.partition, required this.groupName, }) : super.internal(); + final PreferencePartition partition; final String groupName; @override @@ -143,6 +289,7 @@ class _PreferenceSettingGroupProvider dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, + partition: partition, groupName: groupName, ), ); @@ -156,12 +303,14 @@ class _PreferenceSettingGroupProvider @override bool operator ==(Object other) { return other is _PreferenceSettingGroupProvider && + other.partition == partition && other.groupName == groupName; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, partition.hashCode); hash = _SystemHash.combine(hash, groupName.hashCode); return _SystemHash.finish(hash); @@ -171,6 +320,9 @@ class _PreferenceSettingGroupProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element mixin _PreferenceSettingGroupRef on FutureProviderRef { + /// The parameter `partition` of this provider. + PreferencePartition get partition; + /// The parameter `groupName` of this provider. String get groupName; } @@ -180,6 +332,9 @@ class _PreferenceSettingGroupProviderElement with _PreferenceSettingGroupRef { _PreferenceSettingGroupProviderElement(super.provider); + @override + PreferencePartition get partition => + (origin as _PreferenceSettingGroupProvider).partition; @override String get groupName => (origin as _PreferenceSettingGroupProvider).groupName; } @@ -202,33 +357,174 @@ final _preferenceRepositoryProvider = AutoDisposeNotifierProvider< typedef _$PreferenceRepository = AutoDisposeNotifier>>>; -String _$preferenceSettingsGeneralRepositoryHash() => - r'24a70de8284107bb43778e1014abe41bd805987a'; +String _$unifiedPreferenceSettingsRepositoryHash() => + r'35aa16ffa7aa1453c8331ed7470ef85a68d27178'; -/// See also [PreferenceSettingsGeneralRepository]. -@ProviderFor(PreferenceSettingsGeneralRepository) -final preferenceSettingsGeneralRepositoryProvider = - AutoDisposeStreamNotifierProvider>.internal( - PreferenceSettingsGeneralRepository.new, - name: r'preferenceSettingsGeneralRepositoryProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$preferenceSettingsGeneralRepositoryHash, - dependencies: null, - allTransitiveDependencies: null, -); +abstract class _$UnifiedPreferenceSettingsRepository + extends BuildlessAutoDisposeStreamNotifier< + Map> { + late final PreferencePartition partition; + + Stream> build( + PreferencePartition partition, + ); +} + +/// See also [UnifiedPreferenceSettingsRepository]. +@ProviderFor(UnifiedPreferenceSettingsRepository) +const unifiedPreferenceSettingsRepositoryProvider = + UnifiedPreferenceSettingsRepositoryFamily(); + +/// See also [UnifiedPreferenceSettingsRepository]. +class UnifiedPreferenceSettingsRepositoryFamily + extends Family>> { + /// See also [UnifiedPreferenceSettingsRepository]. + const UnifiedPreferenceSettingsRepositoryFamily(); + + /// See also [UnifiedPreferenceSettingsRepository]. + UnifiedPreferenceSettingsRepositoryProvider call( + PreferencePartition partition, + ) { + return UnifiedPreferenceSettingsRepositoryProvider( + partition, + ); + } + + @override + UnifiedPreferenceSettingsRepositoryProvider getProviderOverride( + covariant UnifiedPreferenceSettingsRepositoryProvider provider, + ) { + return call( + provider.partition, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'unifiedPreferenceSettingsRepositoryProvider'; +} + +/// See also [UnifiedPreferenceSettingsRepository]. +class UnifiedPreferenceSettingsRepositoryProvider + extends AutoDisposeStreamNotifierProviderImpl< + UnifiedPreferenceSettingsRepository, + Map> { + /// See also [UnifiedPreferenceSettingsRepository]. + UnifiedPreferenceSettingsRepositoryProvider( + PreferencePartition partition, + ) : this._internal( + () => UnifiedPreferenceSettingsRepository()..partition = partition, + from: unifiedPreferenceSettingsRepositoryProvider, + name: r'unifiedPreferenceSettingsRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$unifiedPreferenceSettingsRepositoryHash, + dependencies: UnifiedPreferenceSettingsRepositoryFamily._dependencies, + allTransitiveDependencies: UnifiedPreferenceSettingsRepositoryFamily + ._allTransitiveDependencies, + partition: partition, + ); + + UnifiedPreferenceSettingsRepositoryProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.partition, + }) : super.internal(); + + final PreferencePartition partition; + + @override + Stream> runNotifierBuild( + covariant UnifiedPreferenceSettingsRepository notifier, + ) { + return notifier.build( + partition, + ); + } + + @override + Override overrideWith(UnifiedPreferenceSettingsRepository Function() create) { + return ProviderOverride( + origin: this, + override: UnifiedPreferenceSettingsRepositoryProvider._internal( + () => create()..partition = partition, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + partition: partition, + ), + ); + } + + @override + AutoDisposeStreamNotifierProviderElement> createElement() { + return _UnifiedPreferenceSettingsRepositoryProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is UnifiedPreferenceSettingsRepositoryProvider && + other.partition == partition; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, partition.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin UnifiedPreferenceSettingsRepositoryRef + on AutoDisposeStreamNotifierProviderRef< + Map> { + /// The parameter `partition` of this provider. + PreferencePartition get partition; +} + +class _UnifiedPreferenceSettingsRepositoryProviderElement + extends AutoDisposeStreamNotifierProviderElement< + UnifiedPreferenceSettingsRepository, + Map> + with UnifiedPreferenceSettingsRepositoryRef { + _UnifiedPreferenceSettingsRepositoryProviderElement(super.provider); + + @override + PreferencePartition get partition => + (origin as UnifiedPreferenceSettingsRepositoryProvider).partition; +} -typedef _$PreferenceSettingsGeneralRepository - = AutoDisposeStreamNotifier>; String _$preferenceSettingsGroupRepositoryHash() => - r'023794294c5639ac6c6941f87a4ed91e1551fbc9'; + r'60f416ffe1cde4d535fb5b643a80aead20836bae'; abstract class _$PreferenceSettingsGroupRepository extends BuildlessAutoDisposeStreamNotifier { + late final PreferencePartition partition; late final String groupName; Stream build( + PreferencePartition partition, String groupName, ); } @@ -246,9 +542,11 @@ class PreferenceSettingsGroupRepositoryFamily /// See also [PreferenceSettingsGroupRepository]. PreferenceSettingsGroupRepositoryProvider call( + PreferencePartition partition, String groupName, ) { return PreferenceSettingsGroupRepositoryProvider( + partition, groupName, ); } @@ -258,6 +556,7 @@ class PreferenceSettingsGroupRepositoryFamily covariant PreferenceSettingsGroupRepositoryProvider provider, ) { return call( + provider.partition, provider.groupName, ); } @@ -283,9 +582,12 @@ class PreferenceSettingsGroupRepositoryProvider PreferenceSettingsGroupRepository, PreferenceSettingGroup> { /// See also [PreferenceSettingsGroupRepository]. PreferenceSettingsGroupRepositoryProvider( + PreferencePartition partition, String groupName, ) : this._internal( - () => PreferenceSettingsGroupRepository()..groupName = groupName, + () => PreferenceSettingsGroupRepository() + ..partition = partition + ..groupName = groupName, from: preferenceSettingsGroupRepositoryProvider, name: r'preferenceSettingsGroupRepositoryProvider', debugGetCreateSourceHash: @@ -295,6 +597,7 @@ class PreferenceSettingsGroupRepositoryProvider dependencies: PreferenceSettingsGroupRepositoryFamily._dependencies, allTransitiveDependencies: PreferenceSettingsGroupRepositoryFamily ._allTransitiveDependencies, + partition: partition, groupName: groupName, ); @@ -305,9 +608,11 @@ class PreferenceSettingsGroupRepositoryProvider required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, + required this.partition, required this.groupName, }) : super.internal(); + final PreferencePartition partition; final String groupName; @override @@ -315,6 +620,7 @@ class PreferenceSettingsGroupRepositoryProvider covariant PreferenceSettingsGroupRepository notifier, ) { return notifier.build( + partition, groupName, ); } @@ -324,12 +630,15 @@ class PreferenceSettingsGroupRepositoryProvider return ProviderOverride( origin: this, override: PreferenceSettingsGroupRepositoryProvider._internal( - () => create()..groupName = groupName, + () => create() + ..partition = partition + ..groupName = groupName, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, + partition: partition, groupName: groupName, ), ); @@ -344,12 +653,14 @@ class PreferenceSettingsGroupRepositoryProvider @override bool operator ==(Object other) { return other is PreferenceSettingsGroupRepositoryProvider && + other.partition == partition && other.groupName == groupName; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, partition.hashCode); hash = _SystemHash.combine(hash, groupName.hashCode); return _SystemHash.finish(hash); @@ -360,6 +671,9 @@ class PreferenceSettingsGroupRepositoryProvider // ignore: unused_element mixin PreferenceSettingsGroupRepositoryRef on AutoDisposeStreamNotifierProviderRef { + /// The parameter `partition` of this provider. + PreferencePartition get partition; + /// The parameter `groupName` of this provider. String get groupName; } @@ -370,6 +684,9 @@ class _PreferenceSettingsGroupRepositoryProviderElement PreferenceSettingGroup> with PreferenceSettingsGroupRepositoryRef { _PreferenceSettingsGroupRepositoryProviderElement(super.provider); + @override + PreferencePartition get partition => + (origin as PreferenceSettingsGroupRepositoryProvider).partition; @override String get groupName => (origin as PreferenceSettingsGroupRepositoryProvider).groupName; diff --git a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart index c5587e4f..9bdc057a 100644 --- a/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart +++ b/app/lib/features/geckoview/features/readerview/presentation/widgets/reader_button.dart @@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:lensai/presentation/widgets/animate_gradient_shader.dart'; class ReaderButton extends HookConsumerWidget { @@ -16,7 +16,8 @@ class ReaderButton extends HookConsumerWidget { final readerChanging = ref.watch(readerableScreenControllerProvider); final enableReadability = ref.watch( - settingsRepositoryProvider.select((value) => value.enableReadability), + generalSettingsRepositoryProvider + .select((value) => value.enableReadability), ); final readerabilityState = ref.watch( diff --git a/app/lib/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart b/app/lib/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart new file mode 100644 index 00000000..e722b219 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart @@ -0,0 +1,20 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; + +class ContainerMetadataConverter + implements TypeConverter { + const ContainerMetadataConverter(); + + @override + ContainerMetadata fromSql(String fromDb) { + final json = jsonDecode(fromDb) as Map; + return ContainerMetadata.fromJson(json); + } + + @override + String toSql(ContainerMetadata value) { + return jsonEncode(value.toJson()); + } +} diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart index 051cc136..c4ef4c3a 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/container.dart @@ -1,7 +1,6 @@ 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'; @@ -12,26 +11,24 @@ class ContainerDao extends DatabaseAccessor with _$ContainerDaoMixin { ContainerDao(super.db); - Future addContainer({String? name, required Color color}) { + Future addContainer(ContainerData container) { return db.container.insertOne( ContainerCompanion.insert( - id: uuid.v7(), - name: Value(name), - color: color, + id: container.id, + name: Value(container.name), + color: container.color, + metadata: Value(container.metadata), ), ); } - Future replaceContainer( - String id, { - required String? name, - required Color color, - }) { + Future replaceContainer(ContainerData container) { return db.container.replaceOne( ContainerCompanion( - id: Value(id), - name: Value(name), - color: Value(color), + id: Value(container.id), + name: Value(container.name), + color: Value(container.color), + metadata: Value(container.metadata), ), ); } diff --git a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart index 302dd869..b8defc40 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/daos/tab.dart @@ -41,7 +41,7 @@ class TabDao extends DatabaseAccessor with _$TabDaoMixin { return query.map((row) => row.read(db.tab.id)!); } - SingleSelectable tabContainerId(String tabId) { + SingleOrNullSelectable tabContainerId(String tabId) { final query = selectOnly(db.tab) ..addColumns([db.tab.containerId]) ..where(db.tab.id.equals(tabId)); @@ -49,7 +49,41 @@ class TabDao extends DatabaseAccessor with _$TabDaoMixin { return query.map((row) => row.read(db.tab.containerId)); } - Future upsertTab( + Future upsertContainerTabTransactional( + Future Function() createTab, { + Value containerId = const Value.absent(), + Value orderKey = const Value.absent(), + }) { + return db.transaction(() async { + final tabId = await createTab(); + + 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; + }); + } + + //Upsert an tab only if there is no container assigned yet + Future upsertUnassignedTab( String tabId, { Value containerId = const Value.absent(), Value orderKey = const Value.absent(), @@ -73,6 +107,7 @@ class TabDao extends DatabaseAccessor with _$TabDaoMixin { (containerId.present) ? Variable(containerId.value) : null, orderKey: (orderKey.present) ? Variable(orderKey.value) : null, ), + where: (old) => old.containerId.isNull(), ), ); diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.dart b/app/lib/features/geckoview/features/tabs/data/database/database.dart index 1c07abff..64ada17f 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; -import 'package:flutter/widgets.dart' show Color, IconData; +import 'package:flutter/widgets.dart' show Color; 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/database/converters/container_metadata_converter.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'; diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.drift b/app/lib/features/geckoview/features/tabs/data/database/database.drift index bf21dc6e..a1392757 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.drift +++ b/app/lib/features/geckoview/features/tabs/data/database/database.drift @@ -1,14 +1,13 @@ 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'; import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.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()` + metadata TEXT MAPPED BY `const ContainerMetadataConverter()` ) WITH ContainerData; CREATE TABLE tab ( @@ -79,7 +78,7 @@ containersWithCount WITH ContainerDataWithCount: 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; + ORDER BY tab_agg.last_updated DESC NULLS LAST; leadingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER): SELECT lexo_rank_previous( diff --git a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart index b629bf0b..f32333de 100644 --- a/app/lib/features/geckoview/features/tabs/data/database/database.g.dart +++ b/app/lib/features/geckoview/features/tabs/data/database/database.g.dart @@ -13,11 +13,6 @@ class Container extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'PRIMARY KEY NOT NULL'); - late final GeneratedColumn contextualIdentity = - GeneratedColumn('contextual_identity', aliasedName, true, - type: DriftSqlType.string, - requiredDuringInsert: false, - $customConstraints: ''); late final GeneratedColumn name = GeneratedColumn( 'name', aliasedName, true, type: DriftSqlType.string, @@ -29,15 +24,14 @@ class Container extends Table with TableInfo { requiredDuringInsert: true, $customConstraints: 'NOT NULL') .withConverter(Container.$convertercolor); - late final GeneratedColumnWithTypeConverter icon = - GeneratedColumn('icon', aliasedName, true, + late final GeneratedColumnWithTypeConverter + metadata = GeneratedColumn('metadata', aliasedName, true, type: DriftSqlType.string, requiredDuringInsert: false, $customConstraints: '') - .withConverter(Container.$convertericonn); + .withConverter(Container.$convertermetadatan); @override - List get $columns => - [id, contextualIdentity, name, color, icon]; + List get $columns => [id, name, color, metadata]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -51,14 +45,13 @@ class Container extends Table with TableInfo { 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'])), + metadata: Container.$convertermetadatan.fromSql(attachedDatabase + .typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}metadata'])), ); } @@ -68,69 +61,62 @@ class Container extends Table with TableInfo { } static TypeConverter $convertercolor = const ColorConverter(); - static TypeConverter $convertericon = - const IconDataTypeConverter(); - static TypeConverter $convertericonn = - NullAwareTypeConverter.wrap($convertericon); + static TypeConverter $convertermetadata = + const ContainerMetadataConverter(); + static TypeConverter $convertermetadatan = + NullAwareTypeConverter.wrap($convertermetadata); @override bool get dontWriteConstraints => true; } class ContainerCompanion extends UpdateCompanion { final Value id; - final Value contextualIdentity; final Value name; final Value color; - final Value icon; + final Value metadata; final Value 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.metadata = 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.metadata = const Value.absent(), this.rowid = const Value.absent(), }) : id = Value(id), color = Value(color); static Insertable custom({ Expression? id, - Expression? contextualIdentity, Expression? name, Expression? color, - Expression? icon, + Expression? metadata, Expression? 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 (metadata != null) 'metadata': metadata, if (rowid != null) 'rowid': rowid, }); } ContainerCompanion copyWith( {Value? id, - Value? contextualIdentity, Value? name, Value? color, - Value? icon, + Value? metadata, Value? rowid}) { return ContainerCompanion( id: id ?? this.id, - contextualIdentity: contextualIdentity ?? this.contextualIdentity, name: name ?? this.name, color: color ?? this.color, - icon: icon ?? this.icon, + metadata: metadata ?? this.metadata, rowid: rowid ?? this.rowid, ); } @@ -141,9 +127,6 @@ class ContainerCompanion extends UpdateCompanion { if (id.present) { map['id'] = Variable(id.value); } - if (contextualIdentity.present) { - map['contextual_identity'] = Variable(contextualIdentity.value); - } if (name.present) { map['name'] = Variable(name.value); } @@ -151,9 +134,9 @@ class ContainerCompanion extends UpdateCompanion { map['color'] = Variable(Container.$convertercolor.toSql(color.value)); } - if (icon.present) { - map['icon'] = - Variable(Container.$convertericonn.toSql(icon.value)); + if (metadata.present) { + map['metadata'] = + Variable(Container.$convertermetadatan.toSql(metadata.value)); } if (rowid.present) { map['rowid'] = Variable(rowid.value); @@ -165,10 +148,9 @@ class ContainerCompanion extends UpdateCompanion { String toString() { return (StringBuffer('ContainerCompanion(') ..write('id: $id, ') - ..write('contextualIdentity: $contextualIdentity, ') ..write('name: $name, ') ..write('color: $color, ') - ..write('icon: $icon, ') + ..write('metadata: $metadata, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -1597,18 +1579,18 @@ abstract class _$TabDatabase extends GeneratedDatabase { Selectable 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', + '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 LAST', variables: [], readsFrom: { container, tab, }).map((QueryRow row) => ContainerDataWithCount( id: row.read('id'), - contextualIdentity: row.readNullable('contextual_identity'), name: row.readNullable('name'), color: Container.$convertercolor.fromSql(row.read('color')), - icon: NullAwareTypeConverter.wrapFromSql( - Container.$convertericon, row.readNullable('icon')), + metadata: NullAwareTypeConverter.wrapFromSql( + Container.$convertermetadata, + row.readNullable('metadata')), tabCount: row.readNullable('tab_count'), )); } @@ -1819,18 +1801,16 @@ abstract class _$TabDatabase extends GeneratedDatabase { typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({ required String id, - Value contextualIdentity, Value name, required Color color, - Value icon, + Value metadata, Value rowid, }); typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({ Value id, - Value contextualIdentity, Value name, Value color, - Value icon, + Value metadata, Value rowid, }); @@ -1864,10 +1844,6 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> { ColumnFilters get id => $composableBuilder( column: $table.id, builder: (column) => ColumnFilters(column)); - ColumnFilters get contextualIdentity => $composableBuilder( - column: $table.contextualIdentity, - builder: (column) => ColumnFilters(column)); - ColumnFilters get name => $composableBuilder( column: $table.name, builder: (column) => ColumnFilters(column)); @@ -1876,9 +1852,9 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> { column: $table.color, builder: (column) => ColumnWithTypeConverterFilters(column)); - ColumnWithTypeConverterFilters get icon => - $composableBuilder( - column: $table.icon, + ColumnWithTypeConverterFilters + get metadata => $composableBuilder( + column: $table.metadata, builder: (column) => ColumnWithTypeConverterFilters(column)); Expression tabRefs(Expression Function($TabFilterComposer f) f) { @@ -1913,18 +1889,14 @@ class $ContainerOrderingComposer extends Composer<_$TabDatabase, Container> { ColumnOrderings get id => $composableBuilder( column: $table.id, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get contextualIdentity => $composableBuilder( - column: $table.contextualIdentity, - builder: (column) => ColumnOrderings(column)); - ColumnOrderings get name => $composableBuilder( column: $table.name, builder: (column) => ColumnOrderings(column)); ColumnOrderings get color => $composableBuilder( column: $table.color, builder: (column) => ColumnOrderings(column)); - ColumnOrderings get icon => $composableBuilder( - column: $table.icon, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get metadata => $composableBuilder( + column: $table.metadata, builder: (column) => ColumnOrderings(column)); } class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> { @@ -1938,17 +1910,14 @@ class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> { GeneratedColumn get id => $composableBuilder(column: $table.id, builder: (column) => column); - GeneratedColumn get contextualIdentity => $composableBuilder( - column: $table.contextualIdentity, builder: (column) => column); - GeneratedColumn get name => $composableBuilder(column: $table.name, builder: (column) => column); GeneratedColumnWithTypeConverter get color => $composableBuilder(column: $table.color, builder: (column) => column); - GeneratedColumnWithTypeConverter get icon => - $composableBuilder(column: $table.icon, builder: (column) => column); + GeneratedColumnWithTypeConverter get metadata => + $composableBuilder(column: $table.metadata, builder: (column) => column); Expression tabRefs( Expression Function($TabAnnotationComposer a) f) { @@ -1996,34 +1965,30 @@ class $ContainerTableManager extends RootTableManager< $ContainerAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value id = const Value.absent(), - Value contextualIdentity = const Value.absent(), Value name = const Value.absent(), Value color = const Value.absent(), - Value icon = const Value.absent(), + Value metadata = const Value.absent(), Value rowid = const Value.absent(), }) => ContainerCompanion( id: id, - contextualIdentity: contextualIdentity, name: name, color: color, - icon: icon, + metadata: metadata, rowid: rowid, ), createCompanionCallback: ({ required String id, - Value contextualIdentity = const Value.absent(), Value name = const Value.absent(), required Color color, - Value icon = const Value.absent(), + Value metadata = const Value.absent(), Value rowid = const Value.absent(), }) => ContainerCompanion.insert( id: id, - contextualIdentity: contextualIdentity, name: name, color: color, - icon: icon, + metadata: metadata, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/app/lib/features/geckoview/features/tabs/data/models/container_data.dart b/app/lib/features/geckoview/features/tabs/data/models/container_data.dart index 9e0313d0..666bd375 100644 --- a/app/lib/features/geckoview/features/tabs/data/models/container_data.dart +++ b/app/lib/features/geckoview/features/tabs/data/models/container_data.dart @@ -1,20 +1,103 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/widgets.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:lensai/data/database/converters/icon_data.dart'; +part 'container_data.g.dart'; + +@CopyWith() +@JsonSerializable() +class ContainerAuthSettings with FastEquatable { + final bool authenticationRequired; + final bool lockOnAppBackground; + final Duration? lockTimeout; + + ContainerAuthSettings({ + required this.authenticationRequired, + required this.lockOnAppBackground, + required this.lockTimeout, + }); + + ContainerAuthSettings.withDefaults({ + bool? authenticationRequired, + bool? lockOnAppBackground, + Duration? lockTimeout, + }) : this( + authenticationRequired: authenticationRequired ?? false, + lockOnAppBackground: lockOnAppBackground ?? false, + lockTimeout: lockTimeout, + ); + + factory ContainerAuthSettings.fromJson(Map json) => + _$ContainerAuthSettingsFromJson(json); + + Map toJson() => _$ContainerAuthSettingsToJson(this); + + @override + List get hashParameters => [ + authenticationRequired, + lockOnAppBackground, + lockTimeout, + ]; + + @override + bool get cacheHash => true; +} + +@CopyWith() +@JsonSerializable(constructor: 'withDefaults') +class ContainerMetadata with FastEquatable { + @IconDataJsonConverter() + final IconData? iconData; + final String? contextualIdentity; + final ContainerAuthSettings authSettings; + + ContainerMetadata({ + required this.iconData, + required this.contextualIdentity, + required this.authSettings, + }); + + ContainerMetadata.withDefaults({ + IconData? iconData, + String? contextualIdentity, + ContainerAuthSettings? authSettings, + }) : this( + iconData: iconData, + contextualIdentity: contextualIdentity, + authSettings: authSettings ?? ContainerAuthSettings.withDefaults(), + ); + + factory ContainerMetadata.fromJson(Map json) => + _$ContainerMetadataFromJson(json); + + Map toJson() => _$ContainerMetadataToJson(this); + + @override + List get hashParameters => [ + iconData, + contextualIdentity, + authSettings, + ]; + + @override + bool get cacheHash => true; +} + +@CopyWith() class ContainerData with FastEquatable { final String id; - final String? contextualIdentity; final String? name; final Color color; - final IconData? icon; + final ContainerMetadata metadata; ContainerData({ required this.id, - this.contextualIdentity, this.name, required this.color, - this.icon, - }); + ContainerMetadata? metadata, + }) : metadata = metadata ?? ContainerMetadata.withDefaults(); @override bool get cacheHash => true; @@ -22,10 +105,9 @@ class ContainerData with FastEquatable { @override List get hashParameters => [ id, - contextualIdentity, name, color, - icon, + metadata, ]; } @@ -34,10 +116,9 @@ class ContainerDataWithCount extends ContainerData { ContainerDataWithCount({ required super.id, - super.contextualIdentity, super.name, required super.color, - super.icon, + super.metadata, required this.tabCount, }); diff --git a/app/lib/features/geckoview/features/tabs/data/models/container_data.g.dart b/app/lib/features/geckoview/features/tabs/data/models/container_data.g.dart new file mode 100644 index 00000000..6fd893c9 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/data/models/container_data.g.dart @@ -0,0 +1,294 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'container_data.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$ContainerAuthSettingsCWProxy { + ContainerAuthSettings authenticationRequired(bool authenticationRequired); + + ContainerAuthSettings lockOnAppBackground(bool lockOnAppBackground); + + ContainerAuthSettings lockTimeout(Duration? lockTimeout); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerAuthSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerAuthSettings(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerAuthSettings call({ + bool authenticationRequired, + bool lockOnAppBackground, + Duration? lockTimeout, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfContainerAuthSettings.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfContainerAuthSettings.copyWith.fieldName(...)` +class _$ContainerAuthSettingsCWProxyImpl + implements _$ContainerAuthSettingsCWProxy { + const _$ContainerAuthSettingsCWProxyImpl(this._value); + + final ContainerAuthSettings _value; + + @override + ContainerAuthSettings authenticationRequired(bool authenticationRequired) => + this(authenticationRequired: authenticationRequired); + + @override + ContainerAuthSettings lockOnAppBackground(bool lockOnAppBackground) => + this(lockOnAppBackground: lockOnAppBackground); + + @override + ContainerAuthSettings lockTimeout(Duration? lockTimeout) => + this(lockTimeout: lockTimeout); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerAuthSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerAuthSettings(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerAuthSettings call({ + Object? authenticationRequired = const $CopyWithPlaceholder(), + Object? lockOnAppBackground = const $CopyWithPlaceholder(), + Object? lockTimeout = const $CopyWithPlaceholder(), + }) { + return ContainerAuthSettings( + authenticationRequired: + authenticationRequired == const $CopyWithPlaceholder() + ? _value.authenticationRequired + // ignore: cast_nullable_to_non_nullable + : authenticationRequired as bool, + lockOnAppBackground: lockOnAppBackground == const $CopyWithPlaceholder() + ? _value.lockOnAppBackground + // ignore: cast_nullable_to_non_nullable + : lockOnAppBackground as bool, + lockTimeout: lockTimeout == const $CopyWithPlaceholder() + ? _value.lockTimeout + // ignore: cast_nullable_to_non_nullable + : lockTimeout as Duration?, + ); + } +} + +extension $ContainerAuthSettingsCopyWith on ContainerAuthSettings { + /// Returns a callable class that can be used as follows: `instanceOfContainerAuthSettings.copyWith(...)` or like so:`instanceOfContainerAuthSettings.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ContainerAuthSettingsCWProxy get copyWith => + _$ContainerAuthSettingsCWProxyImpl(this); +} + +abstract class _$ContainerMetadataCWProxy { + ContainerMetadata iconData(IconData? iconData); + + ContainerMetadata contextualIdentity(String? contextualIdentity); + + ContainerMetadata authSettings(ContainerAuthSettings authSettings); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerMetadata(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerMetadata(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerMetadata call({ + IconData? iconData, + String? contextualIdentity, + ContainerAuthSettings authSettings, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfContainerMetadata.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfContainerMetadata.copyWith.fieldName(...)` +class _$ContainerMetadataCWProxyImpl implements _$ContainerMetadataCWProxy { + const _$ContainerMetadataCWProxyImpl(this._value); + + final ContainerMetadata _value; + + @override + ContainerMetadata iconData(IconData? iconData) => this(iconData: iconData); + + @override + ContainerMetadata contextualIdentity(String? contextualIdentity) => + this(contextualIdentity: contextualIdentity); + + @override + ContainerMetadata authSettings(ContainerAuthSettings authSettings) => + this(authSettings: authSettings); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerMetadata(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerMetadata(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerMetadata call({ + Object? iconData = const $CopyWithPlaceholder(), + Object? contextualIdentity = const $CopyWithPlaceholder(), + Object? authSettings = const $CopyWithPlaceholder(), + }) { + return ContainerMetadata( + iconData: iconData == const $CopyWithPlaceholder() + ? _value.iconData + // ignore: cast_nullable_to_non_nullable + : iconData as IconData?, + contextualIdentity: contextualIdentity == const $CopyWithPlaceholder() + ? _value.contextualIdentity + // ignore: cast_nullable_to_non_nullable + : contextualIdentity as String?, + authSettings: authSettings == const $CopyWithPlaceholder() + ? _value.authSettings + // ignore: cast_nullable_to_non_nullable + : authSettings as ContainerAuthSettings, + ); + } +} + +extension $ContainerMetadataCopyWith on ContainerMetadata { + /// Returns a callable class that can be used as follows: `instanceOfContainerMetadata.copyWith(...)` or like so:`instanceOfContainerMetadata.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ContainerMetadataCWProxy get copyWith => + _$ContainerMetadataCWProxyImpl(this); +} + +abstract class _$ContainerDataCWProxy { + ContainerData id(String id); + + ContainerData name(String? name); + + ContainerData color(Color color); + + ContainerData metadata(ContainerMetadata? metadata); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerData(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerData call({ + String id, + String? name, + Color color, + ContainerMetadata? metadata, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfContainerData.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfContainerData.copyWith.fieldName(...)` +class _$ContainerDataCWProxyImpl implements _$ContainerDataCWProxy { + const _$ContainerDataCWProxyImpl(this._value); + + final ContainerData _value; + + @override + ContainerData id(String id) => this(id: id); + + @override + ContainerData name(String? name) => this(name: name); + + @override + ContainerData color(Color color) => this(color: color); + + @override + ContainerData metadata(ContainerMetadata? metadata) => + this(metadata: metadata); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `ContainerData(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// ContainerData(...).copyWith(id: 12, name: "My name") + /// ```` + ContainerData call({ + Object? id = const $CopyWithPlaceholder(), + Object? name = const $CopyWithPlaceholder(), + Object? color = const $CopyWithPlaceholder(), + Object? metadata = const $CopyWithPlaceholder(), + }) { + return ContainerData( + id: id == const $CopyWithPlaceholder() + ? _value.id + // ignore: cast_nullable_to_non_nullable + : id as String, + name: name == const $CopyWithPlaceholder() + ? _value.name + // ignore: cast_nullable_to_non_nullable + : name as String?, + color: color == const $CopyWithPlaceholder() + ? _value.color + // ignore: cast_nullable_to_non_nullable + : color as Color, + metadata: metadata == const $CopyWithPlaceholder() + ? _value.metadata + // ignore: cast_nullable_to_non_nullable + : metadata as ContainerMetadata?, + ); + } +} + +extension $ContainerDataCopyWith on ContainerData { + /// Returns a callable class that can be used as follows: `instanceOfContainerData.copyWith(...)` or like so:`instanceOfContainerData.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$ContainerDataCWProxy get copyWith => _$ContainerDataCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ContainerAuthSettings _$ContainerAuthSettingsFromJson( + Map json) => + ContainerAuthSettings( + authenticationRequired: json['authenticationRequired'] as bool, + lockOnAppBackground: json['lockOnAppBackground'] as bool, + lockTimeout: json['lockTimeout'] == null + ? null + : Duration(microseconds: (json['lockTimeout'] as num).toInt()), + ); + +Map _$ContainerAuthSettingsToJson( + ContainerAuthSettings instance) => + { + 'authenticationRequired': instance.authenticationRequired, + 'lockOnAppBackground': instance.lockOnAppBackground, + 'lockTimeout': instance.lockTimeout?.inMicroseconds, + }; + +ContainerMetadata _$ContainerMetadataFromJson(Map json) => + ContainerMetadata.withDefaults( + iconData: _$JsonConverterFromJson, IconData>( + json['iconData'], const IconDataJsonConverter().fromJson), + contextualIdentity: json['contextualIdentity'] as String?, + authSettings: json['authSettings'] == null + ? null + : ContainerAuthSettings.fromJson( + json['authSettings'] as Map), + ); + +Map _$ContainerMetadataToJson(ContainerMetadata instance) => + { + 'iconData': _$JsonConverterToJson, IconData>( + instance.iconData, const IconDataJsonConverter().toJson), + 'contextualIdentity': instance.contextualIdentity, + 'authSettings': instance.authSettings, + }; + +Value? _$JsonConverterFromJson( + Object? json, + Value? Function(Json json) fromJson, +) => + json == null ? null : fromJson(json as Json); + +Json? _$JsonConverterToJson( + Value? value, + Json? Function(Value value) toJson, +) => + value == null ? null : toJson(value); diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.dart b/app/lib/features/geckoview/features/tabs/domain/providers.dart index dd7e9607..12354497 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers.dart @@ -58,12 +58,6 @@ AsyncValue> filteredContainersWithCount( ); } -@Riverpod() -Stream tabContainerId(Ref ref, String tabId) { - final db = ref.watch(tabDatabaseProvider); - return db.tabDao.tabContainerId(tabId).watchSingle(); -} - @Riverpod() Stream> containerTabIds( Ref ref, diff --git a/app/lib/features/geckoview/features/tabs/domain/providers.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart index 4ef3f4c8..2a31bd2c 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers.g.dart @@ -206,136 +206,6 @@ class _FilteredContainersWithCountProviderElement (origin as FilteredContainersWithCountProvider).searchText; } -String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d'; - -/// See also [tabContainerId]. -@ProviderFor(tabContainerId) -const tabContainerIdProvider = TabContainerIdFamily(); - -/// See also [tabContainerId]. -class TabContainerIdFamily extends Family> { - /// 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? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'tabContainerIdProvider'; -} - -/// See also [tabContainerId]. -class TabContainerIdProvider extends AutoDisposeStreamProvider { - /// 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 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 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); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin TabContainerIdRef on AutoDisposeStreamProviderRef { - /// The parameter `tabId` of this provider. - String get tabId; -} - -class _TabContainerIdProviderElement - extends AutoDisposeStreamProviderElement with TabContainerIdRef { - _TabContainerIdProviderElement(super.provider); - - @override - String get tabId => (origin as TabContainerIdProvider).tabId; -} - String _$containerTabIdsHash() => r'7545a6c500b1832bf81c0838e257dfe5e051463d'; /// See also [containerTabIds]. diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart index ddf8e18c..e4222d55 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.dart @@ -1,5 +1,8 @@ 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/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:lensai/features/user/domain/services/local_authentication.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -7,15 +10,47 @@ part 'selected_container.g.dart'; @Riverpod(keepAlive: true) class SelectedContainer extends _$SelectedContainer { - void setContainerId(String id) { - state = id; + Future fetchData() async { + if (state != null) { + return ref + .read(containerRepositoryProvider.notifier) + .getContainerData(state!); + } + + return null; } - void toggleContainer(String id) { + Future setContainerId(String id) async { + final container = await ref + .read(containerRepositoryProvider.notifier) + .getContainerData(id); + + if (container != null) { + if (container.metadata.authSettings.authenticationRequired) { + final authResult = await ref + .read(localAuthenticationServiceProvider.notifier) + .authenticate( + authKey: 'container_access::${container.id}', + localizedReason: + 'Require authentication for container ${container.name ?? 'New Container'}', + settings: container.metadata.authSettings, + useAuthCache: true, + ); + + if (authResult) { + state = id; + } + } else { + state = id; + } + } + } + + Future toggleContainer(String id) async { if (state == id) { clearContainer(); } else { - setContainerId(id); + await setContainerId(id); } } @@ -25,6 +60,17 @@ class SelectedContainer extends _$SelectedContainer { @override String? build() { + ref.listen( + containersWithCountProvider, + (previous, next) { + if (state != null && next.valueOrNull != null) { + if (!next.value!.any((container) => container.id == state)) { + clearContainer(); + } + } + }, + ); + return null; } } diff --git a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart index 2f170d39..f9c622b4 100644 --- a/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/providers/selected_container.g.dart @@ -25,7 +25,7 @@ final selectedContainerDataProvider = @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef; -String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf'; +String _$selectedContainerHash() => r'753f30646c4992981ec538dcfd33fa23aa1f673d'; /// See also [SelectedContainer]. @ProviderFor(SelectedContainer) diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart index f906d6f0..bdf1921b 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.dart @@ -1,54 +1,72 @@ import 'dart:ui'; -import 'package:lensai/features/geckoview/features/tabs/data/database/database.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/tab.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 addContainer(ContainerData container) { + return ref.read(tabDatabaseProvider).containerDao.addContainer(container); } - Future addContainer({required String? name, required Color color}) { - return _db.containerDao.addContainer(name: name, color: color); + Future replaceContainer(ContainerData container) { + return ref + .read(tabDatabaseProvider) + .containerDao + .replaceContainer(container); } - Future replaceContainer({ - required String id, - required String? name, - required Color color, - }) { - return _db.containerDao.replaceContainer(id, name: name, color: color); + Future getContainerData(String id) async { + return ref + .read(tabDatabaseProvider) + .containerDao + .getContainerData(id) + .getSingleOrNull(); } - Future deleteContainer(String id) { - return _db.containerDao.deleteContainer(id); + Future deleteContainer(String id) async { + await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id); + + return ref.read(tabDatabaseProvider).containerDao.deleteContainer(id); } Future> getDistinctColors() { - return _db.containerDao + return ref + .read(tabDatabaseProvider) + .containerDao .getDistinctColors() .get() .then((colors) => colors.toSet()); } Future getLeadingOrderKey(String? containerId) { - return _db.containerDao.generateLeadingOrderKey(containerId).getSingle(); + return ref + .read(tabDatabaseProvider) + .containerDao + .generateLeadingOrderKey(containerId) + .getSingle(); } Future getTrailingOrderKey(String? containerId) { - return _db.containerDao.generateTrailingOrderKey(containerId).getSingle(); + return ref + .read(tabDatabaseProvider) + .containerDao + .generateTrailingOrderKey(containerId) + .getSingle(); } Future getOrderKeyAfterTab(String tabId, String? containerId) { - return _db.containerDao + return ref + .read(tabDatabaseProvider) + .containerDao .generateOrderKeyAfterTabId(containerId, tabId) .getSingle(); } + + @override + void build() {} } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart index 65c00e11..54b02740 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/container.g.dart @@ -7,7 +7,7 @@ part of 'container.dart'; // ************************************************************************** String _$containerRepositoryHash() => - r'89ad15861fb213d6a3fa51232fd0ff9943f51e92'; + r'b965883f2279b6f0148592bbac5ff40a60798f51'; /// See also [ContainerRepository]. @ProviderFor(ContainerRepository) diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart index 9f14cef9..3865d5c0 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.dart @@ -1,5 +1,4 @@ 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'; @@ -7,35 +6,40 @@ part 'tab.g.dart'; @Riverpod(keepAlive: true) class TabDataRepository extends _$TabDataRepository { - late TabDatabase _db; - @override - void build() { - _db = ref.watch(tabDatabaseProvider); - } + void build() {} Future assignContainer(String tabId, String? containerId) { - return _db.tabDao.assignContainer( - tabId, - containerId: containerId, - ); + return ref.read(tabDatabaseProvider).tabDao.assignContainer( + tabId, + containerId: containerId, + ); } Future assignOrderKey(String tabId, String orderKey) { - return _db.tabDao.assignOrderKey( - tabId, - orderKey: orderKey, - ); + return ref.read(tabDatabaseProvider).tabDao.assignOrderKey( + tabId, + orderKey: orderKey, + ); } Future closeAllTabs(String? containerId) async { - final tabIds = await _db.tabDao.containerTabIds(containerId).get(); + final tabIds = await ref + .read(tabDatabaseProvider) + .tabDao + .containerTabIds(containerId) + .get(); + if (tabIds.isNotEmpty) { await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); } } Future containerTabId(String tabId) { - return _db.tabDao.tabContainerId(tabId).getSingle(); + return ref + .read(tabDatabaseProvider) + .tabDao + .tabContainerId(tabId) + .getSingleOrNull(); } } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart index 0bd5e2d3..9d0e9d59 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab.g.dart @@ -6,7 +6,7 @@ part of 'tab.dart'; // RiverpodGenerator // ************************************************************************** -String _$tabDataRepositoryHash() => r'c067bbbf319cd95ad869655740fdf49d13159b94'; +String _$tabDataRepositoryHash() => r'30ae87fbcb37facd565cb28de605814cd7db4ce8'; /// See also [TabDataRepository]. @ProviderFor(TabDataRepository) diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.dart index 39139ea8..10132af2 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart'; import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -9,8 +8,6 @@ part 'tab_search.g.dart'; @Riverpod() class TabSearchRepository extends _$TabSearchRepository { - late TabDatabase _db; - Future addQuery( String input, { int snippetLength = 120, @@ -20,7 +17,9 @@ class TabSearchRepository extends _$TabSearchRepository { }) async { if (input.isNotEmpty) { state = await AsyncValue.guard( - () => _db.tabDao + () => ref + .read(tabDatabaseProvider) + .tabDao .queryTabs( matchPrefix: matchPrefix, matchSuffix: matchSuffix, @@ -37,8 +36,6 @@ class TabSearchRepository extends _$TabSearchRepository { @override Future?> build() { - _db = ref.watch(tabDatabaseProvider); - return Future.value(); } } diff --git a/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.g.dart b/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.g.dart index d0cc367a..9f0cbbe9 100644 --- a/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.g.dart +++ b/app/lib/features/geckoview/features/tabs/domain/repositories/tab_search.g.dart @@ -7,7 +7,7 @@ part of 'tab_search.dart'; // ************************************************************************** String _$tabSearchRepositoryHash() => - r'9bd42f89bcc3a8d94add093076737dd57383bae2'; + r'57d130f50aa3feb7a31d27f9a5b54234e0e7064f'; /// See also [TabSearchRepository]. @ProviderFor(TabSearchRepository) diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart index d2181c4a..4bba77d3 100644 --- a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart @@ -6,6 +6,7 @@ import 'package:langchain/langchain.dart' as langchain; import 'package:langchain/langchain.dart'; import 'package:langchain_openai/langchain_openai.dart'; import 'package:lensai/core/models.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart'; @@ -41,21 +42,21 @@ class DocumentRepository extends _$DocumentRepository { chunkOverlap: chunkOverlap, ); - final splittedWithSource = (splitted != null) - ? ( - mainDocumentId: splitted.mainDocumentId, - parts: splitted.parts - .map( - (part) => part.copyWith( - metadata: { - ...part.metadata, - 'source': part.id, - }, - ), - ) - .toList() - ) - : null; + final splittedWithSource = splitted.mapNotNull( + (splitted) => ( + mainDocumentId: splitted.mainDocumentId, + parts: splitted.parts + .map( + (part) => part.copyWith( + metadata: { + ...part.metadata, + 'source': part.id, + }, + ), + ) + .toList() + ), + ); await _vectorDao.insertDocuments( splittedWithSource?.parts ?? [doc], diff --git a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart index d77dfe62..20fbbf34 100644 --- a/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart +++ b/app/lib/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.g.dart @@ -7,7 +7,7 @@ part of 'document.dart'; // ************************************************************************** String _$documentRepositoryHash() => - r'0c93116e0f9887d452b7c493dd4da45e5bf0c2d4'; + r'60e5a0c889d9659c8e0e254c59225c2041a0cb56'; /// See also [DocumentRepository]. @ProviderFor(DocumentRepository) diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart new file mode 100644 index 00000000..062c12fd --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_edit.dart @@ -0,0 +1,299 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/core/uuid.dart'; +import 'package:lensai/extensions/nullable.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart'; +import 'package:lensai/features/user/domain/services/local_authentication.dart'; + +enum _DialogMode { create, edit } + +const _timeoutOptions = >[ + DropdownMenuItem( + child: Text('Immediately'), + ), + DropdownMenuItem( + value: Duration(minutes: 1), + child: Text('1 minute'), + ), + DropdownMenuItem( + value: Duration(minutes: 5), + child: Text('5 minutes'), + ), + DropdownMenuItem( + value: Duration(minutes: 15), + child: Text('15 minutes'), + ), + DropdownMenuItem( + value: Duration(hours: 1), + child: Text('1 hour'), + ), +]; + +class ContainerEditScreen extends HookConsumerWidget { + final _DialogMode _mode; + + final ContainerData initialContainer; + + const ContainerEditScreen._({ + required _DialogMode mode, + required this.initialContainer, + }) : _mode = mode; + + factory ContainerEditScreen.create({ + required ContainerData initialContainer, + }) { + return ContainerEditScreen._( + mode: _DialogMode.create, + initialContainer: initialContainer, + ); + } + + factory ContainerEditScreen.edit({required ContainerData initialContainer}) { + return ContainerEditScreen._( + mode: _DialogMode.edit, + initialContainer: initialContainer, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final selectedColor = useState(initialContainer.color); + final contextualIdentity = + useState(initialContainer.metadata.contextualIdentity); + final authSettings = useState(initialContainer.metadata.authSettings); + + final textController = + useTextEditingController(text: initialContainer.name); + + return Scaffold( + appBar: AppBar( + title: Text( + switch (_mode) { + _DialogMode.create => 'New Container', + _DialogMode.edit => 'Edit Container', + }, + ), + actions: [ + IconButton( + onPressed: () async { + final name = textController.text.trim(); + final container = initialContainer.copyWith( + name: name.isNotEmpty ? name : null, + color: selectedColor.value, + metadata: initialContainer.metadata.mapNotNull( + (metadata) => metadata.copyWith( + contextualIdentity: contextualIdentity.value, + authSettings: authSettings.value, + ), + ) ?? + ContainerMetadata( + iconData: null, + contextualIdentity: contextualIdentity.value, + authSettings: authSettings.value, + ), + ); + + //Check for permissions, when auth is set or getting set + if (initialContainer + .metadata.authSettings.authenticationRequired || + container.metadata.authSettings.authenticationRequired) { + final authResult = await ref + .read(localAuthenticationServiceProvider.notifier) + .authenticate( + authKey: 'container_access::${container.id}', + localizedReason: + 'Require authentication for container ${container.name ?? 'New Container'}', + ); + + if (!authResult) { + return; + } + } + + switch (_mode) { + case _DialogMode.create: + await ref + .read(containerRepositoryProvider.notifier) + .addContainer(container); + case _DialogMode.edit: + await ref + .read(containerRepositoryProvider.notifier) + .replaceContainer(container); + } + + if (context.mounted) { + context.pop(); + } + }, + icon: const Icon(Icons.check), + ), + ], + ), + body: SafeArea( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + Expanded( + child: ListView( + children: [ + TextField( + decoration: InputDecoration( + prefixIcon: Padding( + padding: const EdgeInsets.all(10.0), + child: AnimatedContainer( + duration: const Duration(milliseconds: 300), + height: 24, + width: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: selectedColor.value, + ), + ), + ), + label: const Text('Name'), + ), + controller: textController, + ), + TextButton.icon( + label: const Text('Select Color'), + icon: const Icon(Icons.colorize), + onPressed: () async { + final color = await showDialog( + context: context, + builder: (context) => + ColorPickerDialog(selectedColor.value), + ); + + if (color != null) { + selectedColor.value = color; + } + }, + ), + SwitchListTile.adaptive( + value: contextualIdentity.value != null, + title: const Text('Cookie Isolation'), + secondary: const Icon(MdiIcons.cookieLock), + contentPadding: EdgeInsets.zero, + onChanged: (_mode == _DialogMode.create) + ? (value) { + contextualIdentity.value = value + ? initialContainer + .metadata.contextualIdentity ?? + uuid.v4() + : null; + } + : null, + ), + SwitchListTile.adaptive( + value: authSettings.value.authenticationRequired, + title: const Text('Require Authentication'), + secondary: const Icon(MdiIcons.fingerprint), + contentPadding: EdgeInsets.zero, + onChanged: (value) { + authSettings.value = authSettings.value.copyWith + .authenticationRequired(value); + }, + ), + if (authSettings.value.authenticationRequired) + CheckboxListTile.adaptive( + value: authSettings.value.lockOnAppBackground, + title: const Text( + 'Auto-lock on background', + ), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + authSettings.value = authSettings.value.copyWith + .lockOnAppBackground(value!); + }, + ), + if (authSettings.value.authenticationRequired) + CheckboxListTile.adaptive( + value: authSettings.value.lockTimeout != null, + title: const Text( + 'Timeout', + ), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) { + final newValue = + value! ? _timeoutOptions[1].value : null; + + authSettings.value = + authSettings.value.copyWith.lockTimeout(newValue); + }, + secondary: DropdownButton( + value: authSettings.value.lockTimeout, + items: _timeoutOptions, + onChanged: (value) { + authSettings.value = + authSettings.value.copyWith.lockTimeout(value); + }, + ), + ), + ], + ), + ), + if (_mode == _DialogMode.edit) + SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: BorderSide( + color: Theme.of(context).colorScheme.error, + ), + foregroundColor: Theme.of(context).colorScheme.error, + iconColor: Theme.of(context).colorScheme.error, + ), + label: const Text('Delete'), + icon: const Icon(Icons.delete), + onPressed: () async { + final result = await showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: const Text('Delete Container'), + content: const Text( + 'Are you sure you want to delete this container and close all attached tabs?', + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context, false); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, true); + }, + child: const Text('Delete'), + ), + ], + ); + }, + ); + + if (result == true) { + await ref + .read(containerRepositoryProvider.notifier) + .deleteContainer(initialContainer.id); + + if (context.mounted) { + context.pop(); + } + } + }, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart index aa80684b..d351f648 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/screens/container_list.dart @@ -1,119 +1,17 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_slidable/flutter_slidable.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/core/uuid.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.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/features/geckoview/features/tabs/domain/repositories/container.dart'; -import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart'; import 'package:skeletonizer/skeletonizer.dart'; -class _ContainerTile extends HookWidget { - final ContainerData container; - final bool isSelected; - - final void Function(ContainerResult edited) onEdit; - final void Function() onDelete; - final void Function() onTap; - - const _ContainerTile( - this.container, { - required this.isSelected, - required this.onEdit, - required this.onDelete, - required this.onTap, - super.key, - }); - - @override - Widget build(BuildContext context) { - final menuController = useMemoized(() => MenuController()); - - return ListTile( - selected: isSelected, - leading: CircleAvatar(backgroundColor: container.color), - title: Text(container.name ?? 'New Container'), - trailing: MenuAnchor( - controller: menuController, - builder: (context, controller, child) { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: InkWell( - onTap: () { - if (controller.isOpen) { - controller.close(); - } else { - controller.open(); - } - }, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0), - child: Icon(Icons.more_vert), - ), - ), - ); - }, - menuChildren: [ - MenuItemButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (context) => ContainerDialog.edit( - name: container.name, - initialColor: container.color, - ), - ); - - if (result != null) { - onEdit(result); - } - }, - leadingIcon: const Icon(Icons.edit), - child: const Text('Edit'), - ), - MenuItemButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: const Text('Delete Container'), - content: const Text( - 'Are you sure you want to delete this container and close all attached tabs?', - ), - actions: [ - TextButton( - onPressed: () { - Navigator.pop(context, false); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.pop(context, true); - }, - child: const Text('Delete'), - ), - ], - ); - }, - ); - - if (result == true) { - onDelete(); - } - }, - leadingIcon: const Icon(Icons.delete), - child: const Text('Delete'), - ), - ], - ), - onTap: onTap, - ); - } -} - class ContainerListScreen extends HookConsumerWidget { const ContainerListScreen(); @@ -139,29 +37,36 @@ class ContainerListScreen extends HookConsumerWidget { itemCount: containers.length, itemBuilder: (context, index) { final container = containers[index]; - return _ContainerTile( - container, + return Slidable( key: ValueKey(container.id), - isSelected: container.id == selectedContainer, - onEdit: (edited) async { - await ref - .read(containerRepositoryProvider.notifier) - .replaceContainer( - id: container.id, - name: edited.name, - color: edited.color, - ); - }, - onDelete: () async { - await ref - .read(containerRepositoryProvider.notifier) - .deleteContainer(container.id); - }, - onTap: () { - ref - .read(selectedContainerProvider.notifier) - .toggleContainer(container.id); - }, + endActionPane: ActionPane( + motion: const ScrollMotion(), + children: [ + SlidableAction( + onPressed: (context) async { + await ref + .read(containerRepositoryProvider.notifier) + .deleteContainer(container.id); + }, + backgroundColor: + Theme.of(context).colorScheme.errorContainer, + foregroundColor: Theme.of(context) + .colorScheme + .onErrorContainer, + icon: Icons.delete, + label: 'Delete', + ), + ], + ), + child: ContainerListTile( + container, + isSelected: container.id == selectedContainer, + onTap: () async { + await ref + .read(selectedContainerProvider.notifier) + .toggleContainer(container.id); + }, + ), ); }, ); @@ -170,12 +75,9 @@ class ContainerListScreen extends HookConsumerWidget { error: (error, stackTrace) => SizedBox.shrink(), loading: () => ListView.builder( itemCount: 3, - itemBuilder: (context, index) => _ContainerTile( + itemBuilder: (context, index) => ContainerListTile( ContainerData(id: 'null', color: Colors.transparent), isSelected: false, - onEdit: (_) {}, - onDelete: () {}, - onTap: () {}, ), ), ), @@ -188,17 +90,15 @@ class ContainerListScreen extends HookConsumerWidget { await ref.read(unusedRandomContainerColorProvider.future); if (context.mounted) { - final result = await showDialog( - context: context, - builder: (context) => ContainerDialog.create( - initialColor: initialColor, - ), + final result = await context.push( + ContainerCreateRoute().location, + extra: ContainerData(id: uuid.v7(), color: initialColor), ); if (result != null) { await ref .read(containerRepositoryProvider.notifier) - .addContainer(name: result.name, color: result.color); + .addContainer(result); } } }, diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart new file mode 100644 index 00000000..034cd192 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/color_picker_dialog.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart'; + +class ColorPickerDialog extends HookWidget { + final Color initialColor; + + const ColorPickerDialog(this.initialColor); + + @override + Widget build(BuildContext context) { + final selectedColor = useState(initialColor); + + return AlertDialog( + titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0), + contentPadding: const EdgeInsets.only( + left: 20.0, + right: 20.0, + bottom: 24.0, + ), + insetPadding: const EdgeInsets.symmetric( + horizontal: 20.0, + vertical: 24.0, + ), + title: const Text('Select Color'), + content: MaterialPicker( + pickerColor: selectedColor.value, + onColorChanged: (value) { + selectedColor.value = value; + }, + ), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.pop(context, selectedColor.value); + }, + child: const Text('Select'), + ), + ], + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart index 8e64e81e..05588142 100644 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_chips.dart @@ -5,8 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/providers.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/data/models/drag_data.dart'; +import 'package:lensai/extensions/nullable.dart'; +import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; +import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart'; import 'package:lensai/presentation/hooks/overlay_portal_controller.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart'; @@ -90,12 +93,37 @@ class ContainerChips extends HookConsumerWidget { overlayController.hide(); }, onAcceptWithDetails: (details) async { - await ref + final containerId = await ref .read(tabDataRepositoryProvider.notifier) - .assignContainer( - details.data.tabId, - container.id, - ); + .containerTabId(details.data.tabId); + + final containerData = + await containerId.mapNotNull( + (containerId) => ref + .read(containerRepositoryProvider.notifier) + .getContainerData(containerId), + ); + + if (container.metadata.contextualIdentity == + containerData?.metadata.contextualIdentity) { + await ref + .read(tabDataRepositoryProvider.notifier) + .assignContainer( + details.data.tabId, + container.id, + ); + } else { + await ref + .read(tabRepositoryProvider.notifier) + .duplicateTab( + selectTabId: details.data.tabId, + containerId: container.id, + ); + + await ref + .read(tabRepositoryProvider.notifier) + .closeTab(details.data.tabId); + } dragTargetTabId.value = null; overlayController.hide(); diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart deleted file mode 100644 index 820a9d8e..00000000 --- a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart +++ /dev/null @@ -1,124 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/material_color_picker.dart'; - -typedef ContainerResult = ({String? name, Color color}); - -enum _DialogMode { create, edit } - -class ContainerDialog extends HookWidget { - final _DialogMode _mode; - - final String? initialName; - final Color initialColor; - - const ContainerDialog._({ - required _DialogMode mode, - required this.initialColor, - this.initialName, - }) : _mode = mode; - - factory ContainerDialog.create({required Color initialColor}) { - return ContainerDialog._( - mode: _DialogMode.create, - initialColor: initialColor, - ); - } - - factory ContainerDialog.edit({ - required String? name, - required Color initialColor, - }) { - return ContainerDialog._( - mode: _DialogMode.edit, - initialColor: initialColor, - initialName: name, - ); - } - - @override - Widget build(BuildContext context) { - final selectedColor = useState(initialColor); - final textController = useTextEditingController(text: initialName); - - return SimpleDialog( - titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0), - contentPadding: const EdgeInsets.only( - left: 20.0, - right: 20.0, - bottom: 24.0, - ), - insetPadding: const EdgeInsets.symmetric( - horizontal: 20.0, - vertical: 24.0, - ), - title: Text( - switch (_mode) { - _DialogMode.create => 'New Container', - _DialogMode.edit => 'Edit Container', - }, - ), - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4.0), - child: TextField( - decoration: InputDecoration( - prefixIcon: Padding( - padding: const EdgeInsets.all(10.0), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - height: 24, - width: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: selectedColor.value, - ), - ), - ), - label: const Text('Name'), - ), - controller: textController, - ), - ), - const SizedBox(height: 16), - MaterialPicker( - pickerColor: selectedColor.value, - onColorChanged: (value) { - selectedColor.value = value; - }, - ), - const SizedBox(height: 24), - OverflowBar( - alignment: MainAxisAlignment.end, - spacing: 8.0, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - final name = textController.text.trim(); - Navigator.pop( - context, - ( - name: name.isNotEmpty ? name : null, - color: selectedColor.value, - ), - ); - }, - child: Text( - switch (_mode) { - _DialogMode.create => 'Add', - _DialogMode.edit => 'Edit', - }, - ), - ), - ], - ), - ], - ); - } -} diff --git a/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart new file mode 100644 index 00000000..2fb93717 --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:lensai/core/routing/routes.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; + +class ContainerListTile extends HookWidget { + final ContainerData container; + final bool isSelected; + + final void Function()? onTap; + + const ContainerListTile( + this.container, { + required this.isSelected, + this.onTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListTileTheme( + selectedColor: Theme.of(context).colorScheme.onPrimaryContainer, + selectedTileColor: Theme.of(context).colorScheme.primaryContainer, + child: ListTile( + selected: isSelected, + leading: CircleAvatar(backgroundColor: container.color), + title: Text(container.name ?? 'New Container'), + trailing: IconButton( + onPressed: () async { + await context.push( + ContainerEditRoute().location, + extra: container, + ); + }, + icon: const Icon(Icons.chevron_right), + ), + onTap: onTap, + ), + ); + } +} diff --git a/app/lib/features/geckoview/features/tabs/utils/setting_groups_serializer.dart b/app/lib/features/geckoview/features/tabs/utils/setting_groups_serializer.dart new file mode 100644 index 00000000..32d3c0fd --- /dev/null +++ b/app/lib/features/geckoview/features/tabs/utils/setting_groups_serializer.dart @@ -0,0 +1,34 @@ +import 'package:lensai/features/geckoview/features/preferences/data/models/preference_setting.dart'; + +enum PreferencePartition { + user('user'), + system('system'); + + final String key; + + const PreferencePartition(this.key); +} + +Map deserializePreferenceSettingGroups( + PreferencePartition partition, + Map content, +) { + final parititonedContent = content[partition.key] as Map; + + return parititonedContent.map( + (key, value) => MapEntry( + key, + PreferenceSettingGroup( + // ignore: avoid_dynamic_calls + description: value['description'] as String?, + // ignore: avoid_dynamic_calls + settings: (value['preferences'] as Map).map( + (key, value) => MapEntry( + key, + PreferenceSetting.fromJson(value as Map), + ), + ), + ), + ), + ); +} diff --git a/app/lib/features/kagi/data/services/chat.dart b/app/lib/features/kagi/data/services/chat.dart index 2efe3188..59aec1df 100644 --- a/app/lib/features/kagi/data/services/chat.dart +++ b/app/lib/features/kagi/data/services/chat.dart @@ -17,7 +17,7 @@ class KagiChatService extends _$KagiChatService { throw Exception('Not implemented !'); // final kagiSession = - // ref.read(settingsRepositoryProvider).valueOrNull?.kagiSession; + // ref.read(generalSettingsRepositoryProvider).valueOrNull?.kagiSession; // return Result.fromAsync( // () async { diff --git a/app/lib/features/kagi/presentation/tabs/assistant_tab.dart b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart index 10bc3045..d2a301fe 100644 --- a/app/lib/features/kagi/presentation/tabs/assistant_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/assistant_tab.dart @@ -25,7 +25,7 @@ // @override // Widget build(BuildContext context, WidgetRef ref) { // final incognitoEnabled = ref.watch( -// settingsRepositoryProvider.select((value) => value.incognitoMode), +// generalSettingsRepositoryProvider.select((value) => value.incognitoMode), // ); // final researchVariant = ref.watch(activeResearchVariantProvider); diff --git a/app/lib/features/kagi/presentation/tabs/summarize_tab.dart b/app/lib/features/kagi/presentation/tabs/summarize_tab.dart index 8c53d33b..74df7958 100644 --- a/app/lib/features/kagi/presentation/tabs/summarize_tab.dart +++ b/app/lib/features/kagi/presentation/tabs/summarize_tab.dart @@ -10,7 +10,7 @@ import 'package:lensai/features/geckoview/features/browser/presentation/widgets/ import 'package:lensai/features/kagi/data/entities/modes.dart'; import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; import 'package:lensai/features/share_intent/domain/entities/shared_content.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/utils/uri_parser.dart' as uri_parser; @@ -21,11 +21,7 @@ class _InputField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select( - (value) => value.incognitoMode, - ), - ); + final incognitoEnabled = ref.watch(incognitoModeEnabledProvider); return TextFormField( controller: controller, diff --git a/app/lib/features/search/presentation/screens/search.dart b/app/lib/features/search/presentation/screens/search.dart index 4a1747a8..51853903 100644 --- a/app/lib/features/search/presentation/screens/search.dart +++ b/app/lib/features/search/presentation/screens/search.dart @@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/data/models/equatable_iterable.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart'; @@ -223,9 +224,9 @@ class SearchScreen extends HookConsumerWidget { result.extractedContent ?? result.fullContent; return ListTile( - title: (result.title != null) - ? MarkdownBody(data: result.title!) - : null, + title: result.title.mapNotNull( + (title) => MarkdownBody(data: title), + ), subtitle: (!headHasMatch && bodyResult != null) ? MarkdownBody(data: bodyResult) : null, diff --git a/app/lib/features/search/presentation/widgets/search_field.dart b/app/lib/features/search/presentation/widgets/search_field.dart index 1a5cb23d..3e2336be 100644 --- a/app/lib/features/search/presentation/widgets/search_field.dart +++ b/app/lib/features/search/presentation/widgets/search_field.dart @@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; import 'package:lensai/presentation/widgets/speech_to_text_button.dart'; class SearchField extends HookConsumerWidget { @@ -25,11 +25,7 @@ class SearchField extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final incognitoEnabled = ref.watch( - settingsRepositoryProvider.select( - (value) => value.incognitoMode, - ), - ); + final incognitoEnabled = ref.watch(incognitoModeEnabledProvider); final hasText = useListenableSelector( textEditingController, diff --git a/app/lib/features/settings/presentation/controllers/save_settings.dart b/app/lib/features/settings/presentation/controllers/save_settings.dart index b82d3c83..1d25b3d3 100644 --- a/app/lib/features/settings/presentation/controllers/save_settings.dart +++ b/app/lib/features/settings/presentation/controllers/save_settings.dart @@ -1,18 +1,34 @@ -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/repositories/engine_settings.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'save_settings.g.dart'; @Riverpod(keepAlive: true) -class SaveSettingsController extends _$SaveSettingsController { +class SaveGeneralSettingsController extends _$SaveGeneralSettingsController { @override FutureOr build() {} - Future save(UpdateSettingsFunc updateSettings) async { + Future save(UpdateGeneralSettingsFunc updateSettings) async { state = const AsyncLoading(); state = await AsyncValue.guard( () => ref - .read(settingsRepositoryProvider.notifier) + .read(generalSettingsRepositoryProvider.notifier) + .updateSettings(updateSettings), + ); + } +} + +@Riverpod(keepAlive: true) +class SaveEngineSettingsController extends _$SaveEngineSettingsController { + @override + FutureOr build() {} + + Future save(UpdateEngineSettingsFunc updateSettings) async { + state = const AsyncLoading(); + state = await AsyncValue.guard( + () => ref + .read(engineSettingsRepositoryProvider.notifier) .updateSettings(updateSettings), ); } diff --git a/app/lib/features/settings/presentation/controllers/save_settings.g.dart b/app/lib/features/settings/presentation/controllers/save_settings.g.dart index 45ccd5b3..70279c4b 100644 --- a/app/lib/features/settings/presentation/controllers/save_settings.g.dart +++ b/app/lib/features/settings/presentation/controllers/save_settings.g.dart @@ -6,22 +6,39 @@ part of 'save_settings.dart'; // RiverpodGenerator // ************************************************************************** -String _$saveSettingsControllerHash() => - r'7ffcef95b48dce1f80b56fb3580282ff3df240b4'; +String _$saveGeneralSettingsControllerHash() => + r'ad8b60839cbcb05a964f57e32fc5d9c1aba30377'; -/// See also [SaveSettingsController]. -@ProviderFor(SaveSettingsController) -final saveSettingsControllerProvider = - AsyncNotifierProvider.internal( - SaveSettingsController.new, - name: r'saveSettingsControllerProvider', +/// See also [SaveGeneralSettingsController]. +@ProviderFor(SaveGeneralSettingsController) +final saveGeneralSettingsControllerProvider = + AsyncNotifierProvider.internal( + SaveGeneralSettingsController.new, + name: r'saveGeneralSettingsControllerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$saveSettingsControllerHash, + : _$saveGeneralSettingsControllerHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SaveSettingsController = AsyncNotifier; +typedef _$SaveGeneralSettingsController = AsyncNotifier; +String _$saveEngineSettingsControllerHash() => + r'4ca0ee76a41a72a8e6eacdb0e39045c667390c00'; + +/// See also [SaveEngineSettingsController]. +@ProviderFor(SaveEngineSettingsController) +final saveEngineSettingsControllerProvider = + AsyncNotifierProvider.internal( + SaveEngineSettingsController.new, + name: r'saveEngineSettingsControllerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$saveEngineSettingsControllerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SaveEngineSettingsController = AsyncNotifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/settings/presentation/screens/bang_settings.dart b/app/lib/features/settings/presentation/screens/bang_settings.dart new file mode 100644 index 00000000..bed86bc7 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/bang_settings.dart @@ -0,0 +1,58 @@ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/bangs/data/models/bang.dart'; +import 'package:lensai/features/bangs/domain/repositories/data.dart'; +import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart'; +import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart'; +import 'package:lensai/features/settings/presentation/widgets/sections.dart'; + +class BangSettingsScreen extends HookConsumerWidget { + const BangSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar(title: const Text('Bang Settings')), + body: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + children: [ + CustomListTile( + title: 'Bang Frequencies', + subtitle: 'Tracked usage for Bang recommendations', + suffix: FilledButton.icon( + onPressed: () async { + await ref + .read(bangDataRepositoryProvider.notifier) + .resetFrequencies(); + }, + icon: const Icon(Icons.delete), + label: const Text('Clear'), + ), + ), + const SettingSubSection(name: 'Repositories'), + const BangGroupListTile( + group: BangGroup.general, + title: 'General Bangs', + subtitle: 'Automatically syncs every 7 days', + ), + const BangGroupListTile( + group: BangGroup.assistant, + title: 'Assistant Bangs', + subtitle: 'Automatically syncs every 7 days', + ), + const BangGroupListTile( + group: BangGroup.kagi, + title: 'Kagi Bangs', + subtitle: 'Automatically syncs every 7 days', + ), + ], + ); + }, + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/general_settings.dart b/app/lib/features/settings/presentation/screens/general_settings.dart new file mode 100644 index 00000000..bd1f8c9f --- /dev/null +++ b/app/lib/features/settings/presentation/screens/general_settings.dart @@ -0,0 +1,148 @@ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; +import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart'; +import 'package:lensai/features/user/data/models/general_settings.dart'; +import 'package:lensai/features/user/domain/providers.dart'; +import 'package:lensai/features/user/domain/repositories/cache.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; + +class GeneralSettingsScreen extends HookConsumerWidget { + const GeneralSettingsScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final theme = Theme.of(context); + + final generalSettings = ref.watch(generalSettingsRepositoryProvider); + + return Scaffold( + appBar: AppBar(title: const Text('General Settings')), + body: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Theme'), + leading: Icon(Icons.palette), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.brightness_auto), + label: Text('System'), + ), + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode), + label: Text('Light'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode), + label: Text('Dark'), + ), + ], + selected: {generalSettings.themeMode}, + onSelectionChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider + .notifier) + .save( + (currentSettings) => currentSettings.copyWith + .themeMode(value.first), + ); + }, + ), + ), + ], + ), + ), + SwitchListTile.adaptive( + title: const Text('Enable Reader Mode'), + subtitle: const Text( + 'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.', + ), + secondary: const Icon(MdiIcons.bookOpen), + value: generalSettings.enableReadability, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.enableReadability(value), + ); + }, + ), + Consumer( + builder: (context, ref, child) { + final size = ref.watch( + iconCacheSizeMegabytesProvider.select( + (value) => value.valueOrNull, + ), + ); + + return CustomListTile( + title: 'Icon Cache', + subtitle: 'Stored favicons', + prefix: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Icon( + Icons.image, + size: 24, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + content: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: DefaultTextStyle( + style: GoogleFonts.robotoMono( + textStyle: DefaultTextStyle.of(context).style, + ), + child: Table( + columnWidths: const {0: FixedColumnWidth(100)}, + children: [ + TableRow( + children: [ + const Text('Size'), + Text('${size?.toStringAsFixed(2) ?? 0} MB'), + ], + ), + ], + ), + ), + ), + suffix: FilledButton.icon( + onPressed: () async { + await ref + .read(cacheRepositoryProvider.notifier) + .clearCache(); + }, + icon: const Icon(Icons.delete), + label: const Text('Clear'), + ), + ); + }, + ), + ], + ); + }, + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index 084a19eb..07774289 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -1,48 +1,15 @@ import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; import 'package:go_router/go_router.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; -import 'package:lensai/features/bangs/data/models/bang.dart'; -import 'package:lensai/features/bangs/domain/repositories/data.dart'; -import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; -import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart'; -import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart'; -import 'package:lensai/features/user/data/models/settings.dart'; -import 'package:lensai/features/user/domain/providers.dart'; -import 'package:lensai/features/user/domain/repositories/cache.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); - Widget _buildSection(ThemeData theme, String name) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(name, style: theme.textTheme.titleLarge), - const Divider(), - ], - ), - ); - - Widget _buildSubSection(ThemeData theme, String name) => Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), - child: Text( - name, - style: theme.textTheme.titleLarge?.copyWith(fontSize: 18), - ), - ); - @override Widget build(BuildContext context, WidgetRef ref) { - final theme = Theme.of(context); - - final settings = ref.watch(settingsRepositoryProvider); - return Scaffold( appBar: AppBar(title: const Text('Settings')), body: FadingScroll( @@ -50,189 +17,61 @@ class SettingsScreen extends HookConsumerWidget { builder: (context, controller) { return ListView( controller: controller, + padding: const EdgeInsets.symmetric(horizontal: 12.0), children: [ - _buildSection(theme, 'General'), Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, child: ListTile( - title: const Text('Browser Hardening'), - trailing: IconButton( - onPressed: () async { - await context.push(BrowserHardeningRoute().location); - }, - icon: const Icon(Icons.chevron_right), + title: const Text('General'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, ), + leading: const Icon(Icons.settings), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await context.push(GeneralSettingsRoute().location); + }, ), ), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Theme', - style: theme.textTheme.bodyLarge, - ), - Center( - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.system, - icon: Icon(Icons.brightness_auto), - label: Text('System'), - ), - ButtonSegment( - value: ThemeMode.light, - icon: Icon(Icons.light_mode), - label: Text('Light'), - ), - ButtonSegment( - value: ThemeMode.dark, - icon: Icon(Icons.dark_mode), - label: Text('Dark'), - ), - ], - selected: {settings.themeMode}, - onSelectionChanged: (value) async { - await ref - .read(saveSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .themeMode(value.first), - ); - }, - ), - ), - ], - ), - ), - SwitchListTile.adaptive( - title: const Text('Incognito Mode'), - subtitle: const Text( - 'Deletes all browsing data upon app restart for enhanced privacy.', - ), - value: settings.incognitoMode, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.incognitoMode(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Enable JavaScript'), - subtitle: const Text( - 'While turning off JavaScript boosts security, privacy, and speed, it may cause some sites to not work as intended.', - ), - value: settings.enableJavascript, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.enableJavascript(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Block HTTP Protocol'), - subtitle: const Text( - 'Prevents the loading of unsecure HTTP content. When enabled, only secure HTTPS content is allowed.', - ), - value: settings.blockHttpProtocol, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.blockHttpProtocol(value), - ); - }, - ), - SwitchListTile.adaptive( - title: const Text('Enable Reader Mode'), - subtitle: const Text( - 'Optional browser app bar tool that extracts and simplifies web pages for improved readability by removing ads, sidebars, and other non-essential elements.', - ), - value: settings.enableReadability, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.enableReadability(value), - ); - }, - ), const SizedBox( - height: 16, + height: 8, ), - _buildSection(theme, 'Bangs'), - CustomListTile( - title: 'Bang Frequencies', - subtitle: 'Tracked usage for Bang recommendations', - suffix: FilledButton.icon( - onPressed: () async { - await ref - .read(bangDataRepositoryProvider.notifier) - .resetFrequencies(); + Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Web Engine'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.web), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await context.push(WebEngineSettingsRoute().location); }, - icon: const Icon(Icons.delete), - label: const Text('Clear'), ), ), - Consumer( - builder: (context, ref, child) { - final size = ref.watch( - iconCacheSizeMegabytesProvider.select( - (value) => value.valueOrNull, - ), - ); - - return CustomListTile( - title: 'Icon Cache', - subtitle: 'Stored favicons for Bangs', - content: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: DefaultTextStyle( - style: GoogleFonts.robotoMono( - textStyle: DefaultTextStyle.of(context).style, - ), - child: Table( - columnWidths: const {0: FixedColumnWidth(100)}, - children: [ - TableRow( - children: [ - const Text('Size'), - Text('${size?.toStringAsFixed(2) ?? 0} MB'), - ], - ), - ], - ), - ), - ), - suffix: FilledButton.icon( - onPressed: () async { - await ref - .read(cacheRepositoryProvider.notifier) - .clearCache(); - }, - icon: const Icon(Icons.delete), - label: const Text('Clear'), - ), - ); - }, + const SizedBox( + height: 8, ), - _buildSubSection(theme, 'Repositories'), - const BangGroupListTile( - group: BangGroup.general, - title: 'General Bangs', - subtitle: 'Automatically syncs every 7 days', - ), - const BangGroupListTile( - group: BangGroup.assistant, - title: 'Assistant Bangs', - subtitle: 'Automatically syncs every 7 days', - ), - const BangGroupListTile( - group: BangGroup.kagi, - title: 'Kagi Bangs', - subtitle: 'Automatically syncs every 7 days', + Card( + color: Theme.of(context).highlightColor, + clipBehavior: Clip.antiAlias, + child: ListTile( + title: const Text('Bangs'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.exclamationThick), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await context.push(BangSettingsRoute().location); + }, + ), ), ], ); diff --git a/app/lib/features/settings/presentation/screens/browser_hardening.dart b/app/lib/features/settings/presentation/screens/web_engine_hardening.dart similarity index 77% rename from app/lib/features/settings/presentation/screens/browser_hardening.dart rename to app/lib/features/settings/presentation/screens/web_engine_hardening.dart index 870c5681..f6f49b77 100644 --- a/app/lib/features/settings/presentation/screens/browser_hardening.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_hardening.dart @@ -4,17 +4,20 @@ import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/data/models/equatable_iterable.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart'; +import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart'; import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; -class BrowserHardeningScreen extends HookConsumerWidget { - const BrowserHardeningScreen(); +class WebEngineHardeningScreen extends HookConsumerWidget { + const WebEngineHardeningScreen(); @override Widget build(BuildContext context, WidgetRef ref) { - final preferenceGroups = - ref.watch(preferenceSettingsGeneralRepositoryProvider); + final preferenceGroups = ref.watch( + unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user), + ); final allGroupsActive = useMemoized( () => @@ -28,7 +31,7 @@ class BrowserHardeningScreen extends HookConsumerWidget { final theme = Theme.of(context); return Scaffold( - appBar: AppBar(title: const Text('Browser Hardening')), + appBar: AppBar(title: const Text('Web Engine Hardening')), body: preferenceGroups.when( data: (data) { return Column( @@ -49,7 +52,9 @@ class BrowserHardeningScreen extends HookConsumerWidget { ), onChanged: (value) async { final notifier = ref.read( - preferenceSettingsGeneralRepositoryProvider.notifier, + unifiedPreferenceSettingsRepositoryProvider( + PreferencePartition.user, + ).notifier, ); if (value) { @@ -71,9 +76,9 @@ class BrowserHardeningScreen extends HookConsumerWidget { Expanded( child: ListTile( title: Text(group.key), - subtitle: (group.value.description != null) - ? Text(group.value.description!) - : null, + subtitle: group.value.description.mapNotNull( + (description) => Text(description), + ), leading: HardeningGroupIcon( isActive: group.value.isActive, isPartlyActive: group.value.isPartlyActive, @@ -81,7 +86,7 @@ class BrowserHardeningScreen extends HookConsumerWidget { trailing: const Icon(Icons.chevron_right), onTap: () async { await context.push( - BrowserHardeningGroupRoute( + WebEngineHardeningGroupRoute( group: group.key, ).location, ); @@ -100,8 +105,11 @@ class BrowserHardeningScreen extends HookConsumerWidget { error: (error, stackTrace) => FailureWidget( title: 'Could not load preference settings', exception: error, - onRetry: () => - ref.refresh(preferenceSettingsGeneralRepositoryProvider), + onRetry: () => ref.refresh( + unifiedPreferenceSettingsRepositoryProvider( + PreferencePartition.user, + ), + ), ), loading: () => const SizedBox.shrink(), ), diff --git a/app/lib/features/settings/presentation/screens/browser_hardening_group.dart b/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart similarity index 67% rename from app/lib/features/settings/presentation/screens/browser_hardening_group.dart rename to app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart index 3a0cf81f..08276f5d 100644 --- a/app/lib/features/settings/presentation/screens/browser_hardening_group.dart +++ b/app/lib/features/settings/presentation/screens/web_engine_hardening_group.dart @@ -1,18 +1,24 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart'; +import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart'; import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart'; -class BrowserHardeningGroupScreen extends HookConsumerWidget { +class WebEngineHardeningGroupScreen extends HookConsumerWidget { final String groupName; - const BrowserHardeningGroupScreen({super.key, required this.groupName}); + const WebEngineHardeningGroupScreen({super.key, required this.groupName}); @override Widget build(BuildContext context, WidgetRef ref) { - final settings = - ref.watch(preferenceSettingsGroupRepositoryProvider(groupName)); + final settings = ref.watch( + preferenceSettingsGroupRepositoryProvider( + PreferencePartition.user, + groupName, + ), + ); final theme = Theme.of(context); @@ -36,18 +42,20 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget { color: theme.colorScheme.onPrimaryContainer, ), ), - subtitle: (group.description != null) - ? Text( - group.description!, - style: TextStyle( - color: theme.colorScheme.onPrimaryContainer, - ), - ) - : null, + subtitle: group.description.mapNotNull( + (description) => Text( + description, + style: TextStyle( + color: theme.colorScheme.onPrimaryContainer, + ), + ), + ), onChanged: (value) async { final notifier = ref.read( - preferenceSettingsGroupRepositoryProvider(groupName) - .notifier, + preferenceSettingsGroupRepositoryProvider( + PreferencePartition.user, + groupName, + ).notifier, ); if (value) { @@ -71,16 +79,17 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget { Expanded( child: SwitchListTile( value: setting.value.isActive, - title: Text(setting.value.title), - subtitle: (setting.value.description != null) - ? Text(setting.value.description!) - : null, + title: Text(setting.value.title ?? setting.key), + subtitle: setting.value.description.mapNotNull( + (description) => Text(description), + ), secondary: HardeningGroupIcon( isActive: setting.value.isActive, ), onChanged: (value) async { final notifier = ref.read( preferenceSettingsGroupRepositoryProvider( + PreferencePartition.user, groupName, ).notifier, ); @@ -96,16 +105,16 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget { else Expanded( child: ListTile( - title: Text(setting.value.title), - subtitle: (setting.value.description != null) - ? Text(setting.value.description!) - : null, + title: Text(setting.value.title ?? setting.key), + subtitle: setting.value.description.mapNotNull( + (description) => Text(description), + ), leading: HardeningGroupIcon( isActive: setting.value.isActive, ), - trailing: Padding( - padding: const EdgeInsets.only(right: 18.0), - child: const Icon(Icons.check), + trailing: const Padding( + padding: EdgeInsets.only(right: 18.0), + child: Icon(Icons.check), ), ), ), @@ -121,8 +130,12 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget { error: (error, stackTrace) => FailureWidget( title: 'Could not load preference settings', exception: error, - onRetry: () => - ref.refresh(preferenceSettingsGroupRepositoryProvider(groupName)), + onRetry: () => ref.refresh( + preferenceSettingsGroupRepositoryProvider( + PreferencePartition.user, + groupName, + ), + ), ), loading: () => const SizedBox.shrink(), ), diff --git a/app/lib/features/settings/presentation/screens/web_engine_settings.dart b/app/lib/features/settings/presentation/screens/web_engine_settings.dart new file mode 100644 index 00000000..5505f318 --- /dev/null +++ b/app/lib/features/settings/presentation/screens/web_engine_settings.dart @@ -0,0 +1,315 @@ +import 'package:fading_scroll/fading_scroll.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_material_design_icons/flutter_material_design_icons.dart'; +import 'package:flutter_mozilla_components/flutter_mozilla_components.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/extensions/nullable.dart'; +import 'package:lensai/features/settings/presentation/controllers/save_settings.dart'; +import 'package:lensai/features/user/data/models/engine_settings.dart'; +import 'package:lensai/features/user/data/models/general_settings.dart'; +import 'package:lensai/features/user/domain/repositories/engine_settings.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; + +class WebEngineSettingsScreen extends HookConsumerWidget { + const WebEngineSettingsScreen(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final generalSettings = ref.watch(generalSettingsRepositoryProvider); + final engineSettings = ref.watch(engineSettingsRepositoryProvider); + + return Scaffold( + appBar: AppBar(title: const Text('Web Engine Settings')), + body: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + children: [ + SwitchListTile.adaptive( + title: const Text('Incognito Mode'), + subtitle: const Text( + 'Deletes all browsing data upon app restart for enhanced privacy.', + ), + secondary: const Icon(MdiIcons.incognito), + value: generalSettings.deleteBrowsingDataOnQuit != null, + onChanged: (value) async { + await ref + .read(saveGeneralSettingsControllerProvider.notifier) + .save( + (currentSettings) => value + ? currentSettings.copyWith + .deleteBrowsingDataOnQuit({}) + : currentSettings.copyWith + .deleteBrowsingDataOnQuit(null), + ); + }, + ), + if (generalSettings.deleteBrowsingDataOnQuit != null) + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + children: [ + for (final type in DeleteBrowsingDataType.values) + CheckboxListTile.adaptive( + value: generalSettings.deleteBrowsingDataOnQuit! + .contains(type), + controlAffinity: ListTileControlAffinity.leading, + title: Text(type.title), + subtitle: type.description + .mapNotNull((description) => Text(description)), + onChanged: (value) async { + final notifier = ref.read( + saveGeneralSettingsControllerProvider.notifier, + ); + + if (value == true) { + await notifier.save( + (currentSettings) => currentSettings.copyWith + .deleteBrowsingDataOnQuit({ + ...currentSettings.deleteBrowsingDataOnQuit!, + type, + }), + ); + } else { + await notifier.save( + (currentSettings) => currentSettings.copyWith + .deleteBrowsingDataOnQuit( + { + ...currentSettings + .deleteBrowsingDataOnQuit!, + }..remove(type), + ), + ); + } + }, + ), + ], + ), + ), + SwitchListTile.adaptive( + title: const Text('Enable JavaScript'), + subtitle: const Text( + 'While turning off JavaScript boosts security, privacy, and speed, it may cause some sites to not work as intended.', + ), + secondary: const Icon(MdiIcons.languageJavascript), + value: engineSettings.javascriptEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.javascriptEnabled(value), + ); + }, + ), + SwitchListTile.adaptive( + title: const Text('Enable Global Privacy Control (GPC)'), + secondary: const Icon(MdiIcons.incognitoCircleOff), + value: engineSettings.globalPrivacyControlEnabled, + onChanged: (value) async { + await ref + .read(saveEngineSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .globalPrivacyControlEnabled(value), + ); + }, + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Block insecure HTTP connections'), + leading: Icon(MdiIcons.lockOpen), + contentPadding: EdgeInsets.zero, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: HttpsOnlyMode.disabled, + label: Text('Disabled'), + ), + ButtonSegment( + value: HttpsOnlyMode.enabled, + label: Text('Enabled'), + ), + ButtonSegment( + value: HttpsOnlyMode.privateOnly, + label: Text('Private mode only'), + ), + ], + selected: {engineSettings.httpsOnlyMode}, + onSelectionChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .httpsOnlyMode(value.first), + ); + }, + ), + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Enhanced Tracking Protection'), + leading: Icon(MdiIcons.incognitoCircleOff), + contentPadding: EdgeInsets.zero, + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.none, + title: const Text('Disabled'), + groupValue: engineSettings.trackingProtectionPolicy, + onChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .trackingProtectionPolicy(value), + ); + }, + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.recommended, + title: const Text('Standard'), + subtitle: const Text( + 'Pages will load normally, but block fewer trackers.', + ), + groupValue: engineSettings.trackingProtectionPolicy, + onChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .trackingProtectionPolicy(value), + ); + }, + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.strict, + title: const Text('Strict'), + subtitle: const Text( + 'Stronger tracking protection and faster performance, but some sites may not work properly.', + ), + groupValue: engineSettings.trackingProtectionPolicy, + onChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .trackingProtectionPolicy(value), + ); + }, + ), + RadioListTile.adaptive( + value: TrackingProtectionPolicy.custom, + title: const Text('Custom'), + subtitle: const Text( + 'Choose which trackers and scripts to block.', + ), + groupValue: engineSettings.trackingProtectionPolicy, + onChanged: null, + ), + ], + ), + ), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const ListTile( + title: Text('Cookie Banner Blocker'), + leading: Icon(MdiIcons.cookieSettings), + contentPadding: EdgeInsets.zero, + ), + CheckboxListTile.adaptive( + value: engineSettings.cookieBannerHandlingMode == + CookieBannerHandlingMode.rejectAll, + title: const Text('In normal browsing'), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .cookieBannerHandlingMode( + value! + ? CookieBannerHandlingMode.rejectAll + : CookieBannerHandlingMode.disabled, + ), + ); + }, + ), + CheckboxListTile.adaptive( + value: engineSettings + .cookieBannerHandlingModePrivateBrowsing == + CookieBannerHandlingMode.rejectAll, + title: const Text('In private browsing'), + controlAffinity: ListTileControlAffinity.leading, + onChanged: (value) async { + await ref + .read( + saveEngineSettingsControllerProvider.notifier, + ) + .save( + (currentSettings) => currentSettings.copyWith + .cookieBannerHandlingModePrivateBrowsing( + value! + ? CookieBannerHandlingMode.rejectAll + : CookieBannerHandlingMode.disabled, + ), + ); + }, + ), + ], + ), + ), + ListTile( + title: const Text('Web Engine Hardening'), + contentPadding: const EdgeInsets.symmetric( + vertical: 8.0, + horizontal: 16.0, + ), + leading: const Icon(MdiIcons.shieldLock), + trailing: const Icon(Icons.chevron_right), + onTap: () async { + await context.push(WebEngineHardeningRoute().location); + }, + ), + ], + ); + }, + ), + ); + } +} diff --git a/app/lib/features/settings/presentation/widgets/sections.dart b/app/lib/features/settings/presentation/widgets/sections.dart new file mode 100644 index 00000000..e554a9cb --- /dev/null +++ b/app/lib/features/settings/presentation/widgets/sections.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +class SettingSection extends StatelessWidget { + final String name; + + const SettingSection({required this.name}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(name, style: Theme.of(context).textTheme.titleLarge), + const Divider(), + ], + ), + ); + } +} + +class SettingSubSection extends StatelessWidget { + final String name; + + const SettingSubSection({required this.name}); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0), + child: Text( + name, + style: Theme.of(context).textTheme.titleLarge?.copyWith(fontSize: 18), + ), + ); + } +} diff --git a/app/lib/features/user/data/database/daos/setting.dart b/app/lib/features/user/data/database/daos/setting.dart index c01b7837..96761076 100644 --- a/app/lib/features/user/data/database/daos/setting.dart +++ b/app/lib/features/user/data/database/daos/setting.dart @@ -1,4 +1,7 @@ +import 'dart:convert'; + import 'package:drift/drift.dart'; +import 'package:lensai/extensions/nullable.dart'; import 'package:lensai/features/user/data/database/database.dart'; part 'setting.g.dart'; @@ -7,12 +10,16 @@ part 'setting.g.dart'; class SettingDao extends DatabaseAccessor with _$SettingDaoMixin { SettingDao(super.attachedDatabase); - Future updateSetting(String key, Object? value) { - final driftvalue = (value != null) ? DriftAny(value) : null; + Future updateSetting(String key, String? partitionKey, Object? value) { + final normalizedValue = (value is Iterable) ? jsonEncode(value) : value; + + final driftvalue = normalizedValue + .mapNotNull((normalizedValue) => DriftAny(normalizedValue)); return db.setting.insertOne( SettingCompanion.insert( key: key, + partitionKey: Value(partitionKey), value: Value(driftvalue), ), onConflict: DoUpdate( @@ -21,7 +28,12 @@ class SettingDao extends DatabaseAccessor with _$SettingDaoMixin { ); } - Selectable> allSettings() { - return db.setting.select().map((row) => MapEntry(row.key, row.value)); + Selectable> allSettingsOfPartitionKey( + String? partitionKey, + ) { + final query = db.setting.select() + ..where((r) => r.partitionKey.equalsNullable(partitionKey)); + + return query.map((row) => MapEntry(row.key, row.value)); } } diff --git a/app/lib/features/user/data/database/database.dart b/app/lib/features/user/data/database/database.dart index 148c1c70..85bc3239 100644 --- a/app/lib/features/user/data/database/database.dart +++ b/app/lib/features/user/data/database/database.dart @@ -4,12 +4,15 @@ import 'package:lensai/features/user/data/database/daos/setting.dart'; part 'database.g.dart'; -@DriftDatabase(include: { - 'database.drift' -}, daos: [ - SettingDao, - CacheDao, -]) +@DriftDatabase( + include: { + 'database.drift', + }, + daos: [ + SettingDao, + CacheDao, + ], +) class UserDatabase extends _$UserDatabase { @override final int schemaVersion = 1; diff --git a/app/lib/features/user/data/database/database.drift b/app/lib/features/user/data/database/database.drift index a90c949e..35400919 100644 --- a/app/lib/features/user/data/database/database.drift +++ b/app/lib/features/user/data/database/database.drift @@ -1,5 +1,6 @@ CREATE TABLE setting ( "key" TEXT PRIMARY KEY NOT NULL, + partition_key TEXT, "value" ANY ) STRICT; diff --git a/app/lib/features/user/data/database/database.g.dart b/app/lib/features/user/data/database/database.g.dart index fbc52301..11ea871c 100644 --- a/app/lib/features/user/data/database/database.g.dart +++ b/app/lib/features/user/data/database/database.g.dart @@ -13,13 +13,18 @@ class Setting extends Table with TableInfo { type: DriftSqlType.string, requiredDuringInsert: true, $customConstraints: 'PRIMARY KEY NOT NULL'); + late final GeneratedColumn partitionKey = GeneratedColumn( + 'partition_key', aliasedName, true, + type: DriftSqlType.string, + requiredDuringInsert: false, + $customConstraints: ''); late final GeneratedColumn value = GeneratedColumn( 'value', aliasedName, true, type: DriftSqlType.any, requiredDuringInsert: false, $customConstraints: ''); @override - List get $columns => [key, value]; + List get $columns => [key, partitionKey, value]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -33,6 +38,8 @@ class Setting extends Table with TableInfo { return SettingData( key: attachedDatabase.typeMapping .read(DriftSqlType.string, data['${effectivePrefix}key'])!, + partitionKey: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}partition_key']), value: attachedDatabase.typeMapping .read(DriftSqlType.any, data['${effectivePrefix}value']), ); @@ -51,12 +58,16 @@ class Setting extends Table with TableInfo { class SettingData extends DataClass implements Insertable { final String key; + final String? partitionKey; final DriftAny? value; - const SettingData({required this.key, this.value}); + const SettingData({required this.key, this.partitionKey, this.value}); @override Map toColumns(bool nullToAbsent) { final map = {}; map['key'] = Variable(key); + if (!nullToAbsent || partitionKey != null) { + map['partition_key'] = Variable(partitionKey); + } if (!nullToAbsent || value != null) { map['value'] = Variable(value); } @@ -68,6 +79,7 @@ class SettingData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return SettingData( key: serializer.fromJson(json['key']), + partitionKey: serializer.fromJson(json['partition_key']), value: serializer.fromJson(json['value']), ); } @@ -76,19 +88,27 @@ class SettingData extends DataClass implements Insertable { serializer ??= driftRuntimeOptions.defaultSerializer; return { 'key': serializer.toJson(key), + 'partition_key': serializer.toJson(partitionKey), 'value': serializer.toJson(value), }; } SettingData copyWith( - {String? key, Value value = const Value.absent()}) => + {String? key, + Value partitionKey = const Value.absent(), + Value value = const Value.absent()}) => SettingData( key: key ?? this.key, + partitionKey: + partitionKey.present ? partitionKey.value : this.partitionKey, value: value.present ? value.value : this.value, ); SettingData copyWithCompanion(SettingCompanion data) { return SettingData( key: data.key.present ? data.key.value : this.key, + partitionKey: data.partitionKey.present + ? data.partitionKey.value + : this.partitionKey, value: data.value.present ? data.value.value : this.value, ); } @@ -97,51 +117,62 @@ class SettingData extends DataClass implements Insertable { String toString() { return (StringBuffer('SettingData(') ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') ..write('value: $value') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(key, value); + int get hashCode => Object.hash(key, partitionKey, value); @override bool operator ==(Object other) => identical(this, other) || (other is SettingData && other.key == this.key && + other.partitionKey == this.partitionKey && other.value == this.value); } class SettingCompanion extends UpdateCompanion { final Value key; + final Value partitionKey; final Value value; final Value rowid; const SettingCompanion({ this.key = const Value.absent(), + this.partitionKey = const Value.absent(), this.value = const Value.absent(), this.rowid = const Value.absent(), }); SettingCompanion.insert({ required String key, + this.partitionKey = const Value.absent(), this.value = const Value.absent(), this.rowid = const Value.absent(), }) : key = Value(key); static Insertable custom({ Expression? key, + Expression? partitionKey, Expression? value, Expression? rowid, }) { return RawValuesInsertable({ if (key != null) 'key': key, + if (partitionKey != null) 'partition_key': partitionKey, if (value != null) 'value': value, if (rowid != null) 'rowid': rowid, }); } SettingCompanion copyWith( - {Value? key, Value? value, Value? rowid}) { + {Value? key, + Value? partitionKey, + Value? value, + Value? rowid}) { return SettingCompanion( key: key ?? this.key, + partitionKey: partitionKey ?? this.partitionKey, value: value ?? this.value, rowid: rowid ?? this.rowid, ); @@ -153,6 +184,9 @@ class SettingCompanion extends UpdateCompanion { if (key.present) { map['key'] = Variable(key.value); } + if (partitionKey.present) { + map['partition_key'] = Variable(partitionKey.value); + } if (value.present) { map['value'] = Variable(value.value); } @@ -166,6 +200,7 @@ class SettingCompanion extends UpdateCompanion { String toString() { return (StringBuffer('SettingCompanion(') ..write('key: $key, ') + ..write('partitionKey: $partitionKey, ') ..write('value: $value, ') ..write('rowid: $rowid') ..write(')')) @@ -396,11 +431,13 @@ abstract class _$UserDatabase extends GeneratedDatabase { typedef $SettingCreateCompanionBuilder = SettingCompanion Function({ required String key, + Value partitionKey, Value value, Value rowid, }); typedef $SettingUpdateCompanionBuilder = SettingCompanion Function({ Value key, + Value partitionKey, Value value, Value rowid, }); @@ -416,6 +453,9 @@ class $SettingFilterComposer extends Composer<_$UserDatabase, Setting> { ColumnFilters get key => $composableBuilder( column: $table.key, builder: (column) => ColumnFilters(column)); + ColumnFilters get partitionKey => $composableBuilder( + column: $table.partitionKey, builder: (column) => ColumnFilters(column)); + ColumnFilters get value => $composableBuilder( column: $table.value, builder: (column) => ColumnFilters(column)); } @@ -431,6 +471,10 @@ class $SettingOrderingComposer extends Composer<_$UserDatabase, Setting> { ColumnOrderings get key => $composableBuilder( column: $table.key, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get partitionKey => $composableBuilder( + column: $table.partitionKey, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get value => $composableBuilder( column: $table.value, builder: (column) => ColumnOrderings(column)); } @@ -446,6 +490,9 @@ class $SettingAnnotationComposer extends Composer<_$UserDatabase, Setting> { GeneratedColumn get key => $composableBuilder(column: $table.key, builder: (column) => column); + GeneratedColumn get partitionKey => $composableBuilder( + column: $table.partitionKey, builder: (column) => column); + GeneratedColumn get value => $composableBuilder(column: $table.value, builder: (column) => column); } @@ -474,21 +521,25 @@ class $SettingTableManager extends RootTableManager< $SettingAnnotationComposer($db: db, $table: table), updateCompanionCallback: ({ Value key = const Value.absent(), + Value partitionKey = const Value.absent(), Value value = const Value.absent(), Value rowid = const Value.absent(), }) => SettingCompanion( key: key, + partitionKey: partitionKey, value: value, rowid: rowid, ), createCompanionCallback: ({ required String key, + Value partitionKey = const Value.absent(), Value value = const Value.absent(), Value rowid = const Value.absent(), }) => SettingCompanion.insert( key: key, + partitionKey: partitionKey, value: value, rowid: rowid, ), diff --git a/app/lib/features/user/data/models/engine_settings.dart b/app/lib/features/user/data/models/engine_settings.dart index fbd4fcbd..e3642817 100644 --- a/app/lib/features/user/data/models/engine_settings.dart +++ b/app/lib/features/user/data/models/engine_settings.dart @@ -1,4 +1,5 @@ import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:json_annotation/json_annotation.dart'; @@ -6,18 +7,45 @@ part 'engine_settings.g.dart'; @CopyWith() @JsonSerializable(includeIfNull: true, constructor: 'withDefaults') -class EngineSettings extends GeckoEngineSettings { +class EngineSettings extends GeckoEngineSettings with FastEquatable { + @override + bool get javascriptEnabled => super.javascriptEnabled!; + @override + TrackingProtectionPolicy get trackingProtectionPolicy => + super.trackingProtectionPolicy!; + @override + HttpsOnlyMode get httpsOnlyMode => super.httpsOnlyMode!; + @override + ColorScheme get preferredColorScheme => super.preferredColorScheme!; + @override + bool get globalPrivacyControlEnabled => super.globalPrivacyControlEnabled!; + @override + CookieBannerHandlingMode get cookieBannerHandlingMode => + super.cookieBannerHandlingMode!; + @override + CookieBannerHandlingMode get cookieBannerHandlingModePrivateBrowsing => + super.cookieBannerHandlingModePrivateBrowsing!; + @override + bool get cookieBannerHandlingGlobalRules => + super.cookieBannerHandlingGlobalRules!; + @override + bool get cookieBannerHandlingGlobalRulesSubFrames => + super.cookieBannerHandlingGlobalRulesSubFrames!; + @override + WebContentIsolationStrategy get webContentIsolationStrategy => + super.webContentIsolationStrategy!; + EngineSettings({ - super.javascriptEnabled, - super.trackingProtectionPolicy, - super.httpsOnlyMode, - super.globalPrivacyControlEnabled, - super.preferredColorScheme, - super.cookieBannerHandlingMode, - super.cookieBannerHandlingModePrivateBrowsing, - super.cookieBannerHandlingGlobalRules, - super.cookieBannerHandlingGlobalRulesSubFrames, - super.webContentIsolationStrategy, + required super.javascriptEnabled, + required super.trackingProtectionPolicy, + required super.httpsOnlyMode, + required super.globalPrivacyControlEnabled, + required super.preferredColorScheme, + required super.cookieBannerHandlingMode, + required super.cookieBannerHandlingModePrivateBrowsing, + required super.cookieBannerHandlingGlobalRules, + required super.cookieBannerHandlingGlobalRulesSubFrames, + required super.webContentIsolationStrategy, }); EngineSettings.withDefaults({ @@ -50,4 +78,26 @@ class EngineSettings extends GeckoEngineSettings { webContentIsolationStrategy: webContentIsolationStrategy ?? WebContentIsolationStrategy.isolateHighValue, ); + + factory EngineSettings.fromJson(Map json) => + _$EngineSettingsFromJson(json); + + Map toJson() => _$EngineSettingsToJson(this); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + super.javascriptEnabled, + super.trackingProtectionPolicy, + super.httpsOnlyMode, + super.globalPrivacyControlEnabled, + super.preferredColorScheme, + super.cookieBannerHandlingMode, + super.cookieBannerHandlingModePrivateBrowsing, + super.cookieBannerHandlingGlobalRules, + super.cookieBannerHandlingGlobalRulesSubFrames, + super.webContentIsolationStrategy, + ]; } diff --git a/app/lib/features/user/data/models/engine_settings.g.dart b/app/lib/features/user/data/models/engine_settings.g.dart index 81e9305f..8034c2c2 100644 --- a/app/lib/features/user/data/models/engine_settings.g.dart +++ b/app/lib/features/user/data/models/engine_settings.g.dart @@ -225,22 +225,22 @@ Map _$EngineSettingsToJson(EngineSettings instance) => { 'javascriptEnabled': instance.javascriptEnabled, 'trackingProtectionPolicy': - _$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy], - 'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode], - 'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled, + _$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy]!, + 'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode]!, 'preferredColorScheme': - _$ColorSchemeEnumMap[instance.preferredColorScheme], + _$ColorSchemeEnumMap[instance.preferredColorScheme]!, + 'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled, 'cookieBannerHandlingMode': - _$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode], + _$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode]!, 'cookieBannerHandlingModePrivateBrowsing': _$CookieBannerHandlingModeEnumMap[ - instance.cookieBannerHandlingModePrivateBrowsing], + instance.cookieBannerHandlingModePrivateBrowsing]!, 'cookieBannerHandlingGlobalRules': instance.cookieBannerHandlingGlobalRules, 'cookieBannerHandlingGlobalRulesSubFrames': instance.cookieBannerHandlingGlobalRulesSubFrames, 'webContentIsolationStrategy': _$WebContentIsolationStrategyEnumMap[ - instance.webContentIsolationStrategy], + instance.webContentIsolationStrategy]!, }; const _$TrackingProtectionPolicyEnumMap = { diff --git a/app/lib/features/user/data/models/general_settings.dart b/app/lib/features/user/data/models/general_settings.dart new file mode 100644 index 00000000..3a9367de --- /dev/null +++ b/app/lib/features/user/data/models/general_settings.dart @@ -0,0 +1,56 @@ +import 'package:copy_with_extension/copy_with_extension.dart'; +import 'package:fast_equatable/fast_equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'general_settings.g.dart'; + +enum DeleteBrowsingDataType { + tabs('Open tabs'), + history('Browsing history'), + cookies('Cookies and site data', 'You’ll be logged out of most sites'), + cache('Cached images and files', 'Frees up storage space'), + permissions('Site permissions'), + downloads('Downloads'); + + final String title; + final String? description; + + const DeleteBrowsingDataType(this.title, [this.description]); +} + +@CopyWith() +@JsonSerializable(includeIfNull: true, constructor: 'withDefaults') +class GeneralSettings with FastEquatable { + final ThemeMode themeMode; + final bool enableReadability; + final Set? deleteBrowsingDataOnQuit; + + GeneralSettings({ + required this.themeMode, + required this.enableReadability, + required this.deleteBrowsingDataOnQuit, + }); + + GeneralSettings.withDefaults({ + ThemeMode? themeMode, + bool? enableReadability, + this.deleteBrowsingDataOnQuit, + }) : themeMode = themeMode ?? ThemeMode.dark, + enableReadability = enableReadability ?? true; + + factory GeneralSettings.fromJson(Map json) => + _$GeneralSettingsFromJson(json); + + Map toJson() => _$GeneralSettingsToJson(this); + + @override + bool get cacheHash => true; + + @override + List get hashParameters => [ + themeMode, + enableReadability, + deleteBrowsingDataOnQuit, + ]; +} diff --git a/app/lib/features/user/data/models/general_settings.g.dart b/app/lib/features/user/data/models/general_settings.g.dart new file mode 100644 index 00000000..50a52f29 --- /dev/null +++ b/app/lib/features/user/data/models/general_settings.g.dart @@ -0,0 +1,121 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'general_settings.dart'; + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class _$GeneralSettingsCWProxy { + GeneralSettings themeMode(ThemeMode themeMode); + + GeneralSettings enableReadability(bool enableReadability); + + GeneralSettings deleteBrowsingDataOnQuit( + Set? deleteBrowsingDataOnQuit); + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// GeneralSettings(...).copyWith(id: 12, name: "My name") + /// ```` + GeneralSettings call({ + ThemeMode themeMode, + bool enableReadability, + Set? deleteBrowsingDataOnQuit, + }); +} + +/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfGeneralSettings.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfGeneralSettings.copyWith.fieldName(...)` +class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy { + const _$GeneralSettingsCWProxyImpl(this._value); + + final GeneralSettings _value; + + @override + GeneralSettings themeMode(ThemeMode themeMode) => this(themeMode: themeMode); + + @override + GeneralSettings enableReadability(bool enableReadability) => + this(enableReadability: enableReadability); + + @override + GeneralSettings deleteBrowsingDataOnQuit( + Set? deleteBrowsingDataOnQuit) => + this(deleteBrowsingDataOnQuit: deleteBrowsingDataOnQuit); + + @override + + /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `GeneralSettings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. + /// + /// Usage + /// ```dart + /// GeneralSettings(...).copyWith(id: 12, name: "My name") + /// ```` + GeneralSettings call({ + Object? themeMode = const $CopyWithPlaceholder(), + Object? enableReadability = const $CopyWithPlaceholder(), + Object? deleteBrowsingDataOnQuit = const $CopyWithPlaceholder(), + }) { + return GeneralSettings( + themeMode: themeMode == const $CopyWithPlaceholder() + ? _value.themeMode + // ignore: cast_nullable_to_non_nullable + : themeMode as ThemeMode, + enableReadability: enableReadability == const $CopyWithPlaceholder() + ? _value.enableReadability + // ignore: cast_nullable_to_non_nullable + : enableReadability as bool, + deleteBrowsingDataOnQuit: + deleteBrowsingDataOnQuit == const $CopyWithPlaceholder() + ? _value.deleteBrowsingDataOnQuit + // ignore: cast_nullable_to_non_nullable + : deleteBrowsingDataOnQuit as Set?, + ); + } +} + +extension $GeneralSettingsCopyWith on GeneralSettings { + /// Returns a callable class that can be used as follows: `instanceOfGeneralSettings.copyWith(...)` or like so:`instanceOfGeneralSettings.copyWith.fieldName(...)`. + // ignore: library_private_types_in_public_api + _$GeneralSettingsCWProxy get copyWith => _$GeneralSettingsCWProxyImpl(this); +} + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GeneralSettings _$GeneralSettingsFromJson(Map json) => + GeneralSettings.withDefaults( + themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), + enableReadability: json['enableReadability'] as bool?, + deleteBrowsingDataOnQuit: + (json['deleteBrowsingDataOnQuit'] as List?) + ?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e)) + .toSet(), + ); + +Map _$GeneralSettingsToJson(GeneralSettings instance) => + { + 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, + 'enableReadability': instance.enableReadability, + 'deleteBrowsingDataOnQuit': instance.deleteBrowsingDataOnQuit + ?.map((e) => _$DeleteBrowsingDataTypeEnumMap[e]!) + .toList(), + }; + +const _$ThemeModeEnumMap = { + ThemeMode.system: 'system', + ThemeMode.light: 'light', + ThemeMode.dark: 'dark', +}; + +const _$DeleteBrowsingDataTypeEnumMap = { + DeleteBrowsingDataType.tabs: 'tabs', + DeleteBrowsingDataType.history: 'history', + DeleteBrowsingDataType.cookies: 'cookies', + DeleteBrowsingDataType.cache: 'cache', + DeleteBrowsingDataType.permissions: 'permissions', + DeleteBrowsingDataType.downloads: 'downloads', +}; diff --git a/app/lib/features/user/data/models/settings.dart b/app/lib/features/user/data/models/settings.dart deleted file mode 100644 index 67e25262..00000000 --- a/app/lib/features/user/data/models/settings.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:copy_with_extension/copy_with_extension.dart'; -import 'package:fast_equatable/fast_equatable.dart'; -import 'package:flutter/material.dart'; -import 'package:json_annotation/json_annotation.dart'; - -part 'settings.g.dart'; - -@CopyWith() -@JsonSerializable(includeIfNull: true, constructor: 'withDefaults') -class Settings with FastEquatable { - final bool incognitoMode; - final bool enableJavascript; - final bool blockHttpProtocol; - final ThemeMode themeMode; - final bool enableReadability; - - Settings({ - required this.incognitoMode, - required this.enableJavascript, - required this.blockHttpProtocol, - required this.themeMode, - required this.enableReadability, - }); - - Settings.withDefaults({ - bool? incognitoMode, - bool? enableJavascript, - bool? blockHttpProtocol, - ThemeMode? themeMode, - bool? enableReadability, - }) : incognitoMode = incognitoMode ?? true, - enableJavascript = enableJavascript ?? true, - blockHttpProtocol = blockHttpProtocol ?? false, - themeMode = themeMode ?? ThemeMode.dark, - enableReadability = enableReadability ?? true; - - factory Settings.fromJson(Map json) => - _$SettingsFromJson(json); - - Map toJson() => _$SettingsToJson(this); - - @override - bool get cacheHash => true; - - @override - List get hashParameters => [ - incognitoMode, - enableJavascript, - blockHttpProtocol, - themeMode, - enableReadability, - ]; -} diff --git a/app/lib/features/user/data/models/settings.g.dart b/app/lib/features/user/data/models/settings.g.dart deleted file mode 100644 index ae1ce99c..00000000 --- a/app/lib/features/user/data/models/settings.g.dart +++ /dev/null @@ -1,130 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'settings.dart'; - -// ************************************************************************** -// CopyWithGenerator -// ************************************************************************** - -abstract class _$SettingsCWProxy { - Settings incognitoMode(bool incognitoMode); - - Settings enableJavascript(bool enableJavascript); - - Settings blockHttpProtocol(bool blockHttpProtocol); - - Settings themeMode(ThemeMode themeMode); - - Settings enableReadability(bool enableReadability); - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// Settings(...).copyWith(id: 12, name: "My name") - /// ```` - Settings call({ - bool incognitoMode, - bool enableJavascript, - bool blockHttpProtocol, - ThemeMode themeMode, - bool enableReadability, - }); -} - -/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfSettings.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfSettings.copyWith.fieldName(...)` -class _$SettingsCWProxyImpl implements _$SettingsCWProxy { - const _$SettingsCWProxyImpl(this._value); - - final Settings _value; - - @override - Settings incognitoMode(bool incognitoMode) => - this(incognitoMode: incognitoMode); - - @override - Settings enableJavascript(bool enableJavascript) => - this(enableJavascript: enableJavascript); - - @override - Settings blockHttpProtocol(bool blockHttpProtocol) => - this(blockHttpProtocol: blockHttpProtocol); - - @override - Settings themeMode(ThemeMode themeMode) => this(themeMode: themeMode); - - @override - Settings enableReadability(bool enableReadability) => - this(enableReadability: enableReadability); - - @override - - /// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `Settings(...).copyWith.fieldName(...)` to override fields one at a time with nullification support. - /// - /// Usage - /// ```dart - /// Settings(...).copyWith(id: 12, name: "My name") - /// ```` - Settings call({ - Object? incognitoMode = const $CopyWithPlaceholder(), - Object? enableJavascript = const $CopyWithPlaceholder(), - Object? blockHttpProtocol = const $CopyWithPlaceholder(), - Object? themeMode = const $CopyWithPlaceholder(), - Object? enableReadability = const $CopyWithPlaceholder(), - }) { - return Settings( - incognitoMode: incognitoMode == const $CopyWithPlaceholder() - ? _value.incognitoMode - // ignore: cast_nullable_to_non_nullable - : incognitoMode as bool, - enableJavascript: enableJavascript == const $CopyWithPlaceholder() - ? _value.enableJavascript - // ignore: cast_nullable_to_non_nullable - : enableJavascript as bool, - blockHttpProtocol: blockHttpProtocol == const $CopyWithPlaceholder() - ? _value.blockHttpProtocol - // ignore: cast_nullable_to_non_nullable - : blockHttpProtocol as bool, - themeMode: themeMode == const $CopyWithPlaceholder() - ? _value.themeMode - // ignore: cast_nullable_to_non_nullable - : themeMode as ThemeMode, - enableReadability: enableReadability == const $CopyWithPlaceholder() - ? _value.enableReadability - // ignore: cast_nullable_to_non_nullable - : enableReadability as bool, - ); - } -} - -extension $SettingsCopyWith on Settings { - /// Returns a callable class that can be used as follows: `instanceOfSettings.copyWith(...)` or like so:`instanceOfSettings.copyWith.fieldName(...)`. - // ignore: library_private_types_in_public_api - _$SettingsCWProxy get copyWith => _$SettingsCWProxyImpl(this); -} - -// ************************************************************************** -// JsonSerializableGenerator -// ************************************************************************** - -Settings _$SettingsFromJson(Map json) => Settings.withDefaults( - incognitoMode: json['incognitoMode'] as bool?, - enableJavascript: json['enableJavascript'] as bool?, - blockHttpProtocol: json['blockHttpProtocol'] as bool?, - themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), - enableReadability: json['enableReadability'] as bool?, - ); - -Map _$SettingsToJson(Settings instance) => { - 'incognitoMode': instance.incognitoMode, - 'enableJavascript': instance.enableJavascript, - 'blockHttpProtocol': instance.blockHttpProtocol, - 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, - 'enableReadability': instance.enableReadability, - }; - -const _$ThemeModeEnumMap = { - ThemeMode.system: 'system', - ThemeMode.light: 'light', - ThemeMode.dark: 'dark', -}; diff --git a/app/lib/features/user/domain/providers.dart b/app/lib/features/user/domain/providers.dart index 64fd490a..cb3cd6a7 100644 --- a/app/lib/features/user/domain/providers.dart +++ b/app/lib/features/user/domain/providers.dart @@ -1,5 +1,6 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:lensai/features/user/data/providers.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:pocketbase/pocketbase.dart'; import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -47,3 +48,11 @@ PocketBase pocketBase(Ref ref) { return PocketBase('http://192.168.2.104:8090', authStore: authStore); } + +@Riverpod() +bool incognitoModeEnabled(Ref ref) { + return ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.deleteBrowsingDataOnQuit != null), + ); +} diff --git a/app/lib/features/user/domain/providers.g.dart b/app/lib/features/user/domain/providers.g.dart index c0d44ecf..60f3514e 100644 --- a/app/lib/features/user/domain/providers.g.dart +++ b/app/lib/features/user/domain/providers.g.dart @@ -90,5 +90,23 @@ final pocketBaseProvider = Provider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef PocketBaseRef = ProviderRef; +String _$incognitoModeEnabledHash() => + r'3968c2d4945be09a0bc08ce0d70b0390187c44e8'; + +/// See also [incognitoModeEnabled]. +@ProviderFor(incognitoModeEnabled) +final incognitoModeEnabledProvider = AutoDisposeProvider.internal( + incognitoModeEnabled, + name: r'incognitoModeEnabledProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$incognitoModeEnabledHash, + dependencies: null, + allTransitiveDependencies: null, +); + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +typedef IncognitoModeEnabledRef = AutoDisposeProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/domain/repositories/cache.dart b/app/lib/features/user/domain/repositories/cache.dart index 9bb5f8f7..2eeee594 100644 --- a/app/lib/features/user/domain/repositories/cache.dart +++ b/app/lib/features/user/domain/repositories/cache.dart @@ -1,7 +1,6 @@ import 'dart:typed_data'; import 'package:lensai/features/geckoview/domain/providers.dart'; -import 'package:lensai/features/user/data/database/database.dart'; import 'package:lensai/features/user/data/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -9,29 +8,31 @@ part 'cache.g.dart'; @Riverpod(keepAlive: true) class CacheRepository extends _$CacheRepository { - late UserDatabase _db; - Future clearCache() { - return _db.cacheDao.clearIconCache(); + return ref.read(userDatabaseProvider).cacheDao.clearIconCache(); } Future cacheIcon(Uri url, Uint8List bytes) { - return _db.cacheDao.cacheIcon(url.origin, bytes); + return ref.read(userDatabaseProvider).cacheDao.cacheIcon(url.origin, bytes); } Future getCachedIcon(String origin) { - return _db.cacheDao.getCachedIcon(origin).getSingleOrNull(); + return ref + .read(userDatabaseProvider) + .cacheDao + .getCachedIcon(origin) + .getSingleOrNull(); } @override void build() { final eventService = ref.watch(eventServiceProvider); - _db = ref.watch(userDatabaseProvider); + final db = ref.watch(userDatabaseProvider); final sub = eventService.iconUpdateEvents.listen((event) async { if (Uri.tryParse(event.url) case final Uri url) { - await _db.cacheDao.cacheIcon(url.origin, event.bytes); + await db.cacheDao.cacheIcon(url.origin, event.bytes); } }); diff --git a/app/lib/features/user/domain/repositories/cache.g.dart b/app/lib/features/user/domain/repositories/cache.g.dart index 40ad20f5..ea2d3bb6 100644 --- a/app/lib/features/user/domain/repositories/cache.g.dart +++ b/app/lib/features/user/domain/repositories/cache.g.dart @@ -6,7 +6,7 @@ part of 'cache.dart'; // RiverpodGenerator // ************************************************************************** -String _$cacheRepositoryHash() => r'37b25ff308b9f25575625984e55cb02f9c0293f0'; +String _$cacheRepositoryHash() => r'548b7cba9a23c21bba39f0918d7d90acaaf8d8e9'; /// See also [CacheRepository]. @ProviderFor(CacheRepository) diff --git a/app/lib/features/user/domain/repositories/engine_settings.dart b/app/lib/features/user/domain/repositories/engine_settings.dart new file mode 100644 index 00000000..f016de18 --- /dev/null +++ b/app/lib/features/user/domain/repositories/engine_settings.dart @@ -0,0 +1,78 @@ +import 'dart:async'; + +import 'package:drift/drift.dart'; +import 'package:lensai/features/user/data/database/database.dart'; +import 'package:lensai/features/user/data/models/engine_settings.dart'; +import 'package:lensai/features/user/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'engine_settings.g.dart'; + +typedef UpdateEngineSettingsFunc = EngineSettings Function( + EngineSettings currentSettings, +); + +@Riverpod(keepAlive: true) +class EngineSettingsRepository extends _$EngineSettingsRepository { + final _partitionKey = 'engine'; + + Future updateSettings(UpdateEngineSettingsFunc updateWithCurrent) { + final oldJson = state.toJson(); + final newJson = updateWithCurrent(state).toJson(); + + return ref.read(userDatabaseProvider).transaction(() async { + for (final MapEntry(:key, :value) in newJson.entries) { + if (oldJson[key] != value) { + await ref + .read(userDatabaseProvider) + .settingDao + .updateSetting(key, _partitionKey, value); + } + } + }); + } + + @override + EngineSettings build() { + final db = ref.watch(userDatabaseProvider); + + final watchSub = db.settingDao + .allSettingsOfPartitionKey(_partitionKey) + .watch() + .listen((entries) { + final settings = Map.fromEntries(entries); + + state = EngineSettings.fromJson({ + 'incognitoMode': settings['incognitoMode'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'javascriptEnabled': settings['javascriptEnabled'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'trackingProtectionPolicy': settings['trackingProtectionPolicy'] + ?.readAs(DriftSqlType.string, db.typeMapping), + 'httpsOnlyMode': settings['httpsOnlyMode'] + ?.readAs(DriftSqlType.string, db.typeMapping), + 'globalPrivacyControlEnabled': settings['globalPrivacyControlEnabled'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'cookieBannerHandlingMode': settings['cookieBannerHandlingMode'] + ?.readAs(DriftSqlType.string, db.typeMapping), + 'cookieBannerHandlingModePrivateBrowsing': + settings['cookieBannerHandlingModePrivateBrowsing'] + ?.readAs(DriftSqlType.string, db.typeMapping), + 'cookieBannerHandlingGlobalRules': + settings['cookieBannerHandlingGlobalRules'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'cookieBannerHandlingGlobalRulesSubFrames': + settings['cookieBannerHandlingGlobalRulesSubFrames'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'webContentIsolationStrategy': settings['webContentIsolationStrategy'] + ?.readAs(DriftSqlType.string, db.typeMapping), + }); + }); + + ref.onDispose(() { + unawaited(watchSub.cancel()); + }); + + return EngineSettings.withDefaults(); + } +} diff --git a/app/lib/features/user/domain/repositories/settings.g.dart b/app/lib/features/user/domain/repositories/engine_settings.g.dart similarity index 52% rename from app/lib/features/user/domain/repositories/settings.g.dart rename to app/lib/features/user/domain/repositories/engine_settings.g.dart index a0213564..2c1a1720 100644 --- a/app/lib/features/user/domain/repositories/settings.g.dart +++ b/app/lib/features/user/domain/repositories/engine_settings.g.dart @@ -1,27 +1,27 @@ // GENERATED CODE - DO NOT MODIFY BY HAND -part of 'settings.dart'; +part of 'engine_settings.dart'; // ************************************************************************** // RiverpodGenerator // ************************************************************************** -String _$settingsRepositoryHash() => - r'7e3e032957f5092aa3d445d2b7b05a882e3cfd20'; +String _$engineSettingsRepositoryHash() => + r'113617b3a99377bbb83347cfcb6d432abaae6f71'; -/// See also [SettingsRepository]. -@ProviderFor(SettingsRepository) -final settingsRepositoryProvider = - NotifierProvider.internal( - SettingsRepository.new, - name: r'settingsRepositoryProvider', +/// See also [EngineSettingsRepository]. +@ProviderFor(EngineSettingsRepository) +final engineSettingsRepositoryProvider = + NotifierProvider.internal( + EngineSettingsRepository.new, + name: r'engineSettingsRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$settingsRepositoryHash, + : _$engineSettingsRepositoryHash, dependencies: null, allTransitiveDependencies: null, ); -typedef _$SettingsRepository = Notifier; +typedef _$EngineSettingsRepository = Notifier; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/domain/repositories/general_settings.dart b/app/lib/features/user/domain/repositories/general_settings.dart new file mode 100644 index 00000000..51fd377e --- /dev/null +++ b/app/lib/features/user/domain/repositories/general_settings.dart @@ -0,0 +1,80 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:lensai/extensions/nullable.dart'; +import 'package:lensai/features/user/data/models/general_settings.dart'; +import 'package:lensai/features/user/data/providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'general_settings.g.dart'; + +typedef UpdateGeneralSettingsFunc = GeneralSettings Function( + GeneralSettings currentSettings, +); + +@Riverpod(keepAlive: true) +class GeneralSettingsRepository extends _$GeneralSettingsRepository { + final _partitionKey = 'general'; + + GeneralSettings _deserializeSettings( + List> entries, + ) { + final settings = Map.fromEntries(entries); + + final db = ref.read(userDatabaseProvider); + + return GeneralSettings.fromJson({ + 'themeMode': + settings['themeMode']?.readAs(DriftSqlType.string, db.typeMapping), + 'enableReadability': settings['enableReadability'] + ?.readAs(DriftSqlType.bool, db.typeMapping), + 'deleteBrowsingDataOnQuit': settings['deleteBrowsingDataOnQuit'] + ?.readAs(DriftSqlType.string, db.typeMapping) + .mapNotNull(jsonDecode), + }); + } + + //Eager fetch, when up to date settings are required + Future fetch() async { + return ref + .read(userDatabaseProvider) + .settingDao + .allSettingsOfPartitionKey(_partitionKey) + .get() + .then(_deserializeSettings); + } + + Future updateSettings(UpdateGeneralSettingsFunc updateWithCurrent) { + final oldJson = state.toJson(); + final newJson = updateWithCurrent(state).toJson(); + + final db = ref.read(userDatabaseProvider); + + return db.transaction(() async { + for (final MapEntry(:key, :value) in newJson.entries) { + if (oldJson[key] != value) { + await db.settingDao.updateSetting(key, _partitionKey, value); + } + } + }); + } + + @override + GeneralSettings build() { + final db = ref.watch(userDatabaseProvider); + + final watchSub = + db.settingDao.allSettingsOfPartitionKey(_partitionKey).watch().listen( + (event) { + state = _deserializeSettings(event); + }, + ); + + ref.onDispose(() { + unawaited(watchSub.cancel()); + }); + + return GeneralSettings.withDefaults(); + } +} diff --git a/app/lib/features/user/domain/repositories/general_settings.g.dart b/app/lib/features/user/domain/repositories/general_settings.g.dart new file mode 100644 index 00000000..aa62324f --- /dev/null +++ b/app/lib/features/user/domain/repositories/general_settings.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'general_settings.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$generalSettingsRepositoryHash() => + r'd21cee982ed471dff5ba11415e5557073bd2fca3'; + +/// See also [GeneralSettingsRepository]. +@ProviderFor(GeneralSettingsRepository) +final generalSettingsRepositoryProvider = + NotifierProvider.internal( + GeneralSettingsRepository.new, + name: r'generalSettingsRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$generalSettingsRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$GeneralSettingsRepository = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/features/user/domain/repositories/settings.dart b/app/lib/features/user/domain/repositories/settings.dart deleted file mode 100644 index 1d16efd4..00000000 --- a/app/lib/features/user/domain/repositories/settings.dart +++ /dev/null @@ -1,57 +0,0 @@ -import 'dart:async'; - -import 'package:drift/drift.dart'; -import 'package:lensai/features/user/data/database/database.dart'; -import 'package:lensai/features/user/data/models/settings.dart'; -import 'package:lensai/features/user/data/providers.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'settings.g.dart'; - -typedef UpdateSettingsFunc = Settings Function(Settings currentSettings); - -@Riverpod(keepAlive: true) -class SettingsRepository extends _$SettingsRepository { - late UserDatabase _db; - - Future updateSettings(UpdateSettingsFunc updateWithCurrent) { - final oldJson = state.toJson(); - final newJson = updateWithCurrent(state).toJson(); - - return _db.transaction(() async { - for (final MapEntry(:key, :value) in newJson.entries) { - if (oldJson[key] != value) { - await _db.settingDao.updateSetting(key, value); - } - } - }); - } - - @override - Settings build() { - _db = ref.watch(userDatabaseProvider); - - final watchSub = _db.settingDao.allSettings().watch().listen((entries) { - final settings = Map.fromEntries(entries); - - state = Settings.fromJson({ - 'incognitoMode': settings['incognitoMode'] - ?.readAs(DriftSqlType.bool, _db.typeMapping), - 'enableJavascript': settings['enableJavascript'] - ?.readAs(DriftSqlType.bool, _db.typeMapping), - 'blockHttpProtocol': settings['blockHttpProtocol'] - ?.readAs(DriftSqlType.bool, _db.typeMapping), - 'themeMode': - settings['themeMode']?.readAs(DriftSqlType.string, _db.typeMapping), - 'enableReadability': settings['enableReadability'] - ?.readAs(DriftSqlType.bool, _db.typeMapping), - }); - }); - - ref.onDispose(() { - unawaited(watchSub.cancel()); - }); - - return Settings.withDefaults(); - } -} diff --git a/app/lib/features/user/domain/services/local_authentication.dart b/app/lib/features/user/domain/services/local_authentication.dart new file mode 100644 index 00000000..610dd4c6 --- /dev/null +++ b/app/lib/features/user/domain/services/local_authentication.dart @@ -0,0 +1,61 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:lensai/core/logger.dart'; +import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'local_authentication.g.dart'; + +@Riverpod(keepAlive: true) +class LocalAuthenticationService extends _$LocalAuthenticationService { + final _auth = LocalAuthentication(); + final _cache = {}; + + bool _cacheAuth(String authKey) { + final auth = _cache[authKey]; + if (auth != null && auth.$2.lockTimeout != null) { + return DateTime.now().difference(auth.$1) < auth.$2.lockTimeout!; + } + + return false; + } + + void evictCacheOnBackground() { + _cache.removeWhere( + (key, value) => value.$2.lockOnAppBackground, + ); + } + + Future authenticate({ + required String authKey, + required String localizedReason, + ContainerAuthSettings? settings, + bool useAuthCache = false, + }) async { + try { + var result = useAuthCache && _cacheAuth(authKey); + + if (!result) { + result = await _auth.authenticate( + localizedReason: localizedReason, + ); + } + + if (result && settings != null) { + _cache[authKey] = (DateTime.now(), settings); + } + + return result; + } on PlatformException catch (e, s) { + logger.e('Could not authenticate', error: e, stackTrace: s); + return false; + } + } + + @override + Future build() { + return _auth.canCheckBiometrics; + } +} diff --git a/app/lib/features/user/domain/services/local_authentication.g.dart b/app/lib/features/user/domain/services/local_authentication.g.dart new file mode 100644 index 00000000..3f3b9d3c --- /dev/null +++ b/app/lib/features/user/domain/services/local_authentication.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'local_authentication.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$localAuthenticationServiceHash() => + r'4893be7a11833d77a575f7aa248a9ba65d6717da'; + +/// See also [LocalAuthenticationService]. +@ProviderFor(LocalAuthenticationService) +final localAuthenticationServiceProvider = + AsyncNotifierProvider.internal( + LocalAuthenticationService.new, + name: r'localAuthenticationServiceProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$localAuthenticationServiceHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$LocalAuthenticationService = AsyncNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/app/lib/main.dart b/app/lib/main.dart index 89b15b0d..08b158ae 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -4,7 +4,7 @@ import 'package:home_widget/home_widget.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:lensai/core/error_observer.dart'; import 'package:lensai/domain/services/app_initialization.dart'; -import 'package:lensai/features/user/domain/repositories/settings.dart'; +import 'package:lensai/features/user/domain/repositories/general_settings.dart'; import 'package:lensai/presentation/hooks/on_initialization.dart'; import 'package:lensai/presentation/widgets/main_app.dart'; @@ -23,7 +23,7 @@ void main() async { child: HookConsumer( builder: (context, ref, child) { final themeMode = ref.watch( - settingsRepositoryProvider.select( + generalSettingsRepositoryProvider.select( (value) => value.themeMode, ), ); diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 39294ee8..d4b3e7bd 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -17,7 +17,7 @@ dependencies: expandable_page_view: ^1.0.17 fading_scroll: ^0.9.1 fast_equatable: ^1.1.0 - file_picker: ^8.1.7 + file_picker: ^8.3.1 flutter: sdk: flutter flutter_chat_core: @@ -25,7 +25,7 @@ dependencies: flutter_chat_ui: path: ../../flutter_chat_ui/packages/flutter_chat_ui flutter_hooks: ^0.20.5 - flutter_markdown: ^0.7.5 + flutter_markdown: ^0.7.6 flutter_material_design_icons: ^1.1.7447 flutter_mozilla_components: path: ../packages/flutter_mozilla_components @@ -34,7 +34,8 @@ dependencies: path: ../../flutter-reorderable-grid-view flutter_secure_storage: ^10.0.0-beta.4 flutter_sharing_intent: ^1.1.1 - go_router: ^14.7.1 + flutter_slidable: ^4.0.0 + go_router: ^14.7.2 google_fonts: ^6.2.1 home_widget: ^0.7.0 hooks_riverpod: ^2.6.1 @@ -48,10 +49,11 @@ dependencies: lexo_rank: git: url: https://github.com/FaFre/lexo_rank.git + local_auth: ^2.3.0 logger: ^2.5.0 markdown: ^7.3.0 mime: ^2.0.0 - package_info_plus: ^8.1.3 + package_info_plus: ^8.1.4 path: ^1.9.0 path_provider: ^2.1.5 pocketbase: ^0.21.0 @@ -65,7 +67,7 @@ dependencies: git: url: https://github.com/FaFre/speech_to_text_google_dialog.git sqlite3: ^2.7.2 - sqlite3_flutter_libs: ^0.5.29 + sqlite3_flutter_libs: ^0.5.29+1 sqlite3_vec: path: ../packages/sqlite3_vec synchronized: ^3.3.0+3 @@ -92,7 +94,7 @@ dev_dependencies: drift_dev: ^2.24.0 fast_equatable_lint: ^0.3.0 flutter_launcher_icons: ^0.14.3 - go_router_builder: ^2.7.3 + go_router_builder: ^2.7.5 json_serializable: ^6.9.0 lint: ^2.3.0 riverpod_generator: ^2.6.1 diff --git a/packages/flutter_mozilla_components/android/build.gradle b/packages/flutter_mozilla_components/android/build.gradle index 15c28fcc..43752e5c 100644 --- a/packages/flutter_mozilla_components/android/build.gradle +++ b/packages/flutter_mozilla_components/android/build.gradle @@ -140,8 +140,9 @@ dependencies { implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'com.google.android.material:material:1.12.0' //https://stackoverflow.com/questions/73782320/onbackinvokedcallback-is-not-enabled-for-the-application-in-set-androidenableo - implementation 'androidx.activity:activity-ktx:1.9.3' + implementation 'androidx.activity:activity-ktx:1.10.0' + implementation 'androidx.paging:paging-runtime-ktx:3.3.5' testImplementation("org.jetbrains.kotlin:kotlin-test") - testImplementation("org.mockito:mockito-core:5.2.0") + testImplementation("org.mockito:mockito-core:5.15.2") } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt index 66049b06..b52ae26c 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/FlutterMozillaComponentsPlugin.kt @@ -7,6 +7,7 @@ import eu.lensai.flutter_mozilla_components.activities.NotificationActivity import eu.lensai.flutter_mozilla_components.api.GeckoAddonsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoBrowserApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoCookieApiImpl +import eu.lensai.flutter_mozilla_components.api.GeckoDeleteBrowsingDataControllerImpl import eu.lensai.flutter_mozilla_components.api.GeckoEngineSettingsApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoFindApiImpl import eu.lensai.flutter_mozilla_components.api.GeckoIconsApiImpl @@ -20,6 +21,7 @@ import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonEvents import eu.lensai.flutter_mozilla_components.pigeons.GeckoAddonsApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoBrowserApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoCookieApi +import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController import eu.lensai.flutter_mozilla_components.pigeons.GeckoEngineSettingsApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoFindApi import eu.lensai.flutter_mozilla_components.pigeons.GeckoIconsApi @@ -98,6 +100,7 @@ class FlutterMozillaComponentsPlugin: FlutterPlugin, ActivityAware { GeckoSelectionActionController.setUp(_flutterPluginBinding.binaryMessenger, GeckoSelectionActionControllerImpl( selectionActionDelegate )) + GeckoDeleteBrowsingDataController.setUp(_flutterPluginBinding.binaryMessenger, GeckoDeleteBrowsingDataControllerImpl()) ReaderViewEvents.setUp( _flutterPluginBinding.binaryMessenger, diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/PermissionStorage.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/PermissionStorage.kt new file mode 100644 index 00000000..5c001d56 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/PermissionStorage.kt @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +package eu.lensai.flutter_mozilla_components + +import androidx.paging.DataSource +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import mozilla.components.concept.engine.permission.SitePermissions +import mozilla.components.concept.engine.permission.SitePermissionsStorage +import kotlin.coroutines.CoroutineContext + +class PermissionStorage( + private val permissionsStorage: SitePermissionsStorage, + private val dispatcher: CoroutineContext = Dispatchers.IO, +) { + + /** + * Persists the [sitePermissions] provided as a parameter. + * + * @param sitePermissions The [SitePermissions] to be stored. + * @param private Indicates if the [SitePermissions] belongs to a private session. + */ + suspend fun add(sitePermissions: SitePermissions, private: Boolean) = withContext(dispatcher) { + permissionsStorage.save(sitePermissions, private = private) + } + + /** + * Finds all [SitePermissions] that match the [origin]. + * + * @param origin The site to be used as filter in the search. + * @param private Indicates if the [origin] belongs to a private session. + */ + suspend fun findSitePermissionsBy(origin: String, private: Boolean): SitePermissions? = + withContext(dispatcher) { + permissionsStorage.findSitePermissionsBy(origin, private = private) + } + + /** + * Replaces an existing SitePermissions with the values of provided [sitePermissions]. + * + * @param sitePermissions The [SitePermissions] to be updated. + * @param private Indicates if the [SitePermissions] belongs to a private session. + */ + suspend fun updateSitePermissions(sitePermissions: SitePermissions, private: Boolean) = + withContext(dispatcher) { + permissionsStorage.update(sitePermissions, private = private) + } + + /** + * Returns all saved [SitePermissions] instances as a [DataSource.Factory]. + * + * A consuming app can transform the data source into a `LiveData` of when using RxJava2 into a + * `Flowable` or `Observable`, that can be observed. + * + * - https://developer.android.com/topic/libraries/architecture/paging/data + * - https://developer.android.com/topic/libraries/architecture/paging/ui + */ + suspend fun getSitePermissionsPaged(): DataSource.Factory { + return permissionsStorage.getSitePermissionsPaged() + } + + /** + * Deletes all sitePermissions that match the sitePermissions provided as a parameter. + * @param sitePermissions The [SitePermissions] to be deleted from the storage. + */ + suspend fun deleteSitePermissions(sitePermissions: SitePermissions) = withContext(dispatcher) { + permissionsStorage.remove(sitePermissions, private = false) + } + + /** + * Deletes all sitePermissions sitePermissions. + */ + suspend fun deleteAllSitePermissions() = withContext(dispatcher) { + permissionsStorage.removeAll() + } +} diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionPromptFeature.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionPromptFeature.kt index 3b4e7070..3c0dabc3 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionPromptFeature.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/addons/WebExtensionPromptFeature.kt @@ -282,11 +282,11 @@ class WebExtensionPromptFeature( var title = context.getString(MozComp.string.mozac_feature_addons_cant_install_extension, "") val message = when (exception) { is WebExtensionInstallException.Blocklisted -> { - context.getString(MozComp.string.mozac_feature_addons_blocklisted_1, addonName) + context.getString(MozComp.string.mozac_feature_addons_blocklisted_2, addonName) } is WebExtensionInstallException.SoftBlocked -> { - context.getString(MozComp.string.mozac_feature_addons_soft_blocked, addonName) + context.getString(MozComp.string.mozac_feature_addons_soft_blocked_1, addonName) } is WebExtensionInstallException.UserCancelled -> { diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt new file mode 100644 index 00000000..6bf25533 --- /dev/null +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoDeleteBrowsingDataControllerImpl.kt @@ -0,0 +1,107 @@ +package eu.lensai.flutter_mozilla_components.api + +import eu.lensai.flutter_mozilla_components.GlobalComponents +import eu.lensai.flutter_mozilla_components.pigeons.GeckoDeleteBrowsingDataController +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mozilla.components.browser.state.action.EngineAction +import mozilla.components.browser.state.action.RecentlyClosedAction +import mozilla.components.concept.engine.Engine +import mozilla.components.concept.engine.translate.ModelManagementOptions +import mozilla.components.concept.engine.translate.ModelOperation +import mozilla.components.concept.engine.translate.OperationLevel + +class GeckoDeleteBrowsingDataControllerImpl : GeckoDeleteBrowsingDataController { + companion object { + private val coroutineScope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + } + + private val components by lazy { + requireNotNull(GlobalComponents.components) { "Components not initialized" } + } + + override fun deleteTabs(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.useCases.tabsUseCases.removeAllTabs.invoke(false) + + callback(Result.success(Unit)) + } + } + } + + override fun deleteBrowsingHistory(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.historyStorage.deleteEverything() + components.core.store.dispatch(EngineAction.PurgeHistoryAction) + components.core.icons.clear() + components.core.store.dispatch(RecentlyClosedAction.RemoveAllClosedTabAction) + + callback(Result.success(Unit)) + } + } + } + + override fun deleteCookiesAndSiteData(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.engine.clearData( + Engine.BrowsingData.select( + Engine.BrowsingData.COOKIES, + Engine.BrowsingData.AUTH_SESSIONS, + ), + ) + components.core.engine.clearData(Engine.BrowsingData.select(Engine.BrowsingData.DOM_STORAGES)) + + callback(Result.success(Unit)) + } + } + } + + override fun deleteCachedFiles(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.engine.manageTranslationsLanguageModel( + options = ModelManagementOptions( + operation = ModelOperation.DELETE, + operationLevel = OperationLevel.CACHE, + ), + onSuccess = { }, + onError = { }, + ) + components.core.engine.clearData( + Engine.BrowsingData.select(Engine.BrowsingData.ALL_CACHES), + ) + + callback(Result.success(Unit)) + } + } + } + + override fun deleteSitePermissions(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.core.engine.clearData( + Engine.BrowsingData.select(Engine.BrowsingData.ALL_SITE_SETTINGS), + ) + components.core.permissionStorage.deleteAllSitePermissions() + + callback(Result.success(Unit)) + } + } + } + + override fun deleteDownloads(callback: (Result) -> Unit) { + coroutineScope.launch { + withContext(Dispatchers.Main) { + components.useCases.downloadsUseCases.removeAllDownloads.invoke() + + callback(Result.success(Unit)) + } + } + } +} \ No newline at end of file diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt index 80c31d81..9eb96f6e 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoEngineSettingsApiImpl.kt @@ -27,6 +27,24 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { requireNotNull(GlobalComponents.components) { "Components not initialized" } } + private fun updateFingerprintingProtection(trackingProtectionPolicy: eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy) { + when(trackingProtectionPolicy) { + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.STRICT -> { + components.core.engineSettings.fingerprintingProtection = true + components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true + } + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.RECOMMENDED -> { + components.core.engineSettings.fingerprintingProtection = false + components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true + } + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.CUSTOM -> TODO() + eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.NONE -> { + components.core.engineSettings.fingerprintingProtection = false + components.core.engineSettings.fingerprintingProtectionPrivateBrowsing = true + } + } + } + override fun setDefaultSettings(settings: GeckoEngineSettings) { if(settings.javascriptEnabled != null) { components.core.engineSettings.javascriptEnabled = settings.javascriptEnabled; @@ -38,6 +56,8 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.STRICT -> TrackingProtectionPolicy.strict() eu.lensai.flutter_mozilla_components.pigeons.TrackingProtectionPolicy.CUSTOM -> TODO() } + + updateFingerprintingProtection(settings.trackingProtectionPolicy) } if(settings.httpsOnlyMode != null) { components.core.engineSettings.httpsOnlyMode = when(settings.httpsOnlyMode) { @@ -89,13 +109,18 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { //First parse and set default values setDefaultSettings(settings); + var reloadSession = false + //Then copy default settings into runtime if(settings.javascriptEnabled != null) { components.core.engine.settings.javascriptEnabled = components.core.engineSettings.javascriptEnabled + reloadSession = true } if(settings.trackingProtectionPolicy != null) { components.useCases.settingsUseCases.updateTrackingProtection(components.core.engineSettings.trackingProtectionPolicy!!) - components.useCases.sessionUseCases.reload() + components.core.engine.settings.fingerprintingProtection = components.core.engineSettings.fingerprintingProtection + components.core.engine.settings.fingerprintingProtectionPrivateBrowsing = components.core.engineSettings.fingerprintingProtectionPrivateBrowsing + reloadSession = true } if(settings.httpsOnlyMode != null) { components.core.engineSettings.httpsOnlyMode = components.core.engineSettings.httpsOnlyMode @@ -105,12 +130,15 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { } if(settings.preferredColorScheme != null) { components.core.engineSettings.preferredColorScheme = components.core.engineSettings.preferredColorScheme + reloadSession = true } if(settings.cookieBannerHandlingMode != null) { components.core.engineSettings.cookieBannerHandlingMode = components.core.engineSettings.cookieBannerHandlingMode + reloadSession = true } if(settings.cookieBannerHandlingModePrivateBrowsing != null) { components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing = components.core.engineSettings.cookieBannerHandlingModePrivateBrowsing + reloadSession = true } if(settings.cookieBannerHandlingGlobalRules != null) { components.core.engineSettings.cookieBannerHandlingGlobalRules = components.core.engineSettings.cookieBannerHandlingGlobalRules @@ -121,5 +149,9 @@ class GeckoEngineSettingsApiImpl : GeckoEngineSettingsApi { if(settings.webContentIsolationStrategy != null) { components.core.engineSettings.webContentIsolationStrategy = components.core.engineSettings.webContentIsolationStrategy } + + if(reloadSession) { + components.useCases.sessionUseCases.reload() + } } } diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt index b765324f..13499c21 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/api/GeckoTabsApiImpl.kt @@ -33,6 +33,7 @@ import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.LastMediaAccessState import mozilla.components.browser.state.state.ReaderState import mozilla.components.browser.state.state.SessionState +import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.state.recover.RecoverableTab import mozilla.components.browser.state.state.recover.TabState import mozilla.components.browser.thumbnails.storage.ThumbnailStorage @@ -450,17 +451,33 @@ class GeckoTabsApiImpl : GeckoTabsApi { } } - override fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String { + override fun duplicateTab( + selectTabId: String?, + selectNewTab: Boolean, + newContextId: String? + ): String { try { val tabState = selectTabId?.let { components.core.store.state.findTab(it) } ?: throw IllegalArgumentException("Tab not found") - return components.useCases.tabsUseCases.duplicateTab( - tab = tabState, - selectNewTab = selectNewTab - ).also { - logger.debug("$TAG: Duplicated tab $selectTabId to new tab $it") - } + val duplicate = createTab( + url = tabState.content.url, + private = tabState.content.private, + contextId = newContextId, + parent = tabState, + engineSessionState = tabState.engineState.engineSessionState, + ) + + components.core.store.dispatch( + TabListAction.AddTabAction( + duplicate, + select = selectNewTab, + ), + ) + + logger.debug("$TAG: Duplicated tab $selectTabId to new tab ${duplicate.id}") + + return duplicate.id } catch (e: Exception) { logger.error("$TAG: Failed to duplicate tab", e) throw e diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt index 2872b4eb..5f4122cb 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/components/Core.kt @@ -8,6 +8,7 @@ import eu.lensai.flutter_mozilla_components.Components import eu.lensai.flutter_mozilla_components.interceptor.AppRequestInterceptor import eu.lensai.flutter_mozilla_components.services.DownloadService import eu.lensai.flutter_mozilla_components.EngineProvider +import eu.lensai.flutter_mozilla_components.PermissionStorage import eu.lensai.flutter_mozilla_components.services.MediaSessionService import eu.lensai.flutter_mozilla_components.activities.NotificationActivity import eu.lensai.flutter_mozilla_components.R @@ -212,6 +213,8 @@ class Core(private val context: Context, */ val historyStorage by lazy { lazyHistoryStorage.value } + val permissionStorage by lazy { PermissionStorage( geckoSitePermissionsStorage ) } + /** * Constructs a [TrackingProtectionPolicy] based on current preferences. * diff --git a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt index 52b97dc6..73a560cf 100644 --- a/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt +++ b/packages/flutter_mozilla_components/android/src/main/kotlin/eu/lensai/flutter_mozilla_components/pigeons/Gecko.g.kt @@ -2017,7 +2017,7 @@ interface GeckoTabsApi { fun selectOrAddTabByHistory(url: String, historyMetadata: HistoryMetadataKey): String /** Selects an already existing tab displaying [url] or otherwise creates a new tab. */ fun selectOrAddTabByUrl(url: String, private: Boolean, source: SourceValue, flags: LoadUrlFlagsValue, ignoreFragment: Boolean): String - fun duplicateTab(selectTabId: String?, selectNewTab: Boolean): String + fun duplicateTab(selectTabId: String?, selectNewTab: Boolean, newContextId: String?): String fun moveTabs(tabIds: List, targetTabId: String, placeAfter: Boolean) fun migratePrivateTabUseCase(tabId: String, alternativeUrl: String?): String @@ -2287,8 +2287,9 @@ interface GeckoTabsApi { val args = message as List val selectTabIdArg = args[0] as String? val selectNewTabArg = args[1] as Boolean + val newContextIdArg = args[2] as String? val wrapped: List = try { - listOf(api.duplicateTab(selectTabIdArg, selectNewTabArg)) + listOf(api.duplicateTab(selectTabIdArg, selectNewTabArg, newContextIdArg)) } catch (exception: Throwable) { wrapError(exception) } @@ -3213,3 +3214,126 @@ class GeckoTabContentEvents(private val binaryMessenger: BinaryMessenger, privat } } } +/** Generated interface from Pigeon that represents a handler of messages from Flutter. */ +interface GeckoDeleteBrowsingDataController { + fun deleteTabs(callback: (Result) -> Unit) + fun deleteBrowsingHistory(callback: (Result) -> Unit) + fun deleteCookiesAndSiteData(callback: (Result) -> Unit) + fun deleteCachedFiles(callback: (Result) -> Unit) + fun deleteSitePermissions(callback: (Result) -> Unit) + fun deleteDownloads(callback: (Result) -> Unit) + + companion object { + /** The codec used by GeckoDeleteBrowsingDataController. */ + val codec: MessageCodec by lazy { + GeckoPigeonCodec() + } + /** Sets up an instance of `GeckoDeleteBrowsingDataController` to handle messages through the `binaryMessenger`. */ + @JvmOverloads + fun setUp(binaryMessenger: BinaryMessenger, api: GeckoDeleteBrowsingDataController?, messageChannelSuffix: String = "") { + val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else "" + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteTabs$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteTabs{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteBrowsingHistory$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteBrowsingHistory{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCookiesAndSiteData$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteCookiesAndSiteData{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCachedFiles$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteCachedFiles{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteSitePermissions$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteSitePermissions{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + run { + val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteDownloads$separatedMessageChannelSuffix", codec) + if (api != null) { + channel.setMessageHandler { _, reply -> + api.deleteDownloads{ result: Result -> + val error = result.exceptionOrNull() + if (error != null) { + reply.reply(wrapError(error)) + } else { + reply.reply(wrapResult(null)) + } + } + } + } else { + channel.setMessageHandler(null) + } + } + } + } +} diff --git a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart index 4bf0e46c..55bba7ce 100644 --- a/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart +++ b/packages/flutter_mozilla_components/lib/flutter_mozilla_components.dart @@ -4,6 +4,7 @@ export 'src/data/models/source.dart'; export 'src/domain/entities/default_selection_actions.dart'; export 'src/domain/services/gecko_addon.dart'; export 'src/domain/services/gecko_cookie.dart'; +export 'src/domain/services/gecko_delete_browser_data.dart'; export 'src/domain/services/gecko_engine_settings.dart'; export 'src/domain/services/gecko_event.dart'; export 'src/domain/services/gecko_find_in_page.dart'; diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_delete_browser_data.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_delete_browser_data.dart new file mode 100644 index 00000000..c37c8f25 --- /dev/null +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_delete_browser_data.dart @@ -0,0 +1,34 @@ +import 'package:flutter_mozilla_components/src/pigeons/gecko.g.dart'; + +final _apiInstance = GeckoDeleteBrowsingDataController(); + +class GeckoDeleteBrowserDataService { + final GeckoDeleteBrowsingDataController _api; + + GeckoDeleteBrowserDataService({GeckoDeleteBrowsingDataController? api}) + : _api = api ?? _apiInstance; + + Future deleteTabs() { + return _api.deleteTabs(); + } + + Future deleteBrowsingHistory() { + return _api.deleteBrowsingHistory(); + } + + Future deleteCookiesAndSiteData() { + return _api.deleteCookiesAndSiteData(); + } + + Future deleteCachedFiles() { + return _api.deleteCachedFiles(); + } + + Future deleteSitePermissions() { + return _api.deleteSitePermissions(); + } + + Future deleteDownloads() { + return _api.deleteDownloads(); + } +} diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart index 6ba9caaf..afaa3429 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_engine_settings.dart @@ -8,7 +8,71 @@ class GeckoEngineSettingsService { GeckoEngineSettingsService({GeckoEngineSettingsApi? api}) : _api = api ?? _apiInstance; - Future javaScriptEnabled(bool state) { - return _api.javaScriptEnabled(state); + Future setDefaultSettings(GeckoEngineSettings settings, + {bool updateRuntime = true}) { + return updateRuntime + ? _api.updateRuntimeSettings(settings) + : _api.setDefaultSettings(settings); + } + + Future javascriptEnabled(bool state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(javascriptEnabled: state), + ); + } + + Future trackingProtectionPolicy(TrackingProtectionPolicy state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(trackingProtectionPolicy: state), + ); + } + + Future httpsOnlyMode(HttpsOnlyMode state) { + return _api + .updateRuntimeSettings(GeckoEngineSettings(httpsOnlyMode: state)); + } + + Future globalPrivacyControlEnabled(bool state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(globalPrivacyControlEnabled: state), + ); + } + + Future preferredColorScheme(ColorScheme state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(preferredColorScheme: state), + ); + } + + Future cookieBannerHandlingMode(CookieBannerHandlingMode state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(cookieBannerHandlingMode: state), + ); + } + + Future cookieBannerHandlingModePrivateBrowsing( + CookieBannerHandlingMode state, + ) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(cookieBannerHandlingModePrivateBrowsing: state), + ); + } + + Future cookieBannerHandlingGlobalRules(bool state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(cookieBannerHandlingGlobalRules: state), + ); + } + + Future cookieBannerHandlingGlobalRulesSubFrames(bool state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(cookieBannerHandlingGlobalRulesSubFrames: state), + ); + } + + Future webContentIsolationStrategy(WebContentIsolationStrategy state) { + return _api.updateRuntimeSettings( + GeckoEngineSettings(webContentIsolationStrategy: state), + ); } } diff --git a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart index 0b1b4b3e..e8971ae9 100644 --- a/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart +++ b/packages/flutter_mozilla_components/lib/src/domain/services/gecko_tab.dart @@ -142,12 +142,14 @@ class GeckoTabService { } Future duplicateTab({ - String? selectTabId, + required String? selectTabId, + required String? newContextId, bool selectNewTab = true, }) { return _api.duplicateTab( selectTabId: selectTabId, selectNewTab: selectNewTab, + newContextId: newContextId, ); } diff --git a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart index a62836ea..dd7da229 100644 --- a/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart +++ b/packages/flutter_mozilla_components/lib/src/geckoview_widget.dart @@ -10,8 +10,13 @@ import 'package:flutter_mozilla_components/src/domain/services/gecko_browser.dar class GeckoView extends StatefulWidget { final FutureOr Function()? preInitializationStep; + final FutureOr Function()? postInitializationStep; - const GeckoView({super.key, this.preInitializationStep}); + const GeckoView({ + super.key, + this.preInitializationStep, + this.postInitializationStep, + }); @override State createState() => _GeckoViewState(); @@ -70,6 +75,7 @@ class _GeckoViewState extends State { Duration(milliseconds: ((1000 / 60) * 2).toInt()), ).whenComplete(() async { await browserService.showNativeFragment(); + await widget.postInitializationStep?.call(); }); }); }) diff --git a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart index a08b891c..7214434b 100644 --- a/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart +++ b/packages/flutter_mozilla_components/lib/src/pigeons/gecko.g.dart @@ -2334,7 +2334,7 @@ class GeckoTabsApi { } } - Future duplicateTab({required String? selectTabId, required bool selectNewTab}) async { + Future duplicateTab({required String? selectTabId, required bool selectNewTab, required String? newContextId, }) async { final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoTabsApi.duplicateTab$pigeonVar_messageChannelSuffix'; final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( pigeonVar_channelName, @@ -2342,7 +2342,7 @@ class GeckoTabsApi { binaryMessenger: pigeonVar_binaryMessenger, ); final List? pigeonVar_replyList = - await pigeonVar_channel.send([selectTabId, selectNewTab]) as List?; + await pigeonVar_channel.send([selectTabId, selectNewTab, newContextId]) as List?; if (pigeonVar_replyList == null) { throw _createConnectionError(pigeonVar_channelName); } else if (pigeonVar_replyList.length > 1) { @@ -3602,3 +3602,149 @@ abstract class GeckoTabContentEvents { } } } + +class GeckoDeleteBrowsingDataController { + /// Constructor for [GeckoDeleteBrowsingDataController]. The [binaryMessenger] named argument is + /// available for dependency injection. If it is left null, the default + /// BinaryMessenger will be used which routes to the host platform. + GeckoDeleteBrowsingDataController({BinaryMessenger? binaryMessenger, String messageChannelSuffix = ''}) + : pigeonVar_binaryMessenger = binaryMessenger, + pigeonVar_messageChannelSuffix = messageChannelSuffix.isNotEmpty ? '.$messageChannelSuffix' : ''; + final BinaryMessenger? pigeonVar_binaryMessenger; + + static const MessageCodec pigeonChannelCodec = _PigeonCodec(); + + final String pigeonVar_messageChannelSuffix; + + Future deleteTabs() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteTabs$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } + + Future deleteBrowsingHistory() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteBrowsingHistory$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } + + Future deleteCookiesAndSiteData() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCookiesAndSiteData$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } + + Future deleteCachedFiles() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteCachedFiles$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } + + Future deleteSitePermissions() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteSitePermissions$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } + + Future deleteDownloads() async { + final String pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoDeleteBrowsingDataController.deleteDownloads$pigeonVar_messageChannelSuffix'; + final BasicMessageChannel pigeonVar_channel = BasicMessageChannel( + pigeonVar_channelName, + pigeonChannelCodec, + binaryMessenger: pigeonVar_binaryMessenger, + ); + final List? pigeonVar_replyList = + await pigeonVar_channel.send(null) as List?; + 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; + } + } +} diff --git a/packages/flutter_mozilla_components/pigeons/gecko.dart b/packages/flutter_mozilla_components/pigeons/gecko.dart index d3cb68df..8601869b 100644 --- a/packages/flutter_mozilla_components/pigeons/gecko.dart +++ b/packages/flutter_mozilla_components/pigeons/gecko.dart @@ -835,6 +835,7 @@ abstract class GeckoTabsApi { String duplicateTab({ required String? selectTabId, required bool selectNewTab, + required String? newContextId, }); void moveTabs({ @@ -1016,3 +1017,19 @@ abstract class GeckoTabContentEvents { TabContent content, ); } + +@HostApi() +abstract class GeckoDeleteBrowsingDataController { + @async + void deleteTabs(); + @async + void deleteBrowsingHistory(); + @async + void deleteCookiesAndSiteData(); + @async + void deleteCachedFiles(); + @async + void deleteSitePermissions(); + @async + void deleteDownloads(); +} diff --git a/packages/sqlite3_vec/example/.flutter-plugins b/packages/sqlite3_vec/example/.flutter-plugins index aa393105..7997c595 100644 --- a/packages/sqlite3_vec/example/.flutter-plugins +++ b/packages/sqlite3_vec/example/.flutter-plugins @@ -1,4 +1,4 @@ # This is a generated file; do not edit or check into version control. integration_test=/home/fafre/development/flutter/packages/integration_test/ -sqlite3_flutter_libs=/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/ +sqlite3_flutter_libs=/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/ sqlite3_vec=/home/fafre/development/repos/lensai/packages/sqlite3_vec/ diff --git a/packages/sqlite3_vec/example/.flutter-plugins-dependencies b/packages/sqlite3_vec/example/.flutter-plugins-dependencies index 88b6b9c0..38ce3a75 100644 --- a/packages/sqlite3_vec/example/.flutter-plugins-dependencies +++ b/packages/sqlite3_vec/example/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-26 00:44:40.809594","version":"3.27.3","swift_package_manager_enabled":false} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"android":[{"name":"integration_test","path":"/home/fafre/development/flutter/packages/integration_test/","native_build":true,"dependencies":[]},{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"macos":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/","shared_darwin_source":true,"native_build":true,"dependencies":[]}],"linux":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/","native_build":true,"dependencies":[]},{"name":"sqlite3_vec","path":"/home/fafre/development/repos/lensai/packages/sqlite3_vec/","native_build":true,"dependencies":[]}],"windows":[{"name":"sqlite3_flutter_libs","path":"/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/","native_build":true,"dependencies":[]}],"web":[]},"dependencyGraph":[{"name":"integration_test","dependencies":[]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"sqlite3_vec","dependencies":[]}],"date_created":"2025-01-31 01:49:54.251398","version":"3.27.3","swift_package_manager_enabled":false} \ No newline at end of file diff --git a/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs b/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs index 0a4a00f2..46869393 120000 --- a/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs +++ b/packages/sqlite3_vec/example/linux/flutter/ephemeral/.plugin_symlinks/sqlite3_flutter_libs @@ -1 +1 @@ -/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29/ \ No newline at end of file +/home/fafre/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.29+1/ \ No newline at end of file diff --git a/packages/sqlite3_vec/example/pubspec.yaml b/packages/sqlite3_vec/example/pubspec.yaml index 47522076..0b7751aa 100644 --- a/packages/sqlite3_vec/example/pubspec.yaml +++ b/packages/sqlite3_vec/example/pubspec.yaml @@ -29,7 +29,7 @@ dependencies: # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 sqlite3: ^2.7.2 - sqlite3_flutter_libs: ^0.5.29 + sqlite3_flutter_libs: ^0.5.29+1 dev_dependencies: integration_test: