Files
MrbWebLibre/app/lib/features/search_browser/presentation/screens/browser.dart
T

345 lines
13 KiB
Dart

import 'package:animations/animations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.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:kagi_bang_bang/core/routing/routes.dart';
import 'package:kagi_bang_bang/features/search_browser/domain/entities/modes.dart';
import 'package:kagi_bang_bang/features/search_browser/domain/entities/sheet.dart';
import 'package:kagi_bang_bang/features/search_browser/domain/providers.dart';
import 'package:kagi_bang_bang/features/search_browser/domain/services/create_tab.dart';
import 'package:kagi_bang_bang/features/search_browser/domain/services/session.dart';
import 'package:kagi_bang_bang/features/search_browser/presentation/widgets/app_bar_title.dart';
import 'package:kagi_bang_bang/features/search_browser/presentation/widgets/sheets/shared_content_sheet.dart';
import 'package:kagi_bang_bang/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart';
import 'package:kagi_bang_bang/features/search_browser/presentation/widgets/tabs_action_button.dart';
import 'package:kagi_bang_bang/features/settings/data/repositories/settings_repository.dart';
import 'package:kagi_bang_bang/features/web_view/domain/repositories/web_view.dart';
import 'package:kagi_bang_bang/features/web_view/presentation/controllers/switch_new_tab.dart';
import 'package:kagi_bang_bang/features/web_view/presentation/widgets/web_page_dialog.dart';
import 'package:kagi_bang_bang/presentation/hooks/listenable_callback.dart';
import 'package:kagi_bang_bang/presentation/hooks/overlay_portal_controller.dart';
import 'package:kagi_bang_bang/presentation/widgets/animated_indexed_stack.dart';
import 'package:kagi_bang_bang/utils/ui_helper.dart' as ui_helper;
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
class KagiScreen extends HookConsumerWidget {
const KagiScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final displayedSheet = ref.watch(bottomSheetProvider);
final displayedOverlayDialog = ref.watch(overlayDialogProvider);
final lastBackButtonPress = useRef<DateTime?>(null);
final webViewController = useRef<InAppWebViewController?>(null);
final overlayController = useOverlayPortalController();
ref.listen(
overlayDialogProvider,
(previous, next) {
if (next != null) {
overlayController.show();
} else {
overlayController.hide();
}
},
);
ref.listen(
settingsRepositoryProvider
.select((value) => value.valueOrNull?.kagiSession),
(previous, next) async {
if (next != null && next.isNotEmpty) {
await ref.read(sessionServiceProvider.notifier).setKagiSession(next);
}
},
);
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: AppBarTitle(
onTap: () {
final page = ref.read(webViewTabControllerProvider)?.page.value;
if (page != null) {
ref.watch(overlayDialogProvider.notifier).show(
WebPageDialog(
page: page,
webViewController: webViewController.value,
onDismiss:
ref.watch(overlayDialogProvider.notifier).dismiss,
),
);
}
},
),
actions: [
TabsActionButton(
onTap: () {
if (displayedSheet case ViewTabs()) {
ref.read(bottomSheetProvider.notifier).dismiss();
} else {
ref.read(bottomSheetProvider.notifier).show(ViewTabs());
}
},
),
MenuAnchor(
builder: (context, controller, child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
menuChildren: [
HookConsumer(
builder: (context, ref, child) {
final activeWebView = ref.watch(webViewTabControllerProvider);
final history = useListenableSelector(
activeWebView?.page,
() =>
activeWebView?.page.value.pageHistory ??
(canGoBack: false, canGoForward: false),
);
return Row(
children: [
Expanded(
child: IconButton(
onPressed: (history.canGoBack)
? () async {
await webViewController.value?.goBack();
}
: null,
icon: const Icon(Icons.arrow_back),
),
),
const SizedBox(height: 48, child: VerticalDivider()),
Expanded(
child: IconButton(
onPressed: (history.canGoForward)
? () async {
await webViewController.value?.goForward();
}
: null,
icon: const Icon(Icons.arrow_forward),
),
),
],
);
},
),
const Divider(),
MenuItemButton(
onPressed: () async {
await webViewController.value?.reload();
},
leadingIcon: const Icon(Icons.refresh),
child: const Text('Reload'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
await context.push(SettingsRoute().location);
},
leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'),
),
const Divider(),
MenuItemButton(
onPressed: () async {
final url = await webViewController.value?.getUrl();
if (url != null) {
await Share.shareUri(url);
}
},
leadingIcon: const Icon(Icons.share),
child: const Text('Share'),
),
MenuItemButton(
onPressed: () async {
final url = await webViewController.value?.getUrl();
if (url != null) {
if (!await launchUrl(
url,
mode: LaunchMode.externalApplication,
)) {
if (context.mounted) {
ui_helper.showErrorMessage(
context,
'Could not launch URL ($url)',
);
}
}
}
},
leadingIcon: const Icon(Icons.open_in_browser),
child: const Text('Launch External'),
),
const Divider(),
MenuItemButton(
onPressed: () {
ref.read(createTabStreamProvider.notifier).createTab(
CreateTab(preferredTool: KagiTool.search),
);
},
leadingIcon: const Icon(MdiIcons.searchWeb),
child: const Text('Search'),
),
MenuItemButton(
onPressed: () {
ref.read(createTabStreamProvider.notifier).createTab(
CreateTab(preferredTool: KagiTool.assistant),
);
},
leadingIcon: const Icon(MdiIcons.brain),
child: const Text('Assistant'),
),
MenuItemButton(
onPressed: () {
ref.read(createTabStreamProvider.notifier).createTab(
CreateTab(preferredTool: KagiTool.summarizer),
);
},
leadingIcon: const Icon(MdiIcons.text),
child: const Text('Summarizer'),
),
],
),
],
),
body: OverlayPortal(
controller: overlayController,
overlayChildBuilder: (context) {
return displayedOverlayDialog!;
},
child: Listener(
onPointerDown: (displayedSheet != null)
? (_) {
ref.read(bottomSheetProvider.notifier).dismiss();
}
: null,
child: HookConsumer(
builder: (context, ref, child) {
final webViews = ref.watch(webViewRepositoryProvider);
final activeWebView = ref.watch(webViewTabControllerProvider);
useListenableCallback(activeWebView?.page, () {
webViewController.value = activeWebView?.page.value.controller;
});
return BackButtonListener(
onBackButtonPressed: () async {
if (activeWebView?.page.value.pageHistory.canGoBack == true) {
await activeWebView?.page.value.controller?.goBack();
return true;
}
if (lastBackButtonPress.value != null &&
lastBackButtonPress.value!.difference(DateTime.now()) <
const Duration(seconds: 2)) {
lastBackButtonPress.value = null;
return false;
} else {
lastBackButtonPress.value = DateTime.now();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please click BACK again to exit'),
),
);
return true;
}
},
child: AnimatedIndexedStack(
duration: const Duration(milliseconds: 250),
transitionBuilder: (child, animation, secondaryAnimation) =>
SharedAxisTransition(
animation: animation,
secondaryAnimation: secondaryAnimation,
transitionType: SharedAxisTransitionType.horizontal,
child: child,
),
key: ValueKey(
(activeWebView != null)
? webViews.keys
.toList()
.indexOf(activeWebView.page.value.key)
: null,
),
index: (activeWebView != null)
? webViews.keys
.toList()
.indexOf(activeWebView.page.value.key)
: null,
children: webViews.values.toList(),
),
);
},
),
),
),
bottomSheet: (displayedSheet != null)
? NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {
if (notification.extent <= 0.1) {
ref.read(bottomSheetProvider.notifier).dismiss();
return true;
}
return false;
},
child: switch (displayedSheet) {
ViewTabs() => DraggableScrollableSheet(
expand: false,
minChildSize: 0.1,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: ViewTabsSheet(
onClose: () {
ref.read(bottomSheetProvider.notifier).dismiss();
},
),
);
},
),
final CreateTab parameter => DraggableScrollableSheet(
expand: false,
initialChildSize: 0.8,
minChildSize: 0.1,
builder: (context, scrollController) {
return SingleChildScrollView(
controller: scrollController,
child: SharedContentSheet(
key: ObjectKey(parameter),
parameter: parameter,
onSubmit: (url) async {
await ref
.read(switchNewTabControllerProvider.notifier)
.add(url);
ref.read(bottomSheetProvider.notifier).dismiss();
},
),
);
},
),
},
)
: null,
);
}
}