improved tab & topic handling
This commit is contained in:
@@ -2,12 +2,18 @@
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||
import 'package:lensai/features/search_browser/domain/entities/modes.dart';
|
||||
import 'package:lensai/features/search_browser/domain/entities/sheet.dart';
|
||||
import 'package:lensai/features/search_browser/domain/services/create_tab.dart';
|
||||
import 'package:lensai/features/topics/data/database/database.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/presentation/widgets/web_view.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
@@ -137,3 +143,65 @@ class ShowFindInPage extends _$ShowFindInPage {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod()
|
||||
class WebViewWidgetCache extends _$WebViewWidgetCache {
|
||||
final _requestedWidgets = <String>{};
|
||||
|
||||
List<TabData> _availableTabs = [];
|
||||
|
||||
@override
|
||||
List<WebView> build() {
|
||||
ref.listen(
|
||||
tabRepositoryProvider,
|
||||
(previous, next) {
|
||||
if (next.hasValue && next.valueOrNull != null) {
|
||||
_availableTabs = next.valueOrNull!;
|
||||
|
||||
_removeClosedWidgets();
|
||||
_requestedWidgets.removeWhere(_ensureWidgetCached);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
ref.listen(
|
||||
webViewTabControllerProvider,
|
||||
(_, tabId) {
|
||||
if (tabId != null && !_ensureWidgetCached(tabId)) {
|
||||
_requestedWidgets.add(tabId);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
return stateOrNull ?? [];
|
||||
}
|
||||
|
||||
void _removeClosedWidgets() {
|
||||
state = [...state]..removeWhere(
|
||||
(element) => !_availableTabs.any((tab) => tab.id == element.tabId),
|
||||
);
|
||||
}
|
||||
|
||||
bool _ensureWidgetCached(String tabId) {
|
||||
final index = state.indexWhere((tab) => tab.tabId == tabId);
|
||||
if (index < 0) {
|
||||
final tab = _availableTabs.firstWhereOrNull((tab) => tab.id == tabId);
|
||||
if (tab != null) {
|
||||
state = [
|
||||
...state,
|
||||
WebView(
|
||||
key: GlobalKey(),
|
||||
tabId: tabId,
|
||||
initialUrlRequest: URLRequest(url: WebUri.uri(tab.url)),
|
||||
),
|
||||
];
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,5 +411,22 @@ final showFindInPageProvider = NotifierProvider<ShowFindInPage, bool>.internal(
|
||||
);
|
||||
|
||||
typedef _$ShowFindInPage = Notifier<bool>;
|
||||
String _$webViewWidgetCacheHash() =>
|
||||
r'7d6f769e8c314d9dc579d700d36d6d4c944e600f';
|
||||
|
||||
/// See also [WebViewWidgetCache].
|
||||
@ProviderFor(WebViewWidgetCache)
|
||||
final webViewWidgetCacheProvider =
|
||||
AutoDisposeNotifierProvider<WebViewWidgetCache, List<WebView>>.internal(
|
||||
WebViewWidgetCache.new,
|
||||
name: r'webViewWidgetCacheProvider',
|
||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$webViewWidgetCacheHash,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
typedef _$WebViewWidgetCache = AutoDisposeNotifier<List<WebView>>;
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/web_view_page.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/presentation/widgets/favicon.dart';
|
||||
|
||||
class TabActionDialog extends HookConsumerWidget {
|
||||
final WebViewPage initialTab;
|
||||
|
||||
final void Function()? onDismiss;
|
||||
|
||||
const TabActionDialog({
|
||||
required this.initialTab,
|
||||
this.onDismiss,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
|
||||
final topics =
|
||||
ref.watch(topicRepositoryProvider.select((value) => value.valueOrNull));
|
||||
final selectedTopic =
|
||||
topics?.firstWhereOrNull((topic) => topic.id == tabState.topicId);
|
||||
|
||||
final expansionController = useExpansionTileController();
|
||||
|
||||
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: ListTile(
|
||||
leading: FaviconImage(
|
||||
favicon: tabState.favicon,
|
||||
url: tabState.url,
|
||||
size: 24,
|
||||
),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(tabState.title ?? 'Unknown Title'),
|
||||
subtitle: Text(tabState.url.authority),
|
||||
),
|
||||
children: [
|
||||
SizedBox(
|
||||
//We need this to stretch the dialog, then padding from dialog is applied
|
||||
width: double.maxFinite,
|
||||
child: ExpansionTile(
|
||||
controller: expansionController,
|
||||
leading: (selectedTopic != null)
|
||||
? CircleAvatar(backgroundColor: selectedTopic.color)
|
||||
: null,
|
||||
title: (selectedTopic != null)
|
||||
? Text(selectedTopic.name ?? 'New Topic')
|
||||
: const Text('Assign a Topic'),
|
||||
children: topics
|
||||
?.where((topic) => topic.id != tabState.topicId)
|
||||
.map(
|
||||
(topic) => ListTile(
|
||||
leading: CircleAvatar(backgroundColor: topic.color),
|
||||
title: Text(topic.name ?? 'New Topic'),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(
|
||||
tabStateProvider(initialTab.id).notifier,
|
||||
)
|
||||
.copyWith(topicId: topic.id);
|
||||
|
||||
expansionController.collapse();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
[],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import 'package:lensai/features/search_browser/presentation/widgets/sheets/view_
|
||||
import 'package:lensai/features/search_browser/presentation/widgets/tabs_action_button.dart';
|
||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/consistent_controller.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/presentation/controllers/readerability.dart';
|
||||
import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart';
|
||||
@@ -64,7 +64,7 @@ class KagiScreen extends HookConsumerWidget {
|
||||
),
|
||||
);
|
||||
|
||||
final activeWebView = ref.watch(webViewTabControllerProvider);
|
||||
final activeTabId = ref.watch(webViewTabControllerProvider);
|
||||
|
||||
final menuController = useMemoized(() => MenuController());
|
||||
|
||||
@@ -102,21 +102,28 @@ class KagiScreen extends HookConsumerWidget {
|
||||
child: AppBar(
|
||||
automaticallyImplyLeading: false,
|
||||
titleSpacing: 8.0,
|
||||
title: (activeWebView != null)
|
||||
? AppBarTitle(
|
||||
activeWebView: activeWebView,
|
||||
onTap: () {
|
||||
final page = activeWebView.page.value;
|
||||
title: (activeTabId != null)
|
||||
? Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final page = ref.watch(tabStateProvider(activeTabId));
|
||||
|
||||
ref.read(overlayDialogProvider.notifier).show(
|
||||
WebPageDialog(
|
||||
return (page != null)
|
||||
? AppBarTitle(
|
||||
page: page,
|
||||
webViewController: page.controller,
|
||||
onDismiss: ref
|
||||
.read(overlayDialogProvider.notifier)
|
||||
.dismiss,
|
||||
),
|
||||
);
|
||||
onTap: () {
|
||||
ref.read(overlayDialogProvider.notifier).show(
|
||||
WebPageDialog(
|
||||
url: page.url,
|
||||
precachedInfo: page,
|
||||
webViewController: page.controller,
|
||||
onDismiss: ref
|
||||
.read(overlayDialogProvider.notifier)
|
||||
.dismiss,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
)
|
||||
: HookBuilder(
|
||||
@@ -181,17 +188,13 @@ class KagiScreen extends HookConsumerWidget {
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
if (activeWebView != null)
|
||||
if (activeTabId != null)
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
final controller =
|
||||
useListenable(activeWebView.page).value.controller;
|
||||
final readerabilityState = ref.watch(
|
||||
readerabilityControllerProvider(
|
||||
ConsistentController(controller),
|
||||
),
|
||||
readerabilityControllerProvider(activeTabId),
|
||||
);
|
||||
|
||||
final enableReadability = ref.watch(
|
||||
@@ -231,24 +234,25 @@ class KagiScreen extends HookConsumerWidget {
|
||||
data: (_) => Visibility(
|
||||
visible: isReaderable == true || readerableApplied,
|
||||
child: InkWell(
|
||||
onTap: readerabilityState.isLoading
|
||||
? null
|
||||
: () async {
|
||||
final controller =
|
||||
activeWebView.currentController;
|
||||
onTap: readerabilityState.isLoading
|
||||
? null
|
||||
: () async {
|
||||
final controller = ref.read(
|
||||
webViewControllerProvider(activeTabId),
|
||||
);
|
||||
|
||||
if (controller != null) {
|
||||
final readabilityNotifier = ref.read(
|
||||
readerabilityControllerProvider(
|
||||
ConsistentController(controller),
|
||||
).notifier,
|
||||
);
|
||||
if (controller != null) {
|
||||
final readabilityNotifier = ref.read(
|
||||
readerabilityControllerProvider(
|
||||
activeTabId,
|
||||
).notifier,
|
||||
);
|
||||
|
||||
await readabilityNotifier
|
||||
.toggleReaderable();
|
||||
}
|
||||
},
|
||||
child: icon,
|
||||
await readabilityNotifier
|
||||
.toggleReaderable();
|
||||
}
|
||||
},
|
||||
child: icon,
|
||||
),
|
||||
),
|
||||
error: (error, stackTrace) => SizedBox.shrink(),
|
||||
@@ -271,7 +275,7 @@ class KagiScreen extends HookConsumerWidget {
|
||||
);
|
||||
},
|
||||
),
|
||||
if (activeWebView != null && quickAction != null)
|
||||
if (quickAction != null)
|
||||
InkWell(
|
||||
onTap: () async {
|
||||
var tab = CreateTab(
|
||||
@@ -408,86 +412,106 @@ class KagiScreen extends HookConsumerWidget {
|
||||
child: const Text('Search'),
|
||||
),
|
||||
const Divider(),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final url =
|
||||
await activeWebView?.currentController?.getUrl();
|
||||
if (url != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await ui_helper.launchUrlFeedback(context, url);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.open_in_browser),
|
||||
child: const Text('Launch External'),
|
||||
),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final url =
|
||||
await activeWebView?.currentController?.getUrl();
|
||||
if (url != null) {
|
||||
await Share.shareUri(url);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.share),
|
||||
child: const Text('Share'),
|
||||
),
|
||||
const Divider(),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
ref.read(showFindInPageProvider.notifier).update(true);
|
||||
},
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
child: const Text('Find in page'),
|
||||
),
|
||||
const Divider(),
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
await activeWebView?.currentController?.reload();
|
||||
},
|
||||
leadingIcon: const Icon(Icons.refresh),
|
||||
child: const Text('Reload'),
|
||||
),
|
||||
const Divider(),
|
||||
HookBuilder(
|
||||
builder: (context) {
|
||||
final history = useListenableSelector(
|
||||
activeWebView?.page,
|
||||
() =>
|
||||
activeWebView?.page.value.pageHistory ??
|
||||
(canGoBack: false, canGoForward: false),
|
||||
);
|
||||
if (activeTabId != null)
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final controller =
|
||||
ref.read(webViewControllerProvider(activeTabId));
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: IconButton(
|
||||
onPressed: (history.canGoBack)
|
||||
? () async {
|
||||
await activeWebView?.currentController
|
||||
?.goBack();
|
||||
menuController.close();
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
final url = await controller?.getUrl();
|
||||
if (url != null) {
|
||||
// ignore: use_build_context_synchronously
|
||||
await ui_helper.launchUrlFeedback(context, url);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.open_in_browser),
|
||||
child: const Text('Launch External'),
|
||||
),
|
||||
if (activeTabId != null)
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final controller =
|
||||
ref.read(webViewControllerProvider(activeTabId));
|
||||
|
||||
final url = await controller?.getUrl();
|
||||
if (url != null) {
|
||||
await Share.shareUri(url);
|
||||
}
|
||||
},
|
||||
leadingIcon: const Icon(Icons.share),
|
||||
child: const Text('Share'),
|
||||
),
|
||||
if (activeTabId != null) const Divider(),
|
||||
if (activeTabId != null)
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
ref.read(showFindInPageProvider.notifier).update(true);
|
||||
},
|
||||
leadingIcon: const Icon(Icons.search),
|
||||
child: const Text('Find in page'),
|
||||
),
|
||||
if (activeTabId != null) const Divider(),
|
||||
if (activeTabId != null)
|
||||
MenuItemButton(
|
||||
onPressed: () async {
|
||||
final controller =
|
||||
ref.read(webViewControllerProvider(activeTabId));
|
||||
|
||||
await controller?.reload();
|
||||
},
|
||||
leadingIcon: const Icon(Icons.refresh),
|
||||
child: const Text('Reload'),
|
||||
),
|
||||
if (activeTabId != null) const Divider(),
|
||||
if (activeTabId != null)
|
||||
Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final history = ref.watch(
|
||||
tabStateProvider(activeTabId)
|
||||
.select((value) => value?.pageHistory),
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: IconButton(
|
||||
onPressed: (history?.canGoBack == true)
|
||||
? () async {
|
||||
final controller = ref.read(
|
||||
webViewControllerProvider(
|
||||
activeTabId,
|
||||
),
|
||||
);
|
||||
|
||||
await controller?.goBack();
|
||||
menuController.close();
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48, child: VerticalDivider()),
|
||||
Expanded(
|
||||
child: IconButton(
|
||||
onPressed: (history.canGoForward)
|
||||
? () async {
|
||||
await activeWebView?.currentController
|
||||
?.goForward();
|
||||
menuController.close();
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
const SizedBox(height: 48, child: VerticalDivider()),
|
||||
Expanded(
|
||||
child: IconButton(
|
||||
onPressed: (history?.canGoForward == true)
|
||||
? () async {
|
||||
final controller = ref.read(
|
||||
webViewControllerProvider(
|
||||
activeTabId,
|
||||
),
|
||||
);
|
||||
|
||||
await controller?.goForward();
|
||||
menuController.close();
|
||||
}
|
||||
: null,
|
||||
icon: const Icon(Icons.arrow_forward),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -506,10 +530,18 @@ class KagiScreen extends HookConsumerWidget {
|
||||
: null,
|
||||
child: HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final webViews = ref.watch(webViewRepositoryProvider);
|
||||
final webViewWidgets = ref.watch(webViewWidgetCacheProvider);
|
||||
final activeWebViewStackIndex = (activeTabId != null)
|
||||
? webViewWidgets
|
||||
.indexWhere((view) => view.tabId == activeTabId)
|
||||
: null;
|
||||
|
||||
return BackButtonListener(
|
||||
onBackButtonPressed: () async {
|
||||
final page = (activeTabId != null)
|
||||
? ref.read(tabStateProvider(activeTabId))
|
||||
: null;
|
||||
|
||||
//Don't do anything if a child route is active
|
||||
if (GoRouterState.of(context).topRoute?.name != 'KagiRoute') {
|
||||
return false;
|
||||
@@ -525,10 +557,10 @@ class KagiScreen extends HookConsumerWidget {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (activeWebView?.page.value.pageHistory.canGoBack == true) {
|
||||
if (page?.pageHistory.canGoBack == true) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
await activeWebView?.page.value.controller?.goBack();
|
||||
await page?.controller?.goBack();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -537,10 +569,10 @@ class KagiScreen extends HookConsumerWidget {
|
||||
const Duration(seconds: 2)) {
|
||||
lastBackButtonPress.value = null;
|
||||
|
||||
if (activeWebView?.key != null && webViews.length > 1) {
|
||||
ref
|
||||
.read(webViewRepositoryProvider.notifier)
|
||||
.closeTab(activeWebView!.tabId);
|
||||
if (page != null && webViewWidgets.length > 1) {
|
||||
await ref
|
||||
.read(tabRepositoryProvider.notifier)
|
||||
.deleteTab(page.id);
|
||||
return true;
|
||||
} else {
|
||||
//Mark back as unhandled and navigator will pop
|
||||
@@ -552,7 +584,7 @@ class KagiScreen extends HookConsumerWidget {
|
||||
..clearSnackBars()
|
||||
..showSnackBar(
|
||||
SnackBar(
|
||||
content: (webViews.length > 1)
|
||||
content: (webViewWidgets.length > 1)
|
||||
? const Text(
|
||||
'Please click BACK again to close current tab',
|
||||
)
|
||||
@@ -578,20 +610,16 @@ class KagiScreen extends HookConsumerWidget {
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
child: child,
|
||||
),
|
||||
key: ValueKey(
|
||||
(activeWebView != null)
|
||||
? webViews.keys
|
||||
.toList()
|
||||
.indexOf(activeWebView.tabId)
|
||||
: null,
|
||||
),
|
||||
index: (activeWebView != null)
|
||||
? webViews.keys
|
||||
.toList()
|
||||
.indexOf(activeWebView.tabId) +
|
||||
1
|
||||
// key: ValueKey(
|
||||
// (activeTabId != null)
|
||||
// ? activeWebViewStackIndex
|
||||
// : null,
|
||||
// ),
|
||||
index: (activeWebViewStackIndex != null &&
|
||||
activeWebViewStackIndex > -1)
|
||||
? activeWebViewStackIndex + 1
|
||||
: 0,
|
||||
children: [const LandingContent(), ...webViews.values],
|
||||
children: [const LandingContent(), ...webViewWidgets],
|
||||
),
|
||||
),
|
||||
if (displayedSheet != null)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fading_scroll/fading_scroll.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/search_browser/domain/providers.dart';
|
||||
import 'package:lensai/features/search_browser/presentation/dialogs/tab_action.dart';
|
||||
import 'package:lensai/features/topics/domain/providers.dart';
|
||||
import 'package:lensai/features/topics/domain/repositories/tab.dart';
|
||||
import 'package:lensai/features/topics/presentation/widgets/topic_chips.dart';
|
||||
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.dart';
|
||||
import 'package:lensai/features/web_view/presentation/widgets/web_view_tab.dart';
|
||||
|
||||
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
static const _headerSize = 104.0;
|
||||
|
||||
final VoidCallback onClose;
|
||||
|
||||
_SliverHeaderDelagate({required this.onClose});
|
||||
@@ -43,10 +49,12 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
label: const Text('New Tab'),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
ref
|
||||
.read(webViewRepositoryProvider.notifier)
|
||||
onPressed: () async {
|
||||
final topic = ref.read(selectedTopicProvider);
|
||||
await ref
|
||||
.read(topicTabRepositoryProvider(topic).notifier)
|
||||
.closeAllTabs();
|
||||
|
||||
onClose();
|
||||
},
|
||||
icon: const Icon(Icons.delete),
|
||||
@@ -55,6 +63,7 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
],
|
||||
),
|
||||
TopicChips(),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -64,10 +73,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||
}
|
||||
|
||||
@override
|
||||
double get minExtent => 96;
|
||||
double get minExtent => _headerSize;
|
||||
|
||||
@override
|
||||
double get maxExtent => 96;
|
||||
double get maxExtent => _headerSize;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||
@@ -105,90 +114,135 @@ class ViewTabsSheet extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return CustomScrollView(
|
||||
return FadingScroll(
|
||||
fadingSize: 25,
|
||||
shaderPadding:
|
||||
const EdgeInsets.only(top: _SliverHeaderDelagate._headerSize),
|
||||
controller: sheetScrollController,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverHeaderDelagate(onClose: onClose),
|
||||
),
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final tabs = ref.watch(
|
||||
webViewRepositoryProvider.select((tabs) => tabs.values.toList()),
|
||||
);
|
||||
final activeTab = ref.watch(
|
||||
webViewTabControllerProvider.select(
|
||||
(webView) => webView?.tabId,
|
||||
),
|
||||
);
|
||||
builder: (context, controller) {
|
||||
return CustomScrollView(
|
||||
controller: controller,
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
delegate: _SliverHeaderDelagate(onClose: onClose),
|
||||
),
|
||||
HookConsumer(
|
||||
builder: (context, ref, child) {
|
||||
final topic = ref.watch(selectedTopicProvider);
|
||||
final availableTabs = ref.watch(
|
||||
topicTabRepositoryProvider(topic)
|
||||
.select((value) => value.valueOrNull ?? []),
|
||||
);
|
||||
final activeTab = ref.watch(webViewTabControllerProvider);
|
||||
|
||||
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 =
|
||||
tabs.indexWhere((webView) => webView.tabId == activeTab);
|
||||
useEffect(
|
||||
() {
|
||||
final index = availableTabs
|
||||
.indexWhere((webView) => webView == activeTab);
|
||||
|
||||
if (index > -1) {
|
||||
final reversedIndex = tabs.length - 1 - index;
|
||||
final offset = (reversedIndex ~/ 2) * itemHeight;
|
||||
if (index > -1) {
|
||||
final reversedIndex = availableTabs.length - 1 - index;
|
||||
final offset = (reversedIndex ~/ 2) * itemHeight;
|
||||
|
||||
if (offset != sheetScrollController.offset) {
|
||||
unawaited(
|
||||
sheetScrollController.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (offset != controller.offset) {
|
||||
unawaited(
|
||||
controller.animateTo(
|
||||
offset,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return null;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
sliver: SliverGrid.count(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: 2,
|
||||
children: availableTabs.reversed
|
||||
.map(
|
||||
(tabId) => Consumer(
|
||||
key: ValueKey(tabId),
|
||||
builder: (context, ref, child) {
|
||||
final tab = ref.watch(tabStateProvider(tabId));
|
||||
|
||||
return (tab != null)
|
||||
? WebViewTab(
|
||||
tab: tab,
|
||||
isActive: tabId == activeTab,
|
||||
onTap: () {
|
||||
if (tabId != activeTab) {
|
||||
//Close first to avoid rebuilds
|
||||
onClose();
|
||||
ref
|
||||
.read(
|
||||
webViewTabControllerProvider
|
||||
.notifier,
|
||||
)
|
||||
.showTab(tab.id);
|
||||
} else {
|
||||
onClose();
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
ref
|
||||
.read(
|
||||
overlayDialogProvider.notifier,
|
||||
)
|
||||
.show(
|
||||
TabActionDialog(
|
||||
initialTab: tab,
|
||||
onDismiss: ref
|
||||
.read(
|
||||
overlayDialogProvider
|
||||
.notifier,
|
||||
)
|
||||
.dismiss,
|
||||
),
|
||||
);
|
||||
},
|
||||
onDelete: () async {
|
||||
await ref
|
||||
.read(
|
||||
tabRepositoryProvider.notifier,
|
||||
)
|
||||
.deleteTab(tab.id);
|
||||
},
|
||||
)
|
||||
: const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return SliverPadding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
sliver: SliverGrid.count(
|
||||
//Sync values for itemHeight calculation _calculateItemHeight
|
||||
childAspectRatio: 0.75,
|
||||
mainAxisSpacing: 8.0,
|
||||
crossAxisSpacing: 8.0,
|
||||
crossAxisCount: 2,
|
||||
children: tabs.reversed
|
||||
.map(
|
||||
(webView) => HookBuilder(
|
||||
key: ValueKey(webView.tabId),
|
||||
builder: (context) {
|
||||
final tab = useValueListenable(webView.page) as ITab;
|
||||
|
||||
return WebViewTab(
|
||||
tab: tab,
|
||||
isActive: webView.tabId == activeTab,
|
||||
onClose: onClose,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||
import 'package:lensai/features/web_view/domain/providers.dart';
|
||||
|
||||
class TabsActionButton extends HookConsumerWidget {
|
||||
final bool isActive;
|
||||
@@ -17,7 +17,7 @@ class TabsActionButton extends HookConsumerWidget {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final tabCount =
|
||||
ref.watch(webViewRepositoryProvider.select((tabs) => tabs.length));
|
||||
ref.watch(activeTabsProvider.select((tabs) => tabs.length));
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
|
||||
Reference in New Issue
Block a user