This commit is contained in:
Fabian Freund
2025-02-03 15:09:04 +01:00
parent 36acd46318
commit 025ecaf2e3
132 changed files with 4431 additions and 1644 deletions
+2
View File
@@ -21,6 +21,8 @@ linter:
- collection_methods_unrelated_type - collection_methods_unrelated_type
analyzer: analyzer:
errors:
overridden_fields: ignore
plugins: plugins:
- custom_lint - custom_lint
exclude: exclude:
+136 -91
View File
@@ -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": { "user": {
"Safe Browsing": { "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.", "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, "value": true,
"title": "Force WebRTC inside the proxy" "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": { "media.peerconnection.ice.default_address_only": {
"value": true, "value": true,
"title": "Force a single network interface for ICE candidates generation", "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": { "Secure Connections": {
"description": "SSL/TLS / OCSP / CERTS / HPKP", "description": "SSL/TLS / OCSP / CERTS / HPKP",
"preferences": { "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": { "Miscelaneous": {
"preferences": { "preferences": {
"permissions.manager.defaultsUrl": { "permissions.manager.defaultsUrl": {
@@ -212,48 +292,13 @@
"value": true, "value": true,
"title": "Use Punycode in Internationalized Domain Names to eliminate possible spoofing" "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": { "extensions.postDownloadThirdPartyPrompt": {
"value": false, "value": false,
"title": "Disable bypassing 3rd party extension install prompts" "title": "Disable bypassing 3rd party extension install prompts"
}, },
"pdfjs.enableScripting": { "layout.css.visited_links_enabled": {
"value": false, "value": false,
"title": "Disable PDFJS scripting" "title": "Disable coloring of visited links"
}
}
},
"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
} }
} }
} }
+81 -13
View File
@@ -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/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/dialogs/web_page_dialog.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.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/geckoview/features/tabs/presentation/screens/container_list.dart';
import 'package:lensai/features/search/presentation/screens/search.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/bang_settings.dart';
import 'package:lensai/features/settings/presentation/screens/browser_hardening_group.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/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'; import 'package:lensai/features/user/presentation/screens/auth.dart';
part 'routes.g.dart'; part 'routes.g.dart';
@@ -63,6 +68,16 @@ part 'routes.g.dart';
TypedGoRoute<ContainerListRoute>( TypedGoRoute<ContainerListRoute>(
name: 'ContainerListRoute', name: 'ContainerListRoute',
path: 'containers', path: 'containers',
routes: [
TypedGoRoute<ContainerCreateRoute>(
name: 'ContainerCreateRoute',
path: 'create',
),
TypedGoRoute<ContainerEditRoute>(
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<SettingsRoute>( @TypedGoRoute<SettingsRoute>(
name: 'SettingsRoute', name: 'SettingsRoute',
path: '/settings', path: '/settings',
routes: [ routes: [
TypedGoRoute<BrowserHardeningRoute>( TypedGoRoute<GeneralSettingsRoute>(
name: 'BrowserHardeningRoute', name: 'GeneralSettingsRoute',
path: 'hardening', path: 'general',
),
TypedGoRoute<BangSettingsRoute>(
name: 'BangSettingsRoute',
path: 'bang',
),
TypedGoRoute<WebEngineSettingsRoute>(
name: 'WebEngineSettingsRoute',
path: 'web_engine',
routes: [ routes: [
TypedGoRoute<BrowserHardeningGroupRoute>( TypedGoRoute<WebEngineHardeningRoute>(
name: 'BrowserHardeningGroupRoute', name: 'WebEngineHardeningRoute',
path: 'group/:group', path: 'hardening',
routes: [
TypedGoRoute<WebEngineHardeningGroupRoute>(
name: 'WebEngineHardeningGroupRoute',
path: 'group/:group',
),
],
), ),
], ],
), ),
@@ -202,21 +249,42 @@ class SettingsRoute extends GoRouteData {
} }
} }
class BrowserHardeningRoute extends GoRouteData { class GeneralSettingsRoute extends GoRouteData {
@override @override
Widget build(BuildContext context, GoRouterState state) { 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; final String group;
const BrowserHardeningGroupRoute({required this.group}); const WebEngineHardeningGroupRoute({required this.group});
@override @override
Widget build(BuildContext context, GoRouterState state) { Widget build(BuildContext context, GoRouterState state) {
return BrowserHardeningGroupScreen(groupName: group); return WebEngineHardeningGroupScreen(groupName: group);
} }
} }
+134 -14
View File
@@ -65,6 +65,18 @@ RouteBase get $browserRoute => GoRouteData.$route(
path: 'containers', path: 'containers',
name: 'ContainerListRoute', name: 'ContainerListRoute',
factory: $ContainerListRouteExtension._fromState, 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); 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<T?> push<T>(BuildContext context) => context.push<T>(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<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
RouteBase get $settingsRoute => GoRouteData.$route( RouteBase get $settingsRoute => GoRouteData.$route(
path: '/settings', path: '/settings',
name: 'SettingsRoute', name: 'SettingsRoute',
factory: $SettingsRouteExtension._fromState, factory: $SettingsRouteExtension._fromState,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: 'hardening', path: 'general',
name: 'BrowserHardeningRoute', name: 'GeneralSettingsRoute',
factory: $BrowserHardeningRouteExtension._fromState, factory: $GeneralSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'bang',
name: 'BangSettingsRoute',
factory: $BangSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'web_engine',
name: 'WebEngineSettingsRoute',
factory: $WebEngineSettingsRouteExtension._fromState,
routes: [ routes: [
GoRouteData.$route( GoRouteData.$route(
path: 'group/:group', path: 'hardening',
name: 'BrowserHardeningGroupRoute', name: 'WebEngineHardeningRoute',
factory: $BrowserHardeningGroupRouteExtension._fromState, 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); void replace(BuildContext context) => context.replace(location);
} }
extension $BrowserHardeningRouteExtension on BrowserHardeningRoute { extension $GeneralSettingsRouteExtension on GeneralSettingsRoute {
static BrowserHardeningRoute _fromState(GoRouterState state) => static GeneralSettingsRoute _fromState(GoRouterState state) =>
BrowserHardeningRoute(); GeneralSettingsRoute();
String get location => GoRouteData.$location( String get location => GoRouteData.$location(
'/settings/hardening', '/settings/general',
); );
void go(BuildContext context) => context.go(location); void go(BuildContext context) => context.go(location);
@@ -310,14 +375,69 @@ extension $BrowserHardeningRouteExtension on BrowserHardeningRoute {
void replace(BuildContext context) => context.replace(location); void replace(BuildContext context) => context.replace(location);
} }
extension $BrowserHardeningGroupRouteExtension on BrowserHardeningGroupRoute { extension $BangSettingsRouteExtension on BangSettingsRoute {
static BrowserHardeningGroupRoute _fromState(GoRouterState state) => static BangSettingsRoute _fromState(GoRouterState state) =>
BrowserHardeningGroupRoute( BangSettingsRoute();
String get location => GoRouteData.$location(
'/settings/bang',
);
void go(BuildContext context) => context.go(location);
Future<T?> push<T>(BuildContext context) => context.push<T>(location);
void pushReplacement(BuildContext context) =>
context.pushReplacement(location);
void replace(BuildContext context) => context.replace(location);
}
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<T?> push<T>(BuildContext context) => context.push<T>(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<T?> push<T>(BuildContext context) => context.push<T>(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']!, group: state.pathParameters['group']!,
); );
String get location => GoRouteData.$location( 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); void go(BuildContext context) => context.go(location);
@@ -1,6 +1,21 @@
import 'dart:ui'; import 'dart:ui';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:json_annotation/json_annotation.dart';
class ColorJsonConverter implements JsonConverter<Color, int> {
const ColorJsonConverter();
@override
Color fromJson(int from) {
return Color(from);
}
@override
int toJson(Color value) {
return value.value;
}
}
class ColorConverter extends TypeConverter<Color, int> { class ColorConverter extends TypeConverter<Color, int> {
const ColorConverter(); const ColorConverter();
@@ -1,3 +1,4 @@
import 'package:lensai/extensions/nullable.dart';
import 'package:lexo_rank/lexo_rank.dart'; import 'package:lexo_rank/lexo_rank.dart';
import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart'; import 'package:lexo_rank/lexo_rank/lexo_rank_bucket.dart';
import 'package:sqlite3/common.dart'; import 'package:sqlite3/common.dart';
@@ -27,8 +28,8 @@ String _previousRankOrMiddle(List<Object?> args) {
} }
String _reorderAfter(List<Object?> args) { String _reorderAfter(List<Object?> args) {
final first = (args[0] != null) ? LexoRank.parse(args[0]! as String) : null; final first = args[0].mapNotNull((arg) => LexoRank.parse(arg as String));
final last = (args[1] != null) ? LexoRank.parse(args[1]! as String) : null; final last = args[1].mapNotNull((arg) => LexoRank.parse(arg as String));
if (first == null) { if (first == null) {
throw Exception('Tab not found'); throw Exception('Tab not found');
+2 -1
View File
@@ -10,6 +10,7 @@ import 'package:html/parser.dart' as html_parser;
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:lensai/core/http_error_handler.dart'; import 'package:lensai/core/http_error_handler.dart';
import 'package:lensai/data/models/web_page_info.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/geckoview/domain/entities/browser_icon.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart'; import 'package:lensai/features/user/domain/repositories/cache.dart';
import 'package:lensai/utils/lru_cache.dart'; import 'package:lensai/utils/lru_cache.dart';
@@ -267,7 +268,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
url.origin, url.origin,
await BrowserIcon.fromBytes( await BrowserIcon.fromBytes(
result.image, result.image,
dominantColor: (result.color != null) ? Color(result.color!) : null, dominantColor: result.color.mapNotNull((color) => Color(color)),
source: result.source, source: result.source,
), ),
); );
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
// ************************************************************************** // **************************************************************************
String _$genericWebsiteServiceHash() => String _$genericWebsiteServiceHash() =>
r'8eab8ab1448d4826dc0cda0ff6b47143735ba176'; r'27430ba17a04b0fa3cca20abf00f9d1c15fa843a';
/// See also [GenericWebsiteService]. /// See also [GenericWebsiteService].
@ProviderFor(GenericWebsiteService) @ProviderFor(GenericWebsiteService)
+7
View File
@@ -0,0 +1,7 @@
extension NullableX<T> on T? {
@pragma('vm:prefer-inline')
R? mapNotNull<R>(R? Function(T) callback) {
// ignore: null_check_on_nullable_type_parameter
return (this != null) ? callback(this!) : null;
}
}
@@ -1,4 +1,5 @@
import 'package:drift/drift.dart'; 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/database/database.dart';
import 'package:lensai/features/bangs/data/models/bang.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/bang_data.dart';
@@ -20,7 +21,7 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
SingleSelectable<int> getBangCount({Iterable<BangGroup>? groups}) { SingleSelectable<int> getBangCount({Iterable<BangGroup>? groups}) {
return db.bang.count( return db.bang.count(
where: (groups != null) ? (t) => t.group.isInValues(groups) : null, where: groups.mapNotNull((groups) => (t) => t.group.isInValues(groups)),
); );
} }
@@ -3,7 +3,6 @@ import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:exceptions/exceptions.dart'; import 'package:exceptions/exceptions.dart';
import 'package:lensai/domain/services/generic_website.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.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/data/models/search_history_entry.dart'; import 'package:lensai/features/bangs/data/models/search_history_entry.dart';
@@ -14,23 +13,27 @@ part 'data.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class BangDataRepository extends _$BangDataRepository { class BangDataRepository extends _$BangDataRepository {
late BangDatabase _db;
@override @override
void build() { void build() {}
_db = ref.watch(bangDatabaseProvider);
}
Stream<BangData?> watchBang(String? trigger) { Stream<BangData?> watchBang(String? trigger) {
if (trigger != null) { if (trigger != null) {
return _db.bangDao.getBangData(trigger).watchSingleOrNull(); return ref
.read(bangDatabaseProvider)
.bangDao
.getBangData(trigger)
.watchSingleOrNull();
} else { } else {
return Stream.value(null); return Stream.value(null);
} }
} }
Stream<Map<String, List<String>>> watchCategories() { Stream<Map<String, List<String>>> watchCategories() {
return _db.categoriesJson().watchSingle().map((json) { return ref
.read(bangDatabaseProvider)
.categoriesJson()
.watchSingle()
.map((json) {
final decoded = jsonDecode(json) as Map<String, dynamic>; final decoded = jsonDecode(json) as Map<String, dynamic>;
return decoded.map( return decoded.map(
(key, value) => MapEntry(key, (value as List<dynamic>).cast()), (key, value) => MapEntry(key, (value as List<dynamic>).cast()),
@@ -39,7 +42,10 @@ class BangDataRepository extends _$BangDataRepository {
} }
Stream<int> watchBangCount(BangGroup group) { Stream<int> watchBangCount(BangGroup group) {
return _db.bangDao.getBangCount(groups: [group]).watchSingle(); return ref
.read(bangDatabaseProvider)
.bangDao
.getBangCount(groups: [group]).watchSingle();
} }
Stream<List<BangData>> watchBangs({ Stream<List<BangData>> watchBangs({
@@ -48,7 +54,9 @@ class BangDataRepository extends _$BangDataRepository {
({String category, String? subCategory})? categoryFilter, ({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst, bool? orderMostFrequentFirst,
}) { }) {
return _db.bangDao return ref
.read(bangDatabaseProvider)
.bangDao
.getBangDataList( .getBangDataList(
groups: groups, groups: groups,
domain: domain, domain: domain,
@@ -60,15 +68,25 @@ class BangDataRepository extends _$BangDataRepository {
} }
Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup>? groups}) { Stream<List<BangData>> watchFrequentBangs({Iterable<BangGroup>? groups}) {
return _db.bangDao.getFrequentBangDataList(groups: groups).watch(); return ref
.read(bangDatabaseProvider)
.bangDao
.getFrequentBangDataList(groups: groups)
.watch();
} }
Stream<List<SearchHistoryEntry>> watchSearchHistory({required int limit}) { Stream<List<SearchHistoryEntry>> watchSearchHistory({required int limit}) {
return _db.searchHistoryEntries(limit: limit).watch(); return ref
.read(bangDatabaseProvider)
.searchHistoryEntries(limit: limit)
.watch();
} }
Future<void> increaseFrequency(String trigger) { Future<void> increaseFrequency(String trigger) {
return _db.bangDao.increaseBangFrequency(trigger); return ref
.read(bangDatabaseProvider)
.bangDao
.increaseBangFrequency(trigger);
} }
Future<void> addSearchEntry( Future<void> addSearchEntry(
@@ -76,17 +94,21 @@ class BangDataRepository extends _$BangDataRepository {
String searchQuery, { String searchQuery, {
required int maxEntryCount, required int maxEntryCount,
}) { }) {
final db = ref.read(bangDatabaseProvider);
//Pack in a transaction to bundle rebuilds of watch() queries //Pack in a transaction to bundle rebuilds of watch() queries
return _db.transaction( return db.transaction(
() async { () async {
await _db.bangDao.addSearchEntry(trigger, searchQuery); await db.bangDao.addSearchEntry(trigger, searchQuery);
await _db.evictHistoryEntries(limit: maxEntryCount); await db.evictHistoryEntries(limit: maxEntryCount);
}, },
); );
} }
Future<void> removeSearchEntry(String searchQuery) { Future<void> removeSearchEntry(String searchQuery) {
return _db.bangDao.removeSearchEntry(searchQuery); return ref
.read(bangDatabaseProvider)
.bangDao
.removeSearchEntry(searchQuery);
} }
Future<Result<BangData>> ensureIconAvailable(BangData bang) async { Future<Result<BangData>> ensureIconAvailable(BangData bang) async {
@@ -110,10 +132,13 @@ class BangDataRepository extends _$BangDataRepository {
} }
Future<int> resetFrequencies() { Future<int> resetFrequencies() {
return _db.bangFrequency.deleteAll(); return ref.read(bangDatabaseProvider).bangFrequency.deleteAll();
} }
Future<int> resetFrequency(String trigger) { Future<int> resetFrequency(String trigger) {
return _db.bangFrequency.deleteWhere((t) => t.trigger.equals(trigger)); return ref
.read(bangDatabaseProvider)
.bangFrequency
.deleteWhere((t) => t.trigger.equals(trigger));
} }
} }
@@ -7,7 +7,7 @@ part of 'data.dart';
// ************************************************************************** // **************************************************************************
String _$bangDataRepositoryHash() => String _$bangDataRepositoryHash() =>
r'b51786552a812ddd54d1418450fe03ff89351e28'; r'2c009519cf02df4d3cbb6768dbe55e9eb696e126';
/// See also [BangDataRepository]. /// See also [BangDataRepository].
@ProviderFor(BangDataRepository) @ProviderFor(BangDataRepository)
@@ -1,6 +1,5 @@
import 'dart:async'; 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/models/bang_data.dart';
import 'package:lensai/features/bangs/data/providers.dart'; import 'package:lensai/features/bangs/data/providers.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart';
@@ -11,17 +10,20 @@ part 'search.g.dart';
@Riverpod() @Riverpod()
class BangSearch extends _$BangSearch { class BangSearch extends _$BangSearch {
late StreamController<List<BangData>> _streamController; late StreamController<List<BangData>> _streamController;
late BangDatabase _db;
Future<void> search(String input) async { Future<void> search(String input) async {
if (input.isNotEmpty) { if (input.isNotEmpty) {
await _db.bangDao.queryBangs(input).get().then(_streamController.add); await ref
.read(bangDatabaseProvider)
.bangDao
.queryBangs(input)
.get()
.then(_streamController.add);
} }
} }
@override @override
Stream<List<BangData>> build() { Stream<List<BangData>> build() {
_db = ref.watch(bangDatabaseProvider);
_streamController = StreamController(); _streamController = StreamController();
ref.onDispose(() async { ref.onDispose(() async {
@@ -6,7 +6,7 @@ part of 'search.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$bangSearchHash() => r'abd71f10288ac1f559e66fc11035f9f2b027c353'; String _$bangSearchHash() => r'632ddd09a1198394884cb5c27a2bde7850c0515b';
/// See also [BangSearch]. /// See also [BangSearch].
@ProviderFor(BangSearch) @ProviderFor(BangSearch)
@@ -11,10 +11,6 @@ part 'sync.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class BangSyncRepository extends _$BangSyncRepository { class BangSyncRepository extends _$BangSyncRepository {
late BangDatabase _db;
BangSyncRepository();
static Future<Result<void>> _fetchAndSync({ static Future<Result<void>> _fetchAndSync({
required BangSourceService sourceService, required BangSourceService sourceService,
required BangDatabase db, required BangDatabase db,
@@ -51,22 +47,22 @@ class BangSyncRepository extends _$BangSyncRepository {
) async { ) async {
try { try {
return Result.success( return Result.success(
await _db.computeWithDatabase( await ref.read(bangDatabaseProvider).computeWithDatabase(
connect: BangDatabase.new, connect: BangDatabase.new,
computation: (db) async { computation: (db) async {
final ref = ProviderContainer(); final ref = ProviderContainer();
final result = await _fetchAndSync( final result = await _fetchAndSync(
sourceService: ref.read(bangSourceServiceProvider.notifier), sourceService: ref.read(bangSourceServiceProvider.notifier),
db: db, db: db,
url: Uri.parse(group.url), url: Uri.parse(group.url),
group: group, group: group,
syncInterval: syncInterval, syncInterval: syncInterval,
); );
//Throw if necessary //Throw if necessary
return result.value; return result.value;
}, },
), ),
); );
} catch (e) { } catch (e) {
return Result.failure( return Result.failure(
@@ -80,7 +76,11 @@ class BangSyncRepository extends _$BangSyncRepository {
} }
Stream<DateTime?> watchLastSyncOfGroup(BangGroup group) { Stream<DateTime?> watchLastSyncOfGroup(BangGroup group) {
return _db.syncDao.lastSyncOfGroup(group).watchSingleOrNull(); return ref
.read(bangDatabaseProvider)
.syncDao
.lastSyncOfGroup(group)
.watchSingleOrNull();
} }
Future<Map<BangGroup, Result<void>>> syncBangGroups({ Future<Map<BangGroup, Result<void>>> syncBangGroups({
@@ -100,7 +100,5 @@ class BangSyncRepository extends _$BangSyncRepository {
} }
@override @override
void build() { void build() {}
_db = ref.watch(bangDatabaseProvider);
}
} }
@@ -7,7 +7,7 @@ part of 'sync.dart';
// ************************************************************************** // **************************************************************************
String _$bangSyncRepositoryHash() => String _$bangSyncRepositoryHash() =>
r'86933b05d01629c67ed0443df91e991c40136423'; r'a46813f953e550585f432b11c364d87791819c34';
/// See also [BangSyncRepository]. /// See also [BangSyncRepository].
@ProviderFor(BangSyncRepository) @ProviderFor(BangSyncRepository)
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.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/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart'; import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart'; import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
@@ -21,9 +22,8 @@ class BangListScreen extends HookConsumerWidget {
final bangsAsync = ref.watch( final bangsAsync = ref.watch(
bangDataListProvider( bangDataListProvider(
filter: ( filter: (
categoryFilter: (category != null) categoryFilter: category.mapNotNull(
? (category: category!, subCategory: subCategory) (category) => (category: category, subCategory: subCategory)),
: null,
domain: null, domain: null,
groups: null, groups: null,
orderMostFrequentFirst: null, orderMostFrequentFirst: null,
@@ -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/domain/repositories/search.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_details.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/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/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
@@ -20,9 +20,7 @@ class BangSearchScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final resultsAsync = ref.watch(bangSearchProvider); final resultsAsync = ref.watch(bangSearchProvider);
final incognitoEnabled = ref.watch( final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final textEditingController = final textEditingController =
useTextEditingController(text: initialSearchText); useTextEditingController(text: initialSearchText);
@@ -1,5 +1,4 @@
import 'package:exceptions/exceptions.dart'; 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/models/chat_metadata.dart';
import 'package:lensai/features/chat/features/chat_store/data/providers.dart'; import 'package:lensai/features/chat/features/chat_store/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -8,16 +7,15 @@ part 'chat_metadata.g.dart';
@Riverpod() @Riverpod()
class ChatMetadataRepository extends _$ChatMetadataRepository { class ChatMetadataRepository extends _$ChatMetadataRepository {
late ChatDatabase _db;
Future<Result<void>> updateMetadata(ChatMetadata metadata) { Future<Result<void>> updateMetadata(ChatMetadata metadata) {
return Result.fromAsync(() { return Result.fromAsync(() {
return _db.metadataDao.updateChatMetadata(chatId, metadata); return ref
.read(chatDatabaseProvider)
.metadataDao
.updateChatMetadata(chatId, metadata);
}); });
} }
@override @override
void build(String chatId) { void build(String chatId) {}
_db = ref.watch(chatDatabaseProvider);
}
} }
@@ -7,7 +7,7 @@ part of 'chat_metadata.dart';
// ************************************************************************** // **************************************************************************
String _$chatMetadataRepositoryHash() => String _$chatMetadataRepositoryHash() =>
r'72a0226ed7e87f452f4dfe44594bb30663c2db45'; r'bd6b28b9d034ee82f95e974e4bf8f553546e835a';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -1,3 +1,5 @@
import 'package:lensai/extensions/nullable.dart';
class ChatEntity { class ChatEntity {
static final _namePattern = RegExp(r"^(.*?) - (.*?)\.md$"); static final _namePattern = RegExp(r"^(.*?) - (.*?)\.md$");
@@ -14,7 +16,7 @@ class ChatEntity {
return ChatEntity._( return ChatEntity._(
fileName, fileName,
name: match?.group(1), name: match?.group(1),
dateTime: (match != null) ? DateTime.tryParse(match.group(2)!) : null, dateTime: match.mapNotNull((match) => DateTime.tryParse(match.group(2)!)),
); );
} }
@@ -17,7 +17,6 @@ part 'search.g.dart';
class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository { class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
late Completer<void> _populatedCompleter; late Completer<void> _populatedCompleter;
late StreamController<List<ChatQueryResult>> _streamController; late StreamController<List<ChatQueryResult>> _streamController;
late ChatSearchDatabase _searchDatabase;
Future<ChatCompanion?> _readChat(ChatEntity chat) async { Future<ChatCompanion?> _readChat(ChatEntity chat) async {
final contentResult = await ref final contentResult = await ref
@@ -64,7 +63,9 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
}) async { }) async {
if (input.isNotEmpty) { if (input.isNotEmpty) {
await _populatedCompleter.future; await _populatedCompleter.future;
await _searchDatabase.searchDao await ref
.read(chatSearchDatabaseProvider)
.searchDao
.queryChats( .queryChats(
searchString: input, searchString: input,
snippetLength: snippetLength, snippetLength: snippetLength,
@@ -81,11 +82,12 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
Stream<List<ChatQueryResult>> build() async* { Stream<List<ChatQueryResult>> build() async* {
_populatedCompleter = Completer(); _populatedCompleter = Completer();
_streamController = StreamController(); _streamController = StreamController();
_searchDatabase = ref.watch(chatSearchDatabaseProvider);
final searchDatabase = ref.watch(chatSearchDatabaseProvider);
// populate with initial chats // populate with initial chats
await _searchDatabase.searchDao.deleteAllChats(); await searchDatabase.searchDao.deleteAllChats();
await _searchDatabase.searchDao.indexChats(await _availableChats()); await searchDatabase.searchDao.indexChats(await _availableChats());
_populatedCompleter.complete(); _populatedCompleter.complete();
final changeStreamSubscription = final changeStreamSubscription =
@@ -98,11 +100,11 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
if (chat.name != null) { if (chat.name != null) {
final companion = await _readChat(chat); final companion = await _readChat(chat);
if (companion != null) { if (companion != null) {
await _searchDatabase.searchDao.upsertChat(companion); await searchDatabase.searchDao.upsertChat(companion);
} }
} }
case ChangeType.REMOVE: case ChangeType.REMOVE:
await _searchDatabase.searchDao.deleteChat(chat.fileName); await searchDatabase.searchDao.deleteChat(chat.fileName);
} }
}); });
@@ -7,7 +7,7 @@ part of 'search.dart';
// ************************************************************************** // **************************************************************************
String _$chatArchiveSearchRepositoryHash() => String _$chatArchiveSearchRepositoryHash() =>
r'd7174cfb877c61ee4e9580f44d7ffd11bf3b4b60'; r'6762958d43d2efc95f5d9848bfcae41436564f2b';
/// See also [ChatArchiveSearchRepository]. /// See also [ChatArchiveSearchRepository].
@ProviderFor(ChatArchiveSearchRepository) @ProviderFor(ChatArchiveSearchRepository)
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/date_time.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/features/chat_archive/domain/repositories/archive.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:skeletonizer/skeletonizer.dart'; import 'package:skeletonizer/skeletonizer.dart';
@@ -41,9 +42,10 @@ class ChatArchiveListScreen extends HookConsumerWidget {
return ListTile( return ListTile(
title: Text(chat.toString()), title: Text(chat.toString()),
subtitle: (chat.dateTime != null) subtitle: chat.dateTime.mapNotNull(
? Text(chat.dateTime!.formatWithMinutePrecision()) (chatTime) =>
: null, Text(chatTime.formatWithMinutePrecision()),
),
onTap: () async { onTap: () async {
await context.push( await context.push(
ChatArchiveDetailRoute(fileName: chat.fileName) ChatArchiveDetailRoute(fileName: chat.fileName)
@@ -10,7 +10,7 @@ import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/date_time.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/entities/chat_entity.dart';
import 'package:lensai/features/chat_archive/domain/repositories/search.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/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
@@ -20,9 +20,7 @@ class ChatArchiveSearchScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final resultsAsync = ref.watch(chatArchiveSearchRepositoryProvider); final resultsAsync = ref.watch(chatArchiveSearchRepositoryProvider);
final incognitoEnabled = ref.watch( final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final textEditingController = useTextEditingController(); final textEditingController = useTextEditingController();
@@ -3,6 +3,7 @@ import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/data/models/web_page_info.dart'; import 'package:lensai/data/models/web_page_info.dart';
import 'package:lensai/domain/entities/equatable_image.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/browser_icon.dart';
import 'package:lensai/features/geckoview/domain/entities/find_result_state.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/history_state.dart';
@@ -24,13 +25,13 @@ class TabState extends WebPageInfo with FastEquatable {
final EquatableImage? icon; final EquatableImage? icon;
@override @override
BrowserIcon? get favicon => (icon != null) BrowserIcon? get favicon => icon.mapNotNull(
? BrowserIcon( (icon) => BrowserIcon(
image: icon!, image: icon,
dominantColor: null, dominantColor: null,
source: IconSource.memory, source: IconSource.memory,
) ),
: null; );
final EquatableImage? thumbnail; final EquatableImage? thumbnail;
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/image.dart'; import 'package:lensai/extensions/image.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/find_result_state.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/history_state.dart';
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart'; import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
@@ -39,10 +40,10 @@ class TabStates extends _$TabStates {
Future<void> _onIconChange(IconChangeEvent event) async { Future<void> _onIconChange(IconChangeEvent event) async {
final IconChangeEvent(:tabId, :bytes) = event; final IconChangeEvent(:tabId, :bytes) = event;
final image = (bytes != null) final image = await bytes.mapNotNull(
? (await tryDecodeImage(bytes) (bytes) =>
.then((image) async => image?.toEquatable())) tryDecodeImage(bytes).then((image) async => image?.toEquatable()),
: null; );
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.icon(image); state = {...state}..[tabId] = current.copyWith.icon(image);
@@ -51,10 +52,10 @@ class TabStates extends _$TabStates {
Future<void> _onThumbnailChange(ThumbnailEvent event) async { Future<void> _onThumbnailChange(ThumbnailEvent event) async {
final ThumbnailEvent(:tabId, :bytes) = event; final ThumbnailEvent(:tabId, :bytes) = event;
final image = (bytes != null) final image = await bytes.mapNotNull(
? (await tryDecodeImage(bytes) (bytes) =>
.then((image) async => image?.toEquatable())) tryDecodeImage(bytes).then((image) async => image?.toEquatable()),
: null; );
final current = state[tabId] ?? TabState.$default(tabId); final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.thumbnail(image); state = {...state}..[tabId] = current.copyWith.thumbnail(image);
@@ -173,7 +173,7 @@ final selectedTabStateProvider = AutoDisposeProvider<TabState?>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>; typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
String _$tabStatesHash() => r'77f07f330dfac8dc8d28d18737203c7c122f3b74'; String _$tabStatesHash() => r'296d85ec016b22236b5830fce6b01cda81fbc02d';
/// See also [TabStates]. /// See also [TabStates].
@ProviderFor(TabStates) @ProviderFor(TabStates)
@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/domain/entities/equatable_image.dart'; import 'package:lensai/domain/entities/equatable_image.dart';
import 'package:lensai/extensions/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/entities/web_extension_state.dart';
import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/utils/image_helper.dart'; import 'package:lensai/features/geckoview/utils/image_helper.dart';
@@ -33,12 +34,10 @@ class WebExtensionsState extends _$WebExtensionsState {
title: data.title, title: data.title,
enabled: data.enabled ?? current.enabled, enabled: data.enabled ?? current.enabled,
badgeText: data.badgeText, badgeText: data.badgeText,
badgeTextColor: (data.badgeTextColor != null) badgeTextColor:
? Color(data.badgeTextColor!) data.badgeTextColor.mapNotNull((color) => Color(color)),
: null, badgeBackgroundColor:
badgeBackgroundColor: (data.badgeBackgroundColor != null) data.badgeBackgroundColor.mapNotNull((color) => Color(color)),
? Color(data.badgeBackgroundColor!)
: null,
); );
} else { } else {
if (state.containsKey(extensionId)) { if (state.containsKey(extensionId)) {
@@ -7,7 +7,7 @@ part of 'web_extensions_state.dart';
// ************************************************************************** // **************************************************************************
String _$webExtensionsStateHash() => String _$webExtensionsStateHash() =>
r'bbaba8d27980046275858d2707bc66c6e349a537'; r'4e6dc46e49ff353e1c3cc4616008685c0e21501d';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -2,14 +2,15 @@ import 'dart:async';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers.dart'; import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart'; import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.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/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/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.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:lensai/utils/debouncer.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -19,31 +20,59 @@ part 'tab.g.dart';
class TabRepository extends _$TabRepository { class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService(); final _tabsService = GeckoTabService();
late TabDatabase _db;
Future<String> addTab({ Future<String> addTab({
Uri? url, Uri? url,
bool selectTab = true, bool selectTab = true,
bool startLoading = true, bool startLoading = true,
String? parentId, String? parentId,
LoadUrlFlags flags = LoadUrlFlags.NONE, LoadUrlFlags flags = LoadUrlFlags.NONE,
String? contextId,
Source source = Internal.newTab, Source source = Internal.newTab,
bool private = false, bool private = false,
HistoryMetadataKey? historyMetadata, HistoryMetadataKey? historyMetadata,
Map<String, String>? additionalHeaders, Map<String, String>? additionalHeaders,
}) { }) async {
return _tabsService.addTab( final selectedContainer =
url: url, await ref.read(selectedContainerProvider.notifier).fetchData();
selectTab: selectTab,
startLoading: startLoading, return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional(
parentId: parentId, () {
flags: flags, return _tabsService.addTab(
contextId: contextId, url: url,
source: source, selectTab: selectTab,
private: private, startLoading: startLoading,
historyMetadata: historyMetadata, parentId: parentId,
additionalHeaders: additionalHeaders, flags: flags,
contextId: selectedContainer?.metadata.contextualIdentity,
source: source,
private: private,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
);
},
containerId: Value(selectedContainer?.id),
);
}
Future<String> 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)); final tabStateDebouncer = Debouncer(const Duration(seconds: 3));
Map<String, TabState>? debounceStartValue; Map<String, TabState>? debounceStartValue;
_db = ref.watch(tabDatabaseProvider); final db = ref.watch(tabDatabaseProvider);
final tabAddedSub = eventSerivce.tabAddedStream.listen( final tabAddedSub = eventSerivce.tabAddedStream.listen(
(tabId) async { (tabId) async {
final containerId = ref.read(selectedContainerProvider); final containerId = ref.read(selectedContainerProvider);
await _db.tabDao.upsertTab(tabId, containerId: Value(containerId)); await db.tabDao
.upsertUnassignedTab(tabId, containerId: Value(containerId));
}, },
); );
final tabContentSub = final tabContentSub =
tabContentService.tabContentStream.listen((content) async { tabContentService.tabContentStream.listen((content) async {
await _db.tabDao.updateTabContent( await db.tabDao.updateTabContent(
content.tabId, content.tabId,
isProbablyReaderable: content.isProbablyReaderable, isProbablyReaderable: content.isProbablyReaderable,
extractedContentMarkdown: content.extractedContentMarkdown, extractedContentMarkdown: content.extractedContentMarkdown,
@@ -92,7 +122,7 @@ class TabRepository extends _$TabRepository {
selectedTabProvider, selectedTabProvider,
(previous, tabId) async { (previous, tabId) async {
if (tabId != null) { 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); final syncTabs = next.isNotEmpty || (previous?.isNotEmpty ?? false);
if (syncTabs) { 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 { tabStateDebouncer.eventOccured(() async {
await _db.tabDao.updateTabs(debounceStartValue, next); await db.tabDao.updateTabs(debounceStartValue, next);
}); });
}, },
); );
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabRepositoryHash() => r'b9caf89fe372b79f37c4a3ebff800ccc2d9289d2'; String _$tabRepositoryHash() => r'85af29fa18ea5a4a67fbffbb9478c801dafddac2';
/// See also [TabRepository]. /// See also [TabRepository].
@ProviderFor(TabRepository) @ProviderFor(TabRepository)
@@ -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<void> deleteData(Set<DeleteBrowsingDataType>? 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() {}
}
@@ -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<DeleteBrowserDataService, void>.internal(
DeleteBrowserDataService.new,
name: r'deleteBrowserDataServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$deleteBrowserDataServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DeleteBrowserDataService = AutoDisposeNotifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -1,10 +1,90 @@
import 'package:flutter/material.dart' show ThemeMode;
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart'; 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'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'engine_settings.g.dart'; part 'engine_settings.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
GeckoEngineSettingsService engineSettingsService(Ref ref) { class EngineSettingsReplicationService
return GeckoEngineSettingsService(); 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;
}
},
);
}
} }
@@ -6,24 +6,22 @@ part of 'engine_settings.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$engineSettingsServiceHash() => String _$engineSettingsReplicationServiceHash() =>
r'051d7b6f65eb5dcb07405b4af1bdadd8dd74fdc0'; r'0729260eeb34b58470622ac9a3a6abb257730a61';
/// See also [engineSettingsService]. /// See also [EngineSettingsReplicationService].
@ProviderFor(engineSettingsService) @ProviderFor(EngineSettingsReplicationService)
final engineSettingsServiceProvider = final engineSettingsReplicationServiceProvider =
Provider<GeckoEngineSettingsService>.internal( NotifierProvider<EngineSettingsReplicationService, void>.internal(
engineSettingsService, EngineSettingsReplicationService.new,
name: r'engineSettingsServiceProvider', name: r'engineSettingsReplicationServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$engineSettingsServiceHash, : _$engineSettingsReplicationServiceHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead') typedef _$EngineSettingsReplicationService = Notifier<void>;
// ignore: unused_element
typedef EngineSettingsServiceRef = ProviderRef<GeckoEngineSettingsService>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // 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
@@ -1,103 +1,104 @@
import 'package:collection/collection.dart'; // import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; // import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; // import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart'; // import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart'; // import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; // import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart'; // import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.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 { // class TabActionDialog extends HookConsumerWidget {
final TabState initialTab; // final TabState initialTab;
final void Function()? onDismiss; // final void Function()? onDismiss;
const TabActionDialog({ // const TabActionDialog({
required this.initialTab, // required this.initialTab,
this.onDismiss, // this.onDismiss,
super.key, // super.key,
}); // });
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab; // final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
final tabContainerId = ref.watch( // final tabContainerId = ref.watch(
tabContainerIdProvider(initialTab.id) // tabContainerIdProvider(initialTab.id)
.select((value) => value.valueOrNull), // .select((value) => value.valueOrNull),
); // );
final containers = ref // final containers = ref
.watch( // .watch(
containersWithCountProvider.select( // containersWithCountProvider.select(
(value) => EquatableCollection(value.valueOrNull, immutable: true), // (value) => EquatableCollection(value.valueOrNull, immutable: true),
), // ),
) // )
.collection; // .collection;
final selectedContainer = containers // final selectedContainer = containers
?.firstWhereOrNull((container) => container.id == tabContainerId); // ?.firstWhereOrNull((container) => container.id == tabContainerId);
final expansionController = useExpansionTileController(); // final expansionController = useExpansionTileController();
return Stack( // return Stack(
children: [ // children: [
ModalBarrier( // ModalBarrier(
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54, // color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
onDismiss: onDismiss, // onDismiss: onDismiss,
), // ),
SimpleDialog( // SimpleDialog(
titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0), // titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
contentPadding: EdgeInsets.zero, // contentPadding: EdgeInsets.zero,
insetPadding: const EdgeInsets.symmetric( // insetPadding: const EdgeInsets.symmetric(
horizontal: 20.0, // horizontal: 20.0,
vertical: 24.0, // vertical: 24.0,
), // ),
title: ListTile( // title: ListTile(
leading: RawImage( // leading: RawImage(
image: tabState.icon?.value, // image: tabState.icon?.value,
width: 24, // width: 24,
height: 24, // height: 24,
), // ),
contentPadding: EdgeInsets.zero, // contentPadding: EdgeInsets.zero,
title: Text(tabState.title), // title: Text(tabState.title),
subtitle: Text(tabState.url.authority), // subtitle: Text(tabState.url.authority),
), // ),
children: [ // children: [
SizedBox( // SizedBox(
//We need this to stretch the dialog, then padding from dialog is applied // //We need this to stretch the dialog, then padding from dialog is applied
width: double.maxFinite, // width: double.maxFinite,
child: ExpansionTile( // child: ExpansionTile(
controller: expansionController, // controller: expansionController,
leading: (selectedContainer != null) // leading: selectedContainer?.color.mapNotNull(
? CircleAvatar(backgroundColor: selectedContainer.color) // (color) => CircleAvatar(backgroundColor: color),
: null, // ),
title: (selectedContainer != null) // title: (selectedContainer != null)
? Text(selectedContainer.name ?? 'New Container') // ? Text(selectedContainer.name ?? 'New Container')
: const Text('Assign a Container'), // : const Text('Assign a Container'),
children: containers // children: containers
?.where((container) => container.id != tabContainerId) // ?.where((container) => container.id != tabContainerId)
.map( // .map(
(container) => ListTile( // (container) => ListTile(
leading: // leading:
CircleAvatar(backgroundColor: container.color), // CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'), // title: Text(container.name ?? 'New Container'),
onTap: () async { // onTap: () async {
await ref // await ref
.read(tabDataRepositoryProvider.notifier) // .read(tabDataRepositoryProvider.notifier)
.assignContainer(initialTab.id, container.id); // .assignContainer(initialTab.id, container.id);
expansionController.collapse(); // expansionController.collapse();
}, // },
), // ),
) // )
.toList() ?? // .toList() ??
[], // [],
), // ),
), // ),
], // ],
), // ),
], // ],
); // );
} // }
} // }
@@ -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/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; 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/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/failure_widget.dart';
import 'package:lensai/presentation/widgets/website_title_tile.dart'; import 'package:lensai/presentation/widgets/website_title_tile.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper; import 'package:lensai/utils/ui_helper.dart' as ui_helper;
@@ -40,9 +40,7 @@ class WebPageDialog extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch( final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final availableBangsAsync = ref.watch( final availableBangsAsync = ref.watch(
bangDataListProvider( bangDataListProvider(
@@ -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/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.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/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/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/browser_view.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.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/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/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart';
import 'package:lensai/features/kagi/data/entities/modes.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/draggable_scrollable_controller.dart';
import 'package:lensai/presentation/hooks/menu_controller.dart'; import 'package:lensai/presentation/hooks/menu_controller.dart';
import 'package:lensai/presentation/hooks/overlay_portal_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( // ref.listen(
// settingsRepositoryProvider // generalSettingsRepositoryProvider
// .select((value) => value.valueOrNull?.kagiSession), // .select((value) => value.valueOrNull?.kagiSession),
// (previous, next) async { // (previous, next) async {
// if (next != null && next.isNotEmpty) { // if (next != null && next.isNotEmpty) {
@@ -598,7 +589,7 @@ class BrowserScreen extends HookConsumerWidget {
MenuItemButton( MenuItemButton(
onPressed: () async { onPressed: () async {
await context await context
.push(BrowserHardeningRoute().location); .push(WebEngineHardeningRoute().location);
}, },
leadingIcon: const Icon(Icons.info), leadingIcon: const Icon(Icons.info),
child: const Text('Pref'), child: const Text('Pref'),
@@ -648,7 +639,8 @@ class BrowserScreen extends HookConsumerWidget {
: null; : null;
final tabCount = ref.read( final tabCount = ref.read(
tabListProvider.select((tabs) => tabs.length)); tabListProvider.select((tabs) => tabs.length),
);
//Don't do anything if a child route is active //Don't do anything if a child route is active
if (GoRouterState.of(context).topRoute?.path != if (GoRouterState.of(context).topRoute?.path !=
@@ -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/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/web_extensions_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/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/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/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 { class BrowserView extends StatefulHookConsumerWidget {
final Duration screenshotPeriod; final Duration screenshotPeriod;
final FutureOr<void> Function()? postInitializationStep;
const BrowserView({this.screenshotPeriod = const Duration(seconds: 10)}); const BrowserView({
this.screenshotPeriod = const Duration(seconds: 10),
this.postInitializationStep,
});
@override @override
ConsumerState<ConsumerStatefulWidget> createState() => _BrowserViewState(); ConsumerState<ConsumerStatefulWidget> createState() => _BrowserViewState();
@@ -63,6 +71,15 @@ class _BrowserViewState extends ConsumerState<BrowserView>
return GeckoView( return GeckoView(
preInitializationStep: () async { preInitializationStep: () async {
await ref
.read(generalSettingsRepositoryProvider.notifier)
.fetch()
.then((settings) {
ref
.read(deleteBrowserDataServiceProvider.notifier)
.deleteData(settings.deleteBrowsingDataOnQuit);
});
await ref await ref
.read(eventServiceProvider) .read(eventServiceProvider)
.viewReadyStateEvents .viewReadyStateEvents
@@ -77,6 +94,9 @@ class _BrowserViewState extends ConsumerState<BrowserView>
}, },
); );
}, },
postInitializationStep: () async {
await widget.postInitializationStep?.call();
},
); );
} }
@@ -109,6 +129,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
cacheRepositoryProvider, cacheRepositoryProvider,
(previous, next) {}, (previous, next) {},
); );
ref.listenManual(
engineSettingsReplicationServiceProvider,
(previous, next) {},
);
} }
@override @override
@@ -124,6 +149,10 @@ class _BrowserViewState extends ConsumerState<BrowserView>
_periodicScreenshotUpdate?.cancel(); _periodicScreenshotUpdate?.cancel();
_timerPaused = true; _timerPaused = true;
} }
ref
.read(localAuthenticationServiceProvider.notifier)
.evictCacheOnBackground();
case AppLifecycleState.resumed: case AppLifecycleState.resumed:
if (_timerPaused) { if (_timerPaused) {
_periodicScreenshotUpdate = _periodicScreenshotUpdate =
@@ -13,7 +13,7 @@ class LandingAction extends HookConsumerWidget {
final sessionTokenAvailable = false; final sessionTokenAvailable = false;
// ref.watch( // ref.watch(
// settingsRepositoryProvider.select( // generalSettingsRepositoryProvider.select(
// (value) => value.valueOrNull?.kagiSession?.isNotEmpty ?? false, // (value) => value.valueOrNull?.kagiSession?.isNotEmpty ?? false,
// ), // ),
// ); // );
@@ -2,6 +2,7 @@ import 'package:expandable_page_view/expandable_page_view.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.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/data/entities/modes.dart';
import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart'; import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart';
@@ -24,9 +25,8 @@ class CreateTabSheetWidget extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final sharedContent = useMemoized( final sharedContent = useMemoized(
() => (parameter.content != null) () => parameter.content
? SharedContent.parse(parameter.content!) .mapNotNull((content) => SharedContent.parse(content)),
: null,
[parameter], [parameter],
); );
@@ -56,15 +56,15 @@ class _TabDraggable extends HookConsumerWidget {
onClose(); onClose();
} }
}, },
onDoubleTap: () { // onDoubleTap: () {
ref.read(overlayDialogControllerProvider.notifier).show( // ref.read(overlayDialogControllerProvider.notifier).show(
TabActionDialog( // TabActionDialog(
initialTab: tab, // initialTab: tab,
onDismiss: // onDismiss:
ref.read(overlayDialogControllerProvider.notifier).dismiss, // ref.read(overlayDialogControllerProvider.notifier).dismiss,
), // ),
); // );
}, // },
onDelete: () async { onDelete: () async {
await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id); await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id);
}, },
@@ -178,8 +178,8 @@ class _Tab extends HookConsumerWidget {
return ContainerChips( return ContainerChips(
selectedContainer: selectedContainer, selectedContainer: selectedContainer,
onSelected: (container) { onSelected: (container) async {
ref await ref
.read(selectedContainerProvider.notifier) .read(selectedContainerProvider.notifier)
.setContainerId(container.id); .setContainerId(container.id);
}, },
@@ -40,7 +40,7 @@ class PreferenceSetting with FastEquatable {
bool get isActive => value == actualValue; bool get isActive => value == actualValue;
final String title; final String? title;
final String? description; final String? description;
final bool requireUserOptIn; final bool requireUserOptIn;
@@ -73,7 +73,7 @@ extension $PreferenceSettingGroupCopyWith on PreferenceSettingGroup {
abstract class _$PreferenceSettingCWProxy { abstract class _$PreferenceSettingCWProxy {
PreferenceSetting value(Object value); PreferenceSetting value(Object value);
PreferenceSetting title(String title); PreferenceSetting title(String? title);
PreferenceSetting description(String? description); PreferenceSetting description(String? description);
@@ -91,7 +91,7 @@ abstract class _$PreferenceSettingCWProxy {
/// ```` /// ````
PreferenceSetting call({ PreferenceSetting call({
Object value, Object value,
String title, String? title,
String? description, String? description,
Object? actualValue, Object? actualValue,
bool requireUserOptIn, bool requireUserOptIn,
@@ -109,7 +109,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy {
PreferenceSetting value(Object value) => this(value: value); PreferenceSetting value(Object value) => this(value: value);
@override @override
PreferenceSetting title(String title) => this(title: title); PreferenceSetting title(String? title) => this(title: title);
@override @override
PreferenceSetting description(String? description) => PreferenceSetting description(String? description) =>
@@ -151,7 +151,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy {
title: title == const $CopyWithPlaceholder() title: title == const $CopyWithPlaceholder()
? _value.title ? _value.title
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: title as String, : title as String?,
description: description == const $CopyWithPlaceholder() description: description == const $CopyWithPlaceholder()
? _value.description ? _value.description
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
@@ -186,7 +186,7 @@ extension $PreferenceSettingCopyWith on PreferenceSetting {
PreferenceSetting _$PreferenceSettingFromJson(Map<String, dynamic> json) => PreferenceSetting _$PreferenceSettingFromJson(Map<String, dynamic> json) =>
PreferenceSetting( PreferenceSetting(
value: json['value'] as Object, value: json['value'] as Object,
title: json['title'] as String, title: json['title'] as String?,
description: json['description'] as String?, description: json['description'] as String?,
requireUserOptIn: json['requireUserOptIn'] as bool? ?? false, requireUserOptIn: json['requireUserOptIn'] as bool? ?? false,
shouldBeDefault: json['shouldBeDefault'] as bool? ?? false, shouldBeDefault: json['shouldBeDefault'] as bool? ?? false,
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.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/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/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
@@ -12,40 +13,29 @@ import 'package:rxdart/rxdart.dart';
part 'preference_settings.g.dart'; part 'preference_settings.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
Future<Map<String, PreferenceSettingGroup>> _preferenceSettingGroups( Future<Map<String, dynamic>> _preferenceSettingContent(Ref ref) async {
Ref ref, return await rootBundle
) async {
final content = await rootBundle
.loadString('assets/preferences/settings.json') .loadString('assets/preferences/settings.json')
.then(jsonDecode) as Map<String, dynamic>; .then(jsonDecode) as Map<String, dynamic>;
}
final userContent = content['user'] as Map<String, dynamic>; @Riverpod(keepAlive: true)
Future<Map<String, PreferenceSettingGroup>> _preferenceSettingGroups(
return userContent.map( Ref ref,
(key, value) => MapEntry( PreferencePartition partition,
key, ) async {
PreferenceSettingGroup( final content = await ref.read(_preferenceSettingContentProvider.future);
// ignore: avoid_dynamic_calls return deserializePreferenceSettingGroups(partition, content);
description: value['description'] as String?,
// ignore: avoid_dynamic_calls
settings: (value['preferences'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
PreferenceSetting.fromJson(value as Map<String, dynamic>),
),
),
),
),
);
} }
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
Future<PreferenceSettingGroup> _preferenceSettingGroup( Future<PreferenceSettingGroup> _preferenceSettingGroup(
Ref ref, Ref ref,
PreferencePartition partition,
String groupName, String groupName,
) { ) {
return ref.watch( return ref.watch(
_preferenceSettingGroupsProvider.selectAsync( _preferenceSettingGroupsProvider(partition).selectAsync(
(groups) => (groups) =>
groups[groupName] ?? (throw Exception('Unknown setting group')), groups[groupName] ?? (throw Exception('Unknown setting group')),
), ),
@@ -90,13 +80,16 @@ class _PreferenceRepository extends _$PreferenceRepository {
} }
@Riverpod() @Riverpod()
class PreferenceSettingsGeneralRepository class UnifiedPreferenceSettingsRepository
extends _$PreferenceSettingsGeneralRepository { extends _$UnifiedPreferenceSettingsRepository {
late Map<String, PreferenceSettingGroup> _statelessGroups; Map<String, PreferenceSettingGroup>? _statelessGroups;
Future<void> apply() async { Future<void> apply() async {
_statelessGroups =
await ref.read(_preferenceSettingGroupsProvider(partition).future);
final prefs = { final prefs = {
for (final group in _statelessGroups.values) for (final group in _statelessGroups!.values)
...Map.fromEntries( ...Map.fromEntries(
group.settings.entries group.settings.entries
.where((e) => !e.value.requireUserOptIn) .where((e) => !e.value.requireUserOptIn)
@@ -108,7 +101,10 @@ class PreferenceSettingsGeneralRepository
} }
Future<void> reset() async { Future<void> reset() async {
final prefs = _statelessGroups.values _statelessGroups =
await ref.read(_preferenceSettingGroupsProvider(partition).future);
final prefs = _statelessGroups!.values
.map((group) => group.settings.keys) .map((group) => group.settings.keys)
.flattened .flattened
.toList(); .toList();
@@ -117,13 +113,16 @@ class PreferenceSettingsGeneralRepository
} }
@override @override
Stream<Map<String, PreferenceSettingGroup>> build() async* { Stream<Map<String, PreferenceSettingGroup>> build(
_statelessGroups = await ref.watch(_preferenceSettingGroupsProvider.future); PreferencePartition partition,
) async* {
_statelessGroups =
await ref.watch(_preferenceSettingGroupsProvider(partition).future);
final prefStream = ref.watch(_preferenceRepositoryProvider); final prefStream = ref.watch(_preferenceRepositoryProvider);
yield* prefStream.map( yield* prefStream.map(
(prefs) => _statelessGroups.map( (prefs) => _statelessGroups!.map(
(groupName, group) => MapEntry( (groupName, group) => MapEntry(
groupName, groupName,
group.copyWith.settings( group.copyWith.settings(
@@ -143,18 +142,21 @@ class PreferenceSettingsGeneralRepository
@Riverpod() @Riverpod()
class PreferenceSettingsGroupRepository class PreferenceSettingsGroupRepository
extends _$PreferenceSettingsGroupRepository { extends _$PreferenceSettingsGroupRepository {
late PreferenceSettingGroup _statelessSettingGroup; PreferenceSettingGroup? _statelessSettingGroup;
Future<void> apply({List<String>? filter}) async { Future<void> apply({List<String>? filter}) async {
_statelessSettingGroup ??= await ref
.read(_preferenceSettingGroupProvider(partition, groupName).future);
final prefs = Map.fromEntries( final prefs = Map.fromEntries(
filter?.map( filter?.map(
(e) => MapEntry( (e) => MapEntry(
e, e,
_statelessSettingGroup.settings[e]?.value ?? _statelessSettingGroup!.settings[e]?.value ??
(throw Exception('Preference not part of group')), (throw Exception('Preference not part of group')),
), ),
) ?? ) ??
_statelessSettingGroup.settings.entries _statelessSettingGroup!.settings.entries
.where((e) => !e.value.requireUserOptIn) .where((e) => !e.value.requireUserOptIn)
.map((e) => MapEntry(e.key, e.value.value)), .map((e) => MapEntry(e.key, e.value.value)),
); );
@@ -163,28 +165,34 @@ class PreferenceSettingsGroupRepository
} }
Future<void> reset({List<String>? filter}) async { Future<void> reset({List<String>? filter}) async {
_statelessSettingGroup ??= await ref
.read(_preferenceSettingGroupProvider(partition, groupName).future);
if (filter != null && if (filter != null &&
filter.any( filter.any(
(value) => !_statelessSettingGroup.settings.containsKey(value), (value) => !_statelessSettingGroup!.settings.containsKey(value),
)) { )) {
throw Exception('Preference not part of group'); throw Exception('Preference not part of group');
} }
await ref await ref
.read(_preferenceRepositoryProvider.notifier) .read(_preferenceRepositoryProvider.notifier)
.resetPrefs(filter ?? _statelessSettingGroup.settings.keys.toList()); .resetPrefs(filter ?? _statelessSettingGroup!.settings.keys.toList());
} }
@override @override
Stream<PreferenceSettingGroup> build(String groupName) async* { Stream<PreferenceSettingGroup> build(
_statelessSettingGroup = PreferencePartition partition,
await ref.watch(_preferenceSettingGroupProvider(groupName).future); String groupName,
) async* {
_statelessSettingGroup = await ref
.watch(_preferenceSettingGroupProvider(partition, groupName).future);
final prefStream = ref.watch(_preferenceRepositoryProvider); final prefStream = ref.watch(_preferenceRepositoryProvider);
yield* prefStream.map( yield* prefStream.map(
(prefs) => _statelessSettingGroup.copyWith.settings( (prefs) => _statelessSettingGroup!.copyWith.settings(
_statelessSettingGroup.settings.map( _statelessSettingGroup!.settings.map(
(key, value) => MapEntry(key, value.copyWith.actualValue(prefs[key])), (key, value) => MapEntry(key, value.copyWith.actualValue(prefs[key])),
), ),
), ),
@@ -6,28 +6,27 @@ part of 'preference_settings.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$preferenceSettingGroupsHash() => String _$preferenceSettingContentHash() =>
r'fa9286248cc71c88c888a518441721dc99b7e0c2'; r'a8a61fdf8800cb7adb365c100409ddce09c574b1';
/// See also [_preferenceSettingGroups]. /// See also [_preferenceSettingContent].
@ProviderFor(_preferenceSettingGroups) @ProviderFor(_preferenceSettingContent)
final _preferenceSettingGroupsProvider = final _preferenceSettingContentProvider =
FutureProvider<Map<String, PreferenceSettingGroup>>.internal( FutureProvider<Map<String, dynamic>>.internal(
_preferenceSettingGroups, _preferenceSettingContent,
name: r'_preferenceSettingGroupsProvider', name: r'_preferenceSettingContentProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$preferenceSettingGroupsHash, : _$preferenceSettingContentHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef _PreferenceSettingGroupsRef typedef _PreferenceSettingContentRef = FutureProviderRef<Map<String, dynamic>>;
= FutureProviderRef<Map<String, PreferenceSettingGroup>>; String _$preferenceSettingGroupsHash() =>
String _$preferenceSettingGroupHash() => r'd267dacf82a11568c9cf0cc864f434db35f93394';
r'960169ab9f6985a5b73a63663e671a8eac7cd776';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@@ -50,6 +49,145 @@ class _SystemHash {
} }
} }
/// See also [_preferenceSettingGroups].
@ProviderFor(_preferenceSettingGroups)
const _preferenceSettingGroupsProvider = _PreferenceSettingGroupsFamily();
/// See also [_preferenceSettingGroups].
class _PreferenceSettingGroupsFamily
extends Family<AsyncValue<Map<String, PreferenceSettingGroup>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'_preferenceSettingGroupsProvider';
}
/// See also [_preferenceSettingGroups].
class _PreferenceSettingGroupsProvider
extends FutureProvider<Map<String, PreferenceSettingGroup>> {
/// 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<Map<String, PreferenceSettingGroup>> 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<Map<String, PreferenceSettingGroup>> 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<Map<String, PreferenceSettingGroup>> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
}
class _PreferenceSettingGroupsProviderElement
extends FutureProviderElement<Map<String, PreferenceSettingGroup>>
with _PreferenceSettingGroupsRef {
_PreferenceSettingGroupsProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as _PreferenceSettingGroupsProvider).partition;
}
String _$preferenceSettingGroupHash() =>
r'64d80a37928d7e5aea7d3d567dde8ade7292cd5f';
/// See also [_preferenceSettingGroup]. /// See also [_preferenceSettingGroup].
@ProviderFor(_preferenceSettingGroup) @ProviderFor(_preferenceSettingGroup)
const _preferenceSettingGroupProvider = _PreferenceSettingGroupFamily(); const _preferenceSettingGroupProvider = _PreferenceSettingGroupFamily();
@@ -62,9 +200,11 @@ class _PreferenceSettingGroupFamily
/// See also [_preferenceSettingGroup]. /// See also [_preferenceSettingGroup].
_PreferenceSettingGroupProvider call( _PreferenceSettingGroupProvider call(
PreferencePartition partition,
String groupName, String groupName,
) { ) {
return _PreferenceSettingGroupProvider( return _PreferenceSettingGroupProvider(
partition,
groupName, groupName,
); );
} }
@@ -74,6 +214,7 @@ class _PreferenceSettingGroupFamily
covariant _PreferenceSettingGroupProvider provider, covariant _PreferenceSettingGroupProvider provider,
) { ) {
return call( return call(
provider.partition,
provider.groupName, provider.groupName,
); );
} }
@@ -98,10 +239,12 @@ class _PreferenceSettingGroupProvider
extends FutureProvider<PreferenceSettingGroup> { extends FutureProvider<PreferenceSettingGroup> {
/// See also [_preferenceSettingGroup]. /// See also [_preferenceSettingGroup].
_PreferenceSettingGroupProvider( _PreferenceSettingGroupProvider(
PreferencePartition partition,
String groupName, String groupName,
) : this._internal( ) : this._internal(
(ref) => _preferenceSettingGroup( (ref) => _preferenceSettingGroup(
ref as _PreferenceSettingGroupRef, ref as _PreferenceSettingGroupRef,
partition,
groupName, groupName,
), ),
from: _preferenceSettingGroupProvider, from: _preferenceSettingGroupProvider,
@@ -113,6 +256,7 @@ class _PreferenceSettingGroupProvider
dependencies: _PreferenceSettingGroupFamily._dependencies, dependencies: _PreferenceSettingGroupFamily._dependencies,
allTransitiveDependencies: allTransitiveDependencies:
_PreferenceSettingGroupFamily._allTransitiveDependencies, _PreferenceSettingGroupFamily._allTransitiveDependencies,
partition: partition,
groupName: groupName, groupName: groupName,
); );
@@ -123,9 +267,11 @@ class _PreferenceSettingGroupProvider
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.partition,
required this.groupName, required this.groupName,
}) : super.internal(); }) : super.internal();
final PreferencePartition partition;
final String groupName; final String groupName;
@override @override
@@ -143,6 +289,7 @@ class _PreferenceSettingGroupProvider
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
partition: partition,
groupName: groupName, groupName: groupName,
), ),
); );
@@ -156,12 +303,14 @@ class _PreferenceSettingGroupProvider
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is _PreferenceSettingGroupProvider && return other is _PreferenceSettingGroupProvider &&
other.partition == partition &&
other.groupName == groupName; other.groupName == groupName;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, partition.hashCode);
hash = _SystemHash.combine(hash, groupName.hashCode); hash = _SystemHash.combine(hash, groupName.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
@@ -171,6 +320,9 @@ class _PreferenceSettingGroupProvider
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin _PreferenceSettingGroupRef on FutureProviderRef<PreferenceSettingGroup> { mixin _PreferenceSettingGroupRef on FutureProviderRef<PreferenceSettingGroup> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
/// The parameter `groupName` of this provider. /// The parameter `groupName` of this provider.
String get groupName; String get groupName;
} }
@@ -180,6 +332,9 @@ class _PreferenceSettingGroupProviderElement
with _PreferenceSettingGroupRef { with _PreferenceSettingGroupRef {
_PreferenceSettingGroupProviderElement(super.provider); _PreferenceSettingGroupProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as _PreferenceSettingGroupProvider).partition;
@override @override
String get groupName => (origin as _PreferenceSettingGroupProvider).groupName; String get groupName => (origin as _PreferenceSettingGroupProvider).groupName;
} }
@@ -202,33 +357,174 @@ final _preferenceRepositoryProvider = AutoDisposeNotifierProvider<
typedef _$PreferenceRepository typedef _$PreferenceRepository
= AutoDisposeNotifier<Raw<Stream<Map<String, Object>>>>; = AutoDisposeNotifier<Raw<Stream<Map<String, Object>>>>;
String _$preferenceSettingsGeneralRepositoryHash() => String _$unifiedPreferenceSettingsRepositoryHash() =>
r'24a70de8284107bb43778e1014abe41bd805987a'; r'35aa16ffa7aa1453c8331ed7470ef85a68d27178';
/// See also [PreferenceSettingsGeneralRepository]. abstract class _$UnifiedPreferenceSettingsRepository
@ProviderFor(PreferenceSettingsGeneralRepository) extends BuildlessAutoDisposeStreamNotifier<
final preferenceSettingsGeneralRepositoryProvider = Map<String, PreferenceSettingGroup>> {
AutoDisposeStreamNotifierProvider<PreferenceSettingsGeneralRepository, late final PreferencePartition partition;
Map<String, PreferenceSettingGroup>>.internal(
PreferenceSettingsGeneralRepository.new, Stream<Map<String, PreferenceSettingGroup>> build(
name: r'preferenceSettingsGeneralRepositoryProvider', PreferencePartition partition,
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') );
? null }
: _$preferenceSettingsGeneralRepositoryHash,
dependencies: null, /// See also [UnifiedPreferenceSettingsRepository].
allTransitiveDependencies: null, @ProviderFor(UnifiedPreferenceSettingsRepository)
); const unifiedPreferenceSettingsRepositoryProvider =
UnifiedPreferenceSettingsRepositoryFamily();
/// See also [UnifiedPreferenceSettingsRepository].
class UnifiedPreferenceSettingsRepositoryFamily
extends Family<AsyncValue<Map<String, PreferenceSettingGroup>>> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'unifiedPreferenceSettingsRepositoryProvider';
}
/// See also [UnifiedPreferenceSettingsRepository].
class UnifiedPreferenceSettingsRepositoryProvider
extends AutoDisposeStreamNotifierProviderImpl<
UnifiedPreferenceSettingsRepository,
Map<String, PreferenceSettingGroup>> {
/// 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<Map<String, PreferenceSettingGroup>> 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<UnifiedPreferenceSettingsRepository,
Map<String, PreferenceSettingGroup>> 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<String, PreferenceSettingGroup>> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
}
class _UnifiedPreferenceSettingsRepositoryProviderElement
extends AutoDisposeStreamNotifierProviderElement<
UnifiedPreferenceSettingsRepository,
Map<String, PreferenceSettingGroup>>
with UnifiedPreferenceSettingsRepositoryRef {
_UnifiedPreferenceSettingsRepositoryProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as UnifiedPreferenceSettingsRepositoryProvider).partition;
}
typedef _$PreferenceSettingsGeneralRepository
= AutoDisposeStreamNotifier<Map<String, PreferenceSettingGroup>>;
String _$preferenceSettingsGroupRepositoryHash() => String _$preferenceSettingsGroupRepositoryHash() =>
r'023794294c5639ac6c6941f87a4ed91e1551fbc9'; r'60f416ffe1cde4d535fb5b643a80aead20836bae';
abstract class _$PreferenceSettingsGroupRepository abstract class _$PreferenceSettingsGroupRepository
extends BuildlessAutoDisposeStreamNotifier<PreferenceSettingGroup> { extends BuildlessAutoDisposeStreamNotifier<PreferenceSettingGroup> {
late final PreferencePartition partition;
late final String groupName; late final String groupName;
Stream<PreferenceSettingGroup> build( Stream<PreferenceSettingGroup> build(
PreferencePartition partition,
String groupName, String groupName,
); );
} }
@@ -246,9 +542,11 @@ class PreferenceSettingsGroupRepositoryFamily
/// See also [PreferenceSettingsGroupRepository]. /// See also [PreferenceSettingsGroupRepository].
PreferenceSettingsGroupRepositoryProvider call( PreferenceSettingsGroupRepositoryProvider call(
PreferencePartition partition,
String groupName, String groupName,
) { ) {
return PreferenceSettingsGroupRepositoryProvider( return PreferenceSettingsGroupRepositoryProvider(
partition,
groupName, groupName,
); );
} }
@@ -258,6 +556,7 @@ class PreferenceSettingsGroupRepositoryFamily
covariant PreferenceSettingsGroupRepositoryProvider provider, covariant PreferenceSettingsGroupRepositoryProvider provider,
) { ) {
return call( return call(
provider.partition,
provider.groupName, provider.groupName,
); );
} }
@@ -283,9 +582,12 @@ class PreferenceSettingsGroupRepositoryProvider
PreferenceSettingsGroupRepository, PreferenceSettingGroup> { PreferenceSettingsGroupRepository, PreferenceSettingGroup> {
/// See also [PreferenceSettingsGroupRepository]. /// See also [PreferenceSettingsGroupRepository].
PreferenceSettingsGroupRepositoryProvider( PreferenceSettingsGroupRepositoryProvider(
PreferencePartition partition,
String groupName, String groupName,
) : this._internal( ) : this._internal(
() => PreferenceSettingsGroupRepository()..groupName = groupName, () => PreferenceSettingsGroupRepository()
..partition = partition
..groupName = groupName,
from: preferenceSettingsGroupRepositoryProvider, from: preferenceSettingsGroupRepositoryProvider,
name: r'preferenceSettingsGroupRepositoryProvider', name: r'preferenceSettingsGroupRepositoryProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@@ -295,6 +597,7 @@ class PreferenceSettingsGroupRepositoryProvider
dependencies: PreferenceSettingsGroupRepositoryFamily._dependencies, dependencies: PreferenceSettingsGroupRepositoryFamily._dependencies,
allTransitiveDependencies: PreferenceSettingsGroupRepositoryFamily allTransitiveDependencies: PreferenceSettingsGroupRepositoryFamily
._allTransitiveDependencies, ._allTransitiveDependencies,
partition: partition,
groupName: groupName, groupName: groupName,
); );
@@ -305,9 +608,11 @@ class PreferenceSettingsGroupRepositoryProvider
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.partition,
required this.groupName, required this.groupName,
}) : super.internal(); }) : super.internal();
final PreferencePartition partition;
final String groupName; final String groupName;
@override @override
@@ -315,6 +620,7 @@ class PreferenceSettingsGroupRepositoryProvider
covariant PreferenceSettingsGroupRepository notifier, covariant PreferenceSettingsGroupRepository notifier,
) { ) {
return notifier.build( return notifier.build(
partition,
groupName, groupName,
); );
} }
@@ -324,12 +630,15 @@ class PreferenceSettingsGroupRepositoryProvider
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: PreferenceSettingsGroupRepositoryProvider._internal( override: PreferenceSettingsGroupRepositoryProvider._internal(
() => create()..groupName = groupName, () => create()
..partition = partition
..groupName = groupName,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
partition: partition,
groupName: groupName, groupName: groupName,
), ),
); );
@@ -344,12 +653,14 @@ class PreferenceSettingsGroupRepositoryProvider
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is PreferenceSettingsGroupRepositoryProvider && return other is PreferenceSettingsGroupRepositoryProvider &&
other.partition == partition &&
other.groupName == groupName; other.groupName == groupName;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, partition.hashCode);
hash = _SystemHash.combine(hash, groupName.hashCode); hash = _SystemHash.combine(hash, groupName.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
@@ -360,6 +671,9 @@ class PreferenceSettingsGroupRepositoryProvider
// ignore: unused_element // ignore: unused_element
mixin PreferenceSettingsGroupRepositoryRef mixin PreferenceSettingsGroupRepositoryRef
on AutoDisposeStreamNotifierProviderRef<PreferenceSettingGroup> { on AutoDisposeStreamNotifierProviderRef<PreferenceSettingGroup> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
/// The parameter `groupName` of this provider. /// The parameter `groupName` of this provider.
String get groupName; String get groupName;
} }
@@ -370,6 +684,9 @@ class _PreferenceSettingsGroupRepositoryProviderElement
PreferenceSettingGroup> with PreferenceSettingsGroupRepositoryRef { PreferenceSettingGroup> with PreferenceSettingsGroupRepositoryRef {
_PreferenceSettingsGroupRepositoryProviderElement(super.provider); _PreferenceSettingsGroupRepositoryProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as PreferenceSettingsGroupRepositoryProvider).partition;
@override @override
String get groupName => String get groupName =>
(origin as PreferenceSettingsGroupRepositoryProvider).groupName; (origin as PreferenceSettingsGroupRepositoryProvider).groupName;
@@ -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/entities/readerable_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart'; import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.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'; import 'package:lensai/presentation/widgets/animate_gradient_shader.dart';
class ReaderButton extends HookConsumerWidget { class ReaderButton extends HookConsumerWidget {
@@ -16,7 +16,8 @@ class ReaderButton extends HookConsumerWidget {
final readerChanging = ref.watch(readerableScreenControllerProvider); final readerChanging = ref.watch(readerableScreenControllerProvider);
final enableReadability = ref.watch( final enableReadability = ref.watch(
settingsRepositoryProvider.select((value) => value.enableReadability), generalSettingsRepositoryProvider
.select((value) => value.enableReadability),
); );
final readerabilityState = ref.watch( final readerabilityState = ref.watch(
@@ -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<ContainerMetadata, String> {
const ContainerMetadataConverter();
@override
ContainerMetadata fromSql(String fromDb) {
final json = jsonDecode(fromDb) as Map<String, dynamic>;
return ContainerMetadata.fromJson(json);
}
@override
String toSql(ContainerMetadata value) {
return jsonEncode(value.toJson());
}
}
@@ -1,7 +1,6 @@
import 'dart:ui'; import 'dart:ui';
import 'package:drift/drift.dart'; 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/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
@@ -12,26 +11,24 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
with _$ContainerDaoMixin { with _$ContainerDaoMixin {
ContainerDao(super.db); ContainerDao(super.db);
Future<void> addContainer({String? name, required Color color}) { Future<void> addContainer(ContainerData container) {
return db.container.insertOne( return db.container.insertOne(
ContainerCompanion.insert( ContainerCompanion.insert(
id: uuid.v7(), id: container.id,
name: Value(name), name: Value(container.name),
color: color, color: container.color,
metadata: Value(container.metadata),
), ),
); );
} }
Future<void> replaceContainer( Future<void> replaceContainer(ContainerData container) {
String id, {
required String? name,
required Color color,
}) {
return db.container.replaceOne( return db.container.replaceOne(
ContainerCompanion( ContainerCompanion(
id: Value(id), id: Value(container.id),
name: Value(name), name: Value(container.name),
color: Value(color), color: Value(container.color),
metadata: Value(container.metadata),
), ),
); );
} }
@@ -41,7 +41,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
return query.map((row) => row.read(db.tab.id)!); return query.map((row) => row.read(db.tab.id)!);
} }
SingleSelectable<String?> tabContainerId(String tabId) { SingleOrNullSelectable<String?> tabContainerId(String tabId) {
final query = selectOnly(db.tab) final query = selectOnly(db.tab)
..addColumns([db.tab.containerId]) ..addColumns([db.tab.containerId])
..where(db.tab.id.equals(tabId)); ..where(db.tab.id.equals(tabId));
@@ -49,7 +49,41 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
return query.map((row) => row.read(db.tab.containerId)); return query.map((row) => row.read(db.tab.containerId));
} }
Future<String> upsertTab( Future<String> upsertContainerTabTransactional(
Future<String> Function() createTab, {
Value<String?> containerId = const Value.absent(),
Value<String?> 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<String> upsertUnassignedTab(
String tabId, { String tabId, {
Value<String?> containerId = const Value.absent(), Value<String?> containerId = const Value.absent(),
Value<String?> orderKey = const Value.absent(), Value<String?> orderKey = const Value.absent(),
@@ -73,6 +107,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
(containerId.present) ? Variable(containerId.value) : null, (containerId.present) ? Variable(containerId.value) : null,
orderKey: (orderKey.present) ? Variable(orderKey.value) : null, orderKey: (orderKey.present) ? Variable(orderKey.value) : null,
), ),
where: (old) => old.containerId.isNull(),
), ),
); );
@@ -1,7 +1,7 @@
import 'package:drift/drift.dart'; 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/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/container.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.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'; import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
@@ -1,14 +1,13 @@
import 'package:lensai/data/database/converters/color.dart'; 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/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.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 ( CREATE TABLE container (
id TEXT PRIMARY KEY NOT NULL, id TEXT PRIMARY KEY NOT NULL,
contextual_identity TEXT,
name TEXT, name TEXT,
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`, color INTEGER NOT NULL MAPPED BY `const ColorConverter()`,
icon TEXT MAPPED BY `const IconDataTypeConverter()` metadata TEXT MAPPED BY `const ContainerMetadataConverter()`
) WITH ContainerData; ) WITH ContainerData;
CREATE TABLE tab ( CREATE TABLE tab (
@@ -79,7 +78,7 @@ containersWithCount WITH ContainerDataWithCount:
FROM tab FROM tab
GROUP BY container_id GROUP BY container_id
) AS tab_agg ON container.id = tab_agg.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): leadingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER):
SELECT lexo_rank_previous( SELECT lexo_rank_previous(
@@ -13,11 +13,6 @@ class Container extends Table with TableInfo<Container, ContainerData> {
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL'); $customConstraints: 'PRIMARY KEY NOT NULL');
late final GeneratedColumn<String> contextualIdentity =
GeneratedColumn<String>('contextual_identity', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '');
late final GeneratedColumn<String> name = GeneratedColumn<String>( late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, true, 'name', aliasedName, true,
type: DriftSqlType.string, type: DriftSqlType.string,
@@ -29,15 +24,14 @@ class Container extends Table with TableInfo<Container, ContainerData> {
requiredDuringInsert: true, requiredDuringInsert: true,
$customConstraints: 'NOT NULL') $customConstraints: 'NOT NULL')
.withConverter<Color>(Container.$convertercolor); .withConverter<Color>(Container.$convertercolor);
late final GeneratedColumnWithTypeConverter<IconData?, String> icon = late final GeneratedColumnWithTypeConverter<ContainerMetadata?, String>
GeneratedColumn<String>('icon', aliasedName, true, metadata = GeneratedColumn<String>('metadata', aliasedName, true,
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: false, requiredDuringInsert: false,
$customConstraints: '') $customConstraints: '')
.withConverter<IconData?>(Container.$convertericonn); .withConverter<ContainerMetadata?>(Container.$convertermetadatan);
@override @override
List<GeneratedColumn> get $columns => List<GeneratedColumn> get $columns => [id, name, color, metadata];
[id, contextualIdentity, name, color, icon];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -51,14 +45,13 @@ class Container extends Table with TableInfo<Container, ContainerData> {
return ContainerData( return ContainerData(
id: attachedDatabase.typeMapping id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!, .read(DriftSqlType.string, data['${effectivePrefix}id'])!,
contextualIdentity: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}contextual_identity']),
name: attachedDatabase.typeMapping name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name']), .read(DriftSqlType.string, data['${effectivePrefix}name']),
color: Container.$convertercolor.fromSql(attachedDatabase.typeMapping color: Container.$convertercolor.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}color'])!), .read(DriftSqlType.int, data['${effectivePrefix}color'])!),
icon: Container.$convertericonn.fromSql(attachedDatabase.typeMapping metadata: Container.$convertermetadatan.fromSql(attachedDatabase
.read(DriftSqlType.string, data['${effectivePrefix}icon'])), .typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}metadata'])),
); );
} }
@@ -68,69 +61,62 @@ class Container extends Table with TableInfo<Container, ContainerData> {
} }
static TypeConverter<Color, int> $convertercolor = const ColorConverter(); static TypeConverter<Color, int> $convertercolor = const ColorConverter();
static TypeConverter<IconData, String> $convertericon = static TypeConverter<ContainerMetadata, String> $convertermetadata =
const IconDataTypeConverter(); const ContainerMetadataConverter();
static TypeConverter<IconData?, String?> $convertericonn = static TypeConverter<ContainerMetadata?, String?> $convertermetadatan =
NullAwareTypeConverter.wrap($convertericon); NullAwareTypeConverter.wrap($convertermetadata);
@override @override
bool get dontWriteConstraints => true; bool get dontWriteConstraints => true;
} }
class ContainerCompanion extends UpdateCompanion<ContainerData> { class ContainerCompanion extends UpdateCompanion<ContainerData> {
final Value<String> id; final Value<String> id;
final Value<String?> contextualIdentity;
final Value<String?> name; final Value<String?> name;
final Value<Color> color; final Value<Color> color;
final Value<IconData?> icon; final Value<ContainerMetadata?> metadata;
final Value<int> rowid; final Value<int> rowid;
const ContainerCompanion({ const ContainerCompanion({
this.id = const Value.absent(), this.id = const Value.absent(),
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
this.color = const Value.absent(), this.color = const Value.absent(),
this.icon = const Value.absent(), this.metadata = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
ContainerCompanion.insert({ ContainerCompanion.insert({
required String id, required String id,
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(), this.name = const Value.absent(),
required Color color, required Color color,
this.icon = const Value.absent(), this.metadata = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : id = Value(id), }) : id = Value(id),
color = Value(color); color = Value(color);
static Insertable<ContainerData> custom({ static Insertable<ContainerData> custom({
Expression<String>? id, Expression<String>? id,
Expression<String>? contextualIdentity,
Expression<String>? name, Expression<String>? name,
Expression<int>? color, Expression<int>? color,
Expression<String>? icon, Expression<String>? metadata,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (id != null) 'id': id, if (id != null) 'id': id,
if (contextualIdentity != null) 'contextual_identity': contextualIdentity,
if (name != null) 'name': name, if (name != null) 'name': name,
if (color != null) 'color': color, if (color != null) 'color': color,
if (icon != null) 'icon': icon, if (metadata != null) 'metadata': metadata,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
ContainerCompanion copyWith( ContainerCompanion copyWith(
{Value<String>? id, {Value<String>? id,
Value<String?>? contextualIdentity,
Value<String?>? name, Value<String?>? name,
Value<Color>? color, Value<Color>? color,
Value<IconData?>? icon, Value<ContainerMetadata?>? metadata,
Value<int>? rowid}) { Value<int>? rowid}) {
return ContainerCompanion( return ContainerCompanion(
id: id ?? this.id, id: id ?? this.id,
contextualIdentity: contextualIdentity ?? this.contextualIdentity,
name: name ?? this.name, name: name ?? this.name,
color: color ?? this.color, color: color ?? this.color,
icon: icon ?? this.icon, metadata: metadata ?? this.metadata,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
} }
@@ -141,9 +127,6 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
if (id.present) { if (id.present) {
map['id'] = Variable<String>(id.value); map['id'] = Variable<String>(id.value);
} }
if (contextualIdentity.present) {
map['contextual_identity'] = Variable<String>(contextualIdentity.value);
}
if (name.present) { if (name.present) {
map['name'] = Variable<String>(name.value); map['name'] = Variable<String>(name.value);
} }
@@ -151,9 +134,9 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
map['color'] = map['color'] =
Variable<int>(Container.$convertercolor.toSql(color.value)); Variable<int>(Container.$convertercolor.toSql(color.value));
} }
if (icon.present) { if (metadata.present) {
map['icon'] = map['metadata'] =
Variable<String>(Container.$convertericonn.toSql(icon.value)); Variable<String>(Container.$convertermetadatan.toSql(metadata.value));
} }
if (rowid.present) { if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value); map['rowid'] = Variable<int>(rowid.value);
@@ -165,10 +148,9 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
String toString() { String toString() {
return (StringBuffer('ContainerCompanion(') return (StringBuffer('ContainerCompanion(')
..write('id: $id, ') ..write('id: $id, ')
..write('contextualIdentity: $contextualIdentity, ')
..write('name: $name, ') ..write('name: $name, ')
..write('color: $color, ') ..write('color: $color, ')
..write('icon: $icon, ') ..write('metadata: $metadata, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
.toString(); .toString();
@@ -1597,18 +1579,18 @@ abstract class _$TabDatabase extends GeneratedDatabase {
Selectable<ContainerDataWithCount> containersWithCount() { Selectable<ContainerDataWithCount> containersWithCount() {
return customSelect( 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: [], variables: [],
readsFrom: { readsFrom: {
container, container,
tab, tab,
}).map((QueryRow row) => ContainerDataWithCount( }).map((QueryRow row) => ContainerDataWithCount(
id: row.read<String>('id'), id: row.read<String>('id'),
contextualIdentity: row.readNullable<String>('contextual_identity'),
name: row.readNullable<String>('name'), name: row.readNullable<String>('name'),
color: Container.$convertercolor.fromSql(row.read<int>('color')), color: Container.$convertercolor.fromSql(row.read<int>('color')),
icon: NullAwareTypeConverter.wrapFromSql( metadata: NullAwareTypeConverter.wrapFromSql(
Container.$convertericon, row.readNullable<String>('icon')), Container.$convertermetadata,
row.readNullable<String>('metadata')),
tabCount: row.readNullable<int>('tab_count'), tabCount: row.readNullable<int>('tab_count'),
)); ));
} }
@@ -1819,18 +1801,16 @@ abstract class _$TabDatabase extends GeneratedDatabase {
typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({ typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({
required String id, required String id,
Value<String?> contextualIdentity,
Value<String?> name, Value<String?> name,
required Color color, required Color color,
Value<IconData?> icon, Value<ContainerMetadata?> metadata,
Value<int> rowid, Value<int> rowid,
}); });
typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({ typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({
Value<String> id, Value<String> id,
Value<String?> contextualIdentity,
Value<String?> name, Value<String?> name,
Value<Color> color, Value<Color> color,
Value<IconData?> icon, Value<ContainerMetadata?> metadata,
Value<int> rowid, Value<int> rowid,
}); });
@@ -1864,10 +1844,6 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> {
ColumnFilters<String> get id => $composableBuilder( ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column)); column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get name => $composableBuilder( ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnFilters(column)); column: $table.name, builder: (column) => ColumnFilters(column));
@@ -1876,9 +1852,9 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> {
column: $table.color, column: $table.color,
builder: (column) => ColumnWithTypeConverterFilters(column)); builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnWithTypeConverterFilters<IconData?, IconData, String> get icon => ColumnWithTypeConverterFilters<ContainerMetadata?, ContainerMetadata, String>
$composableBuilder( get metadata => $composableBuilder(
column: $table.icon, column: $table.metadata,
builder: (column) => ColumnWithTypeConverterFilters(column)); builder: (column) => ColumnWithTypeConverterFilters(column));
Expression<bool> tabRefs(Expression<bool> Function($TabFilterComposer f) f) { Expression<bool> tabRefs(Expression<bool> Function($TabFilterComposer f) f) {
@@ -1913,18 +1889,14 @@ class $ContainerOrderingComposer extends Composer<_$TabDatabase, Container> {
ColumnOrderings<String> get id => $composableBuilder( ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column)); column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get name => $composableBuilder( ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnOrderings(column)); column: $table.name, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get color => $composableBuilder( ColumnOrderings<int> get color => $composableBuilder(
column: $table.color, builder: (column) => ColumnOrderings(column)); column: $table.color, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get icon => $composableBuilder( ColumnOrderings<String> get metadata => $composableBuilder(
column: $table.icon, builder: (column) => ColumnOrderings(column)); column: $table.metadata, builder: (column) => ColumnOrderings(column));
} }
class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> { class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> {
@@ -1938,17 +1910,14 @@ class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> {
GeneratedColumn<String> get id => GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column); $composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity, builder: (column) => column);
GeneratedColumn<String> get name => GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column); $composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumnWithTypeConverter<Color, int> get color => GeneratedColumnWithTypeConverter<Color, int> get color =>
$composableBuilder(column: $table.color, builder: (column) => column); $composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumnWithTypeConverter<IconData?, String> get icon => GeneratedColumnWithTypeConverter<ContainerMetadata?, String> get metadata =>
$composableBuilder(column: $table.icon, builder: (column) => column); $composableBuilder(column: $table.metadata, builder: (column) => column);
Expression<T> tabRefs<T extends Object>( Expression<T> tabRefs<T extends Object>(
Expression<T> Function($TabAnnotationComposer a) f) { Expression<T> Function($TabAnnotationComposer a) f) {
@@ -1996,34 +1965,30 @@ class $ContainerTableManager extends RootTableManager<
$ContainerAnnotationComposer($db: db, $table: table), $ContainerAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({ updateCompanionCallback: ({
Value<String> id = const Value.absent(), Value<String> id = const Value.absent(),
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(), Value<String?> name = const Value.absent(),
Value<Color> color = const Value.absent(), Value<Color> color = const Value.absent(),
Value<IconData?> icon = const Value.absent(), Value<ContainerMetadata?> metadata = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
ContainerCompanion( ContainerCompanion(
id: id, id: id,
contextualIdentity: contextualIdentity,
name: name, name: name,
color: color, color: color,
icon: icon, metadata: metadata,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String id, required String id,
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(), Value<String?> name = const Value.absent(),
required Color color, required Color color,
Value<IconData?> icon = const Value.absent(), Value<ContainerMetadata?> metadata = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
ContainerCompanion.insert( ContainerCompanion.insert(
id: id, id: id,
contextualIdentity: contextualIdentity,
name: name, name: name,
color: color, color: color,
icon: icon, metadata: metadata,
rowid: rowid, rowid: rowid,
), ),
withReferenceMapper: (p0) => p0 withReferenceMapper: (p0) => p0
@@ -1,20 +1,103 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart'; import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/widgets.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<String, dynamic> json) =>
_$ContainerAuthSettingsFromJson(json);
Map<String, dynamic> toJson() => _$ContainerAuthSettingsToJson(this);
@override
List<Object?> 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<String, dynamic> json) =>
_$ContainerMetadataFromJson(json);
Map<String, dynamic> toJson() => _$ContainerMetadataToJson(this);
@override
List<Object?> get hashParameters => [
iconData,
contextualIdentity,
authSettings,
];
@override
bool get cacheHash => true;
}
@CopyWith()
class ContainerData with FastEquatable { class ContainerData with FastEquatable {
final String id; final String id;
final String? contextualIdentity;
final String? name; final String? name;
final Color color; final Color color;
final IconData? icon; final ContainerMetadata metadata;
ContainerData({ ContainerData({
required this.id, required this.id,
this.contextualIdentity,
this.name, this.name,
required this.color, required this.color,
this.icon, ContainerMetadata? metadata,
}); }) : metadata = metadata ?? ContainerMetadata.withDefaults();
@override @override
bool get cacheHash => true; bool get cacheHash => true;
@@ -22,10 +105,9 @@ class ContainerData with FastEquatable {
@override @override
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
id, id,
contextualIdentity,
name, name,
color, color,
icon, metadata,
]; ];
} }
@@ -34,10 +116,9 @@ class ContainerDataWithCount extends ContainerData {
ContainerDataWithCount({ ContainerDataWithCount({
required super.id, required super.id,
super.contextualIdentity,
super.name, super.name,
required super.color, required super.color,
super.icon, super.metadata,
required this.tabCount, required this.tabCount,
}); });
@@ -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<String, dynamic> 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<String, dynamic> _$ContainerAuthSettingsToJson(
ContainerAuthSettings instance) =>
<String, dynamic>{
'authenticationRequired': instance.authenticationRequired,
'lockOnAppBackground': instance.lockOnAppBackground,
'lockTimeout': instance.lockTimeout?.inMicroseconds,
};
ContainerMetadata _$ContainerMetadataFromJson(Map<String, dynamic> json) =>
ContainerMetadata.withDefaults(
iconData: _$JsonConverterFromJson<Map<String, dynamic>, IconData>(
json['iconData'], const IconDataJsonConverter().fromJson),
contextualIdentity: json['contextualIdentity'] as String?,
authSettings: json['authSettings'] == null
? null
: ContainerAuthSettings.fromJson(
json['authSettings'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ContainerMetadataToJson(ContainerMetadata instance) =>
<String, dynamic>{
'iconData': _$JsonConverterToJson<Map<String, dynamic>, IconData>(
instance.iconData, const IconDataJsonConverter().toJson),
'contextualIdentity': instance.contextualIdentity,
'authSettings': instance.authSettings,
};
Value? _$JsonConverterFromJson<Json, Value>(
Object? json,
Value? Function(Json json) fromJson,
) =>
json == null ? null : fromJson(json as Json);
Json? _$JsonConverterToJson<Json, Value>(
Value? value,
Json? Function(Value value) toJson,
) =>
value == null ? null : toJson(value);
@@ -58,12 +58,6 @@ AsyncValue<List<ContainerDataWithCount>> filteredContainersWithCount(
); );
} }
@Riverpod()
Stream<String?> tabContainerId(Ref ref, String tabId) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.tabContainerId(tabId).watchSingle();
}
@Riverpod() @Riverpod()
Stream<List<String>> containerTabIds( Stream<List<String>> containerTabIds(
Ref ref, Ref ref,
@@ -206,136 +206,6 @@ class _FilteredContainersWithCountProviderElement
(origin as FilteredContainersWithCountProvider).searchText; (origin as FilteredContainersWithCountProvider).searchText;
} }
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
/// See also [tabContainerId].
@ProviderFor(tabContainerId)
const tabContainerIdProvider = TabContainerIdFamily();
/// See also [tabContainerId].
class TabContainerIdFamily extends Family<AsyncValue<String?>> {
/// See also [tabContainerId].
const TabContainerIdFamily();
/// See also [tabContainerId].
TabContainerIdProvider call(
String tabId,
) {
return TabContainerIdProvider(
tabId,
);
}
@override
TabContainerIdProvider getProviderOverride(
covariant TabContainerIdProvider provider,
) {
return call(
provider.tabId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabContainerIdProvider';
}
/// See also [tabContainerId].
class TabContainerIdProvider extends AutoDisposeStreamProvider<String?> {
/// See also [tabContainerId].
TabContainerIdProvider(
String tabId,
) : this._internal(
(ref) => tabContainerId(
ref as TabContainerIdRef,
tabId,
),
from: tabContainerIdProvider,
name: r'tabContainerIdProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabContainerIdHash,
dependencies: TabContainerIdFamily._dependencies,
allTransitiveDependencies:
TabContainerIdFamily._allTransitiveDependencies,
tabId: tabId,
);
TabContainerIdProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
Stream<String?> Function(TabContainerIdRef provider) create,
) {
return ProviderOverride(
origin: this,
override: TabContainerIdProvider._internal(
(ref) => create(ref as TabContainerIdRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeStreamProviderElement<String?> createElement() {
return _TabContainerIdProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabContainerIdProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin TabContainerIdRef on AutoDisposeStreamProviderRef<String?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabContainerIdProviderElement
extends AutoDisposeStreamProviderElement<String?> with TabContainerIdRef {
_TabContainerIdProviderElement(super.provider);
@override
String get tabId => (origin as TabContainerIdProvider).tabId;
}
String _$containerTabIdsHash() => r'7545a6c500b1832bf81c0838e257dfe5e051463d'; String _$containerTabIdsHash() => r'7545a6c500b1832bf81c0838e257dfe5e051463d';
/// See also [containerTabIds]. /// See also [containerTabIds].
@@ -1,5 +1,8 @@
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.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/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/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -7,15 +10,47 @@ part 'selected_container.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SelectedContainer extends _$SelectedContainer { class SelectedContainer extends _$SelectedContainer {
void setContainerId(String id) { Future<ContainerData?> fetchData() async {
state = id; if (state != null) {
return ref
.read(containerRepositoryProvider.notifier)
.getContainerData(state!);
}
return null;
} }
void toggleContainer(String id) { Future<void> 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<void> toggleContainer(String id) async {
if (state == id) { if (state == id) {
clearContainer(); clearContainer();
} else { } else {
setContainerId(id); await setContainerId(id);
} }
} }
@@ -25,6 +60,17 @@ class SelectedContainer extends _$SelectedContainer {
@override @override
String? build() { String? build() {
ref.listen(
containersWithCountProvider,
(previous, next) {
if (state != null && next.valueOrNull != null) {
if (!next.value!.any((container) => container.id == state)) {
clearContainer();
}
}
},
);
return null; return null;
} }
} }
@@ -25,7 +25,7 @@ final selectedContainerDataProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>; typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf'; String _$selectedContainerHash() => r'753f30646c4992981ec538dcfd33fa23aa1f673d';
/// See also [SelectedContainer]. /// See also [SelectedContainer].
@ProviderFor(SelectedContainer) @ProviderFor(SelectedContainer)
@@ -1,54 +1,72 @@
import 'dart:ui'; 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/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'container.g.dart'; part 'container.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class ContainerRepository extends _$ContainerRepository { class ContainerRepository extends _$ContainerRepository {
late TabDatabase _db; Future<void> addContainer(ContainerData container) {
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
@override
void build() {
_db = ref.watch(tabDatabaseProvider);
} }
Future<void> addContainer({required String? name, required Color color}) { Future<void> replaceContainer(ContainerData container) {
return _db.containerDao.addContainer(name: name, color: color); return ref
.read(tabDatabaseProvider)
.containerDao
.replaceContainer(container);
} }
Future<void> replaceContainer({ Future<ContainerData?> getContainerData(String id) async {
required String id, return ref
required String? name, .read(tabDatabaseProvider)
required Color color, .containerDao
}) { .getContainerData(id)
return _db.containerDao.replaceContainer(id, name: name, color: color); .getSingleOrNull();
} }
Future<void> deleteContainer(String id) { Future<void> deleteContainer(String id) async {
return _db.containerDao.deleteContainer(id); await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id);
return ref.read(tabDatabaseProvider).containerDao.deleteContainer(id);
} }
Future<Set<Color>> getDistinctColors() { Future<Set<Color>> getDistinctColors() {
return _db.containerDao return ref
.read(tabDatabaseProvider)
.containerDao
.getDistinctColors() .getDistinctColors()
.get() .get()
.then((colors) => colors.toSet()); .then((colors) => colors.toSet());
} }
Future<String> getLeadingOrderKey(String? containerId) { Future<String> getLeadingOrderKey(String? containerId) {
return _db.containerDao.generateLeadingOrderKey(containerId).getSingle(); return ref
.read(tabDatabaseProvider)
.containerDao
.generateLeadingOrderKey(containerId)
.getSingle();
} }
Future<String> getTrailingOrderKey(String? containerId) { Future<String> getTrailingOrderKey(String? containerId) {
return _db.containerDao.generateTrailingOrderKey(containerId).getSingle(); return ref
.read(tabDatabaseProvider)
.containerDao
.generateTrailingOrderKey(containerId)
.getSingle();
} }
Future<String> getOrderKeyAfterTab(String tabId, String? containerId) { Future<String> getOrderKeyAfterTab(String tabId, String? containerId) {
return _db.containerDao return ref
.read(tabDatabaseProvider)
.containerDao
.generateOrderKeyAfterTabId(containerId, tabId) .generateOrderKeyAfterTabId(containerId, tabId)
.getSingle(); .getSingle();
} }
@override
void build() {}
} }
@@ -7,7 +7,7 @@ part of 'container.dart';
// ************************************************************************** // **************************************************************************
String _$containerRepositoryHash() => String _$containerRepositoryHash() =>
r'89ad15861fb213d6a3fa51232fd0ff9943f51e92'; r'b965883f2279b6f0148592bbac5ff40a60798f51';
/// See also [ContainerRepository]. /// See also [ContainerRepository].
@ProviderFor(ContainerRepository) @ProviderFor(ContainerRepository)
@@ -1,5 +1,4 @@
import 'package:lensai/features/geckoview/domain/repositories/tab.dart'; 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:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -7,35 +6,40 @@ part 'tab.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class TabDataRepository extends _$TabDataRepository { class TabDataRepository extends _$TabDataRepository {
late TabDatabase _db;
@override @override
void build() { void build() {}
_db = ref.watch(tabDatabaseProvider);
}
Future<void> assignContainer(String tabId, String? containerId) { Future<void> assignContainer(String tabId, String? containerId) {
return _db.tabDao.assignContainer( return ref.read(tabDatabaseProvider).tabDao.assignContainer(
tabId, tabId,
containerId: containerId, containerId: containerId,
); );
} }
Future<void> assignOrderKey(String tabId, String orderKey) { Future<void> assignOrderKey(String tabId, String orderKey) {
return _db.tabDao.assignOrderKey( return ref.read(tabDatabaseProvider).tabDao.assignOrderKey(
tabId, tabId,
orderKey: orderKey, orderKey: orderKey,
); );
} }
Future<void> closeAllTabs(String? containerId) async { Future<void> 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) { if (tabIds.isNotEmpty) {
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds); await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
} }
} }
Future<String?> containerTabId(String tabId) { Future<String?> containerTabId(String tabId) {
return _db.tabDao.tabContainerId(tabId).getSingle(); return ref
.read(tabDatabaseProvider)
.tabDao
.tabContainerId(tabId)
.getSingleOrNull();
} }
} }
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$tabDataRepositoryHash() => r'c067bbbf319cd95ad869655740fdf49d13159b94'; String _$tabDataRepositoryHash() => r'30ae87fbcb37facd565cb28de605814cd7db4ce8';
/// See also [TabDataRepository]. /// See also [TabDataRepository].
@ProviderFor(TabDataRepository) @ProviderFor(TabDataRepository)
@@ -1,6 +1,5 @@
import 'dart:async'; 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/models/tab_query_result.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart'; import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -9,8 +8,6 @@ part 'tab_search.g.dart';
@Riverpod() @Riverpod()
class TabSearchRepository extends _$TabSearchRepository { class TabSearchRepository extends _$TabSearchRepository {
late TabDatabase _db;
Future<void> addQuery( Future<void> addQuery(
String input, { String input, {
int snippetLength = 120, int snippetLength = 120,
@@ -20,7 +17,9 @@ class TabSearchRepository extends _$TabSearchRepository {
}) async { }) async {
if (input.isNotEmpty) { if (input.isNotEmpty) {
state = await AsyncValue.guard( state = await AsyncValue.guard(
() => _db.tabDao () => ref
.read(tabDatabaseProvider)
.tabDao
.queryTabs( .queryTabs(
matchPrefix: matchPrefix, matchPrefix: matchPrefix,
matchSuffix: matchSuffix, matchSuffix: matchSuffix,
@@ -37,8 +36,6 @@ class TabSearchRepository extends _$TabSearchRepository {
@override @override
Future<List<TabQueryResult>?> build() { Future<List<TabQueryResult>?> build() {
_db = ref.watch(tabDatabaseProvider);
return Future.value(); return Future.value();
} }
} }
@@ -7,7 +7,7 @@ part of 'tab_search.dart';
// ************************************************************************** // **************************************************************************
String _$tabSearchRepositoryHash() => String _$tabSearchRepositoryHash() =>
r'9bd42f89bcc3a8d94add093076737dd57383bae2'; r'57d130f50aa3feb7a31d27f9a5b54234e0e7064f';
/// See also [TabSearchRepository]. /// See also [TabSearchRepository].
@ProviderFor(TabSearchRepository) @ProviderFor(TabSearchRepository)
@@ -6,6 +6,7 @@ import 'package:langchain/langchain.dart' as langchain;
import 'package:langchain/langchain.dart'; import 'package:langchain/langchain.dart';
import 'package:langchain_openai/langchain_openai.dart'; import 'package:langchain_openai/langchain_openai.dart';
import 'package:lensai/core/models.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/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/data/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'; 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, chunkOverlap: chunkOverlap,
); );
final splittedWithSource = (splitted != null) final splittedWithSource = splitted.mapNotNull(
? ( (splitted) => (
mainDocumentId: splitted.mainDocumentId, mainDocumentId: splitted.mainDocumentId,
parts: splitted.parts parts: splitted.parts
.map( .map(
(part) => part.copyWith( (part) => part.copyWith(
metadata: { metadata: {
...part.metadata, ...part.metadata,
'source': part.id, 'source': part.id,
}, },
), ),
) )
.toList() .toList()
) ),
: null; );
await _vectorDao.insertDocuments( await _vectorDao.insertDocuments(
splittedWithSource?.parts ?? [doc], splittedWithSource?.parts ?? [doc],
@@ -7,7 +7,7 @@ part of 'document.dart';
// ************************************************************************** // **************************************************************************
String _$documentRepositoryHash() => String _$documentRepositoryHash() =>
r'0c93116e0f9887d452b7c493dd4da45e5bf0c2d4'; r'60e5a0c889d9659c8e0e254c59225c2041a0cb56';
/// See also [DocumentRepository]. /// See also [DocumentRepository].
@ProviderFor(DocumentRepository) @ProviderFor(DocumentRepository)
@@ -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<Duration?>>[
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<Color?>(
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<bool?>(
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: <Widget>[
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();
}
}
},
),
),
],
),
),
),
);
}
}
@@ -1,119 +1,17 @@
import 'package:fading_scroll/fading_scroll.dart'; import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.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/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/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'; 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<ContainerResult?>(
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<bool?>(
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: <Widget>[
TextButton(
onPressed: () {
Navigator.pop(context, false);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop(context, true);
},
child: const Text('Delete'),
),
],
);
},
);
if (result == true) {
onDelete();
}
},
leadingIcon: const Icon(Icons.delete),
child: const Text('Delete'),
),
],
),
onTap: onTap,
);
}
}
class ContainerListScreen extends HookConsumerWidget { class ContainerListScreen extends HookConsumerWidget {
const ContainerListScreen(); const ContainerListScreen();
@@ -139,29 +37,36 @@ class ContainerListScreen extends HookConsumerWidget {
itemCount: containers.length, itemCount: containers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final container = containers[index]; final container = containers[index];
return _ContainerTile( return Slidable(
container,
key: ValueKey(container.id), key: ValueKey(container.id),
isSelected: container.id == selectedContainer, endActionPane: ActionPane(
onEdit: (edited) async { motion: const ScrollMotion(),
await ref children: [
.read(containerRepositoryProvider.notifier) SlidableAction(
.replaceContainer( onPressed: (context) async {
id: container.id, await ref
name: edited.name, .read(containerRepositoryProvider.notifier)
color: edited.color, .deleteContainer(container.id);
); },
}, backgroundColor:
onDelete: () async { Theme.of(context).colorScheme.errorContainer,
await ref foregroundColor: Theme.of(context)
.read(containerRepositoryProvider.notifier) .colorScheme
.deleteContainer(container.id); .onErrorContainer,
}, icon: Icons.delete,
onTap: () { label: 'Delete',
ref ),
.read(selectedContainerProvider.notifier) ],
.toggleContainer(container.id); ),
}, 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(), error: (error, stackTrace) => SizedBox.shrink(),
loading: () => ListView.builder( loading: () => ListView.builder(
itemCount: 3, itemCount: 3,
itemBuilder: (context, index) => _ContainerTile( itemBuilder: (context, index) => ContainerListTile(
ContainerData(id: 'null', color: Colors.transparent), ContainerData(id: 'null', color: Colors.transparent),
isSelected: false, isSelected: false,
onEdit: (_) {},
onDelete: () {},
onTap: () {},
), ),
), ),
), ),
@@ -188,17 +90,15 @@ class ContainerListScreen extends HookConsumerWidget {
await ref.read(unusedRandomContainerColorProvider.future); await ref.read(unusedRandomContainerColorProvider.future);
if (context.mounted) { if (context.mounted) {
final result = await showDialog<ContainerResult?>( final result = await context.push<ContainerData?>(
context: context, ContainerCreateRoute().location,
builder: (context) => ContainerDialog.create( extra: ContainerData(id: uuid.v7(), color: initialColor),
initialColor: initialColor,
),
); );
if (result != null) { if (result != null) {
await ref await ref
.read(containerRepositoryProvider.notifier) .read(containerRepositoryProvider.notifier)
.addContainer(name: result.name, color: result.color); .addContainer(result);
} }
} }
}, },
@@ -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<Color>(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<Color?>(context);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
Navigator.pop<Color?>(context, selectedColor.value);
},
child: const Text('Select'),
),
],
);
}
}
@@ -5,8 +5,11 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/providers.dart'; import 'package:lensai/core/providers.dart';
import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/data/models/drag_data.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/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/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/geckoview/features/tabs/domain/repositories/tab.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/hooks/overlay_portal_controller.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart'; import 'package:lensai/presentation/widgets/selectable_chips.dart';
@@ -90,12 +93,37 @@ class ContainerChips extends HookConsumerWidget {
overlayController.hide(); overlayController.hide();
}, },
onAcceptWithDetails: (details) async { onAcceptWithDetails: (details) async {
await ref final containerId = await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.assignContainer( .containerTabId(details.data.tabId);
details.data.tabId,
container.id, 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; dragTargetTabId.value = null;
overlayController.hide(); overlayController.hide();
@@ -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<Color>(initialColor);
final textController = useTextEditingController(text: initialName);
return SimpleDialog(
titlePadding: const EdgeInsets.fromLTRB(24.0, 24.0, 24.0, 16.0),
contentPadding: const EdgeInsets.only(
left: 20.0,
right: 20.0,
bottom: 24.0,
),
insetPadding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 24.0,
),
title: Text(
switch (_mode) {
_DialogMode.create => 'New 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<ContainerResult?>(context);
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () {
final name = textController.text.trim();
Navigator.pop<ContainerResult?>(
context,
(
name: name.isNotEmpty ? name : null,
color: selectedColor.value,
),
);
},
child: Text(
switch (_mode) {
_DialogMode.create => 'Add',
_DialogMode.edit => 'Edit',
},
),
),
],
),
],
);
}
}
@@ -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,
),
);
}
}
@@ -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<String, PreferenceSettingGroup> deserializePreferenceSettingGroups(
PreferencePartition partition,
Map<String, dynamic> content,
) {
final parititonedContent = content[partition.key] as Map<String, dynamic>;
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<String, dynamic>).map(
(key, value) => MapEntry(
key,
PreferenceSetting.fromJson(value as Map<String, dynamic>),
),
),
),
),
);
}
@@ -17,7 +17,7 @@ class KagiChatService extends _$KagiChatService {
throw Exception('Not implemented !'); throw Exception('Not implemented !');
// final kagiSession = // final kagiSession =
// ref.read(settingsRepositoryProvider).valueOrNull?.kagiSession; // ref.read(generalSettingsRepositoryProvider).valueOrNull?.kagiSession;
// return Result.fromAsync( // return Result.fromAsync(
// () async { // () async {
@@ -25,7 +25,7 @@
// @override // @override
// Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
// final incognitoEnabled = ref.watch( // final incognitoEnabled = ref.watch(
// settingsRepositoryProvider.select((value) => value.incognitoMode), // generalSettingsRepositoryProvider.select((value) => value.incognitoMode),
// ); // );
// final researchVariant = ref.watch(activeResearchVariantProvider); // final researchVariant = ref.watch(activeResearchVariantProvider);
@@ -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/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder; 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/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/presentation/widgets/website_title_tile.dart';
import 'package:lensai/utils/uri_parser.dart' as uri_parser; import 'package:lensai/utils/uri_parser.dart' as uri_parser;
@@ -21,11 +21,7 @@ class _InputField extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch( final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
settingsRepositoryProvider.select(
(value) => value.incognitoMode,
),
);
return TextFormField( return TextFormField(
controller: controller, controller: controller,
@@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart'; import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart'; import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart'; import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart'; import 'package:lensai/features/bangs/domain/providers/search.dart';
@@ -223,9 +224,9 @@ class SearchScreen extends HookConsumerWidget {
result.extractedContent ?? result.fullContent; result.extractedContent ?? result.fullContent;
return ListTile( return ListTile(
title: (result.title != null) title: result.title.mapNotNull(
? MarkdownBody(data: result.title!) (title) => MarkdownBody(data: title),
: null, ),
subtitle: (!headHasMatch && bodyResult != null) subtitle: (!headHasMatch && bodyResult != null)
? MarkdownBody(data: bodyResult) ? MarkdownBody(data: bodyResult)
: null, : null,
@@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.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/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'; import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
class SearchField extends HookConsumerWidget { class SearchField extends HookConsumerWidget {
@@ -25,11 +25,7 @@ class SearchField extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch( final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
settingsRepositoryProvider.select(
(value) => value.incognitoMode,
),
);
final hasText = useListenableSelector( final hasText = useListenableSelector(
textEditingController, textEditingController,
@@ -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'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'save_settings.g.dart'; part 'save_settings.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SaveSettingsController extends _$SaveSettingsController { class SaveGeneralSettingsController extends _$SaveGeneralSettingsController {
@override @override
FutureOr<void> build() {} FutureOr<void> build() {}
Future<void> save(UpdateSettingsFunc updateSettings) async { Future<void> save(UpdateGeneralSettingsFunc updateSettings) async {
state = const AsyncLoading(); state = const AsyncLoading();
state = await AsyncValue.guard( state = await AsyncValue.guard(
() => ref () => ref
.read(settingsRepositoryProvider.notifier) .read(generalSettingsRepositoryProvider.notifier)
.updateSettings(updateSettings),
);
}
}
@Riverpod(keepAlive: true)
class SaveEngineSettingsController extends _$SaveEngineSettingsController {
@override
FutureOr<void> build() {}
Future<void> save(UpdateEngineSettingsFunc updateSettings) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref
.read(engineSettingsRepositoryProvider.notifier)
.updateSettings(updateSettings), .updateSettings(updateSettings),
); );
} }
@@ -6,22 +6,39 @@ part of 'save_settings.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$saveSettingsControllerHash() => String _$saveGeneralSettingsControllerHash() =>
r'7ffcef95b48dce1f80b56fb3580282ff3df240b4'; r'ad8b60839cbcb05a964f57e32fc5d9c1aba30377';
/// See also [SaveSettingsController]. /// See also [SaveGeneralSettingsController].
@ProviderFor(SaveSettingsController) @ProviderFor(SaveGeneralSettingsController)
final saveSettingsControllerProvider = final saveGeneralSettingsControllerProvider =
AsyncNotifierProvider<SaveSettingsController, void>.internal( AsyncNotifierProvider<SaveGeneralSettingsController, void>.internal(
SaveSettingsController.new, SaveGeneralSettingsController.new,
name: r'saveSettingsControllerProvider', name: r'saveGeneralSettingsControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$saveSettingsControllerHash, : _$saveGeneralSettingsControllerHash,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$SaveSettingsController = AsyncNotifier<void>; typedef _$SaveGeneralSettingsController = AsyncNotifier<void>;
String _$saveEngineSettingsControllerHash() =>
r'4ca0ee76a41a72a8e6eacdb0e39045c667390c00';
/// See also [SaveEngineSettingsController].
@ProviderFor(SaveEngineSettingsController)
final saveEngineSettingsControllerProvider =
AsyncNotifierProvider<SaveEngineSettingsController, void>.internal(
SaveEngineSettingsController.new,
name: r'saveEngineSettingsControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$saveEngineSettingsControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SaveEngineSettingsController = AsyncNotifier<void>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // 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
@@ -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',
),
],
);
},
),
);
}
}
@@ -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<ThemeMode>(
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'),
),
);
},
),
],
);
},
),
);
}
}
@@ -1,48 +1,15 @@
import 'package:fading_scroll/fading_scroll.dart'; import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.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:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.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 { class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({super.key}); 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 @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final settings = ref.watch(settingsRepositoryProvider);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Settings')), appBar: AppBar(title: const Text('Settings')),
body: FadingScroll( body: FadingScroll(
@@ -50,189 +17,61 @@ class SettingsScreen extends HookConsumerWidget {
builder: (context, controller) { builder: (context, controller) {
return ListView( return ListView(
controller: controller, controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: [ children: [
_buildSection(theme, 'General'),
Card( Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile( child: ListTile(
title: const Text('Browser Hardening'), title: const Text('General'),
trailing: IconButton( contentPadding: const EdgeInsets.symmetric(
onPressed: () async { vertical: 8.0,
await context.push(BrowserHardeningRoute().location); horizontal: 16.0,
},
icon: const Icon(Icons.chevron_right),
), ),
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<ThemeMode>(
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( const SizedBox(
height: 16, height: 8,
), ),
_buildSection(theme, 'Bangs'), Card(
CustomListTile( color: Theme.of(context).highlightColor,
title: 'Bang Frequencies', clipBehavior: Clip.antiAlias,
subtitle: 'Tracked usage for Bang recommendations', child: ListTile(
suffix: FilledButton.icon( title: const Text('Web Engine'),
onPressed: () async { contentPadding: const EdgeInsets.symmetric(
await ref vertical: 8.0,
.read(bangDataRepositoryProvider.notifier) horizontal: 16.0,
.resetFrequencies(); ),
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( const SizedBox(
builder: (context, ref, child) { height: 8,
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'),
),
);
},
), ),
_buildSubSection(theme, 'Repositories'), Card(
const BangGroupListTile( color: Theme.of(context).highlightColor,
group: BangGroup.general, clipBehavior: Clip.antiAlias,
title: 'General Bangs', child: ListTile(
subtitle: 'Automatically syncs every 7 days', title: const Text('Bangs'),
), contentPadding: const EdgeInsets.symmetric(
const BangGroupListTile( vertical: 8.0,
group: BangGroup.assistant, horizontal: 16.0,
title: 'Assistant Bangs', ),
subtitle: 'Automatically syncs every 7 days', leading: const Icon(MdiIcons.exclamationThick),
), trailing: const Icon(Icons.chevron_right),
const BangGroupListTile( onTap: () async {
group: BangGroup.kagi, await context.push(BangSettingsRoute().location);
title: 'Kagi Bangs', },
subtitle: 'Automatically syncs every 7 days', ),
), ),
], ],
); );
@@ -4,17 +4,20 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart'; import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/data/models/equatable_iterable.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/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/features/settings/presentation/widgets/hardening_group_icon.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
class BrowserHardeningScreen extends HookConsumerWidget { class WebEngineHardeningScreen extends HookConsumerWidget {
const BrowserHardeningScreen(); const WebEngineHardeningScreen();
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final preferenceGroups = final preferenceGroups = ref.watch(
ref.watch(preferenceSettingsGeneralRepositoryProvider); unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user),
);
final allGroupsActive = useMemoized( final allGroupsActive = useMemoized(
() => () =>
@@ -28,7 +31,7 @@ class BrowserHardeningScreen extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Browser Hardening')), appBar: AppBar(title: const Text('Web Engine Hardening')),
body: preferenceGroups.when( body: preferenceGroups.when(
data: (data) { data: (data) {
return Column( return Column(
@@ -49,7 +52,9 @@ class BrowserHardeningScreen extends HookConsumerWidget {
), ),
onChanged: (value) async { onChanged: (value) async {
final notifier = ref.read( final notifier = ref.read(
preferenceSettingsGeneralRepositoryProvider.notifier, unifiedPreferenceSettingsRepositoryProvider(
PreferencePartition.user,
).notifier,
); );
if (value) { if (value) {
@@ -71,9 +76,9 @@ class BrowserHardeningScreen extends HookConsumerWidget {
Expanded( Expanded(
child: ListTile( child: ListTile(
title: Text(group.key), title: Text(group.key),
subtitle: (group.value.description != null) subtitle: group.value.description.mapNotNull(
? Text(group.value.description!) (description) => Text(description),
: null, ),
leading: HardeningGroupIcon( leading: HardeningGroupIcon(
isActive: group.value.isActive, isActive: group.value.isActive,
isPartlyActive: group.value.isPartlyActive, isPartlyActive: group.value.isPartlyActive,
@@ -81,7 +86,7 @@ class BrowserHardeningScreen extends HookConsumerWidget {
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () async { onTap: () async {
await context.push( await context.push(
BrowserHardeningGroupRoute( WebEngineHardeningGroupRoute(
group: group.key, group: group.key,
).location, ).location,
); );
@@ -100,8 +105,11 @@ class BrowserHardeningScreen extends HookConsumerWidget {
error: (error, stackTrace) => FailureWidget( error: (error, stackTrace) => FailureWidget(
title: 'Could not load preference settings', title: 'Could not load preference settings',
exception: error, exception: error,
onRetry: () => onRetry: () => ref.refresh(
ref.refresh(preferenceSettingsGeneralRepositoryProvider), unifiedPreferenceSettingsRepositoryProvider(
PreferencePartition.user,
),
),
), ),
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
), ),
@@ -1,18 +1,24 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.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/features/settings/presentation/widgets/hardening_group_icon.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart'; import 'package:lensai/presentation/widgets/failure_widget.dart';
class BrowserHardeningGroupScreen extends HookConsumerWidget { class WebEngineHardeningGroupScreen extends HookConsumerWidget {
final String groupName; final String groupName;
const BrowserHardeningGroupScreen({super.key, required this.groupName}); const WebEngineHardeningGroupScreen({super.key, required this.groupName});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final settings = final settings = ref.watch(
ref.watch(preferenceSettingsGroupRepositoryProvider(groupName)); preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
),
);
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -36,18 +42,20 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
), ),
), ),
subtitle: (group.description != null) subtitle: group.description.mapNotNull(
? Text( (description) => Text(
group.description!, description,
style: TextStyle( style: TextStyle(
color: theme.colorScheme.onPrimaryContainer, color: theme.colorScheme.onPrimaryContainer,
), ),
) ),
: null, ),
onChanged: (value) async { onChanged: (value) async {
final notifier = ref.read( final notifier = ref.read(
preferenceSettingsGroupRepositoryProvider(groupName) preferenceSettingsGroupRepositoryProvider(
.notifier, PreferencePartition.user,
groupName,
).notifier,
); );
if (value) { if (value) {
@@ -71,16 +79,17 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
Expanded( Expanded(
child: SwitchListTile( child: SwitchListTile(
value: setting.value.isActive, value: setting.value.isActive,
title: Text(setting.value.title), title: Text(setting.value.title ?? setting.key),
subtitle: (setting.value.description != null) subtitle: setting.value.description.mapNotNull(
? Text(setting.value.description!) (description) => Text(description),
: null, ),
secondary: HardeningGroupIcon( secondary: HardeningGroupIcon(
isActive: setting.value.isActive, isActive: setting.value.isActive,
), ),
onChanged: (value) async { onChanged: (value) async {
final notifier = ref.read( final notifier = ref.read(
preferenceSettingsGroupRepositoryProvider( preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName, groupName,
).notifier, ).notifier,
); );
@@ -96,16 +105,16 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
else else
Expanded( Expanded(
child: ListTile( child: ListTile(
title: Text(setting.value.title), title: Text(setting.value.title ?? setting.key),
subtitle: (setting.value.description != null) subtitle: setting.value.description.mapNotNull(
? Text(setting.value.description!) (description) => Text(description),
: null, ),
leading: HardeningGroupIcon( leading: HardeningGroupIcon(
isActive: setting.value.isActive, isActive: setting.value.isActive,
), ),
trailing: Padding( trailing: const Padding(
padding: const EdgeInsets.only(right: 18.0), padding: EdgeInsets.only(right: 18.0),
child: const Icon(Icons.check), child: Icon(Icons.check),
), ),
), ),
), ),
@@ -121,8 +130,12 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
error: (error, stackTrace) => FailureWidget( error: (error, stackTrace) => FailureWidget(
title: 'Could not load preference settings', title: 'Could not load preference settings',
exception: error, exception: error,
onRetry: () => onRetry: () => ref.refresh(
ref.refresh(preferenceSettingsGroupRepositoryProvider(groupName)), preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
),
),
), ),
loading: () => const SizedBox.shrink(), loading: () => const SizedBox.shrink(),
), ),
@@ -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<HttpsOnlyMode>(
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<TrackingProtectionPolicy>.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<TrackingProtectionPolicy>.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<TrackingProtectionPolicy>.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<TrackingProtectionPolicy>.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);
},
),
],
);
},
),
);
}
}
@@ -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),
),
);
}
}
@@ -1,4 +1,7 @@
import 'dart:convert';
import 'package:drift/drift.dart'; import 'package:drift/drift.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/user/data/database/database.dart'; import 'package:lensai/features/user/data/database/database.dart';
part 'setting.g.dart'; part 'setting.g.dart';
@@ -7,12 +10,16 @@ part 'setting.g.dart';
class SettingDao extends DatabaseAccessor<UserDatabase> with _$SettingDaoMixin { class SettingDao extends DatabaseAccessor<UserDatabase> with _$SettingDaoMixin {
SettingDao(super.attachedDatabase); SettingDao(super.attachedDatabase);
Future<int> updateSetting(String key, Object? value) { Future<int> updateSetting(String key, String? partitionKey, Object? value) {
final driftvalue = (value != null) ? DriftAny(value) : null; final normalizedValue = (value is Iterable) ? jsonEncode(value) : value;
final driftvalue = normalizedValue
.mapNotNull((normalizedValue) => DriftAny(normalizedValue));
return db.setting.insertOne( return db.setting.insertOne(
SettingCompanion.insert( SettingCompanion.insert(
key: key, key: key,
partitionKey: Value(partitionKey),
value: Value(driftvalue), value: Value(driftvalue),
), ),
onConflict: DoUpdate( onConflict: DoUpdate(
@@ -21,7 +28,12 @@ class SettingDao extends DatabaseAccessor<UserDatabase> with _$SettingDaoMixin {
); );
} }
Selectable<MapEntry<String, DriftAny?>> allSettings() { Selectable<MapEntry<String, DriftAny?>> allSettingsOfPartitionKey(
return db.setting.select().map((row) => MapEntry(row.key, row.value)); String? partitionKey,
) {
final query = db.setting.select()
..where((r) => r.partitionKey.equalsNullable(partitionKey));
return query.map((row) => MapEntry(row.key, row.value));
} }
} }
@@ -4,12 +4,15 @@ import 'package:lensai/features/user/data/database/daos/setting.dart';
part 'database.g.dart'; part 'database.g.dart';
@DriftDatabase(include: { @DriftDatabase(
'database.drift' include: {
}, daos: [ 'database.drift',
SettingDao, },
CacheDao, daos: [
]) SettingDao,
CacheDao,
],
)
class UserDatabase extends _$UserDatabase { class UserDatabase extends _$UserDatabase {
@override @override
final int schemaVersion = 1; final int schemaVersion = 1;
@@ -1,5 +1,6 @@
CREATE TABLE setting ( CREATE TABLE setting (
"key" TEXT PRIMARY KEY NOT NULL, "key" TEXT PRIMARY KEY NOT NULL,
partition_key TEXT,
"value" ANY "value" ANY
) STRICT; ) STRICT;
@@ -13,13 +13,18 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
type: DriftSqlType.string, type: DriftSqlType.string,
requiredDuringInsert: true, requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL'); $customConstraints: 'PRIMARY KEY NOT NULL');
late final GeneratedColumn<String> partitionKey = GeneratedColumn<String>(
'partition_key', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '');
late final GeneratedColumn<DriftAny> value = GeneratedColumn<DriftAny>( late final GeneratedColumn<DriftAny> value = GeneratedColumn<DriftAny>(
'value', aliasedName, true, 'value', aliasedName, true,
type: DriftSqlType.any, type: DriftSqlType.any,
requiredDuringInsert: false, requiredDuringInsert: false,
$customConstraints: ''); $customConstraints: '');
@override @override
List<GeneratedColumn> get $columns => [key, value]; List<GeneratedColumn> get $columns => [key, partitionKey, value];
@override @override
String get aliasedName => _alias ?? actualTableName; String get aliasedName => _alias ?? actualTableName;
@override @override
@@ -33,6 +38,8 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
return SettingData( return SettingData(
key: attachedDatabase.typeMapping key: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}key'])!, .read(DriftSqlType.string, data['${effectivePrefix}key'])!,
partitionKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}partition_key']),
value: attachedDatabase.typeMapping value: attachedDatabase.typeMapping
.read(DriftSqlType.any, data['${effectivePrefix}value']), .read(DriftSqlType.any, data['${effectivePrefix}value']),
); );
@@ -51,12 +58,16 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
class SettingData extends DataClass implements Insertable<SettingData> { class SettingData extends DataClass implements Insertable<SettingData> {
final String key; final String key;
final String? partitionKey;
final DriftAny? value; final DriftAny? value;
const SettingData({required this.key, this.value}); const SettingData({required this.key, this.partitionKey, this.value});
@override @override
Map<String, Expression> toColumns(bool nullToAbsent) { Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{}; final map = <String, Expression>{};
map['key'] = Variable<String>(key); map['key'] = Variable<String>(key);
if (!nullToAbsent || partitionKey != null) {
map['partition_key'] = Variable<String>(partitionKey);
}
if (!nullToAbsent || value != null) { if (!nullToAbsent || value != null) {
map['value'] = Variable<DriftAny>(value); map['value'] = Variable<DriftAny>(value);
} }
@@ -68,6 +79,7 @@ class SettingData extends DataClass implements Insertable<SettingData> {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return SettingData( return SettingData(
key: serializer.fromJson<String>(json['key']), key: serializer.fromJson<String>(json['key']),
partitionKey: serializer.fromJson<String?>(json['partition_key']),
value: serializer.fromJson<DriftAny?>(json['value']), value: serializer.fromJson<DriftAny?>(json['value']),
); );
} }
@@ -76,19 +88,27 @@ class SettingData extends DataClass implements Insertable<SettingData> {
serializer ??= driftRuntimeOptions.defaultSerializer; serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{ return <String, dynamic>{
'key': serializer.toJson<String>(key), 'key': serializer.toJson<String>(key),
'partition_key': serializer.toJson<String?>(partitionKey),
'value': serializer.toJson<DriftAny?>(value), 'value': serializer.toJson<DriftAny?>(value),
}; };
} }
SettingData copyWith( SettingData copyWith(
{String? key, Value<DriftAny?> value = const Value.absent()}) => {String? key,
Value<String?> partitionKey = const Value.absent(),
Value<DriftAny?> value = const Value.absent()}) =>
SettingData( SettingData(
key: key ?? this.key, key: key ?? this.key,
partitionKey:
partitionKey.present ? partitionKey.value : this.partitionKey,
value: value.present ? value.value : this.value, value: value.present ? value.value : this.value,
); );
SettingData copyWithCompanion(SettingCompanion data) { SettingData copyWithCompanion(SettingCompanion data) {
return SettingData( return SettingData(
key: data.key.present ? data.key.value : this.key, 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, value: data.value.present ? data.value.value : this.value,
); );
} }
@@ -97,51 +117,62 @@ class SettingData extends DataClass implements Insertable<SettingData> {
String toString() { String toString() {
return (StringBuffer('SettingData(') return (StringBuffer('SettingData(')
..write('key: $key, ') ..write('key: $key, ')
..write('partitionKey: $partitionKey, ')
..write('value: $value') ..write('value: $value')
..write(')')) ..write(')'))
.toString(); .toString();
} }
@override @override
int get hashCode => Object.hash(key, value); int get hashCode => Object.hash(key, partitionKey, value);
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
(other is SettingData && (other is SettingData &&
other.key == this.key && other.key == this.key &&
other.partitionKey == this.partitionKey &&
other.value == this.value); other.value == this.value);
} }
class SettingCompanion extends UpdateCompanion<SettingData> { class SettingCompanion extends UpdateCompanion<SettingData> {
final Value<String> key; final Value<String> key;
final Value<String?> partitionKey;
final Value<DriftAny?> value; final Value<DriftAny?> value;
final Value<int> rowid; final Value<int> rowid;
const SettingCompanion({ const SettingCompanion({
this.key = const Value.absent(), this.key = const Value.absent(),
this.partitionKey = const Value.absent(),
this.value = const Value.absent(), this.value = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}); });
SettingCompanion.insert({ SettingCompanion.insert({
required String key, required String key,
this.partitionKey = const Value.absent(),
this.value = const Value.absent(), this.value = const Value.absent(),
this.rowid = const Value.absent(), this.rowid = const Value.absent(),
}) : key = Value(key); }) : key = Value(key);
static Insertable<SettingData> custom({ static Insertable<SettingData> custom({
Expression<String>? key, Expression<String>? key,
Expression<String>? partitionKey,
Expression<DriftAny>? value, Expression<DriftAny>? value,
Expression<int>? rowid, Expression<int>? rowid,
}) { }) {
return RawValuesInsertable({ return RawValuesInsertable({
if (key != null) 'key': key, if (key != null) 'key': key,
if (partitionKey != null) 'partition_key': partitionKey,
if (value != null) 'value': value, if (value != null) 'value': value,
if (rowid != null) 'rowid': rowid, if (rowid != null) 'rowid': rowid,
}); });
} }
SettingCompanion copyWith( SettingCompanion copyWith(
{Value<String>? key, Value<DriftAny?>? value, Value<int>? rowid}) { {Value<String>? key,
Value<String?>? partitionKey,
Value<DriftAny?>? value,
Value<int>? rowid}) {
return SettingCompanion( return SettingCompanion(
key: key ?? this.key, key: key ?? this.key,
partitionKey: partitionKey ?? this.partitionKey,
value: value ?? this.value, value: value ?? this.value,
rowid: rowid ?? this.rowid, rowid: rowid ?? this.rowid,
); );
@@ -153,6 +184,9 @@ class SettingCompanion extends UpdateCompanion<SettingData> {
if (key.present) { if (key.present) {
map['key'] = Variable<String>(key.value); map['key'] = Variable<String>(key.value);
} }
if (partitionKey.present) {
map['partition_key'] = Variable<String>(partitionKey.value);
}
if (value.present) { if (value.present) {
map['value'] = Variable<DriftAny>(value.value); map['value'] = Variable<DriftAny>(value.value);
} }
@@ -166,6 +200,7 @@ class SettingCompanion extends UpdateCompanion<SettingData> {
String toString() { String toString() {
return (StringBuffer('SettingCompanion(') return (StringBuffer('SettingCompanion(')
..write('key: $key, ') ..write('key: $key, ')
..write('partitionKey: $partitionKey, ')
..write('value: $value, ') ..write('value: $value, ')
..write('rowid: $rowid') ..write('rowid: $rowid')
..write(')')) ..write(')'))
@@ -396,11 +431,13 @@ abstract class _$UserDatabase extends GeneratedDatabase {
typedef $SettingCreateCompanionBuilder = SettingCompanion Function({ typedef $SettingCreateCompanionBuilder = SettingCompanion Function({
required String key, required String key,
Value<String?> partitionKey,
Value<DriftAny?> value, Value<DriftAny?> value,
Value<int> rowid, Value<int> rowid,
}); });
typedef $SettingUpdateCompanionBuilder = SettingCompanion Function({ typedef $SettingUpdateCompanionBuilder = SettingCompanion Function({
Value<String> key, Value<String> key,
Value<String?> partitionKey,
Value<DriftAny?> value, Value<DriftAny?> value,
Value<int> rowid, Value<int> rowid,
}); });
@@ -416,6 +453,9 @@ class $SettingFilterComposer extends Composer<_$UserDatabase, Setting> {
ColumnFilters<String> get key => $composableBuilder( ColumnFilters<String> get key => $composableBuilder(
column: $table.key, builder: (column) => ColumnFilters(column)); column: $table.key, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get partitionKey => $composableBuilder(
column: $table.partitionKey, builder: (column) => ColumnFilters(column));
ColumnFilters<DriftAny> get value => $composableBuilder( ColumnFilters<DriftAny> get value => $composableBuilder(
column: $table.value, builder: (column) => ColumnFilters(column)); column: $table.value, builder: (column) => ColumnFilters(column));
} }
@@ -431,6 +471,10 @@ class $SettingOrderingComposer extends Composer<_$UserDatabase, Setting> {
ColumnOrderings<String> get key => $composableBuilder( ColumnOrderings<String> get key => $composableBuilder(
column: $table.key, builder: (column) => ColumnOrderings(column)); column: $table.key, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get partitionKey => $composableBuilder(
column: $table.partitionKey,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DriftAny> get value => $composableBuilder( ColumnOrderings<DriftAny> get value => $composableBuilder(
column: $table.value, builder: (column) => ColumnOrderings(column)); column: $table.value, builder: (column) => ColumnOrderings(column));
} }
@@ -446,6 +490,9 @@ class $SettingAnnotationComposer extends Composer<_$UserDatabase, Setting> {
GeneratedColumn<String> get key => GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column); $composableBuilder(column: $table.key, builder: (column) => column);
GeneratedColumn<String> get partitionKey => $composableBuilder(
column: $table.partitionKey, builder: (column) => column);
GeneratedColumn<DriftAny> get value => GeneratedColumn<DriftAny> get value =>
$composableBuilder(column: $table.value, builder: (column) => column); $composableBuilder(column: $table.value, builder: (column) => column);
} }
@@ -474,21 +521,25 @@ class $SettingTableManager extends RootTableManager<
$SettingAnnotationComposer($db: db, $table: table), $SettingAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({ updateCompanionCallback: ({
Value<String> key = const Value.absent(), Value<String> key = const Value.absent(),
Value<String?> partitionKey = const Value.absent(),
Value<DriftAny?> value = const Value.absent(), Value<DriftAny?> value = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
SettingCompanion( SettingCompanion(
key: key, key: key,
partitionKey: partitionKey,
value: value, value: value,
rowid: rowid, rowid: rowid,
), ),
createCompanionCallback: ({ createCompanionCallback: ({
required String key, required String key,
Value<String?> partitionKey = const Value.absent(),
Value<DriftAny?> value = const Value.absent(), Value<DriftAny?> value = const Value.absent(),
Value<int> rowid = const Value.absent(), Value<int> rowid = const Value.absent(),
}) => }) =>
SettingCompanion.insert( SettingCompanion.insert(
key: key, key: key,
partitionKey: partitionKey,
value: value, value: value,
rowid: rowid, rowid: rowid,
), ),
@@ -1,4 +1,5 @@
import 'package:copy_with_extension/copy_with_extension.dart'; 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:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
@@ -6,18 +7,45 @@ part 'engine_settings.g.dart';
@CopyWith() @CopyWith()
@JsonSerializable(includeIfNull: true, constructor: 'withDefaults') @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({ EngineSettings({
super.javascriptEnabled, required super.javascriptEnabled,
super.trackingProtectionPolicy, required super.trackingProtectionPolicy,
super.httpsOnlyMode, required super.httpsOnlyMode,
super.globalPrivacyControlEnabled, required super.globalPrivacyControlEnabled,
super.preferredColorScheme, required super.preferredColorScheme,
super.cookieBannerHandlingMode, required super.cookieBannerHandlingMode,
super.cookieBannerHandlingModePrivateBrowsing, required super.cookieBannerHandlingModePrivateBrowsing,
super.cookieBannerHandlingGlobalRules, required super.cookieBannerHandlingGlobalRules,
super.cookieBannerHandlingGlobalRulesSubFrames, required super.cookieBannerHandlingGlobalRulesSubFrames,
super.webContentIsolationStrategy, required super.webContentIsolationStrategy,
}); });
EngineSettings.withDefaults({ EngineSettings.withDefaults({
@@ -50,4 +78,26 @@ class EngineSettings extends GeckoEngineSettings {
webContentIsolationStrategy: webContentIsolationStrategy ?? webContentIsolationStrategy: webContentIsolationStrategy ??
WebContentIsolationStrategy.isolateHighValue, WebContentIsolationStrategy.isolateHighValue,
); );
factory EngineSettings.fromJson(Map<String, dynamic> json) =>
_$EngineSettingsFromJson(json);
Map<String, dynamic> toJson() => _$EngineSettingsToJson(this);
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
super.javascriptEnabled,
super.trackingProtectionPolicy,
super.httpsOnlyMode,
super.globalPrivacyControlEnabled,
super.preferredColorScheme,
super.cookieBannerHandlingMode,
super.cookieBannerHandlingModePrivateBrowsing,
super.cookieBannerHandlingGlobalRules,
super.cookieBannerHandlingGlobalRulesSubFrames,
super.webContentIsolationStrategy,
];
} }
@@ -225,22 +225,22 @@ Map<String, dynamic> _$EngineSettingsToJson(EngineSettings instance) =>
<String, dynamic>{ <String, dynamic>{
'javascriptEnabled': instance.javascriptEnabled, 'javascriptEnabled': instance.javascriptEnabled,
'trackingProtectionPolicy': 'trackingProtectionPolicy':
_$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy], _$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy]!,
'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode], 'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode]!,
'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled,
'preferredColorScheme': 'preferredColorScheme':
_$ColorSchemeEnumMap[instance.preferredColorScheme], _$ColorSchemeEnumMap[instance.preferredColorScheme]!,
'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled,
'cookieBannerHandlingMode': 'cookieBannerHandlingMode':
_$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode], _$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode]!,
'cookieBannerHandlingModePrivateBrowsing': 'cookieBannerHandlingModePrivateBrowsing':
_$CookieBannerHandlingModeEnumMap[ _$CookieBannerHandlingModeEnumMap[
instance.cookieBannerHandlingModePrivateBrowsing], instance.cookieBannerHandlingModePrivateBrowsing]!,
'cookieBannerHandlingGlobalRules': 'cookieBannerHandlingGlobalRules':
instance.cookieBannerHandlingGlobalRules, instance.cookieBannerHandlingGlobalRules,
'cookieBannerHandlingGlobalRulesSubFrames': 'cookieBannerHandlingGlobalRulesSubFrames':
instance.cookieBannerHandlingGlobalRulesSubFrames, instance.cookieBannerHandlingGlobalRulesSubFrames,
'webContentIsolationStrategy': _$WebContentIsolationStrategyEnumMap[ 'webContentIsolationStrategy': _$WebContentIsolationStrategyEnumMap[
instance.webContentIsolationStrategy], instance.webContentIsolationStrategy]!,
}; };
const _$TrackingProtectionPolicyEnumMap = { const _$TrackingProtectionPolicyEnumMap = {
@@ -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', 'Youll 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<DeleteBrowsingDataType>? 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<String, dynamic> json) =>
_$GeneralSettingsFromJson(json);
Map<String, dynamic> toJson() => _$GeneralSettingsToJson(this);
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
themeMode,
enableReadability,
deleteBrowsingDataOnQuit,
];
}
@@ -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<DeleteBrowsingDataType>? 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<DeleteBrowsingDataType>? 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<DeleteBrowsingDataType>? 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<DeleteBrowsingDataType>?,
);
}
}
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<String, dynamic> json) =>
GeneralSettings.withDefaults(
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
enableReadability: json['enableReadability'] as bool?,
deleteBrowsingDataOnQuit:
(json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e))
.toSet(),
);
Map<String, dynamic> _$GeneralSettingsToJson(GeneralSettings instance) =>
<String, dynamic>{
'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',
};
@@ -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<String, dynamic> json) =>
_$SettingsFromJson(json);
Map<String, dynamic> toJson() => _$SettingsToJson(this);
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
incognitoMode,
enableJavascript,
blockHttpProtocol,
themeMode,
enableReadability,
];
}
@@ -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<String, dynamic> 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<String, dynamic> _$SettingsToJson(Settings instance) => <String, dynamic>{
'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',
};
@@ -1,5 +1,6 @@
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:lensai/features/user/data/providers.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:pocketbase/pocketbase.dart';
import 'package:riverpod/riverpod.dart'; import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.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); return PocketBase('http://192.168.2.104:8090', authStore: authStore);
} }
@Riverpod()
bool incognitoModeEnabled(Ref ref) {
return ref.watch(
generalSettingsRepositoryProvider
.select((value) => value.deleteBrowsingDataOnQuit != null),
);
}

Some files were not shown because too many files have changed in this diff Show More