intermediate

This commit is contained in:
Fabian Freund
2025-01-23 14:03:55 +01:00
parent bd45ed64ee
commit 78c17a70c5
122 changed files with 8730 additions and 1662 deletions
@@ -26,3 +26,15 @@ class ViewTabsSheet extends Sheet {
@override
List<Object?> get hashParameters => [];
}
class TabQaChatSheet extends Sheet {
final String chatId;
TabQaChatSheet({required this.chatId});
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [chatId];
}
@@ -1,16 +1,24 @@
import 'dart:ui' as ui;
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/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/chat/features/chat_store/data/models/chat_metadata.dart';
import 'package:lensai/features/chat/features/chat_store/domain/repositories/chat_metadata.dart';
import 'package:lensai/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
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/entities/sheet.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/share_intent/domain/entities/shared_content.dart';
@@ -24,12 +32,9 @@ class WebPageDialog extends HookConsumerWidget {
final Uri url;
final WebPageInfo? precachedInfo;
final void Function()? onDismiss;
const WebPageDialog({
required this.url,
this.precachedInfo,
this.onDismiss,
super.key,
});
@@ -56,21 +61,19 @@ class WebPageDialog extends HookConsumerWidget {
useTextEditingController(text: url.toString());
final addressTextFocusNode = useFocusNode();
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: WebsiteTitleTile(url, precachedInfo: precachedInfo),
return MediaQuery.removeViewInsets(
context: context,
removeBottom: true,
child: Dialog(
insetPadding:
const EdgeInsets.symmetric(horizontal: 16.0, vertical: 64.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
child: WebsiteTitleTile(url, precachedInfo: precachedInfo),
),
SizedBox(
//We need this to stretch the dialog, then padding from dialog is applied
width: double.maxFinite,
@@ -82,6 +85,7 @@ class WebPageDialog extends HookConsumerWidget {
controller: addressTextController,
focusNode: addressTextFocusNode,
enableIMEPersonalizedLearning: !incognitoEnabled,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
labelText: 'Address',
),
@@ -106,136 +110,212 @@ class WebPageDialog extends HookConsumerWidget {
.read(tabSessionProvider(tabId: null).notifier)
.loadUrl(url: Uri.tryParse(value)!);
onDismiss?.call();
if (context.mounted) {
context.pop();
}
},
),
),
),
),
const SizedBox(
height: 4,
),
const Divider(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: availableBangsAsync.when(
data: (availableBangs) {
if (availableBangs.isEmpty) {
return const SizedBox.shrink();
}
Flexible(
child: FadingScroll(
fadingSize: 25,
builder: (context, controller) {
return SingleChildScrollView(
controller: controller,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: availableBangsAsync.when(
data: (availableBangs) {
if (availableBangs.isEmpty) {
return const SizedBox.shrink();
}
return SiteSearch(
domain: url.host,
availableBangs: availableBangs,
return SiteSearch(
domain: url.host,
availableBangs: availableBangs,
);
},
error: (error, stackTrace) => FailureWidget(
title: 'Could not load bangs',
exception: error,
),
loading: () => SiteSearch(
domain: url.host,
availableBangs: [
BangData(
websiteName: 'websiteName',
domain: 'domain',
trigger: 'trigger',
urlTemplate: 'urlTemplate',
),
],
),
),
),
if (availableBangsAsync.isLoading ||
availableBangCount == null ||
availableBangCount > 0)
const Divider(),
ListTile(
leading: const Icon(MdiIcons.contentCopy),
title: const Text('Copy address'),
onTap: () async {
await Clipboard.setData(
ClipboardData(text: url.toString()),
);
if (context.mounted) {
context.pop();
}
},
),
ListTile(
onTap: () async {
await ui_helper.launchUrlFeedback(context, url);
},
leading: const Icon(Icons.open_in_browser),
title: const Text('Launch External'),
),
ListTile(
leading: const Icon(MdiIcons.tabPlus),
title: const Text('Clone tab'),
onTap: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: url);
if (context.mounted) {
context.pop();
}
},
),
ListTile(
leading: const Icon(Icons.share),
title: const Text('Share link'),
onTap: () async {
await Share.shareUri(url);
if (context.mounted) {
context.pop();
}
},
),
ListTile(
leading: const Icon(Icons.mobile_screen_share),
title: const Text('Share screenshot'),
onTap: () async {
final screenshot = await ref
.read(selectedTabSessionNotifierProvider)
.requestScreenshot();
if (screenshot != null) {
ui.decodeImageFromList(
screenshot,
(result) async {
final png = await result.toByteData(
format: ui.ImageByteFormat.png,
);
if (png != null) {
final file = XFile.fromData(
png.buffer.asUint8List(),
mimeType: 'image/png',
);
await Share.shareXFiles(
[file],
subject: precachedInfo?.title,
);
}
},
);
}
if (context.mounted) {
context.pop();
}
},
),
const Divider(),
ListTile(
leading: Icon(KagiTool.assistant.icon),
title: const Text('QA Chat'),
onTap: () async {
final selectedTabId =
ref.read(selectedTabStateProvider)?.id;
if (selectedTabId != null) {
final updateResult = await ref
.read(
chatMetadataRepositoryProvider(
selectedTabId)
.notifier,
)
.updateMetadata(
ChatMetadata(mainDocumentId: selectedTabId),
);
updateResult.onSuccess((_) {
ref
.read(
bottomSheetControllerProvider.notifier)
.show(
TabQaChatSheet(chatId: selectedTabId));
});
}
if (context.mounted) {
context.pop();
}
},
),
ListTile(
leading: Icon(KagiTool.summarizer.icon),
title: const Text('Summarize'),
onTap: () async {
final summarizerUrl = uri_builder.summarizerUri(
document: SharedUrl(url),
mode: SummarizerMode.keyMoments,
);
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: summarizerUrl);
if (context.mounted) {
context.pop();
}
},
),
],
),
);
},
error: (error, stackTrace) => FailureWidget(
title: 'Could not load bangs',
exception: error,
),
loading: () => SiteSearch(
domain: url.host,
availableBangs: [
// BangData(
// websiteName: 'websiteName',
// domain: 'domain',
// trigger: 'trigger',
// urlTemplate: 'urlTemplate',
// ),
],
),
),
Align(
alignment: Alignment.bottomRight,
child: Padding(
padding: const EdgeInsets.only(right: 16.0, bottom: 16.0),
child: TextButton(
onPressed: () {
context.pop();
},
child: const Text('Close'),
),
),
),
if (availableBangsAsync.isLoading ||
availableBangCount == null ||
availableBangCount > 0)
const Divider(),
ListTile(
leading: const Icon(MdiIcons.contentCopy),
title: const Text('Copy address'),
onTap: () async {
await Clipboard.setData(
ClipboardData(text: url.toString()),
);
onDismiss?.call();
},
),
ListTile(
onTap: () async {
await ui_helper.launchUrlFeedback(context, url);
},
leading: const Icon(Icons.open_in_browser),
title: const Text('Launch External'),
),
ListTile(
leading: const Icon(MdiIcons.tabPlus),
title: const Text('Clone tab'),
onTap: () async {
await ref.read(tabRepositoryProvider.notifier).addTab(url: url);
onDismiss?.call();
},
),
ListTile(
leading: const Icon(Icons.share),
title: const Text('Share link'),
onTap: () async {
await Share.shareUri(url);
onDismiss?.call();
},
),
ListTile(
leading: const Icon(Icons.mobile_screen_share),
title: const Text('Share screenshot'),
onTap: () async {
final screenshot = await ref
.read(selectedTabSessionNotifierProvider)
.requestScreenshot();
if (screenshot != null) {
ui.decodeImageFromList(
screenshot,
(result) async {
final png = await result.toByteData(
format: ui.ImageByteFormat.png,
);
if (png != null) {
final file = XFile.fromData(
png.buffer.asUint8List(),
mimeType: 'image/png',
);
await Share.shareXFiles(
[file],
subject: precachedInfo?.title,
);
}
},
);
}
onDismiss?.call();
},
),
const Divider(),
ListTile(
leading: Icon(KagiTool.summarizer.icon),
title: const Text('Summarize'),
onTap: () async {
final summarizerUrl = uri_builder.summarizerUri(
document: SharedUrl(url),
mode: SummarizerMode.keyMoments,
);
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: summarizerUrl);
onDismiss?.call();
},
),
],
),
],
),
);
}
}
@@ -5,6 +5,8 @@ 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/features/geckoview/domain/controllers/bottom_sheet.dart';
import 'package:lensai/features/geckoview/domain/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
@@ -17,18 +19,20 @@ import 'package:lensai/features/geckoview/features/browser/domain/services/engin
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/web_page_dialog.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';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/extension_badge_icon.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/create_tab.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/sheets/view_tabs.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart';
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart';
import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
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/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';
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
import 'package:share_plus/share_plus.dart';
@@ -52,7 +56,8 @@ class BrowserScreen extends HookConsumerWidget {
final selectedTabId =
ref.watch(selectedTabStateProvider.select((value) => value?.id));
final menuController = useMemoized(() => MenuController());
final trippleDotMenuController = useMenuController();
final tabMenuController = useMenuController();
final lastBackButtonPress = useRef<DateTime?>(null);
@@ -120,24 +125,13 @@ class BrowserScreen extends HookConsumerWidget {
return (tabState != null)
? AppBarTitle(
tab: tabState,
onTap: () {
ref
.read(
overlayDialogControllerProvider
.notifier,
)
.show(
WebPageDialog(
url: tabState.url,
precachedInfo: tabState,
onDismiss: ref
.read(
overlayDialogControllerProvider
.notifier,
)
.dismiss,
),
);
onTap: () async {
await context.push(
WebPageRoute(
url: tabState.url.toString(),
).location,
extra: tabState,
);
},
)
: const SizedBox.shrink();
@@ -261,22 +255,58 @@ class BrowserScreen extends HookConsumerWidget {
// ),
// ),
// ),
TabsActionButton(
isActive: displayedSheet is ViewTabsSheet,
onTap: () {
if (displayedSheet case ViewTabsSheet()) {
ref
.read(bottomSheetControllerProvider.notifier)
.dismiss();
} else {
ref
.read(bottomSheetControllerProvider.notifier)
.show(ViewTabsSheet());
}
MenuAnchor(
controller: tabMenuController,
builder: (context, controller, child) {
return child!;
},
menuChildren: [
if (selectedTabId != null)
MenuItemButton(
onPressed: () async {
await ref
.read(
tabRepositoryProvider.notifier,
)
.closeTab(selectedTabId);
},
leadingIcon: const Icon(Icons.close),
child: const Text('Close Tab'),
),
MenuItemButton(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: Uri.https('kagi.com'));
},
leadingIcon: const Icon(Icons.add),
child: const Text('Add Tab'),
),
],
child: TabsActionButton(
isActive: displayedSheet is ViewTabsSheet,
onTap: () {
if (displayedSheet case ViewTabsSheet()) {
ref
.read(bottomSheetControllerProvider.notifier)
.dismiss();
} else {
ref
.read(bottomSheetControllerProvider.notifier)
.show(ViewTabsSheet());
}
},
onLongPress: () {
if (tabMenuController.isOpen) {
tabMenuController.close();
} else {
tabMenuController.open();
}
},
),
),
MenuAnchor(
controller: menuController,
controller: trippleDotMenuController,
builder: (context, controller, child) {
return Padding(
padding: const EdgeInsets.only(right: 4.0),
@@ -487,7 +517,7 @@ class BrowserScreen extends HookConsumerWidget {
);
await controller.reload();
menuController.close();
trippleDotMenuController.close();
},
leadingIcon: const Icon(Icons.refresh),
child: const Text('Reload'),
@@ -514,7 +544,7 @@ class BrowserScreen extends HookConsumerWidget {
);
await controller.goBack();
menuController.close();
trippleDotMenuController.close();
},
icon: const Icon(Icons.arrow_back),
)
@@ -526,7 +556,7 @@ class BrowserScreen extends HookConsumerWidget {
.notifier,
)
.closeTab(selectedTabId);
menuController.close();
trippleDotMenuController.close();
},
icon: const Icon(Icons.close),
),
@@ -546,7 +576,7 @@ class BrowserScreen extends HookConsumerWidget {
);
await controller.goForward();
menuController.close();
trippleDotMenuController.close();
}
: null,
icon: const Icon(Icons.arrow_forward),
@@ -556,6 +586,15 @@ class BrowserScreen extends HookConsumerWidget {
);
},
),
MenuItemButton(
onPressed: () async {
final x =
await context.push(UserAuthRoute().location);
print(x);
},
leadingIcon: const Icon(Icons.info),
child: const Text('Auth'),
),
],
),
],
@@ -744,25 +783,36 @@ class BrowserScreen extends HookConsumerWidget {
return false;
},
child: switch (displayedSheet) {
ViewTabsSheet() => DraggableScrollableSheet(
key: ValueKey(displayedSheet),
expand: false,
minChildSize: 0.1,
maxChildSize: _realtiveSafeArea(context),
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: ViewTabsSheetWidget(
sheetScrollController: scrollController,
onClose: () {
ref
.read(bottomSheetControllerProvider.notifier)
.dismiss();
},
),
ViewTabsSheet() => HookBuilder(
builder: (localContext) {
final draggableScrollableController =
useDraggableScrollableController();
return DraggableScrollableSheet(
key: ValueKey(displayedSheet),
controller: draggableScrollableController,
expand: false,
minChildSize: 0.1,
maxChildSize: _realtiveSafeArea(context),
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: ViewTabsSheetWidget(
sheetScrollController: scrollController,
draggableScrollableController:
draggableScrollableController,
onClose: () {
ref
.read(bottomSheetControllerProvider
.notifier)
.dismiss();
},
),
);
},
);
},
),
@@ -794,6 +844,60 @@ class BrowserScreen extends HookConsumerWidget {
);
},
),
final TabQaChatSheet parameter => HookBuilder(
builder: (localContext) {
final draggableScrollableController =
useDraggableScrollableController();
return DraggableScrollableSheet(
key: ValueKey(displayedSheet),
controller: draggableScrollableController,
expand: false,
minChildSize: 0.1,
maxChildSize: _realtiveSafeArea(context),
builder: (context, scrollController) {
return ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Column(
children: [
DraggableScrollableHeader(
controller: draggableScrollableController,
child: Material(
child: SizedBox(
width: double.infinity,
child: Center(
child: Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(
vertical: 16.0,
),
decoration: BoxDecoration(
color: Colors.grey[300],
borderRadius:
BorderRadius.circular(2),
),
),
),
),
),
),
Expanded(
child: TabQaChat(
chatId: parameter.chatId,
scrollController: scrollController,
),
),
],
),
);
},
);
},
),
},
)
: null,
@@ -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/geckoview/features/tabs/features/vector_store/domain/repositories/document.dart';
import 'package:lensai/features/user/domain/repositories/cache.dart';
class BrowserView extends StatefulHookConsumerWidget {
@@ -87,6 +88,7 @@ class _BrowserViewState extends ConsumerState<BrowserView>
//Initialize and register dependencies
ref.listenManual(tabRepositoryProvider, (previous, next) {});
ref.listenManual(documentRepositoryProvider, (previous, next) {});
ref.listenManual(
selectionActionServiceProvider,
@@ -0,0 +1,32 @@
import 'dart:math';
import 'package:flutter/material.dart';
class DraggableScrollableHeader extends StatelessWidget {
final DraggableScrollableController controller;
final Widget child;
const DraggableScrollableHeader({
super.key,
required this.controller,
required this.child,
});
@override
Widget build(BuildContext context) {
return GestureDetector(
onVerticalDragUpdate: (details) {
// Use the DraggableScrollableSheet's controller
controller.jumpTo(
min(
1,
controller.pixelsToSize(
controller.pixels - details.delta.dy,
),
),
);
},
child: child,
);
}
}
@@ -79,10 +79,10 @@ class CreateTabSheetWidget extends HookConsumerWidget {
TabBar(
controller: tabController,
tabs: [
Tab(
icon: Icon(KagiTool.search.icon),
text: 'Search',
),
// Tab(
// icon: Icon(KagiTool.search.icon),
// text: 'Search',
// ),
Tab(
icon: Icon(KagiTool.summarizer.icon),
text: 'Summarize',
@@ -99,10 +99,10 @@ class CreateTabSheetWidget extends HookConsumerWidget {
child: ExpandablePageView(
controller: pageController,
children: [
SearchTab(
sharedContent: sharedContent,
onSubmit: onSubmit,
),
// SearchTab(
// sharedContent: sharedContent,
// onSubmit: onSubmit,
// ),
SummarizeTab(
sharedContent: sharedContent,
onSubmit: onSubmit,
@@ -10,9 +10,10 @@ 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/draggable_scrollable_header.dart';
import 'package:lensai/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/domain/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';
@@ -22,6 +23,8 @@ import 'package:lensai/presentation/hooks/listenable_callback.dart';
import 'package:reorderable_grid/reorderable_grid.dart';
class _Tab extends HookConsumerWidget {
static const headerSize = 124.0;
final VoidCallback onClose;
const _Tab({required this.onClose});
@@ -148,40 +151,15 @@ class _Tab extends HookConsumerWidget {
}
}
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
static const headerSize = 124.0;
final VoidCallback onClose;
_SliverHeaderDelagate({required this.onClose});
@override
Widget build(
BuildContext context,
double shrinkOffset,
bool overlapsContent,
) {
return _Tab(onClose: onClose);
}
@override
double get minExtent => headerSize;
@override
double get maxExtent => headerSize;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
false;
}
class ViewTabsSheetWidget extends HookConsumerWidget {
final ScrollController sheetScrollController;
final DraggableScrollableController draggableScrollableController;
final VoidCallback onClose;
const ViewTabsSheetWidget({
required this.onClose,
required this.sheetScrollController,
required this.draggableScrollableController,
super.key,
});
@@ -209,181 +187,191 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
return Stack(
alignment: Alignment.bottomRight,
children: [
CustomScrollView(
controller: sheetScrollController,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeaderDelagate(onClose: onClose),
Column(
children: [
DraggableScrollableHeader(
controller: draggableScrollableController,
child: _Tab(onClose: onClose),
),
HookConsumer(
builder: (context, ref, child) {
final container = ref.watch(selectedContainerProvider);
Expanded(
child: CustomScrollView(
controller: sheetScrollController,
slivers: [
HookConsumer(
builder: (context, ref, child) {
final container = ref.watch(selectedContainerProvider);
final filteredTabs = ref
.watch(
seamlessFilteredTabsProvider(container).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
final filteredTabs = ref
.watch(
seamlessFilteredTabsProvider(container).select(
(value) =>
EquatableCollection(value, immutable: true),
),
)
.collection;
final activeTab = ref.watch(selectedTabProvider);
final activeTab = ref.watch(selectedTabProvider);
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],
);
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);
useEffect(
() {
final index = filteredTabs
.indexWhere((webView) => webView == activeTab);
if (index > -1) {
final offset = (index ~/ 2) * itemHeight;
if (index > -1) {
final offset = (index ~/ 2) * itemHeight;
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
return null;
},
[filteredTabs, activeTab],
);
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(
tabRepositoryProvider
.notifier,
)
.selectTab(tab.id);
} else {
onClose();
}
},
onDoubleTap: () {
ref
.read(
overlayDialogControllerProvider
.notifier,
)
.show(
TabActionDialog(
initialTab: tab,
onDismiss: ref
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();
},
[
EquatableCollection(filteredTabs, immutable: true),
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 = filteredTabs[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 >= filteredTabs.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex;
key = await containerRepository.getOrderKeyAfterTab(
filteredTabs[orderAfterIndex],
containerId,
);
}
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
},
),
);
},
),
);
},
],
),
),
],
),
Padding(
padding: const EdgeInsets.only(
top: _SliverHeaderDelagate.headerSize + 4,
top: _Tab.headerSize + 4,
right: 4,
),
child: FloatingActionButton.small(
@@ -1,32 +0,0 @@
import 'package:flutter/material.dart';
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
import 'package:speech_to_text_google_dialog/speech_to_text_google_dialog.dart';
class SpeechToTextButton extends StatelessWidget {
final Function(dynamic data) onTextReceived;
const SpeechToTextButton({required this.onTextReceived, super.key});
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: () async {
final isServiceAvailable =
await SpeechToTextGoogleDialog.getInstance().showGoogleDialog(
onTextReceived: onTextReceived,
// locale: "en-US",
);
if (!isServiceAvailable) {
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Service is not available',
);
}
}
},
icon: const Icon(Icons.mic),
);
}
}
@@ -5,9 +5,11 @@ import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
class TabsActionButton extends HookConsumerWidget {
final bool isActive;
final VoidCallback onTap;
final VoidCallback onLongPress;
const TabsActionButton({
required this.onTap,
required this.onLongPress,
this.isActive = false,
super.key,
});
@@ -20,6 +22,7 @@ class TabsActionButton extends HookConsumerWidget {
return InkWell(
onTap: onTap,
onLongPress: onLongPress,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0,