diff --git a/app/lib/features/app_widget/domain/services/home_widget.dart b/app/lib/features/app_widget/domain/services/home_widget.dart index cf665cae..1ad71638 100644 --- a/app/lib/features/app_widget/domain/services/home_widget.dart +++ b/app/lib/features/app_widget/domain/services/home_widget.dart @@ -10,7 +10,7 @@ FutureOr widgetPinnable(WidgetPinnableRef ref) async { return await HomeWidget.isRequestPinWidgetSupported() ?? false; } -@riverpod +@Riverpod() Raw> appWidgetLaunchStream( AppWidgetLaunchStreamRef ref, ) { diff --git a/app/lib/features/app_widget/domain/services/home_widget.g.dart b/app/lib/features/app_widget/domain/services/home_widget.g.dart index 7db4df8a..6b87905e 100644 --- a/app/lib/features/app_widget/domain/services/home_widget.g.dart +++ b/app/lib/features/app_widget/domain/services/home_widget.g.dart @@ -22,7 +22,7 @@ final widgetPinnableProvider = FutureProvider.internal( typedef WidgetPinnableRef = FutureProviderRef; String _$appWidgetLaunchStreamHash() => - r'1cda869c62e270be74efcb9b052bc498cfbd98a5'; + r'8c5e21346d492f89f546b4e32135a283719e70e1'; /// See also [appWidgetLaunchStream]. @ProviderFor(appWidgetLaunchStream) diff --git a/app/lib/features/bangs/presentation/screens/categories.dart b/app/lib/features/bangs/presentation/screens/categories.dart index e74fecdb..3ab66e62 100644 --- a/app/lib/features/bangs/presentation/screens/categories.dart +++ b/app/lib/features/bangs/presentation/screens/categories.dart @@ -1,3 +1,4 @@ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; @@ -26,55 +27,61 @@ class BangCategoriesScreen extends HookConsumerWidget { ), body: categoriesAsync.when( data: (categories) { - return SingleChildScrollView( - child: HookBuilder( - builder: (context) { - final expanded = useState({}); + return FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return SingleChildScrollView( + controller: controller, + child: HookBuilder( + builder: (context) { + final expanded = useState({}); - return ExpansionPanelList( - expansionCallback: (index, expand) { - final key = categories.keys.elementAt(index); - if (!expanded.value.contains(key)) { - expanded.value = {...expanded.value, key}; - } else { - expanded.value = {...expanded.value}..remove(key); - } - }, - children: categories.entries - .map( - (category) => ExpansionPanel( - canTapOnHeader: true, - isExpanded: expanded.value.contains(category.key), - headerBuilder: (context, isExpanded) => ListTile( - title: Text(category.key), - ), - body: Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: category.value - .map( - (subCategory) => ListTile( - title: Text(subCategory), - onTap: () async { - await context.push( - BangSubCategoryRoute( - category: category.key, - subCategory: subCategory, - ).location, - ); - }, - ), - ) - .toList(), + return ExpansionPanelList( + expansionCallback: (index, expand) { + final key = categories.keys.elementAt(index); + if (!expanded.value.contains(key)) { + expanded.value = {...expanded.value, key}; + } else { + expanded.value = {...expanded.value}..remove(key); + } + }, + children: categories.entries + .map( + (category) => ExpansionPanel( + canTapOnHeader: true, + isExpanded: expanded.value.contains(category.key), + headerBuilder: (context, isExpanded) => ListTile( + title: Text(category.key), + ), + body: Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: category.value + .map( + (subCategory) => ListTile( + title: Text(subCategory), + onTap: () async { + await context.push( + BangSubCategoryRoute( + category: category.key, + subCategory: subCategory, + ).location, + ); + }, + ), + ) + .toList(), + ), + ), ), - ), - ), - ) - .toList(), - ); - }, - ), + ) + .toList(), + ); + }, + ), + ); + }, ); }, error: (error, stackTrace) => Center( diff --git a/app/lib/features/bangs/presentation/screens/search.dart b/app/lib/features/bangs/presentation/screens/search.dart index 70e2bf0f..aa8a4838 100644 --- a/app/lib/features/bangs/presentation/screens/search.dart +++ b/app/lib/features/bangs/presentation/screens/search.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; @@ -61,24 +62,30 @@ class BangSearchScreen extends HookConsumerWidget { ), body: resultsAsync.when( skipLoadingOnReload: true, - data: (bangs) => ListView.builder( - itemCount: bangs.length, - itemBuilder: (context, index) { - final bang = bangs[index]; - return BangDetails( - bang, - onTap: () { - ref - .read(selectedBangTriggerProvider().notifier) - .setTrigger(bang.trigger); + data: (bangs) => FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: bangs.length, + itemBuilder: (context, index) { + final bang = bangs[index]; + return BangDetails( + bang, + onTap: () { + ref + .read(selectedBangTriggerProvider().notifier) + .setTrigger(bang.trigger); - if (ref.read(bottomSheetProvider) is! CreateTab) { - ref.read(bottomSheetProvider.notifier).show( - CreateTab(preferredTool: KagiTool.search), - ); - } + if (ref.read(bottomSheetProvider) is! CreateTab) { + ref.read(bottomSheetProvider.notifier).show( + CreateTab(preferredTool: KagiTool.search), + ); + } - context.go(KagiRoute().location); + context.go(KagiRoute().location); + }, + ); }, ); }, diff --git a/app/lib/features/chat_archive/presentation/screens/list.dart b/app/lib/features/chat_archive/presentation/screens/list.dart index 5d63b89c..03488f2c 100644 --- a/app/lib/features/chat_archive/presentation/screens/list.dart +++ b/app/lib/features/chat_archive/presentation/screens/list.dart @@ -1,3 +1,4 @@ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -29,19 +30,26 @@ class ChatArchiveListScreen extends HookConsumerWidget { enabled: chatsAsync.isLoading, child: chatsAsync.when( data: (chats) { - return ListView.builder( - itemCount: chats.length, - itemBuilder: (context, index) { - final chat = chats[index]; + return FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: chats.length, + itemBuilder: (context, index) { + final chat = chats[index]; - return ListTile( - title: Text(chat.toString()), - subtitle: (chat.dateTime != null) - ? Text(chat.dateTime!.formatWithMinutePrecision()) - : null, - onTap: () async { - await context.push( - ChatArchiveDetailRoute(fileName: chat.fileName).location, + return ListTile( + title: Text(chat.toString()), + subtitle: (chat.dateTime != null) + ? Text(chat.dateTime!.formatWithMinutePrecision()) + : null, + onTap: () async { + await context.push( + ChatArchiveDetailRoute(fileName: chat.fileName) + .location, + ); + }, ); }, ); diff --git a/app/lib/features/chat_archive/presentation/screens/search.dart b/app/lib/features/chat_archive/presentation/screens/search.dart index c7de1f8d..af914209 100644 --- a/app/lib/features/chat_archive/presentation/screens/search.dart +++ b/app/lib/features/chat_archive/presentation/screens/search.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -60,43 +61,52 @@ class ChatArchiveSearchScreen extends HookConsumerWidget { ), body: resultsAsync.when( skipLoadingOnReload: true, - data: (chats) => ListView.builder( - itemCount: chats.length, - itemBuilder: (context, index) { - final chat = chats[index]; - final chatEntity = ChatEntity.fromFileName(chat.fileName); + data: (chats) => FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView.builder( + controller: controller, + itemCount: chats.length, + itemBuilder: (context, index) { + final chat = chats[index]; + final chatEntity = ChatEntity.fromFileName(chat.fileName); - return Card( - clipBehavior: Clip.antiAlias, - child: InkWell( - onTap: () async { - await context.push( - ChatArchiveDetailRoute(fileName: chat.fileName).location, - ); - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Markdown( - shrinkWrap: true, - padding: EdgeInsets.zero, - data: '## ${chat.title}', + return Card( + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: () async { + await context.push( + ChatArchiveDetailRoute(fileName: chat.fileName) + .location, + ); + }, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Markdown( + shrinkWrap: true, + padding: EdgeInsets.zero, + data: '## ${chat.title}', + ), + if (chatEntity.dateTime != null) + Text( + chatEntity.dateTime!.formatWithMinutePrecision(), + ), + const SizedBox(height: 8.0), + Markdown( + shrinkWrap: true, + padding: EdgeInsets.zero, + data: chat.contentSnippet, + ), + ], ), - if (chatEntity.dateTime != null) - Text(chatEntity.dateTime!.formatWithMinutePrecision()), - const SizedBox(height: 8.0), - Markdown( - shrinkWrap: true, - padding: EdgeInsets.zero, - data: chat.contentSnippet, - ), - ], + ), ), - ), - ), + ); + }, ); }, ), diff --git a/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart b/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart index eb6c8785..6d3bb807 100644 --- a/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart +++ b/app/lib/features/search_browser/presentation/widgets/app_bar_title.dart @@ -1,17 +1,15 @@ 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:lensai/features/web_view/domain/entities/web_view_page.dart'; import 'package:lensai/features/web_view/presentation/widgets/favicon.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_view.dart'; import 'package:text_scroll/text_scroll.dart'; -class AppBarTitle extends HookWidget { - final WebView activeWebView; +class AppBarTitle extends StatelessWidget { + final WebViewPage page; final void Function()? onTap; - const AppBarTitle({required this.activeWebView, this.onTap, super.key}); + const AppBarTitle({required this.page, this.onTap, super.key}); Icon _securityStatusIcon(BuildContext context, WebViewPage page) { if (page.url.isScheme('http')) { @@ -36,8 +34,6 @@ class AppBarTitle extends HookWidget { @override Widget build(BuildContext context) { - final page = useValueListenable(activeWebView.page); - final theme = Theme.of(context); return GestureDetector( onTap: onTap, diff --git a/app/lib/features/search_browser/presentation/widgets/landing/content.dart b/app/lib/features/search_browser/presentation/widgets/landing/content.dart index 9a5b17df..26f74f87 100644 --- a/app/lib/features/search_browser/presentation/widgets/landing/content.dart +++ b/app/lib/features/search_browser/presentation/widgets/landing/content.dart @@ -1,3 +1,4 @@ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show rootBundle; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -22,115 +23,127 @@ class LandingContent extends HookConsumerWidget { () async => rootBundle.loadString('assets/landing/changelog.md'), ); - return SingleChildScrollView( - child: Column( - children: [ - Text( - 'Lensai', - style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - Text( - 'The Privacy-Focused & AI-Powered Research Browser with Kagi integration', - style: textTheme.titleMedium, - textAlign: TextAlign.center, - ), - FractionallySizedBox( - widthFactor: 0.5, - child: Image.asset('assets/icon/icon.png'), - ), - const LandingAction(), - Consumer( - builder: (context, ref, child) { - final errors = ref.watch( - appInitializationServiceProvider - .select((result) => result.valueOrNull?.errors), - ); + return FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return SingleChildScrollView( + controller: controller, + child: Column( + children: [ + Text( + 'Lensai', + style: + textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + 'The Privacy-Focused & AI-Powered Research Browser with Kagi integration', + style: textTheme.titleMedium, + textAlign: TextAlign.center, + ), + ), + FractionallySizedBox( + widthFactor: 0.5, + child: Image.asset('assets/icon/icon.png'), + ), + const LandingAction(), + Consumer( + builder: (context, ref, child) { + final errors = ref.watch( + appInitializationServiceProvider + .select((result) => result.valueOrNull?.errors), + ); - if (errors == null || errors.isEmpty) { - return const SizedBox.shrink(); - } + if (errors == null || errors.isEmpty) { + return const SizedBox.shrink(); + } - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...errors.map( - (error) { - final theme = Theme.of(context); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8.0), - child: ErrorContainer( - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Error during App Initialization!', - style: theme.textTheme.titleMedium, + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...errors.map( + (error) { + final theme = Theme.of(context); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: ErrorContainer( + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Error during App Initialization!', + style: theme.textTheme.titleMedium, + ), + Text( + error.message, + style: theme.textTheme.titleSmall, + ), + if (error.details != null) + Text(error.details.toString()), + ], ), - Text( - error.message, - style: theme.textTheme.titleSmall, - ), - if (error.details != null) - Text(error.details.toString()), - ], + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: SizedBox( + width: double.infinity, + child: FilledButton.tonalIcon( + onPressed: () async { + await ref + .read( + appInitializationServiceProvider.notifier) + .reinitialize(); + }, + style: OutlinedButton.styleFrom( + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + label: const Text('Restart App'), + icon: const Icon(Icons.refresh_outlined), ), ), - ); - }, - ), - const SizedBox( - height: 8, - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: SizedBox( - width: double.infinity, - child: FilledButton.tonalIcon( - onPressed: () async { - await ref - .read(appInitializationServiceProvider.notifier) - .reinitialize(); - }, - style: OutlinedButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - label: const Text('Restart App'), - icon: const Icon(Icons.refresh_outlined), ), - ), - ), - ], - ); - }, + ], + ); + }, + ), + Markdown( + data: descriptionAsset.data ?? '', + onTapLink: (text, href, title) async { + if (href != null) { + await ref + .read(switchNewTabControllerProvider.notifier) + .add(Uri.parse(href)); + } + }, + selectable: true, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + Text( + 'Changelog', + style: + textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + Markdown( + data: changelogAsset.data ?? '', + selectable: true, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + ), + ], ), - Markdown( - data: descriptionAsset.data ?? '', - onTapLink: (text, href, title) async { - if (href != null) { - await ref - .read(switchNewTabControllerProvider.notifier) - .add(Uri.parse(href)); - } - }, - selectable: true, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - ), - Text( - 'Changelog', - style: textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold), - textAlign: TextAlign.center, - ), - Markdown( - data: changelogAsset.data ?? '', - selectable: true, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - ), - ], - ), + ); + }, ); } } diff --git a/app/lib/features/settings/presentation/screens/settings.dart b/app/lib/features/settings/presentation/screens/settings.dart index 7115320a..e00fc66c 100644 --- a/app/lib/features/settings/presentation/screens/settings.dart +++ b/app/lib/features/settings/presentation/screens/settings.dart @@ -1,3 +1,4 @@ +import 'package:fading_scroll/fading_scroll.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -82,377 +83,386 @@ class SettingsScreen extends HookConsumerWidget { return Scaffold( appBar: AppBar(title: const Text('Settings')), - body: ListView( - children: [ - _buildSection(theme, 'Kagi'), - Padding( - padding: const EdgeInsets.only( - left: 16, - right: 16, - bottom: 8, - ), - child: TextField( - enableIMEPersonalizedLearning: false, - autocorrect: false, - controller: kagiSessionTextController, - obscureText: hideSessionText.value, - decoration: InputDecoration( - label: const Text('Kagi Session Token'), - hintText: 'https://kagi.com/search?token=...', - helperMaxLines: 2, - helper: Markdown( - shrinkWrap: true, - padding: EdgeInsets.zero, - data: - 'You can visit your [Kagi Account Settings](user_details) to get your Session Link.', - onTapLink: (text, href, title) async { - if (href == 'user_details') { - await ui_helper.launchUrlFeedback( - context, - Uri.parse( - 'https://kagi.com/settings?p=user_details', - ), - ); - } - }, + body: FadingScroll( + fadingSize: 25, + builder: (context, controller) { + return ListView( + controller: controller, + children: [ + _buildSection(theme, 'Kagi'), + Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 8, ), - suffixIcon: IconButton( - onPressed: () { - hideSessionText.value = !hideSessionText.value; - }, - icon: Icon( - hideSessionText.value - ? Icons.visibility - : Icons.visibility_off, + child: TextField( + enableIMEPersonalizedLearning: false, + autocorrect: false, + controller: kagiSessionTextController, + obscureText: hideSessionText.value, + decoration: InputDecoration( + label: const Text('Kagi Session Token'), + hintText: 'https://kagi.com/search?token=...', + helperMaxLines: 2, + helper: Markdown( + shrinkWrap: true, + padding: EdgeInsets.zero, + data: + 'You can visit your [Kagi Account Settings](user_details) to get your Session Link.', + onTapLink: (text, href, title) async { + if (href == 'user_details') { + await ui_helper.launchUrlFeedback( + context, + Uri.parse( + 'https://kagi.com/settings?p=user_details', + ), + ); + } + }, + ), + suffixIcon: IconButton( + onPressed: () { + hideSessionText.value = !hideSessionText.value; + }, + icon: Icon( + hideSessionText.value + ? Icons.visibility + : Icons.visibility_off, + ), + ), ), ), ), - ), - ), - SwitchListTile.adaptive( - title: const Text('Show Early Access Features'), - subtitle: const Text( - "Displays Kagi's early access features in the UI. As an Ultimate subscriber, you will likely want to have this enabled.", - ), - value: settings.showEarlyAccessFeatures, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.showEarlyAccessFeatures(value), - ); - }, - ), - const SizedBox( - height: 16, - ), - _buildSection(theme, 'General'), - 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, + SwitchListTile.adaptive( + title: const Text('Show Early Access Features'), + subtitle: const Text( + "Displays Kagi's early access features in the UI. As an Ultimate subscriber, you will likely want to have this enabled.", ), - Center( - child: SegmentedButton( - segments: const [ - ButtonSegment( - value: ThemeMode.system, - icon: Icon(Icons.brightness_auto), - label: Text('System'), + value: settings.showEarlyAccessFeatures, + onChanged: (value) async { + await ref.read(saveSettingsControllerProvider.notifier).save( + (currentSettings) => currentSettings.copyWith + .showEarlyAccessFeatures(value), + ); + }, + ), + const SizedBox( + height: 16, + ), + _buildSection(theme, 'General'), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Theme', + style: theme.textTheme.bodyLarge, + ), + Center( + child: SegmentedButton( + segments: const [ + ButtonSegment( + value: ThemeMode.system, + icon: Icon(Icons.brightness_auto), + label: Text('System'), + ), + ButtonSegment( + value: ThemeMode.light, + icon: Icon(Icons.light_mode), + label: Text('Light'), + ), + ButtonSegment( + value: ThemeMode.dark, + icon: Icon(Icons.dark_mode), + label: Text('Dark'), + ), + ], + selected: {settings.themeMode}, + onSelectionChanged: (value) async { + await ref + .read(saveSettingsControllerProvider.notifier) + .save( + (currentSettings) => currentSettings.copyWith + .themeMode(value.first), + ); + }, ), - 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('Launch Links Externally'), - subtitle: const Text( - 'Opens all links (except for kagi.com) in your default browser.', - ), - value: settings.launchUrlExternal, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.launchUrlExternal(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, - ), - _buildSection(theme, 'Appearance'), - Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const CustomListTile( - title: 'Quick Action', - subtitle: - 'Appears in the browser app bar between website title and tab count.', + ), + SwitchListTile.adaptive( + title: const Text('Incognito Mode'), + subtitle: const Text( + 'Deletes all browsing data upon app restart for enhanced privacy.', ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Center( - child: SegmentedButton( - emptySelectionAllowed: true, - segments: [ - ButtonSegment( - value: KagiTool.search, - icon: Icon(KagiTool.search.icon), - label: const Text('Search'), + 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('Launch Links Externally'), + subtitle: const Text( + 'Opens all links (except for kagi.com) in your default browser.', + ), + value: settings.launchUrlExternal, + onChanged: (value) async { + await ref.read(saveSettingsControllerProvider.notifier).save( + (currentSettings) => + currentSettings.copyWith.launchUrlExternal(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, + ), + _buildSection(theme, 'Appearance'), + Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const CustomListTile( + title: 'Quick Action', + subtitle: + 'Appears in the browser app bar between website title and tab count.', + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Center( + child: SegmentedButton( + emptySelectionAllowed: true, + segments: [ + ButtonSegment( + value: KagiTool.search, + icon: Icon(KagiTool.search.icon), + label: const Text('Search'), + ), + ButtonSegment( + value: KagiTool.summarizer, + icon: Icon(KagiTool.summarizer.icon), + label: const Text('Summarizer'), + ), + ButtonSegment( + value: KagiTool.assistant, + icon: Icon(KagiTool.assistant.icon), + label: const Text('Assistant'), + ), + ], + selected: { + if (settings.quickAction != null) + settings.quickAction!, + }, + onSelectionChanged: (value) async { + await ref + .read(saveSettingsControllerProvider.notifier) + .save( + (currentSettings) => + currentSettings.copyWith.quickAction( + value.isNotEmpty ? value.first : null, + ), + ); + }, ), - ButtonSegment( - value: KagiTool.summarizer, - icon: Icon(KagiTool.summarizer.icon), - label: const Text('Summarizer'), - ), - ButtonSegment( - value: KagiTool.assistant, - icon: Icon(KagiTool.assistant.icon), - label: const Text('Assistant'), - ), - ], - selected: { - if (settings.quickAction != null) settings.quickAction!, - }, - onSelectionChanged: (value) async { + ), + ), + ], + ), + ), + SwitchListTile.adaptive( + title: const Text('Quick Action - STT'), + subtitle: const Text( + 'Quick option will open with speech-to-text input.', + ), + value: settings.quickActionVoiceInput, + onChanged: (settings.quickAction != null) + ? (value) async { await ref .read(saveSettingsControllerProvider.notifier) .save( - (currentSettings) => - currentSettings.copyWith.quickAction( - value.isNotEmpty ? value.first : null, - ), + (currentSettings) => currentSettings.copyWith + .quickActionVoiceInput(value), ); - }, - ), - ), - ), - ], - ), - ), - SwitchListTile.adaptive( - title: const Text('Quick Action - STT'), - subtitle: const Text( - 'Quick option will open with speech-to-text input.', - ), - value: settings.quickActionVoiceInput, - onChanged: (settings.quickAction != null) - ? (value) async { - await ref - .read(saveSettingsControllerProvider.notifier) - .save( - (currentSettings) => currentSettings.copyWith - .quickActionVoiceInput(value), - ); - } - : null, - ), - _buildSection(theme, 'Content Blocking'), - SwitchListTile.adaptive( - title: const Text('Enable Content Blocking'), - subtitle: const Text( - 'Prevents access to unwanted websites and ads, as defined in the selected lists below.', - ), - value: settings.enableContentBlocking, - onChanged: (value) async { - await ref.read(saveSettingsControllerProvider.notifier).save( - (currentSettings) => - currentSettings.copyWith.enableContentBlocking(value), - ); - }, - ), - _buildSubSection(theme, 'Lists'), - HostListTile( - enableContentBlocking: settings.enableContentBlocking, - enableHostLists: settings.enableHostList, - source: HostSource.stevenBlackUnified, - title: 'StevenBlack: Unified', - subtitle: 'Blocks domains containing adware, malware and trackers.', - ), - HostListTile( - enableContentBlocking: settings.enableContentBlocking, - enableHostLists: settings.enableHostList, - source: HostSource.stevenBlackFakeNews, - title: 'StevenBlack: Fake News', - subtitle: 'Blocks domains known for spreading fake news.', - ), - HostListTile( - enableContentBlocking: settings.enableContentBlocking, - enableHostLists: settings.enableHostList, - source: HostSource.stevenBlackGambling, - title: 'StevenBlack: Gambling', - subtitle: 'Blocks domains related to gambling.', - ), - HostListTile( - enableContentBlocking: settings.enableContentBlocking, - enableHostLists: settings.enableHostList, - source: HostSource.stevenBlackPorn, - title: 'StevenBlack: Porn', - subtitle: 'Blocks adult content domains.', - ), - HostListTile( - enableContentBlocking: settings.enableContentBlocking, - enableHostLists: settings.enableHostList, - source: HostSource.stevenBlackSocial, - title: 'StevenBlack: Social', - subtitle: 'Blocks social media domains.', - ), - const SizedBox( - height: 16, - ), - _buildSection(theme, 'Bangs'), - 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'), - ), - ), - Consumer( - builder: (context, ref, child) { - final size = ref.watch( - bangIconCacheSizeMegabytesProvider.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'), - ], - ), - ], - ), - ), + } + : null, + ), + _buildSection(theme, 'Content Blocking'), + SwitchListTile.adaptive( + title: const Text('Enable Content Blocking'), + subtitle: const Text( + 'Prevents access to unwanted websites and ads, as defined in the selected lists below.', ), + value: settings.enableContentBlocking, + onChanged: (value) async { + await ref.read(saveSettingsControllerProvider.notifier).save( + (currentSettings) => currentSettings.copyWith + .enableContentBlocking(value), + ); + }, + ), + _buildSubSection(theme, 'Lists'), + HostListTile( + enableContentBlocking: settings.enableContentBlocking, + enableHostLists: settings.enableHostList, + source: HostSource.stevenBlackUnified, + title: 'StevenBlack: Unified', + subtitle: + 'Blocks domains containing adware, malware and trackers.', + ), + HostListTile( + enableContentBlocking: settings.enableContentBlocking, + enableHostLists: settings.enableHostList, + source: HostSource.stevenBlackFakeNews, + title: 'StevenBlack: Fake News', + subtitle: 'Blocks domains known for spreading fake news.', + ), + HostListTile( + enableContentBlocking: settings.enableContentBlocking, + enableHostLists: settings.enableHostList, + source: HostSource.stevenBlackGambling, + title: 'StevenBlack: Gambling', + subtitle: 'Blocks domains related to gambling.', + ), + HostListTile( + enableContentBlocking: settings.enableContentBlocking, + enableHostLists: settings.enableHostList, + source: HostSource.stevenBlackPorn, + title: 'StevenBlack: Porn', + subtitle: 'Blocks adult content domains.', + ), + HostListTile( + enableContentBlocking: settings.enableContentBlocking, + enableHostLists: settings.enableHostList, + source: HostSource.stevenBlackSocial, + title: 'StevenBlack: Social', + subtitle: 'Blocks social media domains.', + ), + const SizedBox( + height: 16, + ), + _buildSection(theme, 'Bangs'), + CustomListTile( + title: 'Bang Frequencies', + subtitle: 'Tracked usage for Bang recommendations', suffix: FilledButton.icon( onPressed: () async { await ref .read(bangDataRepositoryProvider.notifier) - .clearIconData(); + .resetFrequencies(); }, icon: const Icon(Icons.delete), label: const Text('Clear'), ), - ); - }, - ), - _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', - ), - ], + ), + Consumer( + builder: (context, ref, child) { + final size = ref.watch( + bangIconCacheSizeMegabytesProvider.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(bangDataRepositoryProvider.notifier) + .clearIconData(); + }, + icon: const Icon(Icons.delete), + label: const Text('Clear'), + ), + ); + }, + ), + _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', + ), + ], + ); + }, ), ); } diff --git a/app/lib/features/share_intent/domain/services/sharing_intent.dart b/app/lib/features/share_intent/domain/services/sharing_intent.dart index 82b78f68..a45fea1b 100644 --- a/app/lib/features/share_intent/domain/services/sharing_intent.dart +++ b/app/lib/features/share_intent/domain/services/sharing_intent.dart @@ -47,7 +47,7 @@ final _sharingIntentTransformer = }, ); -@riverpod +@Riverpod() Raw> sharingIntentStream(SharingIntentStreamRef ref) { final initialStream = FlutterSharingIntent.instance // ignore: discarded_futures diff --git a/app/lib/features/share_intent/domain/services/sharing_intent.g.dart b/app/lib/features/share_intent/domain/services/sharing_intent.g.dart index 6c66274f..6de1b8e0 100644 --- a/app/lib/features/share_intent/domain/services/sharing_intent.g.dart +++ b/app/lib/features/share_intent/domain/services/sharing_intent.g.dart @@ -7,7 +7,7 @@ part of 'sharing_intent.dart'; // ************************************************************************** String _$sharingIntentStreamHash() => - r'e396c15da84d6e726f9e77996ef9d2422f6269e1'; + r'ce060aadf885ab0a14d90acfe986447ecb05f4f9'; /// See also [sharingIntentStream]. @ProviderFor(sharingIntentStream)