intermediate

This commit is contained in:
Fabian Freund
2025-01-07 00:06:31 +01:00
parent ea0880b97a
commit 4a2191b01d
84 changed files with 2655 additions and 1383 deletions
@@ -2,10 +2,12 @@
import 'dart:async';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:riverpod/riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -97,3 +99,34 @@ List<String> availableTabIds(
return containerTabs?.where((tabId) => tabStates.contains(tabId)).toList() ??
[];
}
@Riverpod()
List<String> seamlessFilteredTabs(
Ref ref,
String? containerId,
) {
final tabSearchResults = ref
.watch(
tabSearchRepositoryProvider.select(
(value) => EquatableCollection(
value.valueOrNull?.map((tab) => tab.id).toList(),
immutable: true,
),
),
)
.collection;
final availableTabs = ref
.watch(
availableTabIdsProvider(containerId).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
if (tabSearchResults == null) {
return availableTabs;
}
return tabSearchResults.where((tab) => availableTabs.contains(tab)).toList();
}
@@ -288,6 +288,140 @@ class _AvailableTabIdsProviderElement
String? get containerId => (origin as AvailableTabIdsProvider).containerId;
}
String _$seamlessFilteredTabsHash() =>
r'1abfb0e9ae834cdde5ab64b6174f24f45f36a272';
/// See also [seamlessFilteredTabs].
@ProviderFor(seamlessFilteredTabs)
const seamlessFilteredTabsProvider = SeamlessFilteredTabsFamily();
/// See also [seamlessFilteredTabs].
class SeamlessFilteredTabsFamily extends Family<List<String>> {
/// See also [seamlessFilteredTabs].
const SeamlessFilteredTabsFamily();
/// See also [seamlessFilteredTabs].
SeamlessFilteredTabsProvider call(
String? containerId,
) {
return SeamlessFilteredTabsProvider(
containerId,
);
}
@override
SeamlessFilteredTabsProvider getProviderOverride(
covariant SeamlessFilteredTabsProvider provider,
) {
return call(
provider.containerId,
);
}
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'seamlessFilteredTabsProvider';
}
/// See also [seamlessFilteredTabs].
class SeamlessFilteredTabsProvider extends AutoDisposeProvider<List<String>> {
/// See also [seamlessFilteredTabs].
SeamlessFilteredTabsProvider(
String? containerId,
) : this._internal(
(ref) => seamlessFilteredTabs(
ref as SeamlessFilteredTabsRef,
containerId,
),
from: seamlessFilteredTabsProvider,
name: r'seamlessFilteredTabsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$seamlessFilteredTabsHash,
dependencies: SeamlessFilteredTabsFamily._dependencies,
allTransitiveDependencies:
SeamlessFilteredTabsFamily._allTransitiveDependencies,
containerId: containerId,
);
SeamlessFilteredTabsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.containerId,
}) : super.internal();
final String? containerId;
@override
Override overrideWith(
List<String> Function(SeamlessFilteredTabsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: SeamlessFilteredTabsProvider._internal(
(ref) => create(ref as SeamlessFilteredTabsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
containerId: containerId,
),
);
}
@override
AutoDisposeProviderElement<List<String>> createElement() {
return _SeamlessFilteredTabsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SeamlessFilteredTabsProvider &&
other.containerId == containerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SeamlessFilteredTabsRef on AutoDisposeProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
}
class _SeamlessFilteredTabsProviderElement
extends AutoDisposeProviderElement<List<String>>
with SeamlessFilteredTabsRef {
_SeamlessFilteredTabsProviderElement(super.provider);
@override
String? get containerId =>
(origin as SeamlessFilteredTabsProvider).containerId;
}
String _$selectedBangTriggerHash() =>
r'b0e12bc95e93d50d04f1c658230cea9f15ff0385';
@@ -6,7 +6,6 @@ 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:lensai/data/models/web_page_info.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/presentation/widgets/site_search.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
@@ -14,13 +13,11 @@ import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/utils/url_builder.dart' as uri_builder;
import 'package:lensai/features/settings/data/models/settings.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/features/user/domain/repositories/settings.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;
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
import 'package:share_plus/share_plus.dart';
class WebPageDialog extends HookConsumerWidget {
@@ -39,9 +36,7 @@ class WebPageDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final incognitoEnabled = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults()).incognitoMode,
),
settingsRepositoryProvider.select((value) => value.incognitoMode),
);
final availableBangsAsync = ref.watch(
@@ -57,8 +52,9 @@ class WebPageDialog extends HookConsumerWidget {
final availableBangCount = availableBangsAsync.valueOrNull?.length;
final formKey = useMemoized(() => GlobalKey<FormState>());
final urlTextController = useTextEditingController(text: url.toString());
final urlTextFocusNode = useFocusNode();
final addressTextController =
useTextEditingController(text: url.toString());
final addressTextFocusNode = useFocusNode();
return Stack(
children: [
@@ -83,46 +79,35 @@ class WebPageDialog extends HookConsumerWidget {
child: Form(
key: formKey,
child: TextFormField(
controller: urlTextController,
focusNode: urlTextFocusNode,
controller: addressTextController,
focusNode: addressTextFocusNode,
enableIMEPersonalizedLearning: !incognitoEnabled,
decoration: InputDecoration(
labelText: 'URL',
suffixIcon: IconButton(
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
await ref
.read(tabSessionProvider(tabId: null).notifier)
.loadUrl(
url: uri_parser.tryParseUrl(
urlTextController.text,
eagerParsing: true,
)!,
);
onDismiss?.call();
}
},
icon: const Icon(Icons.send),
),
decoration: const InputDecoration(
labelText: 'Address',
),
validator: (value) {
if (uri_parser.tryParseUrl(value, eagerParsing: true) !=
null) {
if (value != null && Uri.tryParse(value) != null) {
return null;
}
return 'Invalid URL';
return 'Invalid Address';
},
onTap: () {
if (!urlTextFocusNode.hasFocus) {
if (!addressTextFocusNode.hasFocus) {
// Select all text when the field is tapped
urlTextController.selection = TextSelection(
addressTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: urlTextController.text.length,
extentOffset: addressTextController.text.length,
);
}
},
onFieldSubmitted: (value) async {
await ref
.read(tabSessionProvider(tabId: null).notifier)
.loadUrl(url: Uri.tryParse(value)!);
onDismiss?.call();
},
),
),
),
@@ -148,12 +133,12 @@ class WebPageDialog extends HookConsumerWidget {
loading: () => SiteSearch(
domain: url.host,
availableBangs: [
BangData(
websiteName: 'websiteName',
domain: 'domain',
trigger: 'trigger',
urlTemplate: 'urlTemplate',
),
// BangData(
// websiteName: 'websiteName',
// domain: 'domain',
// trigger: 'trigger',
// urlTemplate: 'urlTemplate',
// ),
],
),
),
@@ -181,7 +166,7 @@ class WebPageDialog extends HookConsumerWidget {
),
ListTile(
leading: const Icon(MdiIcons.tabPlus),
title: const Text('Open in new tab'),
title: const Text('Clone tab'),
onTap: () async {
await ref.read(tabRepositoryProvider.notifier).addTab(url: url);
@@ -1,5 +1,3 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
@@ -30,13 +28,10 @@ import 'package:lensai/features/geckoview/features/find_in_page/presentation/wid
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/data/services/session.dart';
import 'package:lensai/features/settings/data/models/settings.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
import 'package:lensai/features/user/domain/repositories/settings.dart';
import 'package:lensai/presentation/hooks/overlay_portal_controller.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
import 'package:share_plus/share_plus.dart';
import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart';
class BrowserScreen extends HookConsumerWidget {
const BrowserScreen({super.key});
@@ -54,26 +49,6 @@ class BrowserScreen extends HookConsumerWidget {
final displayedSheet = ref.watch(bottomSheetControllerProvider);
final displayedOverlayDialog = ref.watch(overlayDialogControllerProvider);
final showEarlyAccessFeatures = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults())
.showEarlyAccessFeatures,
),
);
final quickAction = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults()).quickAction,
),
);
final quickActionVoice = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults())
.quickActionVoiceInput,
),
);
final selectedTabId =
ref.watch(selectedTabStateProvider.select((value) => value?.id));
@@ -97,26 +72,23 @@ class BrowserScreen extends HookConsumerWidget {
);
ref.listen(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.enableJavascript),
settingsRepositoryProvider.select((value) => value.enableJavascript),
(previous, next) async {
if (next != null) {
await ref.read(engineSettingsServiceProvider).javaScriptEnabled(next);
}
await ref.read(engineSettingsServiceProvider).javaScriptEnabled(next);
},
);
ref.listen(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.kagiSession),
(previous, next) async {
if (next != null && next.isNotEmpty) {
await ref
.read(kagiSessionServiceProvider.notifier)
.setKagiSession(next);
}
},
);
// ref.listen(
// settingsRepositoryProvider
// .select((value) => value.valueOrNull?.kagiSession),
// (previous, next) async {
// if (next != null && next.isNotEmpty) {
// await ref
// .read(kagiSessionServiceProvider.notifier)
// .setKagiSession(next);
// }
// },
// );
return PopScope(
//We need this for BackButtonListener to work downstream
@@ -217,78 +189,78 @@ class BrowserScreen extends HookConsumerWidget {
},
icon: Icon(KagiTool.summarizer.icon),
),
if (showEarlyAccessFeatures)
IconButton(
color: (activeTool == KagiTool.assistant)
? Theme.of(context).colorScheme.primary
: null,
onPressed: () {
ref
.read(
createTabStreamProvider.notifier,
)
.createTab(
CreateTabSheet(
preferredTool: KagiTool.assistant,
),
);
},
icon: Icon(KagiTool.assistant.icon),
),
// if (showEarlyAccessFeatures)
// IconButton(
// color: (activeTool == KagiTool.assistant)
// ? Theme.of(context).colorScheme.primary
// : null,
// onPressed: () {
// ref
// .read(
// createTabStreamProvider.notifier,
// )
// .createTab(
// CreateTabSheet(
// preferredTool: KagiTool.assistant,
// ),
// );
// },
// icon: Icon(KagiTool.assistant.icon),
// ),
],
);
},
),
actions: [
if (selectedTabId != null) ReaderButton(),
if (quickAction != null)
InkWell(
onTap: () async {
var tab = CreateTabSheet(
preferredTool: quickAction,
);
// if (quickAction != null)
// InkWell(
// onTap: () async {
// var tab = CreateTabSheet(
// preferredTool: quickAction,
// );
if (quickActionVoice) {
final completer = Completer<String>();
// if (quickActionVoice) {
// final completer = Completer<String>();
final isServiceAvailable =
await SpeechToTextGoogleDialog.getInstance()
.showGoogleDialog(
onTextReceived: (data) {
completer.complete(data.toString());
},
// locale: "en-US",
);
// final isServiceAvailable =
// await SpeechToTextGoogleDialog.getInstance()
// .showGoogleDialog(
// onTextReceived: (data) {
// completer.complete(data.toString());
// },
// // locale: "en-US",
// );
if (!isServiceAvailable) {
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Service is not available',
);
}
}
// if (!isServiceAvailable) {
// if (context.mounted) {
// ui_helper.showErrorMessage(
// context,
// 'Service is not available',
// );
// }
// }
tab = CreateTabSheet(
preferredTool: quickAction,
content: await completer.future,
);
}
// tab = CreateTabSheet(
// preferredTool: quickAction,
// content: await completer.future,
// );
// }
ref
.read(createTabStreamProvider.notifier)
.createTab(tab);
},
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 15.0,
horizontal: 8.0,
),
child: Icon(
quickActionVoice ? Icons.mic : quickAction.icon,
),
),
),
// ref
// .read(createTabStreamProvider.notifier)
// .createTab(tab);
// },
// child: Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 15.0,
// horizontal: 8.0,
// ),
// child: Icon(
// quickActionVoice ? Icons.mic : quickAction.icon,
// ),
// ),
// ),
TabsActionButton(
isActive: displayedSheet is ViewTabsSheet,
onTap: () {
@@ -394,8 +366,7 @@ class BrowserScreen extends HookConsumerWidget {
),
MenuItemButton(
onPressed: () async {
await ref
.read(addonServiceProvider)
await addonService
.startAddonManagerActivity();
},
leadingIcon: const Icon(MdiIcons.puzzleEdit),
@@ -8,6 +8,7 @@ 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/user/domain/repositories/cache.dart';
class BrowserView extends StatefulHookConsumerWidget {
final Duration screenshotPeriod;
@@ -101,6 +102,11 @@ class _BrowserViewState extends ConsumerState<BrowserView>
webExtensionsStateProvider(WebExtensionActionType.page),
(previous, next) {},
);
ref.listenManual(
cacheRepositoryProvider,
(previous, next) {},
);
}
@override
@@ -4,17 +4,19 @@ import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/error_container.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
class LandingAction extends HookConsumerWidget {
const LandingAction({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final sessionTokenAvailable = ref.watch(
settingsRepositoryProvider.select(
(value) => value.valueOrNull?.kagiSession?.isNotEmpty ?? false,
),
);
//TODO: REWORK!
final sessionTokenAvailable = false;
// ref.watch(
// settingsRepositoryProvider.select(
// (value) => value.valueOrNull?.kagiSession?.isNotEmpty ?? false,
// ),
// );
return Visibility(
visible: !sessionTokenAvailable,
@@ -4,11 +4,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/assistant_tab.dart';
import 'package:lensai/features/kagi/presentation/tabs/search_tab.dart';
import 'package:lensai/features/kagi/presentation/tabs/summarize_tab.dart';
import 'package:lensai/features/settings/data/models/settings.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
import 'package:lensai/presentation/hooks/sync_page_tab.dart';
@@ -27,13 +24,6 @@ class CreateTabSheetWidget extends HookConsumerWidget {
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final showEarlyAccessFeatures = ref.watch(
settingsRepositoryProvider.select(
(value) => (value.valueOrNull ?? Settings.withDefaults())
.showEarlyAccessFeatures,
),
);
final sharedContent = useMemoized(
() => (parameter.content != null)
? SharedContent.parse(parameter.content!)
@@ -45,20 +35,22 @@ class CreateTabSheetWidget extends HookConsumerWidget {
() =>
parameter.preferredTool?.index ??
switch (sharedContent) {
SharedText(text: final text) =>
(showEarlyAccessFeatures && text.length > 25)
? KagiTool.assistant.index
: KagiTool.search.index,
SharedText(text: final text) => KagiTool.search.index
// (showEarlyAccessFeatures && text.length > 25)
// ? KagiTool.assistant.index
// : KagiTool.search.index,
,
SharedUrl() => KagiTool.summarizer.index,
null => showEarlyAccessFeatures
? KagiTool.assistant.index
: KagiTool.search.index,
null => KagiTool.search.index
// showEarlyAccessFeatures
// ? KagiTool.assistant.index
// : KagiTool.search.index,
},
[sharedContent],
);
final tabController = useTabController(
initialLength: KagiTool.values.length - (showEarlyAccessFeatures ? 0 : 1),
initialLength: KagiTool.values.length - 1,
initialIndex: initialIndex,
);
final pageController = usePageController(initialPage: tabController.index);
@@ -95,11 +87,11 @@ class CreateTabSheetWidget extends HookConsumerWidget {
icon: Icon(KagiTool.summarizer.icon),
text: 'Summarize',
),
if (showEarlyAccessFeatures)
Tab(
icon: Icon(KagiTool.assistant.icon),
text: 'Assistant',
),
// if (showEarlyAccessFeatures)
// Tab(
// icon: Icon(KagiTool.assistant.icon),
// text: 'Assistant',
// ),
],
),
Padding(
@@ -115,11 +107,11 @@ class CreateTabSheetWidget extends HookConsumerWidget {
sharedContent: sharedContent,
onSubmit: onSubmit,
),
if (showEarlyAccessFeatures)
AssistantTab(
sharedContent: sharedContent,
onSubmit: onSubmit,
),
// if (showEarlyAccessFeatures)
// AssistantTab(
// sharedContent: sharedContent,
// onSubmit: onSubmit,
// ),
],
),
),
@@ -10,48 +10,65 @@ import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/speech_to_text_button.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:reorderable_grid/reorderable_grid.dart';
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
static const _headerSize = 104.0;
class _Tab extends HookConsumerWidget {
final VoidCallback onClose;
_SliverHeaderDelagate({required this.onClose});
const _Tab({required this.onClose});
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
Widget build(BuildContext context, WidgetRef ref) {
final searchMode = useState(false);
final searchTextFocus = useFocusNode();
final searchTextController = useTextEditingController();
final hasSearchText = useListenableSelector(
searchTextController,
() => searchTextController.text.isNotEmpty,
);
useListenableCallback(
searchTextController,
() async {
await ref
.read(tabSearchRepositoryProvider.notifier)
.addQuery(searchTextController.text);
},
);
return Material(
child: Consumer(
builder: (context, ref, child) {
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
return Align(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
child: Align(
child: Padding(
padding: const EdgeInsets.only(
right: 8.0,
left: 8.0,
top: 8.0,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (!searchMode.value)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
TextButton.icon(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: Uri.https('kagi.com'));
onClose();
icon: const Icon(Icons.search),
label: const Text('Search'),
onPressed: () {
searchMode.value = true;
searchTextFocus.requestFocus();
},
icon: const Icon(Icons.add),
label: const Text('New Tab'),
),
TextButton.icon(
onPressed: () async {
@@ -66,46 +83,92 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
label: const Text('Close All'),
),
],
),
Padding(
padding: const EdgeInsets.only(left: 16.0),
child: Consumer(
builder: (context, ref, child) {
final selectedContainer = ref.watch(
selectedContainerDataProvider
.select((value) => value.valueOrNull),
);
return ContainerChips(
selectedContainer: selectedContainer,
onSelected: (container) {
ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
},
onDeleted: (container) async {
ref
.read(selectedContainerProvider.notifier)
.clearContainer();
},
);
},
)
else
TextField(
controller: searchTextController,
focusNode: searchTextFocus,
// enableIMEPersonalizedLearning: !incognitoEnabled,
decoration: InputDecoration(
// border: InputBorder.none,
prefixIcon: const Icon(Icons.search),
hintText: 'Search inside tabs...',
floatingLabelBehavior: FloatingLabelBehavior.always,
suffixIcon: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (!hasSearchText)
SpeechToTextButton(
onTextReceived: (data) {
searchTextController.text = data.toString();
},
),
IconButton(
onPressed: () {
searchTextController.clear();
searchMode.value = false;
},
icon: const Icon(Icons.clear),
),
],
),
),
),
const SizedBox(height: 8),
],
),
);
},
Padding(
padding: const EdgeInsets.only(left: 8.0, top: 12),
child: Consumer(
builder: (context, ref, child) {
final selectedContainer = ref.watch(
selectedContainerDataProvider
.select((value) => value.valueOrNull),
);
return ContainerChips(
selectedContainer: selectedContainer,
onSelected: (container) {
ref
.read(selectedContainerProvider.notifier)
.setContainerId(container.id);
},
onDeleted: (container) async {
ref
.read(selectedContainerProvider.notifier)
.clearContainer();
},
);
},
),
),
const SizedBox(height: 8),
],
),
),
),
);
}
}
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
static const headerSize = 124.0;
final VoidCallback onClose;
_SliverHeaderDelagate({required this.onClose});
@override
double get minExtent => _headerSize;
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return _Tab(onClose: onClose);
}
@override
double get maxExtent => _headerSize;
double get minExtent => headerSize;
@override
double get maxExtent => headerSize;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
@@ -143,165 +206,196 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return CustomScrollView(
controller: sheetScrollController,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeaderDelagate(onClose: onClose),
),
HookConsumer(
builder: (context, ref, child) {
final container = ref.watch(selectedContainerProvider);
final availableTabs = ref
.watch(
availableTabIdsProvider(container).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
final activeTab = ref.watch(selectedTabProvider);
return Stack(
alignment: Alignment.bottomRight,
children: [
CustomScrollView(
controller: sheetScrollController,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeaderDelagate(onClose: onClose),
),
HookConsumer(
builder: (context, ref, child) {
final container = ref.watch(selectedContainerProvider);
final itemHeight = useMemoized(
() => _calculateItemHeight(
screenWidth: MediaQuery.of(context).size.width,
childAspectRatio: 0.75,
horizontalPadding: 4.0,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
[MediaQuery.of(context).size.width],
);
useEffect(
() {
final index =
availableTabs.indexWhere((webView) => webView == activeTab);
if (index > -1) {
final offset = (index ~/ 2) * itemHeight;
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
final filteredTabs = ref
.watch(
seamlessFilteredTabsProvider(container).select(
(value) => EquatableCollection(value, immutable: true),
),
);
}
}
)
.collection;
return null;
},
[availableTabs, activeTab],
);
final activeTab = ref.watch(selectedTabProvider);
final tabs = useMemoized(
() => availableTabs
.mapIndexed(
(index, tabId) => ReorderableGridDelayedDragStartListener(
key: ValueKey(tabId),
index: index,
child: Consumer(
builder: (context, ref, child) {
final tab = ref.watch(tabStateProvider(tabId));
return (tab != null)
? TabPreview(
tab: tab,
isActive: tabId == activeTab,
onTap: () async {
if (tabId != activeTab) {
//Close first to avoid rebuilds
onClose();
await ref
.read(
tabRepositoryProvider.notifier,
)
.selectTab(tab.id);
} else {
onClose();
}
},
onDoubleTap: () {
ref
.read(
overlayDialogControllerProvider
.notifier,
)
.show(
TabActionDialog(
initialTab: tab,
onDismiss: ref
final itemHeight = useMemoized(
() => _calculateItemHeight(
screenWidth: MediaQuery.of(context).size.width,
childAspectRatio: 0.75,
horizontalPadding: 4.0,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
[MediaQuery.of(context).size.width],
);
useEffect(
() {
final index = filteredTabs
.indexWhere((webView) => webView == activeTab);
if (index > -1) {
final offset = (index ~/ 2) * itemHeight;
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
return null;
},
[filteredTabs, activeTab],
);
final tabs = useMemoized(
() {
return filteredTabs
.mapIndexed(
(index, tabId) =>
ReorderableGridDelayedDragStartListener(
key: ValueKey(tabId),
index: index,
child: Consumer(
builder: (context, ref, child) {
final tab = ref.watch(tabStateProvider(tabId));
return (tab != null)
? TabPreview(
tab: tab,
isActive: tabId == activeTab,
onTap: () async {
if (tabId != activeTab) {
//Close first to avoid rebuilds
onClose();
await ref
.read(
overlayDialogControllerProvider
tabRepositoryProvider
.notifier,
)
.dismiss,
),
);
},
onDelete: () async {
await ref
.read(
tabRepositoryProvider.notifier,
)
.closeTab(tab.id);
},
)
: const SizedBox.shrink();
},
),
.selectTab(tab.id);
} else {
onClose();
}
},
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);
},
)
: const SizedBox.shrink();
},
),
),
)
.toList();
},
[
EquatableCollection(filteredTabs, immutable: true),
activeTab
],
);
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
sliver: SliverReorderableGrid(
gridDelegate:
const SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
)
.toList(),
[availableTabs, activeTab],
);
itemCount: tabs.length,
itemBuilder: (context, index) => tabs[index],
onReorder: (oldIndex, newIndex) async {
final containerRepository =
ref.read(containerRepositoryProvider.notifier);
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
sliver: SliverReorderableGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
itemCount: tabs.length,
itemBuilder: (context, index) => tabs[index],
onReorder: (oldIndex, newIndex) async {
final containerRepository =
ref.read(containerRepositoryProvider.notifier);
final tabId = filteredTabs[oldIndex];
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId);
final tabId = availableTabs[oldIndex];
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId);
final String key;
if (newIndex <= 0) {
key = await containerRepository
.getLeadingOrderKey(containerId);
} else if (newIndex >= filteredTabs.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex;
key = await containerRepository.getOrderKeyAfterTab(
filteredTabs[orderAfterIndex],
containerId,
);
}
final String key;
if (newIndex <= 0) {
key = await containerRepository
.getLeadingOrderKey(containerId);
} else if (newIndex >= availableTabs.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex - 1;
key = await containerRepository.getOrderKeyAfterTab(
availableTabs[orderAfterIndex],
containerId,
);
}
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
},
),
);
},
),
],
),
Padding(
padding: const EdgeInsets.only(
top: _SliverHeaderDelagate.headerSize + 4,
right: 4,
),
child: FloatingActionButton.small(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: Uri.https('kagi.com'));
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
},
),
);
},
onClose();
},
child: Icon(Icons.add),
),
),
],
);
@@ -91,9 +91,11 @@ class TabPreview extends StatelessWidget {
),
child: SizedBox(
width: double.infinity,
child: RawImage(
image: tab.thumbnail!.value,
fit: BoxFit.fitWidth,
child: RepaintBoundary(
child: RawImage(
image: tab.thumbnail!.value,
fit: BoxFit.fitWidth,
),
),
),
),