intermediate
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
||||
+213
-133
@@ -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,
|
||||
|
||||
+32
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -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,
|
||||
|
||||
+169
-181
@@ -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(
|
||||
|
||||
-32
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user