diff --git a/app/lib/core/uuid.dart b/app/lib/core/uuid.dart new file mode 100644 index 00000000..c479a486 --- /dev/null +++ b/app/lib/core/uuid.dart @@ -0,0 +1,5 @@ +import 'package:uuid/data.dart'; +import 'package:uuid/rng.dart'; +import 'package:uuid/uuid.dart'; + +final uuid = Uuid(goptions: GlobalOptions(CryptoRNG())); diff --git a/app/lib/features/search_browser/presentation/screens/browser.dart b/app/lib/features/search_browser/presentation/screens/browser.dart index f267473b..cffabfe3 100644 --- a/app/lib/features/search_browser/presentation/screens/browser.dart +++ b/app/lib/features/search_browser/presentation/screens/browser.dart @@ -542,7 +542,7 @@ class KagiScreen extends HookConsumerWidget { if (activeWebView?.key != null && webViews.length > 1) { ref .read(webViewRepositoryProvider.notifier) - .closeTab(activeWebView!.key!); + .closeTab(activeWebView!.tabId); return true; } else { //Mark back as unhandled and navigator will pop @@ -584,13 +584,13 @@ class KagiScreen extends HookConsumerWidget { (activeWebView != null) ? webViews.keys .toList() - .indexOf(activeWebView.page.value.key) + .indexOf(activeWebView.tabId) : null, ), index: (activeWebView != null) ? webViews.keys .toList() - .indexOf(activeWebView.page.value.key) + + .indexOf(activeWebView.tabId) + 1 : 0, children: [const LandingContent(), ...webViews.values], diff --git a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart b/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart index 9cbd59e5..ce4891b4 100644 --- a/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart +++ b/app/lib/features/search_browser/presentation/widgets/sheets/view_tabs_sheet.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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'; @@ -109,7 +110,7 @@ class ViewTabsSheet extends HookConsumerWidget { ); final activeTab = ref.watch( webViewTabControllerProvider.select( - (webView) => webView?.page.value.key, + (webView) => webView?.tabId, ), ); @@ -128,7 +129,7 @@ class ViewTabsSheet extends HookConsumerWidget { useEffect( () { final index = - tabs.indexWhere((webView) => webView.key == activeTab); + tabs.indexWhere((webView) => webView.tabId == activeTab); if (index > -1) { final reversedIndex = tabs.length - 1 - index; @@ -160,10 +161,17 @@ class ViewTabsSheet extends HookConsumerWidget { crossAxisCount: 2, children: tabs.reversed .map( - (webView) => WebViewTab( - webView: webView, - isActive: webView.key == activeTab, - onClose: onClose, + (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(), diff --git a/app/lib/features/web_view/domain/entities/abstract/tab.dart b/app/lib/features/web_view/domain/entities/abstract/tab.dart new file mode 100644 index 00000000..ee92387d --- /dev/null +++ b/app/lib/features/web_view/domain/entities/abstract/tab.dart @@ -0,0 +1,10 @@ +import 'package:drift/drift.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +abstract interface class ITab { + String get id; + Uri get url; + String? get title; + Favicon? get favicon; + Uint8List? get screenshot; +} diff --git a/app/lib/features/web_view/domain/entities/web_view_page.dart b/app/lib/features/web_view/domain/entities/web_view_page.dart index a24cc869..d14f2ef1 100644 --- a/app/lib/features/web_view/domain/entities/web_view_page.dart +++ b/app/lib/features/web_view/domain/entities/web_view_page.dart @@ -3,25 +3,35 @@ import 'package:fast_equatable/fast_equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:lensai/core/uuid.dart'; import 'package:lensai/domain/entities/web_page_info.dart'; +import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart'; part 'web_view_page.g.dart'; typedef PageHistory = ({bool canGoBack, bool canGoForward}); @CopyWith(constructor: '_') -class WebViewPage extends WebPageInfo with FastEquatable { +class WebViewPage extends WebPageInfo with FastEquatable implements ITab { @CopyWithField(immutable: true) final Key key; + @override + @CopyWithField(immutable: true) + final String id; + final InAppWebViewController? controller; // ignore: missing_field_in_equatable_props final SslError? sslError; + + @override final Uint8List? screenshot; + final PageHistory pageHistory; WebViewPage({ + String? id, this.controller, required super.url, this.sslError, @@ -29,10 +39,12 @@ class WebViewPage extends WebPageInfo with FastEquatable { super.favicon, this.screenshot, this.pageHistory = (canGoBack: false, canGoForward: false), - }) : key = GlobalKey(); + }) : key = GlobalKey(), + id = id ?? uuid.v7(); WebViewPage._({ required this.key, + required this.id, required this.controller, required super.url, required this.sslError, @@ -48,8 +60,9 @@ class WebViewPage extends WebPageInfo with FastEquatable { @override List get hashParameters => [ key, + id, controller, - url, + super.url, sslError?.toString(), title, favicon?.toString(), diff --git a/app/lib/features/web_view/domain/entities/web_view_page.g.dart b/app/lib/features/web_view/domain/entities/web_view_page.g.dart index d76e0c7e..891d3222 100644 --- a/app/lib/features/web_view/domain/entities/web_view_page.g.dart +++ b/app/lib/features/web_view/domain/entities/web_view_page.g.dart @@ -86,6 +86,7 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy { }) { return WebViewPage._( key: _value.key, + id: _value.id, controller: controller == const $CopyWithPlaceholder() ? _value.controller // ignore: cast_nullable_to_non_nullable diff --git a/app/lib/features/web_view/domain/repositories/web_view.dart b/app/lib/features/web_view/domain/repositories/web_view.dart index a5a212f7..faba38fe 100644 --- a/app/lib/features/web_view/domain/repositories/web_view.dart +++ b/app/lib/features/web_view/domain/repositories/web_view.dart @@ -8,35 +8,34 @@ part 'web_view.g.dart'; @Riverpod(keepAlive: true) class WebViewTabController extends _$WebViewTabController { - late Map _webViewTabs; + late Map _webViewTabs; @override WebView? build() { _webViewTabs = ref.watch(webViewRepositoryProvider); return (stateOrNull != null) - ? _webViewTabs[stateOrNull!.page.value.key] ?? - _webViewTabs.values.lastOrNull + ? _webViewTabs[stateOrNull!.tabId] ?? _webViewTabs.values.lastOrNull : null; } - void showTab(Key? key) { - state = (key != null) ? _webViewTabs[key] : null; + void showTab(String? id) { + state = (id != null) ? _webViewTabs[id] : null; } } @Riverpod(keepAlive: true) class WebViewRepository extends _$WebViewRepository { @override - Map build() { + Map build() { return stateOrNull ?? {}; } void addTab(WebView webView) { - state = {...state, webView.page.value.key: webView}; + state = {...state, webView.tabId: webView}; } - void closeTab(Key key) { - state = Map.of(state)..remove(key); + void closeTab(String id) { + state = Map.of(state)..remove(id); } void closeAllTabs() { diff --git a/app/lib/features/web_view/domain/repositories/web_view.g.dart b/app/lib/features/web_view/domain/repositories/web_view.g.dart index c776082f..d0ae0e0d 100644 --- a/app/lib/features/web_view/domain/repositories/web_view.g.dart +++ b/app/lib/features/web_view/domain/repositories/web_view.g.dart @@ -7,7 +7,7 @@ part of 'web_view.dart'; // ************************************************************************** String _$webViewTabControllerHash() => - r'45eaa10e6032b04717e8ba08b8dce8ed22764f46'; + r'3f1d7460295c6399382881ee56c4ed7f1fd5fe03'; /// See also [WebViewTabController]. @ProviderFor(WebViewTabController) @@ -23,12 +23,12 @@ final webViewTabControllerProvider = ); typedef _$WebViewTabController = Notifier; -String _$webViewRepositoryHash() => r'6732fd1b8a465073b599f64d6710a61cd605c779'; +String _$webViewRepositoryHash() => r'dae5e1527ab4ad27f6a882fb63d8e88cd812b20b'; /// See also [WebViewRepository]. @ProviderFor(WebViewRepository) final webViewRepositoryProvider = - NotifierProvider>.internal( + NotifierProvider>.internal( WebViewRepository.new, name: r'webViewRepositoryProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -38,6 +38,6 @@ final webViewRepositoryProvider = allTransitiveDependencies: null, ); -typedef _$WebViewRepository = Notifier>; +typedef _$WebViewRepository = Notifier>; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart b/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart index 4c896fee..99f7f9d1 100644 --- a/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart +++ b/app/lib/features/web_view/presentation/controllers/switch_new_tab.dart @@ -23,7 +23,7 @@ class SwitchNewTabController extends _$SwitchNewTabController { .read(webViewRepositoryProvider.notifier) .addTab(WebView(tab: newTab)); - ref.read(webViewTabControllerProvider.notifier).showTab(newTab.key); + ref.read(webViewTabControllerProvider.notifier).showTab(newTab.id); }, ); } diff --git a/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart b/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart index d409c61e..9968cef0 100644 --- a/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart +++ b/app/lib/features/web_view/presentation/controllers/switch_new_tab.g.dart @@ -7,7 +7,7 @@ part of 'switch_new_tab.dart'; // ************************************************************************** String _$switchNewTabControllerHash() => - r'acc20371e95db182cfc5cdc924a289ff56981692'; + r'a5b8f7fd71c8cb6cc2023b41504e5cd467c5b063'; /// See also [SwitchNewTabController]. @ProviderFor(SwitchNewTabController) diff --git a/app/lib/features/web_view/presentation/widgets/web_view.dart b/app/lib/features/web_view/presentation/widgets/web_view.dart index 8aaa935a..b9964b70 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -37,6 +37,8 @@ const _webViewSupportedSchemes = [ ]; class WebView extends StatefulHookConsumerWidget { + final String tabId; + final ValueNotifier _pageNotifier; final ValueNotifier _isReaderable; @@ -76,6 +78,7 @@ class WebView extends StatefulHookConsumerWidget { : _pageNotifier = ValueNotifier(tab), _isReaderable = ValueNotifier(null), _readerableApplied = ValueNotifier(false), + tabId = tab.id, super(key: tab.key); @override diff --git a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart b/app/lib/features/web_view/presentation/widgets/web_view_tab.dart index de443a96..90f3a1a7 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view_tab.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view_tab.dart @@ -1,18 +1,17 @@ import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.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/widgets/favicon.dart'; -import 'package:lensai/features/web_view/presentation/widgets/web_view.dart'; class WebViewTab extends HookConsumerWidget { - final WebView webView; + final ITab tab; final bool isActive; final VoidCallback onClose; const WebViewTab({ - required this.webView, + required this.tab, required this.isActive, required this.onClose, super.key, @@ -20,10 +19,8 @@ class WebViewTab extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final colorScheme = Theme.of(context).colorScheme; - final page = useValueListenable(webView.page); return Container( - key: UniqueKey(), decoration: BoxDecoration( border: Border.all( color: isActive ? colorScheme.primary : colorScheme.outline, @@ -38,8 +35,9 @@ class WebViewTab extends HookConsumerWidget { borderRadius: const BorderRadius.all(Radius.circular(16.0)), onTap: () { if (!isActive) { - ref.read(webViewTabControllerProvider.notifier).showTab(page.key); + //Close first to avoid rebuilds onClose(); + ref.read(webViewTabControllerProvider.notifier).showTab(tab.id); } else { onClose(); } @@ -53,7 +51,7 @@ class WebViewTab extends HookConsumerWidget { padding: const EdgeInsets.only(left: 6.0), child: Text( overflow: TextOverflow.ellipsis, - page.title ?? 'New Tab', + tab.title ?? 'New Tab', maxLines: 2, ), ), @@ -64,7 +62,7 @@ class WebViewTab extends HookConsumerWidget { onPressed: () { ref .read(webViewRepositoryProvider.notifier) - .closeTab(page.key); + .closeTab(tab.id); }, icon: const Icon(Icons.close), ), @@ -76,14 +74,15 @@ class WebViewTab extends HookConsumerWidget { width: 6.0, ), FaviconImage( - webPageInfo: page, + url: tab.url, + favicon: tab.favicon, ), const SizedBox( width: 6.0, ), Expanded( child: Text( - page.url.authority, + tab.url.authority, style: Theme.of(context).textTheme.bodySmall, ), ), @@ -92,7 +91,7 @@ class WebViewTab extends HookConsumerWidget { const SizedBox( height: 6, ), - if (page.screenshot != null) + if (tab.screenshot != null) Expanded( child: ClipRRect( borderRadius: const BorderRadius.only( @@ -103,7 +102,7 @@ class WebViewTab extends HookConsumerWidget { width: double.infinity, child: Image.memory( fit: BoxFit.fitWidth, - page.screenshot!, + tab.screenshot!, ), ), ),