Add Supa account and search changes

This commit is contained in:
Fabian Freund
2026-05-22 18:10:22 +02:00
parent 3a19865b2e
commit 51289f1266
374 changed files with 54061 additions and 5013 deletions
@@ -17,89 +17,117 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:fading_scroll/fading_scroll.dart';
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:hooks_riverpod/hooks_riverpod.dart';
import 'package:nullability/nullability.dart';
import 'package:weblibre/core/routing/routes.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
import 'package:weblibre/features/search/domain/entities/abstract/i_search_suggestion_provider.dart';
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
import 'package:weblibre/features/settings/presentation/widgets/bang_icon.dart';
import 'package:weblibre/features/settings/presentation/widgets/default_search_selector.dart';
import 'package:weblibre/features/settings/presentation/widgets/sections.dart';
import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart';
import 'package:weblibre/features/user/data/models/general_settings.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
const List<SettingsSectionDefinition> searchSettingsSections = [
SettingsSectionDefinition(
title: 'Providers',
keywords: ['engines'],
entries: [
SettingsEntryDefinition(
title: 'Default Search Provider',
subtitle: 'Choose the default engine for searches',
keywords: ['search engine'],
child: _DefaultSearchProviderSection(),
),
SettingsEntryDefinition(
title: 'Default Autocomplete Provider',
subtitle: 'Choose the provider for search suggestions',
keywords: ['suggestions'],
child: _AutocompleteProviderSection(),
),
SettingsEntryDefinition(
title: 'Custom Search Engines',
subtitle: 'Add and manage your own search providers',
keywords: ['user bangs', 'providers'],
child: _CustomSearchEnginesTile(),
),
],
),
SettingsSectionDefinition(
title: 'Bang Shortcuts',
keywords: ['bangs'],
entries: [
SettingsEntryDefinition(
title: 'Bang Settings',
subtitle: 'Manage bang repositories and usage data',
keywords: ['shortcuts', 'bangs'],
child: _BangsTile(),
),
],
),
SettingsSectionDefinition(
title: 'History & Suggestions',
entries: [
SettingsEntryDefinition(
title: 'Search History Limit',
subtitle: 'Maximum number of recent searches to remember',
keywords: ['history', 'entries'],
child: _MaxSearchHistoryEntriesSection(),
),
SettingsEntryDefinition(
title: 'Allow clipboard access for suggestions',
subtitle: 'Browser can read clipboard to suggest URLs',
keywords: ['clipboard'],
child: _AllowClipboardAccessTile(),
),
SettingsEntryDefinition(
title: 'Autocomplete on enter',
subtitle: 'Accept the inline suggestion when pressing enter',
keywords: ['submit', 'keyboard', 'suggestions'],
child: _AcceptSuggestionOnSubmitTile(),
),
],
),
SettingsSectionDefinition(
title: 'Local Search Index',
keywords: ['on device search', 'index'],
entries: [
SettingsEntryDefinition(
title: 'Enable local search index',
subtitle: 'Index visited pages locally for content search',
keywords: ['page text', 'history'],
child: _LocalIndexEnabledTile(),
),
SettingsEntryDefinition(
title: 'Index private tabs',
subtitle: 'Include private tabs in the local index',
keywords: ['incognito'],
child: _IndexPrivateTabsTile(),
),
SettingsEntryDefinition(
title: 'Indexed pages',
subtitle: 'View and clear the local index',
keywords: ['clear index', 'stats'],
child: _LocalIndexStatsTile(),
),
],
),
];
class SearchSettingsScreen extends StatelessWidget {
const SearchSettingsScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Search')),
body: SafeArea(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return ListView(
controller: controller,
padding: const EdgeInsets.symmetric(horizontal: 12.0),
children: const [
_ProvidersSection(),
_BangShortcutsSection(),
_HistorySuggestionsSection(),
],
);
},
),
),
);
}
}
class _ProvidersSection extends StatelessWidget {
const _ProvidersSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Providers'),
_DefaultSearchProviderSection(),
_AutocompleteProviderSection(),
_CustomSearchEnginesTile(),
],
);
}
}
class _BangShortcutsSection extends StatelessWidget {
const _BangShortcutsSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'Bang Shortcuts'),
_BangsTile(),
],
);
}
}
class _HistorySuggestionsSection extends StatelessWidget {
const _HistorySuggestionsSection();
@override
Widget build(BuildContext context) {
return const Column(
children: [
SettingSection(name: 'History & Suggestions'),
_MaxSearchHistoryEntriesSection(),
_AllowClipboardAccessTile(),
],
return const SettingsDetailScaffold(
title: 'Search',
subtitle: 'Providers, bangs, history suggestions, and on-device search.',
icon: MdiIcons.magnify,
sections: searchSettingsSections,
);
}
}
@@ -328,3 +356,153 @@ class _AllowClipboardAccessTile extends HookConsumerWidget {
);
}
}
class _AcceptSuggestionOnSubmitTile extends HookConsumerWidget {
const _AcceptSuggestionOnSubmitTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final acceptSuggestionOnSubmit = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.acceptSuggestionOnSubmit,
),
);
return SwitchListTile.adaptive(
title: const Text('Autocomplete on enter'),
subtitle: const Text(
'Accept the inline suggestion when pressing enter on the keyboard',
),
secondary: const Icon(Icons.keyboard_return),
value: acceptSuggestionOnSubmit,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.acceptSuggestionOnSubmit(value),
);
},
);
}
}
class _LocalIndexEnabledTile extends HookConsumerWidget {
const _LocalIndexEnabledTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final enabled = ref.watch(
generalSettingsWithDefaultsProvider.select(
(s) => s.enableLocalSearchIndex,
),
);
return SwitchListTile.adaptive(
title: const Text('Enable local search index'),
subtitle: const Text(
'Index visited pages locally so the browser can search their '
'content. Visit metadata stays in the engine; only page text is '
'stored on-device.',
),
secondary: const Icon(MdiIcons.bookSearchOutline),
value: enabled,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.enableLocalSearchIndex(value),
);
},
);
}
}
class _IndexPrivateTabsTile extends HookConsumerWidget {
const _IndexPrivateTabsTile();
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(generalSettingsWithDefaultsProvider);
final enabled = settings.enableLocalSearchIndex;
final indexPrivate = settings.indexPrivateTabs;
return SwitchListTile.adaptive(
title: const Text('Index private tabs'),
subtitle: const Text(
'Include pages opened in private tabs in the local index. '
'Off by default.',
),
secondary: const Icon(MdiIcons.incognito),
value: indexPrivate,
onChanged: enabled
? (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) =>
currentSettings.copyWith.indexPrivateTabs(value),
);
}
: null,
);
}
}
class _LocalIndexStatsTile extends HookConsumerWidget {
const _LocalIndexStatsTile();
Future<bool?> _confirmClear(BuildContext context) {
return showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Clear local search index?'),
content: const Text(
'This removes all locally indexed page content. Engine history '
'(visit metadata) is not affected.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Clear'),
),
],
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
// Bump on Clear to re-trigger the count query.
final refreshTick = useState(0);
final countSnapshot = useFuture(
useMemoized(() => ref.read(tabDatabaseProvider).historyDao.countRows(), [
refreshTick.value,
]),
);
final count = countSnapshot.data;
return ListTile(
leading: const Icon(MdiIcons.databaseOutline),
title: const Text('Indexed pages'),
subtitle: Text(count.mapNotNull((c) => '$c pages indexed') ?? 'Loading…'),
trailing: TextButton.icon(
icon: const Icon(MdiIcons.deleteOutline),
label: const Text('Clear'),
onPressed: count == null || count == 0
? null
: () async {
final confirmed = await _confirmClear(context);
if (confirmed != true) return;
await ref.read(tabDatabaseProvider).historyDao.clear();
refreshTick.value++;
},
),
);
}
}