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
analyzer:
errors:
overridden_fields: ignore
plugins:
- custom_lint
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": {
"Safe Browsing": {
"description": "Google Safe Browsing is a service from Google that warns users when they attempt to navigate to a dangerous website or download dangerous files.",
@@ -114,6 +84,11 @@
"value": true,
"title": "Force WebRTC inside the proxy"
},
"media.peerconnection.enabled": {
"value": false,
"title": "Disable WebRTC PeerConnection",
"description": "Prevent exposing local IP Address"
},
"media.peerconnection.ice.default_address_only": {
"value": true,
"title": "Force a single network interface for ICE candidates generation",
@@ -121,30 +96,6 @@
}
}
},
"Disk Avoidance": {
"description": "Control what data to persist to disk",
"preferences": {
"browser.cache.disk.enable": {
"value": false,
"title": "Disable disk cache",
"description": "Don't cache data to disk. Might impact performance negatively",
"requireUserOptIn": true
},
"browser.privatebrowsing.forceMediaMemoryCache": {
"value": true,
"title": "Set media cache in Private Browsing to in-memory"
},
"media.memory_cache_max_size": {
"value": 65536,
"title": "Increase in-memory media cache maximum size"
},
"browser.sessionstore.privacy_level": {
"value": 2,
"title": "Disable storing extra session data",
"description": "Extra session data such as form content, cookies and POST data"
}
}
},
"Secure Connections": {
"description": "SSL/TLS / OCSP / CERTS / HPKP",
"preferences": {
@@ -202,6 +153,135 @@
}
}
},
"Privacy": {
"preferences": {
"network.http.referer.XOriginTrimmingPolicy": {
"value": 2,
"title": "Trim cross-origin referrers",
"description": "Trims down referrers to just the scheme, hostname and port"
},
"network.http.referer.XOriginPolicy": {
"value": 2,
"title": "Only send cross-origin referer if host matches"
},
"network.http.referer.disallowCrossSiteRelaxingDefault": {
"value": true,
"title": "Disallow relaxing referrer policy for cross-site requests"
},
"network.http.referer.disallowCrossSiteRelaxingDefault.top_navigation": {
"value": true,
"title": "Disallow relaxing referrer policy for cross-site requests",
"description": "On top navigation"
},
"general.useragent.updates.enabled": {
"value": false,
"title": "Disable automatic User Agent updates",
"description": "This preference controls whether Firefox automatically updates its User-Agent string. When set to false, it prevents Firefox from automatically updating the browser's User-Agent string, which is a text identifier that tells websites which browser and version you're using."
},
"privacy.query_stripping.enabled": {
"value": true,
"title": "Strip known tracking query parameters from URLs"
},
"dom.private-attribution.submission.enabled": {
"value": false,
"title": "Disable Private Attribution Submission Control",
"shouldBeDefault": true
},
"privacy.trackingprotection.enabled": {
"value": true,
"title": "Enable Tracking Protection"
},
"privacy.trackingprotection.socialtracking.enabled": {
"value": true,
"title": "Enable Social Tracking Protection"
},
"privacy.trackingprotection.cryptomining.enabled": {
"value": true,
"title": "Enable Cryptomining Protection"
},
"privacy.trackingprotection.fingerprinting.enabled": {
"value": true,
"title": "Enable Fingerprinting Tracking Protection"
}
}
},
"Resist Fingerprinting": {
"description": "Advanced Fingerprinting protection. This might break websites. For best results make sure to enable \"Privacy\" hardenings.",
"preferences": {
"privacy.resistFingerprinting": {
"value": true,
"title": "Resist Fingerprinting"
},
"privacy.resistFingerprinting.letterboxing": {
"value": true,
"title": "Enable Letterboxing"
},
"webgl.disabled": {
"value": true,
"title": "Disable WebGL (Web Graphics Library)"
},
"privacy.partition.network_state.ocsp_cache": {
"value": true,
"title": "Enable OCSP cache"
}
}
},
"Attack Surface Reduction": {
"preferences": {
"pdfjs.enableScripting": {
"value": false,
"title": "Disable PDFJS scripting"
},
"mathml.disabled": {
"value": true,
"title": "Disable MathML (Mathematical Markup Language)"
},
"gfx.font_rendering.graphite.enabled": {
"value": false,
"title": "Disable graphite"
},
"javascript.options.ion": {
"value": false,
"title": "Disable Ion"
},
"javascript.options.baselinejit": {
"value": false,
"title": "Disable baseline JIT"
},
"javascript.options.jit_trustedprincipals": {
"value": false,
"title": "Disable JIT trustedprincipals"
},
"javascript.options.asmjs": {
"value": false,
"title": "Disable asm.js"
}
}
},
"Disk Avoidance": {
"description": "Control what data to persist to disk",
"preferences": {
"browser.cache.disk.enable": {
"value": false,
"title": "Disable disk cache",
"description": "Don't cache data to disk. Might impact performance negatively",
"requireUserOptIn": true
},
"browser.privatebrowsing.forceMediaMemoryCache": {
"value": true,
"title": "Set media cache in Private Browsing to in-memory"
},
"media.memory_cache_max_size": {
"value": 65536,
"title": "Increase in-memory media cache maximum size"
},
"browser.sessionstore.privacy_level": {
"value": 2,
"title": "Disable storing extra session data",
"description": "Extra session data such as form content, cookies and POST data"
}
}
},
"Miscelaneous": {
"preferences": {
"permissions.manager.defaultsUrl": {
@@ -212,48 +292,13 @@
"value": true,
"title": "Use Punycode in Internationalized Domain Names to eliminate possible spoofing"
},
"extensions.enabledScopes": {
"value": 5,
"title": "Limit allowed extension directories",
"description": "Only allow profile and application directories",
"shouldBeDefault": true
},
"extensions.postDownloadThirdPartyPrompt": {
"value": false,
"title": "Disable bypassing 3rd party extension install prompts"
},
"pdfjs.enableScripting": {
"layout.css.visited_links_enabled": {
"value": false,
"title": "Disable PDFJS scripting"
}
}
},
"Privacy": {
"preferences": {
"privacy.globalprivacycontrol.enabled": {
"value": true,
"title": "Enable Global Privacy Control (GPC)",
"shouldBeDefault": true
},
"browser.contentblocking.category": {
"value": "strict",
"title": "Enable ETP Strict Mode",
"description": "ETP Strict Mode enables Total Cookie Protection (TCP)"
},
"network.http.referer.XOriginTrimmingPolicy": {
"value": 2,
"title": "Trim cross-origin referrers",
"description": "Trims down referrers to just the scheme, hostname and port"
},
"general.useragent.updates.enabled": {
"value": false,
"title": "Disable automatic User Agent updates",
"description": "This preference controls whether Firefox automatically updates its User-Agent string. When set to false, it prevents Firefox from automatically updating the browser's User-Agent string, which is a text identifier that tells websites which browser and version you're using."
},
"dom.private-attribution.submission.enabled": {
"value": false,
"title": "Disable Private Attribution Submission Control",
"shouldBeDefault": true
"title": "Disable coloring of visited links"
}
}
}
+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/geckoview/features/browser/presentation/dialogs/web_page_dialog.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/screens/browser.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_edit.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/screens/container_list.dart';
import 'package:lensai/features/search/presentation/screens/search.dart';
import 'package:lensai/features/settings/presentation/screens/browser_hardening.dart';
import 'package:lensai/features/settings/presentation/screens/browser_hardening_group.dart';
import 'package:lensai/features/settings/presentation/screens/bang_settings.dart';
import 'package:lensai/features/settings/presentation/screens/general_settings.dart';
import 'package:lensai/features/settings/presentation/screens/settings.dart';
import 'package:lensai/features/settings/presentation/screens/web_engine_hardening.dart';
import 'package:lensai/features/settings/presentation/screens/web_engine_hardening_group.dart';
import 'package:lensai/features/settings/presentation/screens/web_engine_settings.dart';
import 'package:lensai/features/user/presentation/screens/auth.dart';
part 'routes.g.dart';
@@ -63,6 +68,16 @@ part 'routes.g.dart';
TypedGoRoute<ContainerListRoute>(
name: 'ContainerListRoute',
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>(
name: 'SettingsRoute',
path: '/settings',
routes: [
TypedGoRoute<BrowserHardeningRoute>(
name: 'BrowserHardeningRoute',
path: 'hardening',
TypedGoRoute<GeneralSettingsRoute>(
name: 'GeneralSettingsRoute',
path: 'general',
),
TypedGoRoute<BangSettingsRoute>(
name: 'BangSettingsRoute',
path: 'bang',
),
TypedGoRoute<WebEngineSettingsRoute>(
name: 'WebEngineSettingsRoute',
path: 'web_engine',
routes: [
TypedGoRoute<BrowserHardeningGroupRoute>(
name: 'BrowserHardeningGroupRoute',
path: 'group/:group',
TypedGoRoute<WebEngineHardeningRoute>(
name: 'WebEngineHardeningRoute',
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
Widget build(BuildContext context, GoRouterState state) {
return const BrowserHardeningScreen();
return const GeneralSettingsScreen();
}
}
class BrowserHardeningGroupRoute extends GoRouteData {
class BangSettingsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const BangSettingsScreen();
}
}
class WebEngineSettingsRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const WebEngineSettingsScreen();
}
}
class WebEngineHardeningRoute extends GoRouteData {
@override
Widget build(BuildContext context, GoRouterState state) {
return const WebEngineHardeningScreen();
}
}
class WebEngineHardeningGroupRoute extends GoRouteData {
final String group;
const BrowserHardeningGroupRoute({required this.group});
const WebEngineHardeningGroupRoute({required this.group});
@override
Widget build(BuildContext context, GoRouterState state) {
return BrowserHardeningGroupScreen(groupName: group);
return WebEngineHardeningGroupScreen(groupName: group);
}
}
+134 -14
View File
@@ -65,6 +65,18 @@ RouteBase get $browserRoute => GoRouteData.$route(
path: 'containers',
name: 'ContainerListRoute',
factory: $ContainerListRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'create',
name: 'ContainerCreateRoute',
factory: $ContainerCreateRouteExtension._fromState,
),
GoRouteData.$route(
path: 'edit',
name: 'ContainerEditRoute',
factory: $ContainerEditRouteExtension._fromState,
),
],
),
],
);
@@ -255,20 +267,73 @@ extension $ContainerListRouteExtension on ContainerListRoute {
void replace(BuildContext context) => context.replace(location);
}
extension $ContainerCreateRouteExtension on ContainerCreateRoute {
static ContainerCreateRoute _fromState(GoRouterState state) =>
ContainerCreateRoute();
String get location => GoRouteData.$location(
'/containers/create',
);
void go(BuildContext context) => context.go(location);
Future<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(
path: '/settings',
name: 'SettingsRoute',
factory: $SettingsRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'hardening',
name: 'BrowserHardeningRoute',
factory: $BrowserHardeningRouteExtension._fromState,
path: 'general',
name: 'GeneralSettingsRoute',
factory: $GeneralSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'bang',
name: 'BangSettingsRoute',
factory: $BangSettingsRouteExtension._fromState,
),
GoRouteData.$route(
path: 'web_engine',
name: 'WebEngineSettingsRoute',
factory: $WebEngineSettingsRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'group/:group',
name: 'BrowserHardeningGroupRoute',
factory: $BrowserHardeningGroupRouteExtension._fromState,
path: 'hardening',
name: 'WebEngineHardeningRoute',
factory: $WebEngineHardeningRouteExtension._fromState,
routes: [
GoRouteData.$route(
path: 'group/:group',
name: 'WebEngineHardeningGroupRoute',
factory: $WebEngineHardeningGroupRouteExtension._fromState,
),
],
),
],
),
@@ -292,12 +357,12 @@ extension $SettingsRouteExtension on SettingsRoute {
void replace(BuildContext context) => context.replace(location);
}
extension $BrowserHardeningRouteExtension on BrowserHardeningRoute {
static BrowserHardeningRoute _fromState(GoRouterState state) =>
BrowserHardeningRoute();
extension $GeneralSettingsRouteExtension on GeneralSettingsRoute {
static GeneralSettingsRoute _fromState(GoRouterState state) =>
GeneralSettingsRoute();
String get location => GoRouteData.$location(
'/settings/hardening',
'/settings/general',
);
void go(BuildContext context) => context.go(location);
@@ -310,14 +375,69 @@ extension $BrowserHardeningRouteExtension on BrowserHardeningRoute {
void replace(BuildContext context) => context.replace(location);
}
extension $BrowserHardeningGroupRouteExtension on BrowserHardeningGroupRoute {
static BrowserHardeningGroupRoute _fromState(GoRouterState state) =>
BrowserHardeningGroupRoute(
extension $BangSettingsRouteExtension on BangSettingsRoute {
static BangSettingsRoute _fromState(GoRouterState state) =>
BangSettingsRoute();
String get location => GoRouteData.$location(
'/settings/bang',
);
void go(BuildContext context) => context.go(location);
Future<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']!,
);
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);
@@ -1,6 +1,21 @@
import 'dart:ui';
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> {
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/lexo_rank_bucket.dart';
import 'package:sqlite3/common.dart';
@@ -27,8 +28,8 @@ String _previousRankOrMiddle(List<Object?> args) {
}
String _reorderAfter(List<Object?> args) {
final first = (args[0] != null) ? LexoRank.parse(args[0]! as String) : null;
final last = (args[1] != null) ? LexoRank.parse(args[1]! as String) : null;
final first = args[0].mapNotNull((arg) => LexoRank.parse(arg as String));
final last = args[1].mapNotNull((arg) => LexoRank.parse(arg as String));
if (first == null) {
throw Exception('Tab not found');
+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:lensai/core/http_error_handler.dart';
import 'package:lensai/data/models/web_page_info.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart';
import 'package:lensai/utils/lru_cache.dart';
@@ -267,7 +268,7 @@ class GenericWebsiteService extends _$GenericWebsiteService {
url.origin,
await BrowserIcon.fromBytes(
result.image,
dominantColor: (result.color != null) ? Color(result.color!) : null,
dominantColor: result.color.mapNotNull((color) => Color(color)),
source: result.source,
),
);
@@ -7,7 +7,7 @@ part of 'generic_website.dart';
// **************************************************************************
String _$genericWebsiteServiceHash() =>
r'8eab8ab1448d4826dc0cda0ff6b47143735ba176';
r'27430ba17a04b0fa3cca20abf00f9d1c15fa843a';
/// See also [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:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/database/database.dart';
import 'package:lensai/features/bangs/data/models/bang.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
@@ -20,7 +21,7 @@ class BangDao extends DatabaseAccessor<BangDatabase> with _$BangDaoMixin {
SingleSelectable<int> getBangCount({Iterable<BangGroup>? groups}) {
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:exceptions/exceptions.dart';
import 'package:lensai/domain/services/generic_website.dart';
import 'package:lensai/features/bangs/data/database/database.dart';
import 'package:lensai/features/bangs/data/models/bang.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/data/models/search_history_entry.dart';
@@ -14,23 +13,27 @@ part 'data.g.dart';
@Riverpod(keepAlive: true)
class BangDataRepository extends _$BangDataRepository {
late BangDatabase _db;
@override
void build() {
_db = ref.watch(bangDatabaseProvider);
}
void build() {}
Stream<BangData?> watchBang(String? trigger) {
if (trigger != null) {
return _db.bangDao.getBangData(trigger).watchSingleOrNull();
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangData(trigger)
.watchSingleOrNull();
} else {
return Stream.value(null);
}
}
Stream<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>;
return decoded.map(
(key, value) => MapEntry(key, (value as List<dynamic>).cast()),
@@ -39,7 +42,10 @@ class BangDataRepository extends _$BangDataRepository {
}
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({
@@ -48,7 +54,9 @@ class BangDataRepository extends _$BangDataRepository {
({String category, String? subCategory})? categoryFilter,
bool? orderMostFrequentFirst,
}) {
return _db.bangDao
return ref
.read(bangDatabaseProvider)
.bangDao
.getBangDataList(
groups: groups,
domain: domain,
@@ -60,15 +68,25 @@ class BangDataRepository extends _$BangDataRepository {
}
Stream<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}) {
return _db.searchHistoryEntries(limit: limit).watch();
return ref
.read(bangDatabaseProvider)
.searchHistoryEntries(limit: limit)
.watch();
}
Future<void> increaseFrequency(String trigger) {
return _db.bangDao.increaseBangFrequency(trigger);
return ref
.read(bangDatabaseProvider)
.bangDao
.increaseBangFrequency(trigger);
}
Future<void> addSearchEntry(
@@ -76,17 +94,21 @@ class BangDataRepository extends _$BangDataRepository {
String searchQuery, {
required int maxEntryCount,
}) {
final db = ref.read(bangDatabaseProvider);
//Pack in a transaction to bundle rebuilds of watch() queries
return _db.transaction(
return db.transaction(
() async {
await _db.bangDao.addSearchEntry(trigger, searchQuery);
await _db.evictHistoryEntries(limit: maxEntryCount);
await db.bangDao.addSearchEntry(trigger, searchQuery);
await db.evictHistoryEntries(limit: maxEntryCount);
},
);
}
Future<void> removeSearchEntry(String searchQuery) {
return _db.bangDao.removeSearchEntry(searchQuery);
return ref
.read(bangDatabaseProvider)
.bangDao
.removeSearchEntry(searchQuery);
}
Future<Result<BangData>> ensureIconAvailable(BangData bang) async {
@@ -110,10 +132,13 @@ class BangDataRepository extends _$BangDataRepository {
}
Future<int> resetFrequencies() {
return _db.bangFrequency.deleteAll();
return ref.read(bangDatabaseProvider).bangFrequency.deleteAll();
}
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() =>
r'b51786552a812ddd54d1418450fe03ff89351e28';
r'2c009519cf02df4d3cbb6768dbe55e9eb696e126';
/// See also [BangDataRepository].
@ProviderFor(BangDataRepository)
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:lensai/features/bangs/data/database/database.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/data/providers.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
@@ -11,17 +10,20 @@ part 'search.g.dart';
@Riverpod()
class BangSearch extends _$BangSearch {
late StreamController<List<BangData>> _streamController;
late BangDatabase _db;
Future<void> search(String input) async {
if (input.isNotEmpty) {
await _db.bangDao.queryBangs(input).get().then(_streamController.add);
await ref
.read(bangDatabaseProvider)
.bangDao
.queryBangs(input)
.get()
.then(_streamController.add);
}
}
@override
Stream<List<BangData>> build() {
_db = ref.watch(bangDatabaseProvider);
_streamController = StreamController();
ref.onDispose(() async {
@@ -6,7 +6,7 @@ part of 'search.dart';
// RiverpodGenerator
// **************************************************************************
String _$bangSearchHash() => r'abd71f10288ac1f559e66fc11035f9f2b027c353';
String _$bangSearchHash() => r'632ddd09a1198394884cb5c27a2bde7850c0515b';
/// See also [BangSearch].
@ProviderFor(BangSearch)
@@ -11,10 +11,6 @@ part 'sync.g.dart';
@Riverpod(keepAlive: true)
class BangSyncRepository extends _$BangSyncRepository {
late BangDatabase _db;
BangSyncRepository();
static Future<Result<void>> _fetchAndSync({
required BangSourceService sourceService,
required BangDatabase db,
@@ -51,22 +47,22 @@ class BangSyncRepository extends _$BangSyncRepository {
) async {
try {
return Result.success(
await _db.computeWithDatabase(
connect: BangDatabase.new,
computation: (db) async {
final ref = ProviderContainer();
final result = await _fetchAndSync(
sourceService: ref.read(bangSourceServiceProvider.notifier),
db: db,
url: Uri.parse(group.url),
group: group,
syncInterval: syncInterval,
);
await ref.read(bangDatabaseProvider).computeWithDatabase(
connect: BangDatabase.new,
computation: (db) async {
final ref = ProviderContainer();
final result = await _fetchAndSync(
sourceService: ref.read(bangSourceServiceProvider.notifier),
db: db,
url: Uri.parse(group.url),
group: group,
syncInterval: syncInterval,
);
//Throw if necessary
return result.value;
},
),
//Throw if necessary
return result.value;
},
),
);
} catch (e) {
return Result.failure(
@@ -80,7 +76,11 @@ class BangSyncRepository extends _$BangSyncRepository {
}
Stream<DateTime?> watchLastSyncOfGroup(BangGroup group) {
return _db.syncDao.lastSyncOfGroup(group).watchSingleOrNull();
return ref
.read(bangDatabaseProvider)
.syncDao
.lastSyncOfGroup(group)
.watchSingleOrNull();
}
Future<Map<BangGroup, Result<void>>> syncBangGroups({
@@ -100,7 +100,5 @@ class BangSyncRepository extends _$BangSyncRepository {
}
@override
void build() {
_db = ref.watch(bangDatabaseProvider);
}
void build() {}
}
@@ -7,7 +7,7 @@ part of 'sync.dart';
// **************************************************************************
String _$bangSyncRepositoryHash() =>
r'86933b05d01629c67ed0443df91e991c40136423';
r'a46813f953e550585f432b11c364d87791819c34';
/// See also [BangSyncRepository].
@ProviderFor(BangSyncRepository)
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
@@ -21,9 +22,8 @@ class BangListScreen extends HookConsumerWidget {
final bangsAsync = ref.watch(
bangDataListProvider(
filter: (
categoryFilter: (category != null)
? (category: category!, subCategory: subCategory)
: null,
categoryFilter: category.mapNotNull(
(category) => (category: category, subCategory: subCategory)),
domain: null,
groups: null,
orderMostFrequentFirst: null,
@@ -8,7 +8,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/domain/repositories/search.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_details.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
@@ -20,9 +20,7 @@ class BangSearchScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final resultsAsync = ref.watch(bangSearchProvider);
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
final textEditingController =
useTextEditingController(text: initialSearchText);
@@ -1,5 +1,4 @@
import 'package:exceptions/exceptions.dart';
import 'package:lensai/features/chat/features/chat_store/data/database/database.dart';
import 'package:lensai/features/chat/features/chat_store/data/models/chat_metadata.dart';
import 'package:lensai/features/chat/features/chat_store/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -8,16 +7,15 @@ part 'chat_metadata.g.dart';
@Riverpod()
class ChatMetadataRepository extends _$ChatMetadataRepository {
late ChatDatabase _db;
Future<Result<void>> updateMetadata(ChatMetadata metadata) {
return Result.fromAsync(() {
return _db.metadataDao.updateChatMetadata(chatId, metadata);
return ref
.read(chatDatabaseProvider)
.metadataDao
.updateChatMetadata(chatId, metadata);
});
}
@override
void build(String chatId) {
_db = ref.watch(chatDatabaseProvider);
}
void build(String chatId) {}
}
@@ -7,7 +7,7 @@ part of 'chat_metadata.dart';
// **************************************************************************
String _$chatMetadataRepositoryHash() =>
r'72a0226ed7e87f452f4dfe44594bb30663c2db45';
r'bd6b28b9d034ee82f95e974e4bf8f553546e835a';
/// Copied from Dart SDK
class _SystemHash {
@@ -1,3 +1,5 @@
import 'package:lensai/extensions/nullable.dart';
class ChatEntity {
static final _namePattern = RegExp(r"^(.*?) - (.*?)\.md$");
@@ -14,7 +16,7 @@ class ChatEntity {
return ChatEntity._(
fileName,
name: match?.group(1),
dateTime: (match != null) ? DateTime.tryParse(match.group(2)!) : null,
dateTime: match.mapNotNull((match) => DateTime.tryParse(match.group(2)!)),
);
}
@@ -17,7 +17,6 @@ part 'search.g.dart';
class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
late Completer<void> _populatedCompleter;
late StreamController<List<ChatQueryResult>> _streamController;
late ChatSearchDatabase _searchDatabase;
Future<ChatCompanion?> _readChat(ChatEntity chat) async {
final contentResult = await ref
@@ -64,7 +63,9 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
}) async {
if (input.isNotEmpty) {
await _populatedCompleter.future;
await _searchDatabase.searchDao
await ref
.read(chatSearchDatabaseProvider)
.searchDao
.queryChats(
searchString: input,
snippetLength: snippetLength,
@@ -81,11 +82,12 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
Stream<List<ChatQueryResult>> build() async* {
_populatedCompleter = Completer();
_streamController = StreamController();
_searchDatabase = ref.watch(chatSearchDatabaseProvider);
final searchDatabase = ref.watch(chatSearchDatabaseProvider);
// populate with initial chats
await _searchDatabase.searchDao.deleteAllChats();
await _searchDatabase.searchDao.indexChats(await _availableChats());
await searchDatabase.searchDao.deleteAllChats();
await searchDatabase.searchDao.indexChats(await _availableChats());
_populatedCompleter.complete();
final changeStreamSubscription =
@@ -98,11 +100,11 @@ class ChatArchiveSearchRepository extends _$ChatArchiveSearchRepository {
if (chat.name != null) {
final companion = await _readChat(chat);
if (companion != null) {
await _searchDatabase.searchDao.upsertChat(companion);
await searchDatabase.searchDao.upsertChat(companion);
}
}
case ChangeType.REMOVE:
await _searchDatabase.searchDao.deleteChat(chat.fileName);
await searchDatabase.searchDao.deleteChat(chat.fileName);
}
});
@@ -7,7 +7,7 @@ part of 'search.dart';
// **************************************************************************
String _$chatArchiveSearchRepositoryHash() =>
r'd7174cfb877c61ee4e9580f44d7ffd11bf3b4b60';
r'6762958d43d2efc95f5d9848bfcae41436564f2b';
/// See also [ChatArchiveSearchRepository].
@ProviderFor(ChatArchiveSearchRepository)
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/date_time.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/chat_archive/domain/repositories/archive.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:skeletonizer/skeletonizer.dart';
@@ -41,9 +42,10 @@ class ChatArchiveListScreen extends HookConsumerWidget {
return ListTile(
title: Text(chat.toString()),
subtitle: (chat.dateTime != null)
? Text(chat.dateTime!.formatWithMinutePrecision())
: null,
subtitle: chat.dateTime.mapNotNull(
(chatTime) =>
Text(chatTime.formatWithMinutePrecision()),
),
onTap: () async {
await context.push(
ChatArchiveDetailRoute(fileName: chat.fileName)
@@ -10,7 +10,7 @@ import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/extensions/date_time.dart';
import 'package:lensai/features/chat_archive/domain/entities/chat_entity.dart';
import 'package:lensai/features/chat_archive/domain/repositories/search.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
@@ -20,9 +20,7 @@ class ChatArchiveSearchScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final resultsAsync = ref.watch(chatArchiveSearchRepositoryProvider);
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
final textEditingController = useTextEditingController();
@@ -3,6 +3,7 @@ import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/data/models/web_page_info.dart';
import 'package:lensai/domain/entities/equatable_image.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/browser_icon.dart';
import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart';
import 'package:lensai/features/geckoview/domain/entities/history_state.dart';
@@ -24,13 +25,13 @@ class TabState extends WebPageInfo with FastEquatable {
final EquatableImage? icon;
@override
BrowserIcon? get favicon => (icon != null)
? BrowserIcon(
image: icon!,
BrowserIcon? get favicon => icon.mapNotNull(
(icon) => BrowserIcon(
image: icon,
dominantColor: null,
source: IconSource.memory,
)
: null;
),
);
final EquatableImage? thumbnail;
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/image.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/find_result_state.dart';
import 'package:lensai/features/geckoview/domain/entities/history_state.dart';
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
@@ -39,10 +40,10 @@ class TabStates extends _$TabStates {
Future<void> _onIconChange(IconChangeEvent event) async {
final IconChangeEvent(:tabId, :bytes) = event;
final image = (bytes != null)
? (await tryDecodeImage(bytes)
.then((image) async => image?.toEquatable()))
: null;
final image = await bytes.mapNotNull(
(bytes) =>
tryDecodeImage(bytes).then((image) async => image?.toEquatable()),
);
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.icon(image);
@@ -51,10 +52,10 @@ class TabStates extends _$TabStates {
Future<void> _onThumbnailChange(ThumbnailEvent event) async {
final ThumbnailEvent(:tabId, :bytes) = event;
final image = (bytes != null)
? (await tryDecodeImage(bytes)
.then((image) async => image?.toEquatable()))
: null;
final image = await bytes.mapNotNull(
(bytes) =>
tryDecodeImage(bytes).then((image) async => image?.toEquatable()),
);
final current = state[tabId] ?? TabState.$default(tabId);
state = {...state}..[tabId] = current.copyWith.thumbnail(image);
@@ -173,7 +173,7 @@ final selectedTabStateProvider = AutoDisposeProvider<TabState?>.internal(
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SelectedTabStateRef = AutoDisposeProviderRef<TabState?>;
String _$tabStatesHash() => r'77f07f330dfac8dc8d28d18737203c7c122f3b74';
String _$tabStatesHash() => r'296d85ec016b22236b5830fce6b01cda81fbc02d';
/// See also [TabStates].
@ProviderFor(TabStates)
@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/domain/entities/equatable_image.dart';
import 'package:lensai/extensions/image.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/web_extension_state.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/utils/image_helper.dart';
@@ -33,12 +34,10 @@ class WebExtensionsState extends _$WebExtensionsState {
title: data.title,
enabled: data.enabled ?? current.enabled,
badgeText: data.badgeText,
badgeTextColor: (data.badgeTextColor != null)
? Color(data.badgeTextColor!)
: null,
badgeBackgroundColor: (data.badgeBackgroundColor != null)
? Color(data.badgeBackgroundColor!)
: null,
badgeTextColor:
data.badgeTextColor.mapNotNull((color) => Color(color)),
badgeBackgroundColor:
data.badgeBackgroundColor.mapNotNull((color) => Color(color)),
);
} else {
if (state.containsKey(extensionId)) {
@@ -7,7 +7,7 @@ part of 'web_extensions_state.dart';
// **************************************************************************
String _$webExtensionsStateHash() =>
r'bbaba8d27980046275858d2707bc66c6e349a537';
r'4e6dc46e49ff353e1c3cc4616008685c0e21501d';
/// Copied from Dart SDK
class _SystemHash {
@@ -2,14 +2,15 @@ import 'dart:async';
import 'package:drift/drift.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/utils/debouncer.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -19,31 +20,59 @@ part 'tab.g.dart';
class TabRepository extends _$TabRepository {
final _tabsService = GeckoTabService();
late TabDatabase _db;
Future<String> addTab({
Uri? url,
bool selectTab = true,
bool startLoading = true,
String? parentId,
LoadUrlFlags flags = LoadUrlFlags.NONE,
String? contextId,
Source source = Internal.newTab,
bool private = false,
HistoryMetadataKey? historyMetadata,
Map<String, String>? additionalHeaders,
}) {
return _tabsService.addTab(
url: url,
selectTab: selectTab,
startLoading: startLoading,
parentId: parentId,
flags: flags,
contextId: contextId,
source: source,
private: private,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
}) async {
final selectedContainer =
await ref.read(selectedContainerProvider.notifier).fetchData();
return ref.read(tabDatabaseProvider).tabDao.upsertContainerTabTransactional(
() {
return _tabsService.addTab(
url: url,
selectTab: selectTab,
startLoading: startLoading,
parentId: parentId,
flags: flags,
contextId: selectedContainer?.metadata.contextualIdentity,
source: source,
private: private,
historyMetadata: historyMetadata,
additionalHeaders: additionalHeaders,
);
},
containerId: Value(selectedContainer?.id),
);
}
Future<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));
Map<String, TabState>? debounceStartValue;
_db = ref.watch(tabDatabaseProvider);
final db = ref.watch(tabDatabaseProvider);
final tabAddedSub = eventSerivce.tabAddedStream.listen(
(tabId) async {
final containerId = ref.read(selectedContainerProvider);
await _db.tabDao.upsertTab(tabId, containerId: Value(containerId));
await db.tabDao
.upsertUnassignedTab(tabId, containerId: Value(containerId));
},
);
final tabContentSub =
tabContentService.tabContentStream.listen((content) async {
await _db.tabDao.updateTabContent(
await db.tabDao.updateTabContent(
content.tabId,
isProbablyReaderable: content.isProbablyReaderable,
extractedContentMarkdown: content.extractedContentMarkdown,
@@ -92,7 +122,7 @@ class TabRepository extends _$TabRepository {
selectedTabProvider,
(previous, tabId) async {
if (tabId != null) {
await _db.tabDao.touchTab(tabId, timestamp: DateTime.now());
await db.tabDao.touchTab(tabId, timestamp: DateTime.now());
}
},
);
@@ -104,7 +134,7 @@ class TabRepository extends _$TabRepository {
final syncTabs = next.isNotEmpty || (previous?.isNotEmpty ?? false);
if (syncTabs) {
await _db.tabDao.syncTabs(retainTabIds: next);
await db.tabDao.syncTabs(retainTabIds: next);
}
},
);
@@ -120,7 +150,7 @@ class TabRepository extends _$TabRepository {
}
tabStateDebouncer.eventOccured(() async {
await _db.tabDao.updateTabs(debounceStartValue, next);
await db.tabDao.updateTabs(debounceStartValue, next);
});
},
);
@@ -6,7 +6,7 @@ part of 'tab.dart';
// RiverpodGenerator
// **************************************************************************
String _$tabRepositoryHash() => r'b9caf89fe372b79f37c4a3ebff800ccc2d9289d2';
String _$tabRepositoryHash() => r'85af29fa18ea5a4a67fbffbb9478c801dafddac2';
/// See also [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:riverpod/riverpod.dart';
import 'package:lensai/features/user/domain/repositories/engine_settings.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'engine_settings.g.dart';
@Riverpod(keepAlive: true)
GeckoEngineSettingsService engineSettingsService(Ref ref) {
return GeckoEngineSettingsService();
class EngineSettingsReplicationService
extends _$EngineSettingsReplicationService {
final _service = GeckoEngineSettingsService();
@override
void build() {
var initialSettingsSent = false;
ref.listen(
generalSettingsRepositoryProvider
.select((settings) => settings.themeMode),
(previous, next) async {
final theme = switch (next) {
ThemeMode.system => ColorScheme.system,
ThemeMode.light => ColorScheme.light,
ThemeMode.dark => ColorScheme.dark,
};
await _service.preferredColorScheme(theme);
},
);
ref.listen(
engineSettingsRepositoryProvider,
(previous, next) async {
if (initialSettingsSent && previous != null) {
if (previous.javascriptEnabled != next.javascriptEnabled) {
await _service.javascriptEnabled(next.javascriptEnabled);
}
if (previous.trackingProtectionPolicy !=
next.trackingProtectionPolicy) {
await _service
.trackingProtectionPolicy(next.trackingProtectionPolicy);
}
if (previous.httpsOnlyMode != next.httpsOnlyMode) {
await _service.httpsOnlyMode(next.httpsOnlyMode);
}
if (previous.globalPrivacyControlEnabled !=
next.globalPrivacyControlEnabled) {
await _service
.globalPrivacyControlEnabled(next.globalPrivacyControlEnabled);
}
if (previous.preferredColorScheme != next.preferredColorScheme) {
await _service.preferredColorScheme(next.preferredColorScheme);
}
if (previous.cookieBannerHandlingMode !=
next.cookieBannerHandlingMode) {
await _service
.cookieBannerHandlingMode(next.cookieBannerHandlingMode);
}
if (previous.cookieBannerHandlingModePrivateBrowsing !=
next.cookieBannerHandlingModePrivateBrowsing) {
await _service.cookieBannerHandlingModePrivateBrowsing(
next.cookieBannerHandlingModePrivateBrowsing,
);
}
if (previous.cookieBannerHandlingGlobalRules !=
next.cookieBannerHandlingGlobalRules) {
await _service.cookieBannerHandlingGlobalRules(
next.cookieBannerHandlingGlobalRules,
);
}
if (previous.cookieBannerHandlingGlobalRulesSubFrames !=
next.cookieBannerHandlingGlobalRulesSubFrames) {
await _service.cookieBannerHandlingGlobalRulesSubFrames(
next.cookieBannerHandlingGlobalRulesSubFrames,
);
}
if (previous.webContentIsolationStrategy !=
next.webContentIsolationStrategy) {
await _service
.webContentIsolationStrategy(next.webContentIsolationStrategy);
}
} else {
await _service.setDefaultSettings(next);
initialSettingsSent = true;
}
},
);
}
}
@@ -6,24 +6,22 @@ part of 'engine_settings.dart';
// RiverpodGenerator
// **************************************************************************
String _$engineSettingsServiceHash() =>
r'051d7b6f65eb5dcb07405b4af1bdadd8dd74fdc0';
String _$engineSettingsReplicationServiceHash() =>
r'0729260eeb34b58470622ac9a3a6abb257730a61';
/// See also [engineSettingsService].
@ProviderFor(engineSettingsService)
final engineSettingsServiceProvider =
Provider<GeckoEngineSettingsService>.internal(
engineSettingsService,
name: r'engineSettingsServiceProvider',
/// See also [EngineSettingsReplicationService].
@ProviderFor(EngineSettingsReplicationService)
final engineSettingsReplicationServiceProvider =
NotifierProvider<EngineSettingsReplicationService, void>.internal(
EngineSettingsReplicationService.new,
name: r'engineSettingsReplicationServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$engineSettingsServiceHash,
: _$engineSettingsReplicationServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef EngineSettingsServiceRef = ProviderRef<GeckoEngineSettingsService>;
typedef _$EngineSettingsReplicationService = Notifier<void>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
@@ -1,103 +1,104 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
// import 'package:collection/collection.dart';
// import 'package:flutter/material.dart';
// import 'package:flutter_hooks/flutter_hooks.dart';
// import 'package:hooks_riverpod/hooks_riverpod.dart';
// import 'package:lensai/data/models/equatable_iterable.dart';
// import 'package:lensai/extensions/nullable.dart';
// import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
// import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
// import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
// import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
class TabActionDialog extends HookConsumerWidget {
final TabState initialTab;
// class TabActionDialog extends HookConsumerWidget {
// final TabState initialTab;
final void Function()? onDismiss;
// final void Function()? onDismiss;
const TabActionDialog({
required this.initialTab,
this.onDismiss,
super.key,
});
// const TabActionDialog({
// required this.initialTab,
// this.onDismiss,
// super.key,
// });
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
final tabContainerId = ref.watch(
tabContainerIdProvider(initialTab.id)
.select((value) => value.valueOrNull),
);
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
// final tabContainerId = ref.watch(
// tabContainerIdProvider(initialTab.id)
// .select((value) => value.valueOrNull),
// );
final containers = ref
.watch(
containersWithCountProvider.select(
(value) => EquatableCollection(value.valueOrNull, immutable: true),
),
)
.collection;
// final containers = ref
// .watch(
// containersWithCountProvider.select(
// (value) => EquatableCollection(value.valueOrNull, immutable: true),
// ),
// )
// .collection;
final selectedContainer = containers
?.firstWhereOrNull((container) => container.id == tabContainerId);
// final selectedContainer = containers
// ?.firstWhereOrNull((container) => container.id == tabContainerId);
final expansionController = useExpansionTileController();
// final expansionController = useExpansionTileController();
return Stack(
children: [
ModalBarrier(
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
onDismiss: onDismiss,
),
SimpleDialog(
titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
contentPadding: EdgeInsets.zero,
insetPadding: const EdgeInsets.symmetric(
horizontal: 20.0,
vertical: 24.0,
),
title: ListTile(
leading: RawImage(
image: tabState.icon?.value,
width: 24,
height: 24,
),
contentPadding: EdgeInsets.zero,
title: Text(tabState.title),
subtitle: Text(tabState.url.authority),
),
children: [
SizedBox(
//We need this to stretch the dialog, then padding from dialog is applied
width: double.maxFinite,
child: ExpansionTile(
controller: expansionController,
leading: (selectedContainer != null)
? CircleAvatar(backgroundColor: selectedContainer.color)
: null,
title: (selectedContainer != null)
? Text(selectedContainer.name ?? 'New Container')
: const Text('Assign a Container'),
children: containers
?.where((container) => container.id != tabContainerId)
.map(
(container) => ListTile(
leading:
CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'),
onTap: () async {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(initialTab.id, container.id);
// return Stack(
// children: [
// ModalBarrier(
// color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
// onDismiss: onDismiss,
// ),
// SimpleDialog(
// titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
// contentPadding: EdgeInsets.zero,
// insetPadding: const EdgeInsets.symmetric(
// horizontal: 20.0,
// vertical: 24.0,
// ),
// title: ListTile(
// leading: RawImage(
// image: tabState.icon?.value,
// width: 24,
// height: 24,
// ),
// contentPadding: EdgeInsets.zero,
// title: Text(tabState.title),
// subtitle: Text(tabState.url.authority),
// ),
// children: [
// SizedBox(
// //We need this to stretch the dialog, then padding from dialog is applied
// width: double.maxFinite,
// child: ExpansionTile(
// controller: expansionController,
// leading: selectedContainer?.color.mapNotNull(
// (color) => CircleAvatar(backgroundColor: color),
// ),
// title: (selectedContainer != null)
// ? Text(selectedContainer.name ?? 'New Container')
// : const Text('Assign a Container'),
// children: containers
// ?.where((container) => container.id != tabContainerId)
// .map(
// (container) => ListTile(
// leading:
// CircleAvatar(backgroundColor: container.color),
// title: Text(container.name ?? 'New Container'),
// onTap: () async {
// await ref
// .read(tabDataRepositoryProvider.notifier)
// .assignContainer(initialTab.id, container.id);
expansionController.collapse();
},
),
)
.toList() ??
[],
),
),
],
),
],
);
}
}
// expansionController.collapse();
// },
// ),
// )
// .toList() ??
// [],
// ),
// ),
// ],
// ),
// ],
// );
// }
// }
@@ -22,7 +22,7 @@ import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
import 'package:lensai/presentation/widgets/website_title_tile.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
@@ -40,9 +40,7 @@ class WebPageDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
final availableBangsAsync = ref.watch(
bangDataListProvider(
@@ -17,7 +17,6 @@ import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/create_tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/app_bar_title.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/browser_view.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
@@ -31,7 +30,6 @@ import 'package:lensai/features/geckoview/features/readerview/presentation/widge
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
import 'package:lensai/features/geckoview/features/tabs/features/chat/presentation/widgets/tab_qa_chat.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/presentation/hooks/draggable_scrollable_controller.dart';
import 'package:lensai/presentation/hooks/menu_controller.dart';
import 'package:lensai/presentation/hooks/overlay_portal_controller.dart';
@@ -77,15 +75,8 @@ class BrowserScreen extends HookConsumerWidget {
},
);
ref.listen(
settingsRepositoryProvider.select((value) => value.enableJavascript),
(previous, next) async {
await ref.read(engineSettingsServiceProvider).javaScriptEnabled(next);
},
);
// ref.listen(
// settingsRepositoryProvider
// generalSettingsRepositoryProvider
// .select((value) => value.valueOrNull?.kagiSession),
// (previous, next) async {
// if (next != null && next.isNotEmpty) {
@@ -598,7 +589,7 @@ class BrowserScreen extends HookConsumerWidget {
MenuItemButton(
onPressed: () async {
await context
.push(BrowserHardeningRoute().location);
.push(WebEngineHardeningRoute().location);
},
leadingIcon: const Icon(Icons.info),
child: const Text('Pref'),
@@ -648,7 +639,8 @@ class BrowserScreen extends HookConsumerWidget {
: null;
final tabCount = ref.read(
tabListProvider.select((tabs) => tabs.length));
tabListProvider.select((tabs) => tabs.length),
);
//Don't do anything if a child route is active
if (GoRouterState.of(context).topRoute?.path !=
@@ -8,13 +8,21 @@ import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/web_extensions_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/delete_browser_data.dart';
import 'package:lensai/features/geckoview/features/browser/domain/services/engine_settings.dart';
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:lensai/features/user/domain/services/local_authentication.dart';
class BrowserView extends StatefulHookConsumerWidget {
final Duration screenshotPeriod;
final FutureOr<void> Function()? postInitializationStep;
const BrowserView({this.screenshotPeriod = const Duration(seconds: 10)});
const BrowserView({
this.screenshotPeriod = const Duration(seconds: 10),
this.postInitializationStep,
});
@override
ConsumerState<ConsumerStatefulWidget> createState() => _BrowserViewState();
@@ -63,6 +71,15 @@ class _BrowserViewState extends ConsumerState<BrowserView>
return GeckoView(
preInitializationStep: () async {
await ref
.read(generalSettingsRepositoryProvider.notifier)
.fetch()
.then((settings) {
ref
.read(deleteBrowserDataServiceProvider.notifier)
.deleteData(settings.deleteBrowsingDataOnQuit);
});
await ref
.read(eventServiceProvider)
.viewReadyStateEvents
@@ -77,6 +94,9 @@ class _BrowserViewState extends ConsumerState<BrowserView>
},
);
},
postInitializationStep: () async {
await widget.postInitializationStep?.call();
},
);
}
@@ -109,6 +129,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
cacheRepositoryProvider,
(previous, next) {},
);
ref.listenManual(
engineSettingsReplicationServiceProvider,
(previous, next) {},
);
}
@override
@@ -124,6 +149,10 @@ class _BrowserViewState extends ConsumerState<BrowserView>
_periodicScreenshotUpdate?.cancel();
_timerPaused = true;
}
ref
.read(localAuthenticationServiceProvider.notifier)
.evictCacheOnBackground();
case AppLifecycleState.resumed:
if (_timerPaused) {
_periodicScreenshotUpdate =
@@ -13,7 +13,7 @@ class LandingAction extends HookConsumerWidget {
final sessionTokenAvailable = false;
// ref.watch(
// settingsRepositoryProvider.select(
// generalSettingsRepositoryProvider.select(
// (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_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart';
@@ -24,9 +25,8 @@ class CreateTabSheetWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final sharedContent = useMemoized(
() => (parameter.content != null)
? SharedContent.parse(parameter.content!)
: null,
() => parameter.content
.mapNotNull((content) => SharedContent.parse(content)),
[parameter],
);
@@ -56,15 +56,15 @@ class _TabDraggable extends HookConsumerWidget {
onClose();
}
},
onDoubleTap: () {
ref.read(overlayDialogControllerProvider.notifier).show(
TabActionDialog(
initialTab: tab,
onDismiss:
ref.read(overlayDialogControllerProvider.notifier).dismiss,
),
);
},
// onDoubleTap: () {
// ref.read(overlayDialogControllerProvider.notifier).show(
// TabActionDialog(
// initialTab: tab,
// onDismiss:
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
// ),
// );
// },
onDelete: () async {
await ref.read(tabRepositoryProvider.notifier).closeTab(tab.id);
},
@@ -178,8 +178,8 @@ class _Tab extends HookConsumerWidget {
return ContainerChips(
selectedContainer: selectedContainer,
onSelected: (container) {
ref
onSelected: (container) async {
await ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
},
@@ -40,7 +40,7 @@ class PreferenceSetting with FastEquatable {
bool get isActive => value == actualValue;
final String title;
final String? title;
final String? description;
final bool requireUserOptIn;
@@ -73,7 +73,7 @@ extension $PreferenceSettingGroupCopyWith on PreferenceSettingGroup {
abstract class _$PreferenceSettingCWProxy {
PreferenceSetting value(Object value);
PreferenceSetting title(String title);
PreferenceSetting title(String? title);
PreferenceSetting description(String? description);
@@ -91,7 +91,7 @@ abstract class _$PreferenceSettingCWProxy {
/// ````
PreferenceSetting call({
Object value,
String title,
String? title,
String? description,
Object? actualValue,
bool requireUserOptIn,
@@ -109,7 +109,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy {
PreferenceSetting value(Object value) => this(value: value);
@override
PreferenceSetting title(String title) => this(title: title);
PreferenceSetting title(String? title) => this(title: title);
@override
PreferenceSetting description(String? description) =>
@@ -151,7 +151,7 @@ class _$PreferenceSettingCWProxyImpl implements _$PreferenceSettingCWProxy {
title: title == const $CopyWithPlaceholder()
? _value.title
// ignore: cast_nullable_to_non_nullable
: title as String,
: title as String?,
description: description == const $CopyWithPlaceholder()
? _value.description
// ignore: cast_nullable_to_non_nullable
@@ -186,7 +186,7 @@ extension $PreferenceSettingCopyWith on PreferenceSetting {
PreferenceSetting _$PreferenceSettingFromJson(Map<String, dynamic> json) =>
PreferenceSetting(
value: json['value'] as Object,
title: json['title'] as String,
title: json['title'] as String?,
description: json['description'] as String?,
requireUserOptIn: json['requireUserOptIn'] as bool? ?? false,
shouldBeDefault: json['shouldBeDefault'] as bool? ?? false,
@@ -5,6 +5,7 @@ import 'package:collection/collection.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:lensai/features/geckoview/features/preferences/data/models/preference_setting.dart';
import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
@@ -12,40 +13,29 @@ import 'package:rxdart/rxdart.dart';
part 'preference_settings.g.dart';
@Riverpod(keepAlive: true)
Future<Map<String, PreferenceSettingGroup>> _preferenceSettingGroups(
Ref ref,
) async {
final content = await rootBundle
Future<Map<String, dynamic>> _preferenceSettingContent(Ref ref) async {
return await rootBundle
.loadString('assets/preferences/settings.json')
.then(jsonDecode) as Map<String, dynamic>;
}
final userContent = content['user'] as Map<String, dynamic>;
return userContent.map(
(key, value) => MapEntry(
key,
PreferenceSettingGroup(
// ignore: avoid_dynamic_calls
description: value['description'] as String?,
// ignore: avoid_dynamic_calls
settings: (value['preferences'] as Map<String, dynamic>).map(
(key, value) => MapEntry(
key,
PreferenceSetting.fromJson(value as Map<String, dynamic>),
),
),
),
),
);
@Riverpod(keepAlive: true)
Future<Map<String, PreferenceSettingGroup>> _preferenceSettingGroups(
Ref ref,
PreferencePartition partition,
) async {
final content = await ref.read(_preferenceSettingContentProvider.future);
return deserializePreferenceSettingGroups(partition, content);
}
@Riverpod(keepAlive: true)
Future<PreferenceSettingGroup> _preferenceSettingGroup(
Ref ref,
PreferencePartition partition,
String groupName,
) {
return ref.watch(
_preferenceSettingGroupsProvider.selectAsync(
_preferenceSettingGroupsProvider(partition).selectAsync(
(groups) =>
groups[groupName] ?? (throw Exception('Unknown setting group')),
),
@@ -90,13 +80,16 @@ class _PreferenceRepository extends _$PreferenceRepository {
}
@Riverpod()
class PreferenceSettingsGeneralRepository
extends _$PreferenceSettingsGeneralRepository {
late Map<String, PreferenceSettingGroup> _statelessGroups;
class UnifiedPreferenceSettingsRepository
extends _$UnifiedPreferenceSettingsRepository {
Map<String, PreferenceSettingGroup>? _statelessGroups;
Future<void> apply() async {
_statelessGroups =
await ref.read(_preferenceSettingGroupsProvider(partition).future);
final prefs = {
for (final group in _statelessGroups.values)
for (final group in _statelessGroups!.values)
...Map.fromEntries(
group.settings.entries
.where((e) => !e.value.requireUserOptIn)
@@ -108,7 +101,10 @@ class PreferenceSettingsGeneralRepository
}
Future<void> reset() async {
final prefs = _statelessGroups.values
_statelessGroups =
await ref.read(_preferenceSettingGroupsProvider(partition).future);
final prefs = _statelessGroups!.values
.map((group) => group.settings.keys)
.flattened
.toList();
@@ -117,13 +113,16 @@ class PreferenceSettingsGeneralRepository
}
@override
Stream<Map<String, PreferenceSettingGroup>> build() async* {
_statelessGroups = await ref.watch(_preferenceSettingGroupsProvider.future);
Stream<Map<String, PreferenceSettingGroup>> build(
PreferencePartition partition,
) async* {
_statelessGroups =
await ref.watch(_preferenceSettingGroupsProvider(partition).future);
final prefStream = ref.watch(_preferenceRepositoryProvider);
yield* prefStream.map(
(prefs) => _statelessGroups.map(
(prefs) => _statelessGroups!.map(
(groupName, group) => MapEntry(
groupName,
group.copyWith.settings(
@@ -143,18 +142,21 @@ class PreferenceSettingsGeneralRepository
@Riverpod()
class PreferenceSettingsGroupRepository
extends _$PreferenceSettingsGroupRepository {
late PreferenceSettingGroup _statelessSettingGroup;
PreferenceSettingGroup? _statelessSettingGroup;
Future<void> apply({List<String>? filter}) async {
_statelessSettingGroup ??= await ref
.read(_preferenceSettingGroupProvider(partition, groupName).future);
final prefs = Map.fromEntries(
filter?.map(
(e) => MapEntry(
e,
_statelessSettingGroup.settings[e]?.value ??
_statelessSettingGroup!.settings[e]?.value ??
(throw Exception('Preference not part of group')),
),
) ??
_statelessSettingGroup.settings.entries
_statelessSettingGroup!.settings.entries
.where((e) => !e.value.requireUserOptIn)
.map((e) => MapEntry(e.key, e.value.value)),
);
@@ -163,28 +165,34 @@ class PreferenceSettingsGroupRepository
}
Future<void> reset({List<String>? filter}) async {
_statelessSettingGroup ??= await ref
.read(_preferenceSettingGroupProvider(partition, groupName).future);
if (filter != null &&
filter.any(
(value) => !_statelessSettingGroup.settings.containsKey(value),
(value) => !_statelessSettingGroup!.settings.containsKey(value),
)) {
throw Exception('Preference not part of group');
}
await ref
.read(_preferenceRepositoryProvider.notifier)
.resetPrefs(filter ?? _statelessSettingGroup.settings.keys.toList());
.resetPrefs(filter ?? _statelessSettingGroup!.settings.keys.toList());
}
@override
Stream<PreferenceSettingGroup> build(String groupName) async* {
_statelessSettingGroup =
await ref.watch(_preferenceSettingGroupProvider(groupName).future);
Stream<PreferenceSettingGroup> build(
PreferencePartition partition,
String groupName,
) async* {
_statelessSettingGroup = await ref
.watch(_preferenceSettingGroupProvider(partition, groupName).future);
final prefStream = ref.watch(_preferenceRepositoryProvider);
yield* prefStream.map(
(prefs) => _statelessSettingGroup.copyWith.settings(
_statelessSettingGroup.settings.map(
(prefs) => _statelessSettingGroup!.copyWith.settings(
_statelessSettingGroup!.settings.map(
(key, value) => MapEntry(key, value.copyWith.actualValue(prefs[key])),
),
),
@@ -6,28 +6,27 @@ part of 'preference_settings.dart';
// RiverpodGenerator
// **************************************************************************
String _$preferenceSettingGroupsHash() =>
r'fa9286248cc71c88c888a518441721dc99b7e0c2';
String _$preferenceSettingContentHash() =>
r'a8a61fdf8800cb7adb365c100409ddce09c574b1';
/// See also [_preferenceSettingGroups].
@ProviderFor(_preferenceSettingGroups)
final _preferenceSettingGroupsProvider =
FutureProvider<Map<String, PreferenceSettingGroup>>.internal(
_preferenceSettingGroups,
name: r'_preferenceSettingGroupsProvider',
/// See also [_preferenceSettingContent].
@ProviderFor(_preferenceSettingContent)
final _preferenceSettingContentProvider =
FutureProvider<Map<String, dynamic>>.internal(
_preferenceSettingContent,
name: r'_preferenceSettingContentProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$preferenceSettingGroupsHash,
: _$preferenceSettingContentHash,
dependencies: null,
allTransitiveDependencies: null,
);
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef _PreferenceSettingGroupsRef
= FutureProviderRef<Map<String, PreferenceSettingGroup>>;
String _$preferenceSettingGroupHash() =>
r'960169ab9f6985a5b73a63663e671a8eac7cd776';
typedef _PreferenceSettingContentRef = FutureProviderRef<Map<String, dynamic>>;
String _$preferenceSettingGroupsHash() =>
r'd267dacf82a11568c9cf0cc864f434db35f93394';
/// Copied from Dart SDK
class _SystemHash {
@@ -50,6 +49,145 @@ class _SystemHash {
}
}
/// See also [_preferenceSettingGroups].
@ProviderFor(_preferenceSettingGroups)
const _preferenceSettingGroupsProvider = _PreferenceSettingGroupsFamily();
/// See also [_preferenceSettingGroups].
class _PreferenceSettingGroupsFamily
extends Family<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].
@ProviderFor(_preferenceSettingGroup)
const _preferenceSettingGroupProvider = _PreferenceSettingGroupFamily();
@@ -62,9 +200,11 @@ class _PreferenceSettingGroupFamily
/// See also [_preferenceSettingGroup].
_PreferenceSettingGroupProvider call(
PreferencePartition partition,
String groupName,
) {
return _PreferenceSettingGroupProvider(
partition,
groupName,
);
}
@@ -74,6 +214,7 @@ class _PreferenceSettingGroupFamily
covariant _PreferenceSettingGroupProvider provider,
) {
return call(
provider.partition,
provider.groupName,
);
}
@@ -98,10 +239,12 @@ class _PreferenceSettingGroupProvider
extends FutureProvider<PreferenceSettingGroup> {
/// See also [_preferenceSettingGroup].
_PreferenceSettingGroupProvider(
PreferencePartition partition,
String groupName,
) : this._internal(
(ref) => _preferenceSettingGroup(
ref as _PreferenceSettingGroupRef,
partition,
groupName,
),
from: _preferenceSettingGroupProvider,
@@ -113,6 +256,7 @@ class _PreferenceSettingGroupProvider
dependencies: _PreferenceSettingGroupFamily._dependencies,
allTransitiveDependencies:
_PreferenceSettingGroupFamily._allTransitiveDependencies,
partition: partition,
groupName: groupName,
);
@@ -123,9 +267,11 @@ class _PreferenceSettingGroupProvider
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.partition,
required this.groupName,
}) : super.internal();
final PreferencePartition partition;
final String groupName;
@override
@@ -143,6 +289,7 @@ class _PreferenceSettingGroupProvider
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
partition: partition,
groupName: groupName,
),
);
@@ -156,12 +303,14 @@ class _PreferenceSettingGroupProvider
@override
bool operator ==(Object other) {
return other is _PreferenceSettingGroupProvider &&
other.partition == partition &&
other.groupName == groupName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, partition.hashCode);
hash = _SystemHash.combine(hash, groupName.hashCode);
return _SystemHash.finish(hash);
@@ -171,6 +320,9 @@ class _PreferenceSettingGroupProvider
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin _PreferenceSettingGroupRef on FutureProviderRef<PreferenceSettingGroup> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
/// The parameter `groupName` of this provider.
String get groupName;
}
@@ -180,6 +332,9 @@ class _PreferenceSettingGroupProviderElement
with _PreferenceSettingGroupRef {
_PreferenceSettingGroupProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as _PreferenceSettingGroupProvider).partition;
@override
String get groupName => (origin as _PreferenceSettingGroupProvider).groupName;
}
@@ -202,33 +357,174 @@ final _preferenceRepositoryProvider = AutoDisposeNotifierProvider<
typedef _$PreferenceRepository
= AutoDisposeNotifier<Raw<Stream<Map<String, Object>>>>;
String _$preferenceSettingsGeneralRepositoryHash() =>
r'24a70de8284107bb43778e1014abe41bd805987a';
String _$unifiedPreferenceSettingsRepositoryHash() =>
r'35aa16ffa7aa1453c8331ed7470ef85a68d27178';
/// See also [PreferenceSettingsGeneralRepository].
@ProviderFor(PreferenceSettingsGeneralRepository)
final preferenceSettingsGeneralRepositoryProvider =
AutoDisposeStreamNotifierProvider<PreferenceSettingsGeneralRepository,
Map<String, PreferenceSettingGroup>>.internal(
PreferenceSettingsGeneralRepository.new,
name: r'preferenceSettingsGeneralRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$preferenceSettingsGeneralRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
abstract class _$UnifiedPreferenceSettingsRepository
extends BuildlessAutoDisposeStreamNotifier<
Map<String, PreferenceSettingGroup>> {
late final PreferencePartition partition;
Stream<Map<String, PreferenceSettingGroup>> build(
PreferencePartition partition,
);
}
/// See also [UnifiedPreferenceSettingsRepository].
@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() =>
r'023794294c5639ac6c6941f87a4ed91e1551fbc9';
r'60f416ffe1cde4d535fb5b643a80aead20836bae';
abstract class _$PreferenceSettingsGroupRepository
extends BuildlessAutoDisposeStreamNotifier<PreferenceSettingGroup> {
late final PreferencePartition partition;
late final String groupName;
Stream<PreferenceSettingGroup> build(
PreferencePartition partition,
String groupName,
);
}
@@ -246,9 +542,11 @@ class PreferenceSettingsGroupRepositoryFamily
/// See also [PreferenceSettingsGroupRepository].
PreferenceSettingsGroupRepositoryProvider call(
PreferencePartition partition,
String groupName,
) {
return PreferenceSettingsGroupRepositoryProvider(
partition,
groupName,
);
}
@@ -258,6 +556,7 @@ class PreferenceSettingsGroupRepositoryFamily
covariant PreferenceSettingsGroupRepositoryProvider provider,
) {
return call(
provider.partition,
provider.groupName,
);
}
@@ -283,9 +582,12 @@ class PreferenceSettingsGroupRepositoryProvider
PreferenceSettingsGroupRepository, PreferenceSettingGroup> {
/// See also [PreferenceSettingsGroupRepository].
PreferenceSettingsGroupRepositoryProvider(
PreferencePartition partition,
String groupName,
) : this._internal(
() => PreferenceSettingsGroupRepository()..groupName = groupName,
() => PreferenceSettingsGroupRepository()
..partition = partition
..groupName = groupName,
from: preferenceSettingsGroupRepositoryProvider,
name: r'preferenceSettingsGroupRepositoryProvider',
debugGetCreateSourceHash:
@@ -295,6 +597,7 @@ class PreferenceSettingsGroupRepositoryProvider
dependencies: PreferenceSettingsGroupRepositoryFamily._dependencies,
allTransitiveDependencies: PreferenceSettingsGroupRepositoryFamily
._allTransitiveDependencies,
partition: partition,
groupName: groupName,
);
@@ -305,9 +608,11 @@ class PreferenceSettingsGroupRepositoryProvider
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.partition,
required this.groupName,
}) : super.internal();
final PreferencePartition partition;
final String groupName;
@override
@@ -315,6 +620,7 @@ class PreferenceSettingsGroupRepositoryProvider
covariant PreferenceSettingsGroupRepository notifier,
) {
return notifier.build(
partition,
groupName,
);
}
@@ -324,12 +630,15 @@ class PreferenceSettingsGroupRepositoryProvider
return ProviderOverride(
origin: this,
override: PreferenceSettingsGroupRepositoryProvider._internal(
() => create()..groupName = groupName,
() => create()
..partition = partition
..groupName = groupName,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
partition: partition,
groupName: groupName,
),
);
@@ -344,12 +653,14 @@ class PreferenceSettingsGroupRepositoryProvider
@override
bool operator ==(Object other) {
return other is PreferenceSettingsGroupRepositoryProvider &&
other.partition == partition &&
other.groupName == groupName;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, partition.hashCode);
hash = _SystemHash.combine(hash, groupName.hashCode);
return _SystemHash.finish(hash);
@@ -360,6 +671,9 @@ class PreferenceSettingsGroupRepositoryProvider
// ignore: unused_element
mixin PreferenceSettingsGroupRepositoryRef
on AutoDisposeStreamNotifierProviderRef<PreferenceSettingGroup> {
/// The parameter `partition` of this provider.
PreferencePartition get partition;
/// The parameter `groupName` of this provider.
String get groupName;
}
@@ -370,6 +684,9 @@ class _PreferenceSettingsGroupRepositoryProviderElement
PreferenceSettingGroup> with PreferenceSettingsGroupRepositoryRef {
_PreferenceSettingsGroupRepositoryProviderElement(super.provider);
@override
PreferencePartition get partition =>
(origin as PreferenceSettingsGroupRepositoryProvider).partition;
@override
String get groupName =>
(origin as PreferenceSettingsGroupRepositoryProvider).groupName;
@@ -5,7 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/geckoview/domain/entities/readerable_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:lensai/presentation/widgets/animate_gradient_shader.dart';
class ReaderButton extends HookConsumerWidget {
@@ -16,7 +16,8 @@ class ReaderButton extends HookConsumerWidget {
final readerChanging = ref.watch(readerableScreenControllerProvider);
final enableReadability = ref.watch(
settingsRepositoryProvider.select((value) => value.enableReadability),
generalSettingsRepositoryProvider
.select((value) => value.enableReadability),
);
final readerabilityState = ref.watch(
@@ -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 'package:drift/drift.dart';
import 'package:lensai/core/uuid.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
@@ -12,26 +11,24 @@ class ContainerDao extends DatabaseAccessor<TabDatabase>
with _$ContainerDaoMixin {
ContainerDao(super.db);
Future<void> addContainer({String? name, required Color color}) {
Future<void> addContainer(ContainerData container) {
return db.container.insertOne(
ContainerCompanion.insert(
id: uuid.v7(),
name: Value(name),
color: color,
id: container.id,
name: Value(container.name),
color: container.color,
metadata: Value(container.metadata),
),
);
}
Future<void> replaceContainer(
String id, {
required String? name,
required Color color,
}) {
Future<void> replaceContainer(ContainerData container) {
return db.container.replaceOne(
ContainerCompanion(
id: Value(id),
name: Value(name),
color: Value(color),
id: Value(container.id),
name: Value(container.name),
color: Value(container.color),
metadata: Value(container.metadata),
),
);
}
@@ -41,7 +41,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
return query.map((row) => row.read(db.tab.id)!);
}
SingleSelectable<String?> tabContainerId(String tabId) {
SingleOrNullSelectable<String?> tabContainerId(String tabId) {
final query = selectOnly(db.tab)
..addColumns([db.tab.containerId])
..where(db.tab.id.equals(tabId));
@@ -49,7 +49,41 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
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, {
Value<String?> containerId = 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,
orderKey: (orderKey.present) ? Variable(orderKey.value) : null,
),
where: (old) => old.containerId.isNull(),
),
);
@@ -1,7 +1,7 @@
import 'package:drift/drift.dart';
import 'package:flutter/widgets.dart' show Color, IconData;
import 'package:flutter/widgets.dart' show Color;
import 'package:lensai/data/database/converters/color.dart';
import 'package:lensai/data/database/converters/icon_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/container.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/daos/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
@@ -1,14 +1,13 @@
import 'package:lensai/data/database/converters/color.dart';
import 'package:lensai/data/database/converters/icon_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/converters/container_metadata_converter.dart';
CREATE TABLE container (
id TEXT PRIMARY KEY NOT NULL,
contextual_identity TEXT,
name TEXT,
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`,
icon TEXT MAPPED BY `const IconDataTypeConverter()`
metadata TEXT MAPPED BY `const ContainerMetadataConverter()`
) WITH ContainerData;
CREATE TABLE tab (
@@ -79,7 +78,7 @@ containersWithCount WITH ContainerDataWithCount:
FROM tab
GROUP BY container_id
) AS tab_agg ON container.id = tab_agg.container_id
ORDER BY tab_agg.last_updated DESC NULLS FIRST;
ORDER BY tab_agg.last_updated DESC NULLS LAST;
leadingOrderKey(:container_id AS TEXT OR NULL, :bucket AS INTEGER):
SELECT lexo_rank_previous(
@@ -13,11 +13,6 @@ class Container extends Table with TableInfo<Container, ContainerData> {
type: DriftSqlType.string,
requiredDuringInsert: true,
$customConstraints: 'PRIMARY KEY NOT NULL');
late final GeneratedColumn<String> contextualIdentity =
GeneratedColumn<String>('contextual_identity', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '');
late final GeneratedColumn<String> name = GeneratedColumn<String>(
'name', aliasedName, true,
type: DriftSqlType.string,
@@ -29,15 +24,14 @@ class Container extends Table with TableInfo<Container, ContainerData> {
requiredDuringInsert: true,
$customConstraints: 'NOT NULL')
.withConverter<Color>(Container.$convertercolor);
late final GeneratedColumnWithTypeConverter<IconData?, String> icon =
GeneratedColumn<String>('icon', aliasedName, true,
late final GeneratedColumnWithTypeConverter<ContainerMetadata?, String>
metadata = GeneratedColumn<String>('metadata', aliasedName, true,
type: DriftSqlType.string,
requiredDuringInsert: false,
$customConstraints: '')
.withConverter<IconData?>(Container.$convertericonn);
.withConverter<ContainerMetadata?>(Container.$convertermetadatan);
@override
List<GeneratedColumn> get $columns =>
[id, contextualIdentity, name, color, icon];
List<GeneratedColumn> get $columns => [id, name, color, metadata];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -51,14 +45,13 @@ class Container extends Table with TableInfo<Container, ContainerData> {
return ContainerData(
id: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}id'])!,
contextualIdentity: attachedDatabase.typeMapping.read(
DriftSqlType.string, data['${effectivePrefix}contextual_identity']),
name: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}name']),
color: Container.$convertercolor.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.int, data['${effectivePrefix}color'])!),
icon: Container.$convertericonn.fromSql(attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}icon'])),
metadata: Container.$convertermetadatan.fromSql(attachedDatabase
.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}metadata'])),
);
}
@@ -68,69 +61,62 @@ class Container extends Table with TableInfo<Container, ContainerData> {
}
static TypeConverter<Color, int> $convertercolor = const ColorConverter();
static TypeConverter<IconData, String> $convertericon =
const IconDataTypeConverter();
static TypeConverter<IconData?, String?> $convertericonn =
NullAwareTypeConverter.wrap($convertericon);
static TypeConverter<ContainerMetadata, String> $convertermetadata =
const ContainerMetadataConverter();
static TypeConverter<ContainerMetadata?, String?> $convertermetadatan =
NullAwareTypeConverter.wrap($convertermetadata);
@override
bool get dontWriteConstraints => true;
}
class ContainerCompanion extends UpdateCompanion<ContainerData> {
final Value<String> id;
final Value<String?> contextualIdentity;
final Value<String?> name;
final Value<Color> color;
final Value<IconData?> icon;
final Value<ContainerMetadata?> metadata;
final Value<int> rowid;
const ContainerCompanion({
this.id = const Value.absent(),
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(),
this.color = const Value.absent(),
this.icon = const Value.absent(),
this.metadata = const Value.absent(),
this.rowid = const Value.absent(),
});
ContainerCompanion.insert({
required String id,
this.contextualIdentity = const Value.absent(),
this.name = const Value.absent(),
required Color color,
this.icon = const Value.absent(),
this.metadata = const Value.absent(),
this.rowid = const Value.absent(),
}) : id = Value(id),
color = Value(color);
static Insertable<ContainerData> custom({
Expression<String>? id,
Expression<String>? contextualIdentity,
Expression<String>? name,
Expression<int>? color,
Expression<String>? icon,
Expression<String>? metadata,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (id != null) 'id': id,
if (contextualIdentity != null) 'contextual_identity': contextualIdentity,
if (name != null) 'name': name,
if (color != null) 'color': color,
if (icon != null) 'icon': icon,
if (metadata != null) 'metadata': metadata,
if (rowid != null) 'rowid': rowid,
});
}
ContainerCompanion copyWith(
{Value<String>? id,
Value<String?>? contextualIdentity,
Value<String?>? name,
Value<Color>? color,
Value<IconData?>? icon,
Value<ContainerMetadata?>? metadata,
Value<int>? rowid}) {
return ContainerCompanion(
id: id ?? this.id,
contextualIdentity: contextualIdentity ?? this.contextualIdentity,
name: name ?? this.name,
color: color ?? this.color,
icon: icon ?? this.icon,
metadata: metadata ?? this.metadata,
rowid: rowid ?? this.rowid,
);
}
@@ -141,9 +127,6 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
if (id.present) {
map['id'] = Variable<String>(id.value);
}
if (contextualIdentity.present) {
map['contextual_identity'] = Variable<String>(contextualIdentity.value);
}
if (name.present) {
map['name'] = Variable<String>(name.value);
}
@@ -151,9 +134,9 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
map['color'] =
Variable<int>(Container.$convertercolor.toSql(color.value));
}
if (icon.present) {
map['icon'] =
Variable<String>(Container.$convertericonn.toSql(icon.value));
if (metadata.present) {
map['metadata'] =
Variable<String>(Container.$convertermetadatan.toSql(metadata.value));
}
if (rowid.present) {
map['rowid'] = Variable<int>(rowid.value);
@@ -165,10 +148,9 @@ class ContainerCompanion extends UpdateCompanion<ContainerData> {
String toString() {
return (StringBuffer('ContainerCompanion(')
..write('id: $id, ')
..write('contextualIdentity: $contextualIdentity, ')
..write('name: $name, ')
..write('color: $color, ')
..write('icon: $icon, ')
..write('metadata: $metadata, ')
..write('rowid: $rowid')
..write(')'))
.toString();
@@ -1597,18 +1579,18 @@ abstract class _$TabDatabase extends GeneratedDatabase {
Selectable<ContainerDataWithCount> containersWithCount() {
return customSelect(
'SELECT container.*, tab_agg.tab_count FROM container LEFT JOIN (SELECT container_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY container_id) AS tab_agg ON container.id = tab_agg.container_id ORDER BY tab_agg.last_updated DESC NULLS FIRST',
'SELECT container.*, tab_agg.tab_count FROM container LEFT JOIN (SELECT container_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY container_id) AS tab_agg ON container.id = tab_agg.container_id ORDER BY tab_agg.last_updated DESC NULLS LAST',
variables: [],
readsFrom: {
container,
tab,
}).map((QueryRow row) => ContainerDataWithCount(
id: row.read<String>('id'),
contextualIdentity: row.readNullable<String>('contextual_identity'),
name: row.readNullable<String>('name'),
color: Container.$convertercolor.fromSql(row.read<int>('color')),
icon: NullAwareTypeConverter.wrapFromSql(
Container.$convertericon, row.readNullable<String>('icon')),
metadata: NullAwareTypeConverter.wrapFromSql(
Container.$convertermetadata,
row.readNullable<String>('metadata')),
tabCount: row.readNullable<int>('tab_count'),
));
}
@@ -1819,18 +1801,16 @@ abstract class _$TabDatabase extends GeneratedDatabase {
typedef $ContainerCreateCompanionBuilder = ContainerCompanion Function({
required String id,
Value<String?> contextualIdentity,
Value<String?> name,
required Color color,
Value<IconData?> icon,
Value<ContainerMetadata?> metadata,
Value<int> rowid,
});
typedef $ContainerUpdateCompanionBuilder = ContainerCompanion Function({
Value<String> id,
Value<String?> contextualIdentity,
Value<String?> name,
Value<Color> color,
Value<IconData?> icon,
Value<ContainerMetadata?> metadata,
Value<int> rowid,
});
@@ -1864,10 +1844,6 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> {
ColumnFilters<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnFilters(column));
ColumnFilters<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnFilters(column));
@@ -1876,9 +1852,9 @@ class $ContainerFilterComposer extends Composer<_$TabDatabase, Container> {
column: $table.color,
builder: (column) => ColumnWithTypeConverterFilters(column));
ColumnWithTypeConverterFilters<IconData?, IconData, String> get icon =>
$composableBuilder(
column: $table.icon,
ColumnWithTypeConverterFilters<ContainerMetadata?, ContainerMetadata, String>
get metadata => $composableBuilder(
column: $table.metadata,
builder: (column) => ColumnWithTypeConverterFilters(column));
Expression<bool> tabRefs(Expression<bool> Function($TabFilterComposer f) f) {
@@ -1913,18 +1889,14 @@ class $ContainerOrderingComposer extends Composer<_$TabDatabase, Container> {
ColumnOrderings<String> get id => $composableBuilder(
column: $table.id, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get name => $composableBuilder(
column: $table.name, builder: (column) => ColumnOrderings(column));
ColumnOrderings<int> get color => $composableBuilder(
column: $table.color, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get icon => $composableBuilder(
column: $table.icon, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get metadata => $composableBuilder(
column: $table.metadata, builder: (column) => ColumnOrderings(column));
}
class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> {
@@ -1938,17 +1910,14 @@ class $ContainerAnnotationComposer extends Composer<_$TabDatabase, Container> {
GeneratedColumn<String> get id =>
$composableBuilder(column: $table.id, builder: (column) => column);
GeneratedColumn<String> get contextualIdentity => $composableBuilder(
column: $table.contextualIdentity, builder: (column) => column);
GeneratedColumn<String> get name =>
$composableBuilder(column: $table.name, builder: (column) => column);
GeneratedColumnWithTypeConverter<Color, int> get color =>
$composableBuilder(column: $table.color, builder: (column) => column);
GeneratedColumnWithTypeConverter<IconData?, String> get icon =>
$composableBuilder(column: $table.icon, builder: (column) => column);
GeneratedColumnWithTypeConverter<ContainerMetadata?, String> get metadata =>
$composableBuilder(column: $table.metadata, builder: (column) => column);
Expression<T> tabRefs<T extends Object>(
Expression<T> Function($TabAnnotationComposer a) f) {
@@ -1996,34 +1965,30 @@ class $ContainerTableManager extends RootTableManager<
$ContainerAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> id = const Value.absent(),
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(),
Value<Color> color = const Value.absent(),
Value<IconData?> icon = const Value.absent(),
Value<ContainerMetadata?> metadata = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ContainerCompanion(
id: id,
contextualIdentity: contextualIdentity,
name: name,
color: color,
icon: icon,
metadata: metadata,
rowid: rowid,
),
createCompanionCallback: ({
required String id,
Value<String?> contextualIdentity = const Value.absent(),
Value<String?> name = const Value.absent(),
required Color color,
Value<IconData?> icon = const Value.absent(),
Value<ContainerMetadata?> metadata = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
ContainerCompanion.insert(
id: id,
contextualIdentity: contextualIdentity,
name: name,
color: color,
icon: icon,
metadata: metadata,
rowid: rowid,
),
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: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 {
final String id;
final String? contextualIdentity;
final String? name;
final Color color;
final IconData? icon;
final ContainerMetadata metadata;
ContainerData({
required this.id,
this.contextualIdentity,
this.name,
required this.color,
this.icon,
});
ContainerMetadata? metadata,
}) : metadata = metadata ?? ContainerMetadata.withDefaults();
@override
bool get cacheHash => true;
@@ -22,10 +105,9 @@ class ContainerData with FastEquatable {
@override
List<Object?> get hashParameters => [
id,
contextualIdentity,
name,
color,
icon,
metadata,
];
}
@@ -34,10 +116,9 @@ class ContainerDataWithCount extends ContainerData {
ContainerDataWithCount({
required super.id,
super.contextualIdentity,
super.name,
required super.color,
super.icon,
super.metadata,
required this.tabCount,
});
@@ -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()
Stream<List<String>> containerTabIds(
Ref ref,
@@ -206,136 +206,6 @@ class _FilteredContainersWithCountProviderElement
(origin as FilteredContainersWithCountProvider).searchText;
}
String _$tabContainerIdHash() => r'726ddd000d23935a2c8ae39c7957412fab16016d';
/// See also [tabContainerId].
@ProviderFor(tabContainerId)
const tabContainerIdProvider = TabContainerIdFamily();
/// See also [tabContainerId].
class TabContainerIdFamily extends Family<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';
/// 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/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/user/domain/services/local_authentication.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -7,15 +10,47 @@ part 'selected_container.g.dart';
@Riverpod(keepAlive: true)
class SelectedContainer extends _$SelectedContainer {
void setContainerId(String id) {
state = id;
Future<ContainerData?> fetchData() async {
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) {
clearContainer();
} else {
setContainerId(id);
await setContainerId(id);
}
}
@@ -25,6 +60,17 @@ class SelectedContainer extends _$SelectedContainer {
@override
String? build() {
ref.listen(
containersWithCountProvider,
(previous, next) {
if (state != null && next.valueOrNull != null) {
if (!next.value!.any((container) => container.id == state)) {
clearContainer();
}
}
},
);
return null;
}
}
@@ -25,7 +25,7 @@ final selectedContainerDataProvider =
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
typedef SelectedContainerDataRef = AutoDisposeStreamProviderRef<ContainerData?>;
String _$selectedContainerHash() => r'e38e86db5bd0584af9561156c26ceaea3d23aebf';
String _$selectedContainerHash() => r'753f30646c4992981ec538dcfd33fa23aa1f673d';
/// See also [SelectedContainer].
@ProviderFor(SelectedContainer)
@@ -1,54 +1,72 @@
import 'dart:ui';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'container.g.dart';
@Riverpod(keepAlive: true)
class ContainerRepository extends _$ContainerRepository {
late TabDatabase _db;
@override
void build() {
_db = ref.watch(tabDatabaseProvider);
Future<void> addContainer(ContainerData container) {
return ref.read(tabDatabaseProvider).containerDao.addContainer(container);
}
Future<void> addContainer({required String? name, required Color color}) {
return _db.containerDao.addContainer(name: name, color: color);
Future<void> replaceContainer(ContainerData container) {
return ref
.read(tabDatabaseProvider)
.containerDao
.replaceContainer(container);
}
Future<void> replaceContainer({
required String id,
required String? name,
required Color color,
}) {
return _db.containerDao.replaceContainer(id, name: name, color: color);
Future<ContainerData?> getContainerData(String id) async {
return ref
.read(tabDatabaseProvider)
.containerDao
.getContainerData(id)
.getSingleOrNull();
}
Future<void> deleteContainer(String id) {
return _db.containerDao.deleteContainer(id);
Future<void> deleteContainer(String id) async {
await ref.read(tabDataRepositoryProvider.notifier).closeAllTabs(id);
return ref.read(tabDatabaseProvider).containerDao.deleteContainer(id);
}
Future<Set<Color>> getDistinctColors() {
return _db.containerDao
return ref
.read(tabDatabaseProvider)
.containerDao
.getDistinctColors()
.get()
.then((colors) => colors.toSet());
}
Future<String> getLeadingOrderKey(String? containerId) {
return _db.containerDao.generateLeadingOrderKey(containerId).getSingle();
return ref
.read(tabDatabaseProvider)
.containerDao
.generateLeadingOrderKey(containerId)
.getSingle();
}
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) {
return _db.containerDao
return ref
.read(tabDatabaseProvider)
.containerDao
.generateOrderKeyAfterTabId(containerId, tabId)
.getSingle();
}
@override
void build() {}
}
@@ -7,7 +7,7 @@ part of 'container.dart';
// **************************************************************************
String _$containerRepositoryHash() =>
r'89ad15861fb213d6a3fa51232fd0ff9943f51e92';
r'b965883f2279b6f0148592bbac5ff40a60798f51';
/// See also [ContainerRepository].
@ProviderFor(ContainerRepository)
@@ -1,5 +1,4 @@
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -7,35 +6,40 @@ part 'tab.g.dart';
@Riverpod(keepAlive: true)
class TabDataRepository extends _$TabDataRepository {
late TabDatabase _db;
@override
void build() {
_db = ref.watch(tabDatabaseProvider);
}
void build() {}
Future<void> assignContainer(String tabId, String? containerId) {
return _db.tabDao.assignContainer(
tabId,
containerId: containerId,
);
return ref.read(tabDatabaseProvider).tabDao.assignContainer(
tabId,
containerId: containerId,
);
}
Future<void> assignOrderKey(String tabId, String orderKey) {
return _db.tabDao.assignOrderKey(
tabId,
orderKey: orderKey,
);
return ref.read(tabDatabaseProvider).tabDao.assignOrderKey(
tabId,
orderKey: orderKey,
);
}
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) {
await ref.read(tabRepositoryProvider.notifier).closeTabs(tabIds);
}
}
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
// **************************************************************************
String _$tabDataRepositoryHash() => r'c067bbbf319cd95ad869655740fdf49d13159b94';
String _$tabDataRepositoryHash() => r'30ae87fbcb37facd565cb28de605814cd7db4ce8';
/// See also [TabDataRepository].
@ProviderFor(TabDataRepository)
@@ -1,6 +1,5 @@
import 'dart:async';
import 'package:lensai/features/geckoview/features/tabs/data/database/database.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/tab_query_result.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -9,8 +8,6 @@ part 'tab_search.g.dart';
@Riverpod()
class TabSearchRepository extends _$TabSearchRepository {
late TabDatabase _db;
Future<void> addQuery(
String input, {
int snippetLength = 120,
@@ -20,7 +17,9 @@ class TabSearchRepository extends _$TabSearchRepository {
}) async {
if (input.isNotEmpty) {
state = await AsyncValue.guard(
() => _db.tabDao
() => ref
.read(tabDatabaseProvider)
.tabDao
.queryTabs(
matchPrefix: matchPrefix,
matchSuffix: matchSuffix,
@@ -37,8 +36,6 @@ class TabSearchRepository extends _$TabSearchRepository {
@override
Future<List<TabQueryResult>?> build() {
_db = ref.watch(tabDatabaseProvider);
return Future.value();
}
}
@@ -7,7 +7,7 @@ part of 'tab_search.dart';
// **************************************************************************
String _$tabSearchRepositoryHash() =>
r'9bd42f89bcc3a8d94add093076737dd57383bae2';
r'57d130f50aa3feb7a31d27f9a5b54234e0e7064f';
/// See also [TabSearchRepository].
@ProviderFor(TabSearchRepository)
@@ -6,6 +6,7 @@ import 'package:langchain/langchain.dart' as langchain;
import 'package:langchain/langchain.dart';
import 'package:langchain_openai/langchain_openai.dart';
import 'package:lensai/core/models.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/data/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/features/vector_store/data/database/daos/vector.dart';
@@ -41,21 +42,21 @@ class DocumentRepository extends _$DocumentRepository {
chunkOverlap: chunkOverlap,
);
final splittedWithSource = (splitted != null)
? (
mainDocumentId: splitted.mainDocumentId,
parts: splitted.parts
.map(
(part) => part.copyWith(
metadata: {
...part.metadata,
'source': part.id,
},
),
)
.toList()
)
: null;
final splittedWithSource = splitted.mapNotNull(
(splitted) => (
mainDocumentId: splitted.mainDocumentId,
parts: splitted.parts
.map(
(part) => part.copyWith(
metadata: {
...part.metadata,
'source': part.id,
},
),
)
.toList()
),
);
await _vectorDao.insertDocuments(
splittedWithSource?.parts ?? [doc],
@@ -7,7 +7,7 @@ part of 'document.dart';
// **************************************************************************
String _$documentRepositoryHash() =>
r'0c93116e0f9887d452b7c493dd4da45e5bf0c2d4';
r'60e5a0c889d9659c8e0e254c59225c2041a0cb56';
/// See also [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:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/core/uuid.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_dialog.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_list_tile.dart';
import 'package:skeletonizer/skeletonizer.dart';
class _ContainerTile extends HookWidget {
final ContainerData container;
final bool isSelected;
final void Function(ContainerResult edited) onEdit;
final void Function() onDelete;
final void Function() onTap;
const _ContainerTile(
this.container, {
required this.isSelected,
required this.onEdit,
required this.onDelete,
required this.onTap,
super.key,
});
@override
Widget build(BuildContext context) {
final menuController = useMemoized(() => MenuController());
return ListTile(
selected: isSelected,
leading: CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'),
trailing: MenuAnchor(
controller: menuController,
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
child: InkWell(
onTap: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
child: const Padding(
padding: EdgeInsets.symmetric(vertical: 15.0, horizontal: 8.0),
child: Icon(Icons.more_vert),
),
),
);
},
menuChildren: [
MenuItemButton(
onPressed: () async {
final result = await showDialog<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 {
const ContainerListScreen();
@@ -139,29 +37,36 @@ class ContainerListScreen extends HookConsumerWidget {
itemCount: containers.length,
itemBuilder: (context, index) {
final container = containers[index];
return _ContainerTile(
container,
return Slidable(
key: ValueKey(container.id),
isSelected: container.id == selectedContainer,
onEdit: (edited) async {
await ref
.read(containerRepositoryProvider.notifier)
.replaceContainer(
id: container.id,
name: edited.name,
color: edited.color,
);
},
onDelete: () async {
await ref
.read(containerRepositoryProvider.notifier)
.deleteContainer(container.id);
},
onTap: () {
ref
.read(selectedContainerProvider.notifier)
.toggleContainer(container.id);
},
endActionPane: ActionPane(
motion: const ScrollMotion(),
children: [
SlidableAction(
onPressed: (context) async {
await ref
.read(containerRepositoryProvider.notifier)
.deleteContainer(container.id);
},
backgroundColor:
Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context)
.colorScheme
.onErrorContainer,
icon: Icons.delete,
label: 'Delete',
),
],
),
child: ContainerListTile(
container,
isSelected: container.id == selectedContainer,
onTap: () async {
await ref
.read(selectedContainerProvider.notifier)
.toggleContainer(container.id);
},
),
);
},
);
@@ -170,12 +75,9 @@ class ContainerListScreen extends HookConsumerWidget {
error: (error, stackTrace) => SizedBox.shrink(),
loading: () => ListView.builder(
itemCount: 3,
itemBuilder: (context, index) => _ContainerTile(
itemBuilder: (context, index) => ContainerListTile(
ContainerData(id: 'null', color: Colors.transparent),
isSelected: false,
onEdit: (_) {},
onDelete: () {},
onTap: () {},
),
),
),
@@ -188,17 +90,15 @@ class ContainerListScreen extends HookConsumerWidget {
await ref.read(unusedRandomContainerColorProvider.future);
if (context.mounted) {
final result = await showDialog<ContainerResult?>(
context: context,
builder: (context) => ContainerDialog.create(
initialColor: initialColor,
),
final result = await context.push<ContainerData?>(
ContainerCreateRoute().location,
extra: ContainerData(id: uuid.v7(), color: initialColor),
);
if (result != null) {
await ref
.read(containerRepositoryProvider.notifier)
.addContainer(name: result.name, color: result.color);
.addContainer(result);
}
}
},
@@ -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/routing/routes.dart';
import 'package:lensai/data/models/drag_data.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/data/models/container_data.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:lensai/presentation/hooks/overlay_portal_controller.dart';
import 'package:lensai/presentation/widgets/selectable_chips.dart';
@@ -90,12 +93,37 @@ class ContainerChips extends HookConsumerWidget {
overlayController.hide();
},
onAcceptWithDetails: (details) async {
await ref
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(
details.data.tabId,
container.id,
);
.containerTabId(details.data.tabId);
final containerData =
await containerId.mapNotNull(
(containerId) => ref
.read(containerRepositoryProvider.notifier)
.getContainerData(containerId),
);
if (container.metadata.contextualIdentity ==
containerData?.metadata.contextualIdentity) {
await ref
.read(tabDataRepositoryProvider.notifier)
.assignContainer(
details.data.tabId,
container.id,
);
} else {
await ref
.read(tabRepositoryProvider.notifier)
.duplicateTab(
selectTabId: details.data.tabId,
containerId: container.id,
);
await ref
.read(tabRepositoryProvider.notifier)
.closeTab(details.data.tabId);
}
dragTargetTabId.value = null;
overlayController.hide();
@@ -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 !');
// final kagiSession =
// ref.read(settingsRepositoryProvider).valueOrNull?.kagiSession;
// ref.read(generalSettingsRepositoryProvider).valueOrNull?.kagiSession;
// return Result.fromAsync(
// () async {
@@ -25,7 +25,7 @@
// @override
// Widget build(BuildContext context, WidgetRef ref) {
// final incognitoEnabled = ref.watch(
// settingsRepositoryProvider.select((value) => value.incognitoMode),
// generalSettingsRepositoryProvider.select((value) => value.incognitoMode),
// );
// final researchVariant = ref.watch(activeResearchVariantProvider);
@@ -10,7 +10,7 @@ import 'package:lensai/features/geckoview/features/browser/presentation/widgets/
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/widgets/website_title_tile.dart';
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
@@ -21,11 +21,7 @@ class _InputField extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select(
(value) => value.incognitoMode,
),
);
final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
return TextFormField(
controller: controller,
@@ -4,6 +4,7 @@ import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers/bangs.dart';
import 'package:lensai/features/bangs/domain/providers/search.dart';
@@ -223,9 +224,9 @@ class SearchScreen extends HookConsumerWidget {
result.extractedContent ?? result.fullContent;
return ListTile(
title: (result.title != null)
? MarkdownBody(data: result.title!)
: null,
title: result.title.mapNotNull(
(title) => MarkdownBody(data: title),
),
subtitle: (!headHasMatch && bodyResult != null)
? MarkdownBody(data: bodyResult)
: null,
@@ -3,7 +3,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/presentation/widgets/bang_icon.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/presentation/widgets/speech_to_text_button.dart';
class SearchField extends HookConsumerWidget {
@@ -25,11 +25,7 @@ class SearchField extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select(
(value) => value.incognitoMode,
),
);
final incognitoEnabled = ref.watch(incognitoModeEnabledProvider);
final hasText = useListenableSelector(
textEditingController,
@@ -1,18 +1,34 @@
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/features/user/domain/repositories/engine_settings.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'save_settings.g.dart';
@Riverpod(keepAlive: true)
class SaveSettingsController extends _$SaveSettingsController {
class SaveGeneralSettingsController extends _$SaveGeneralSettingsController {
@override
FutureOr<void> build() {}
Future<void> save(UpdateSettingsFunc updateSettings) async {
Future<void> save(UpdateGeneralSettingsFunc updateSettings) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref
.read(settingsRepositoryProvider.notifier)
.read(generalSettingsRepositoryProvider.notifier)
.updateSettings(updateSettings),
);
}
}
@Riverpod(keepAlive: true)
class SaveEngineSettingsController extends _$SaveEngineSettingsController {
@override
FutureOr<void> build() {}
Future<void> save(UpdateEngineSettingsFunc updateSettings) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref
.read(engineSettingsRepositoryProvider.notifier)
.updateSettings(updateSettings),
);
}
@@ -6,22 +6,39 @@ part of 'save_settings.dart';
// RiverpodGenerator
// **************************************************************************
String _$saveSettingsControllerHash() =>
r'7ffcef95b48dce1f80b56fb3580282ff3df240b4';
String _$saveGeneralSettingsControllerHash() =>
r'ad8b60839cbcb05a964f57e32fc5d9c1aba30377';
/// See also [SaveSettingsController].
@ProviderFor(SaveSettingsController)
final saveSettingsControllerProvider =
AsyncNotifierProvider<SaveSettingsController, void>.internal(
SaveSettingsController.new,
name: r'saveSettingsControllerProvider',
/// See also [SaveGeneralSettingsController].
@ProviderFor(SaveGeneralSettingsController)
final saveGeneralSettingsControllerProvider =
AsyncNotifierProvider<SaveGeneralSettingsController, void>.internal(
SaveGeneralSettingsController.new,
name: r'saveGeneralSettingsControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$saveSettingsControllerHash,
: _$saveGeneralSettingsControllerHash,
dependencies: 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: 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:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/bangs/data/models/bang.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/settings/presentation/controllers/save_settings.dart';
import 'package:lensai/features/settings/presentation/widgets/bang_group_list_tile.dart';
import 'package:lensai/features/settings/presentation/widgets/custom_list_tile.dart';
import 'package:lensai/features/user/data/models/settings.dart';
import 'package:lensai/features/user/domain/providers.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
class SettingsScreen extends HookConsumerWidget {
const SettingsScreen({super.key});
Widget _buildSection(ThemeData theme, String name) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(name, style: theme.textTheme.titleLarge),
const Divider(),
],
),
);
Widget _buildSubSection(ThemeData theme, String name) => Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
child: Text(
name,
style: theme.textTheme.titleLarge?.copyWith(fontSize: 18),
),
);
@override
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final settings = ref.watch(settingsRepositoryProvider);
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: FadingScroll(
@@ -50,189 +17,61 @@ class SettingsScreen extends HookConsumerWidget {
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: [
_buildSection(theme, 'General'),
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Browser Hardening'),
trailing: IconButton(
onPressed: () async {
await context.push(BrowserHardeningRoute().location);
},
icon: const Icon(Icons.chevron_right),
title: const Text('General'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(Icons.settings),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await context.push(GeneralSettingsRoute().location);
},
),
),
Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Theme',
style: theme.textTheme.bodyLarge,
),
Center(
child: SegmentedButton<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(
height: 16,
height: 8,
),
_buildSection(theme, 'Bangs'),
CustomListTile(
title: 'Bang Frequencies',
subtitle: 'Tracked usage for Bang recommendations',
suffix: FilledButton.icon(
onPressed: () async {
await ref
.read(bangDataRepositoryProvider.notifier)
.resetFrequencies();
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Web Engine'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.web),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await context.push(WebEngineSettingsRoute().location);
},
icon: const Icon(Icons.delete),
label: const Text('Clear'),
),
),
Consumer(
builder: (context, ref, child) {
final size = ref.watch(
iconCacheSizeMegabytesProvider.select(
(value) => value.valueOrNull,
),
);
return CustomListTile(
title: 'Icon Cache',
subtitle: 'Stored favicons for Bangs',
content: Padding(
padding: const EdgeInsets.only(top: 8.0),
child: DefaultTextStyle(
style: GoogleFonts.robotoMono(
textStyle: DefaultTextStyle.of(context).style,
),
child: Table(
columnWidths: const {0: FixedColumnWidth(100)},
children: [
TableRow(
children: [
const Text('Size'),
Text('${size?.toStringAsFixed(2) ?? 0} MB'),
],
),
],
),
),
),
suffix: FilledButton.icon(
onPressed: () async {
await ref
.read(cacheRepositoryProvider.notifier)
.clearCache();
},
icon: const Icon(Icons.delete),
label: const Text('Clear'),
),
);
},
const SizedBox(
height: 8,
),
_buildSubSection(theme, 'Repositories'),
const BangGroupListTile(
group: BangGroup.general,
title: 'General Bangs',
subtitle: 'Automatically syncs every 7 days',
),
const BangGroupListTile(
group: BangGroup.assistant,
title: 'Assistant Bangs',
subtitle: 'Automatically syncs every 7 days',
),
const BangGroupListTile(
group: BangGroup.kagi,
title: 'Kagi Bangs',
subtitle: 'Automatically syncs every 7 days',
Card(
color: Theme.of(context).highlightColor,
clipBehavior: Clip.antiAlias,
child: ListTile(
title: const Text('Bangs'),
contentPadding: const EdgeInsets.symmetric(
vertical: 8.0,
horizontal: 16.0,
),
leading: const Icon(MdiIcons.exclamationThick),
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await context.push(BangSettingsRoute().location);
},
),
),
],
);
@@ -4,17 +4,20 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart';
import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
class BrowserHardeningScreen extends HookConsumerWidget {
const BrowserHardeningScreen();
class WebEngineHardeningScreen extends HookConsumerWidget {
const WebEngineHardeningScreen();
@override
Widget build(BuildContext context, WidgetRef ref) {
final preferenceGroups =
ref.watch(preferenceSettingsGeneralRepositoryProvider);
final preferenceGroups = ref.watch(
unifiedPreferenceSettingsRepositoryProvider(PreferencePartition.user),
);
final allGroupsActive = useMemoized(
() =>
@@ -28,7 +31,7 @@ class BrowserHardeningScreen extends HookConsumerWidget {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Browser Hardening')),
appBar: AppBar(title: const Text('Web Engine Hardening')),
body: preferenceGroups.when(
data: (data) {
return Column(
@@ -49,7 +52,9 @@ class BrowserHardeningScreen extends HookConsumerWidget {
),
onChanged: (value) async {
final notifier = ref.read(
preferenceSettingsGeneralRepositoryProvider.notifier,
unifiedPreferenceSettingsRepositoryProvider(
PreferencePartition.user,
).notifier,
);
if (value) {
@@ -71,9 +76,9 @@ class BrowserHardeningScreen extends HookConsumerWidget {
Expanded(
child: ListTile(
title: Text(group.key),
subtitle: (group.value.description != null)
? Text(group.value.description!)
: null,
subtitle: group.value.description.mapNotNull(
(description) => Text(description),
),
leading: HardeningGroupIcon(
isActive: group.value.isActive,
isPartlyActive: group.value.isPartlyActive,
@@ -81,7 +86,7 @@ class BrowserHardeningScreen extends HookConsumerWidget {
trailing: const Icon(Icons.chevron_right),
onTap: () async {
await context.push(
BrowserHardeningGroupRoute(
WebEngineHardeningGroupRoute(
group: group.key,
).location,
);
@@ -100,8 +105,11 @@ class BrowserHardeningScreen extends HookConsumerWidget {
error: (error, stackTrace) => FailureWidget(
title: 'Could not load preference settings',
exception: error,
onRetry: () =>
ref.refresh(preferenceSettingsGeneralRepositoryProvider),
onRetry: () => ref.refresh(
unifiedPreferenceSettingsRepositoryProvider(
PreferencePartition.user,
),
),
),
loading: () => const SizedBox.shrink(),
),
@@ -1,18 +1,24 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/extensions/nullable.dart';
import 'package:lensai/features/geckoview/features/preferences/data/repositories/preference_settings.dart';
import 'package:lensai/features/geckoview/features/tabs/utils/setting_groups_serializer.dart';
import 'package:lensai/features/settings/presentation/widgets/hardening_group_icon.dart';
import 'package:lensai/presentation/widgets/failure_widget.dart';
class BrowserHardeningGroupScreen extends HookConsumerWidget {
class WebEngineHardeningGroupScreen extends HookConsumerWidget {
final String groupName;
const BrowserHardeningGroupScreen({super.key, required this.groupName});
const WebEngineHardeningGroupScreen({super.key, required this.groupName});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings =
ref.watch(preferenceSettingsGroupRepositoryProvider(groupName));
final settings = ref.watch(
preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
),
);
final theme = Theme.of(context);
@@ -36,18 +42,20 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
color: theme.colorScheme.onPrimaryContainer,
),
),
subtitle: (group.description != null)
? Text(
group.description!,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
),
)
: null,
subtitle: group.description.mapNotNull(
(description) => Text(
description,
style: TextStyle(
color: theme.colorScheme.onPrimaryContainer,
),
),
),
onChanged: (value) async {
final notifier = ref.read(
preferenceSettingsGroupRepositoryProvider(groupName)
.notifier,
preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
).notifier,
);
if (value) {
@@ -71,16 +79,17 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
Expanded(
child: SwitchListTile(
value: setting.value.isActive,
title: Text(setting.value.title),
subtitle: (setting.value.description != null)
? Text(setting.value.description!)
: null,
title: Text(setting.value.title ?? setting.key),
subtitle: setting.value.description.mapNotNull(
(description) => Text(description),
),
secondary: HardeningGroupIcon(
isActive: setting.value.isActive,
),
onChanged: (value) async {
final notifier = ref.read(
preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
).notifier,
);
@@ -96,16 +105,16 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
else
Expanded(
child: ListTile(
title: Text(setting.value.title),
subtitle: (setting.value.description != null)
? Text(setting.value.description!)
: null,
title: Text(setting.value.title ?? setting.key),
subtitle: setting.value.description.mapNotNull(
(description) => Text(description),
),
leading: HardeningGroupIcon(
isActive: setting.value.isActive,
),
trailing: Padding(
padding: const EdgeInsets.only(right: 18.0),
child: const Icon(Icons.check),
trailing: const Padding(
padding: EdgeInsets.only(right: 18.0),
child: Icon(Icons.check),
),
),
),
@@ -121,8 +130,12 @@ class BrowserHardeningGroupScreen extends HookConsumerWidget {
error: (error, stackTrace) => FailureWidget(
title: 'Could not load preference settings',
exception: error,
onRetry: () =>
ref.refresh(preferenceSettingsGroupRepositoryProvider(groupName)),
onRetry: () => ref.refresh(
preferenceSettingsGroupRepositoryProvider(
PreferencePartition.user,
groupName,
),
),
),
loading: () => const SizedBox.shrink(),
),
@@ -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:lensai/extensions/nullable.dart';
import 'package:lensai/features/user/data/database/database.dart';
part 'setting.g.dart';
@@ -7,12 +10,16 @@ part 'setting.g.dart';
class SettingDao extends DatabaseAccessor<UserDatabase> with _$SettingDaoMixin {
SettingDao(super.attachedDatabase);
Future<int> updateSetting(String key, Object? value) {
final driftvalue = (value != null) ? DriftAny(value) : null;
Future<int> updateSetting(String key, String? partitionKey, Object? value) {
final normalizedValue = (value is Iterable) ? jsonEncode(value) : value;
final driftvalue = normalizedValue
.mapNotNull((normalizedValue) => DriftAny(normalizedValue));
return db.setting.insertOne(
SettingCompanion.insert(
key: key,
partitionKey: Value(partitionKey),
value: Value(driftvalue),
),
onConflict: DoUpdate(
@@ -21,7 +28,12 @@ class SettingDao extends DatabaseAccessor<UserDatabase> with _$SettingDaoMixin {
);
}
Selectable<MapEntry<String, DriftAny?>> allSettings() {
return db.setting.select().map((row) => MapEntry(row.key, row.value));
Selectable<MapEntry<String, DriftAny?>> allSettingsOfPartitionKey(
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';
@DriftDatabase(include: {
'database.drift'
}, daos: [
SettingDao,
CacheDao,
])
@DriftDatabase(
include: {
'database.drift',
},
daos: [
SettingDao,
CacheDao,
],
)
class UserDatabase extends _$UserDatabase {
@override
final int schemaVersion = 1;
@@ -1,5 +1,6 @@
CREATE TABLE setting (
"key" TEXT PRIMARY KEY NOT NULL,
partition_key TEXT,
"value" ANY
) STRICT;
@@ -13,13 +13,18 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
type: DriftSqlType.string,
requiredDuringInsert: true,
$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>(
'value', aliasedName, true,
type: DriftSqlType.any,
requiredDuringInsert: false,
$customConstraints: '');
@override
List<GeneratedColumn> get $columns => [key, value];
List<GeneratedColumn> get $columns => [key, partitionKey, value];
@override
String get aliasedName => _alias ?? actualTableName;
@override
@@ -33,6 +38,8 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
return SettingData(
key: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}key'])!,
partitionKey: attachedDatabase.typeMapping
.read(DriftSqlType.string, data['${effectivePrefix}partition_key']),
value: attachedDatabase.typeMapping
.read(DriftSqlType.any, data['${effectivePrefix}value']),
);
@@ -51,12 +58,16 @@ class Setting extends Table with TableInfo<Setting, SettingData> {
class SettingData extends DataClass implements Insertable<SettingData> {
final String key;
final String? partitionKey;
final DriftAny? value;
const SettingData({required this.key, this.value});
const SettingData({required this.key, this.partitionKey, this.value});
@override
Map<String, Expression> toColumns(bool nullToAbsent) {
final map = <String, Expression>{};
map['key'] = Variable<String>(key);
if (!nullToAbsent || partitionKey != null) {
map['partition_key'] = Variable<String>(partitionKey);
}
if (!nullToAbsent || value != null) {
map['value'] = Variable<DriftAny>(value);
}
@@ -68,6 +79,7 @@ class SettingData extends DataClass implements Insertable<SettingData> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return SettingData(
key: serializer.fromJson<String>(json['key']),
partitionKey: serializer.fromJson<String?>(json['partition_key']),
value: serializer.fromJson<DriftAny?>(json['value']),
);
}
@@ -76,19 +88,27 @@ class SettingData extends DataClass implements Insertable<SettingData> {
serializer ??= driftRuntimeOptions.defaultSerializer;
return <String, dynamic>{
'key': serializer.toJson<String>(key),
'partition_key': serializer.toJson<String?>(partitionKey),
'value': serializer.toJson<DriftAny?>(value),
};
}
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(
key: key ?? this.key,
partitionKey:
partitionKey.present ? partitionKey.value : this.partitionKey,
value: value.present ? value.value : this.value,
);
SettingData copyWithCompanion(SettingCompanion data) {
return SettingData(
key: data.key.present ? data.key.value : this.key,
partitionKey: data.partitionKey.present
? data.partitionKey.value
: this.partitionKey,
value: data.value.present ? data.value.value : this.value,
);
}
@@ -97,51 +117,62 @@ class SettingData extends DataClass implements Insertable<SettingData> {
String toString() {
return (StringBuffer('SettingData(')
..write('key: $key, ')
..write('partitionKey: $partitionKey, ')
..write('value: $value')
..write(')'))
.toString();
}
@override
int get hashCode => Object.hash(key, value);
int get hashCode => Object.hash(key, partitionKey, value);
@override
bool operator ==(Object other) =>
identical(this, other) ||
(other is SettingData &&
other.key == this.key &&
other.partitionKey == this.partitionKey &&
other.value == this.value);
}
class SettingCompanion extends UpdateCompanion<SettingData> {
final Value<String> key;
final Value<String?> partitionKey;
final Value<DriftAny?> value;
final Value<int> rowid;
const SettingCompanion({
this.key = const Value.absent(),
this.partitionKey = const Value.absent(),
this.value = const Value.absent(),
this.rowid = const Value.absent(),
});
SettingCompanion.insert({
required String key,
this.partitionKey = const Value.absent(),
this.value = const Value.absent(),
this.rowid = const Value.absent(),
}) : key = Value(key);
static Insertable<SettingData> custom({
Expression<String>? key,
Expression<String>? partitionKey,
Expression<DriftAny>? value,
Expression<int>? rowid,
}) {
return RawValuesInsertable({
if (key != null) 'key': key,
if (partitionKey != null) 'partition_key': partitionKey,
if (value != null) 'value': value,
if (rowid != null) 'rowid': rowid,
});
}
SettingCompanion copyWith(
{Value<String>? key, Value<DriftAny?>? value, Value<int>? rowid}) {
{Value<String>? key,
Value<String?>? partitionKey,
Value<DriftAny?>? value,
Value<int>? rowid}) {
return SettingCompanion(
key: key ?? this.key,
partitionKey: partitionKey ?? this.partitionKey,
value: value ?? this.value,
rowid: rowid ?? this.rowid,
);
@@ -153,6 +184,9 @@ class SettingCompanion extends UpdateCompanion<SettingData> {
if (key.present) {
map['key'] = Variable<String>(key.value);
}
if (partitionKey.present) {
map['partition_key'] = Variable<String>(partitionKey.value);
}
if (value.present) {
map['value'] = Variable<DriftAny>(value.value);
}
@@ -166,6 +200,7 @@ class SettingCompanion extends UpdateCompanion<SettingData> {
String toString() {
return (StringBuffer('SettingCompanion(')
..write('key: $key, ')
..write('partitionKey: $partitionKey, ')
..write('value: $value, ')
..write('rowid: $rowid')
..write(')'))
@@ -396,11 +431,13 @@ abstract class _$UserDatabase extends GeneratedDatabase {
typedef $SettingCreateCompanionBuilder = SettingCompanion Function({
required String key,
Value<String?> partitionKey,
Value<DriftAny?> value,
Value<int> rowid,
});
typedef $SettingUpdateCompanionBuilder = SettingCompanion Function({
Value<String> key,
Value<String?> partitionKey,
Value<DriftAny?> value,
Value<int> rowid,
});
@@ -416,6 +453,9 @@ class $SettingFilterComposer extends Composer<_$UserDatabase, Setting> {
ColumnFilters<String> get key => $composableBuilder(
column: $table.key, builder: (column) => ColumnFilters(column));
ColumnFilters<String> get partitionKey => $composableBuilder(
column: $table.partitionKey, builder: (column) => ColumnFilters(column));
ColumnFilters<DriftAny> get value => $composableBuilder(
column: $table.value, builder: (column) => ColumnFilters(column));
}
@@ -431,6 +471,10 @@ class $SettingOrderingComposer extends Composer<_$UserDatabase, Setting> {
ColumnOrderings<String> get key => $composableBuilder(
column: $table.key, builder: (column) => ColumnOrderings(column));
ColumnOrderings<String> get partitionKey => $composableBuilder(
column: $table.partitionKey,
builder: (column) => ColumnOrderings(column));
ColumnOrderings<DriftAny> get value => $composableBuilder(
column: $table.value, builder: (column) => ColumnOrderings(column));
}
@@ -446,6 +490,9 @@ class $SettingAnnotationComposer extends Composer<_$UserDatabase, Setting> {
GeneratedColumn<String> get key =>
$composableBuilder(column: $table.key, builder: (column) => column);
GeneratedColumn<String> get partitionKey => $composableBuilder(
column: $table.partitionKey, builder: (column) => column);
GeneratedColumn<DriftAny> get value =>
$composableBuilder(column: $table.value, builder: (column) => column);
}
@@ -474,21 +521,25 @@ class $SettingTableManager extends RootTableManager<
$SettingAnnotationComposer($db: db, $table: table),
updateCompanionCallback: ({
Value<String> key = const Value.absent(),
Value<String?> partitionKey = const Value.absent(),
Value<DriftAny?> value = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
SettingCompanion(
key: key,
partitionKey: partitionKey,
value: value,
rowid: rowid,
),
createCompanionCallback: ({
required String key,
Value<String?> partitionKey = const Value.absent(),
Value<DriftAny?> value = const Value.absent(),
Value<int> rowid = const Value.absent(),
}) =>
SettingCompanion.insert(
key: key,
partitionKey: partitionKey,
value: value,
rowid: rowid,
),
@@ -1,4 +1,5 @@
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:json_annotation/json_annotation.dart';
@@ -6,18 +7,45 @@ part 'engine_settings.g.dart';
@CopyWith()
@JsonSerializable(includeIfNull: true, constructor: 'withDefaults')
class EngineSettings extends GeckoEngineSettings {
class EngineSettings extends GeckoEngineSettings with FastEquatable {
@override
bool get javascriptEnabled => super.javascriptEnabled!;
@override
TrackingProtectionPolicy get trackingProtectionPolicy =>
super.trackingProtectionPolicy!;
@override
HttpsOnlyMode get httpsOnlyMode => super.httpsOnlyMode!;
@override
ColorScheme get preferredColorScheme => super.preferredColorScheme!;
@override
bool get globalPrivacyControlEnabled => super.globalPrivacyControlEnabled!;
@override
CookieBannerHandlingMode get cookieBannerHandlingMode =>
super.cookieBannerHandlingMode!;
@override
CookieBannerHandlingMode get cookieBannerHandlingModePrivateBrowsing =>
super.cookieBannerHandlingModePrivateBrowsing!;
@override
bool get cookieBannerHandlingGlobalRules =>
super.cookieBannerHandlingGlobalRules!;
@override
bool get cookieBannerHandlingGlobalRulesSubFrames =>
super.cookieBannerHandlingGlobalRulesSubFrames!;
@override
WebContentIsolationStrategy get webContentIsolationStrategy =>
super.webContentIsolationStrategy!;
EngineSettings({
super.javascriptEnabled,
super.trackingProtectionPolicy,
super.httpsOnlyMode,
super.globalPrivacyControlEnabled,
super.preferredColorScheme,
super.cookieBannerHandlingMode,
super.cookieBannerHandlingModePrivateBrowsing,
super.cookieBannerHandlingGlobalRules,
super.cookieBannerHandlingGlobalRulesSubFrames,
super.webContentIsolationStrategy,
required super.javascriptEnabled,
required super.trackingProtectionPolicy,
required super.httpsOnlyMode,
required super.globalPrivacyControlEnabled,
required super.preferredColorScheme,
required super.cookieBannerHandlingMode,
required super.cookieBannerHandlingModePrivateBrowsing,
required super.cookieBannerHandlingGlobalRules,
required super.cookieBannerHandlingGlobalRulesSubFrames,
required super.webContentIsolationStrategy,
});
EngineSettings.withDefaults({
@@ -50,4 +78,26 @@ class EngineSettings extends GeckoEngineSettings {
webContentIsolationStrategy: webContentIsolationStrategy ??
WebContentIsolationStrategy.isolateHighValue,
);
factory EngineSettings.fromJson(Map<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>{
'javascriptEnabled': instance.javascriptEnabled,
'trackingProtectionPolicy':
_$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy],
'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode],
'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled,
_$TrackingProtectionPolicyEnumMap[instance.trackingProtectionPolicy]!,
'httpsOnlyMode': _$HttpsOnlyModeEnumMap[instance.httpsOnlyMode]!,
'preferredColorScheme':
_$ColorSchemeEnumMap[instance.preferredColorScheme],
_$ColorSchemeEnumMap[instance.preferredColorScheme]!,
'globalPrivacyControlEnabled': instance.globalPrivacyControlEnabled,
'cookieBannerHandlingMode':
_$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode],
_$CookieBannerHandlingModeEnumMap[instance.cookieBannerHandlingMode]!,
'cookieBannerHandlingModePrivateBrowsing':
_$CookieBannerHandlingModeEnumMap[
instance.cookieBannerHandlingModePrivateBrowsing],
instance.cookieBannerHandlingModePrivateBrowsing]!,
'cookieBannerHandlingGlobalRules':
instance.cookieBannerHandlingGlobalRules,
'cookieBannerHandlingGlobalRulesSubFrames':
instance.cookieBannerHandlingGlobalRulesSubFrames,
'webContentIsolationStrategy': _$WebContentIsolationStrategyEnumMap[
instance.webContentIsolationStrategy],
instance.webContentIsolationStrategy]!,
};
const _$TrackingProtectionPolicyEnumMap = {
@@ -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:lensai/features/user/data/providers.dart';
import 'package:lensai/features/user/domain/repositories/general_settings.dart';
import 'package:pocketbase/pocketbase.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -47,3 +48,11 @@ PocketBase pocketBase(Ref ref) {
return PocketBase('http://192.168.2.104:8090', authStore: authStore);
}
@Riverpod()
bool incognitoModeEnabled(Ref ref) {
return ref.watch(
generalSettingsRepositoryProvider
.select((value) => value.deleteBrowsingDataOnQuit != null),
);
}

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