improved tab handling by id

This commit is contained in:
Fabian Freund
2024-08-19 04:27:47 +02:00
parent a465195ec4
commit 23dad27d61
12 changed files with 78 additions and 40 deletions
+5
View File
@@ -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()));
@@ -542,7 +542,7 @@ class KagiScreen extends HookConsumerWidget {
if (activeWebView?.key != null && webViews.length > 1) { if (activeWebView?.key != null && webViews.length > 1) {
ref ref
.read(webViewRepositoryProvider.notifier) .read(webViewRepositoryProvider.notifier)
.closeTab(activeWebView!.key!); .closeTab(activeWebView!.tabId);
return true; return true;
} else { } else {
//Mark back as unhandled and navigator will pop //Mark back as unhandled and navigator will pop
@@ -584,13 +584,13 @@ class KagiScreen extends HookConsumerWidget {
(activeWebView != null) (activeWebView != null)
? webViews.keys ? webViews.keys
.toList() .toList()
.indexOf(activeWebView.page.value.key) .indexOf(activeWebView.tabId)
: null, : null,
), ),
index: (activeWebView != null) index: (activeWebView != null)
? webViews.keys ? webViews.keys
.toList() .toList()
.indexOf(activeWebView.page.value.key) + .indexOf(activeWebView.tabId) +
1 1
: 0, : 0,
children: [const LandingContent(), ...webViews.values], children: [const LandingContent(), ...webViews.values],
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/domain/repositories/web_view.dart';
import 'package:lensai/features/web_view/presentation/controllers/switch_new_tab.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'; import 'package:lensai/features/web_view/presentation/widgets/web_view_tab.dart';
@@ -109,7 +110,7 @@ class ViewTabsSheet extends HookConsumerWidget {
); );
final activeTab = ref.watch( final activeTab = ref.watch(
webViewTabControllerProvider.select( webViewTabControllerProvider.select(
(webView) => webView?.page.value.key, (webView) => webView?.tabId,
), ),
); );
@@ -128,7 +129,7 @@ class ViewTabsSheet extends HookConsumerWidget {
useEffect( useEffect(
() { () {
final index = final index =
tabs.indexWhere((webView) => webView.key == activeTab); tabs.indexWhere((webView) => webView.tabId == activeTab);
if (index > -1) { if (index > -1) {
final reversedIndex = tabs.length - 1 - index; final reversedIndex = tabs.length - 1 - index;
@@ -160,10 +161,17 @@ class ViewTabsSheet extends HookConsumerWidget {
crossAxisCount: 2, crossAxisCount: 2,
children: tabs.reversed children: tabs.reversed
.map( .map(
(webView) => WebViewTab( (webView) => HookBuilder(
webView: webView, key: ValueKey(webView.tabId),
isActive: webView.key == activeTab, builder: (context) {
onClose: onClose, final tab = useValueListenable(webView.page) as ITab;
return WebViewTab(
tab: tab,
isActive: webView.tabId == activeTab,
onClose: onClose,
);
},
), ),
) )
.toList(), .toList(),
@@ -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;
}
@@ -3,25 +3,35 @@ import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.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/domain/entities/web_page_info.dart';
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
part 'web_view_page.g.dart'; part 'web_view_page.g.dart';
typedef PageHistory = ({bool canGoBack, bool canGoForward}); typedef PageHistory = ({bool canGoBack, bool canGoForward});
@CopyWith(constructor: '_') @CopyWith(constructor: '_')
class WebViewPage extends WebPageInfo with FastEquatable { class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
@CopyWithField(immutable: true) @CopyWithField(immutable: true)
final Key key; final Key key;
@override
@CopyWithField(immutable: true)
final String id;
final InAppWebViewController? controller; final InAppWebViewController? controller;
// ignore: missing_field_in_equatable_props // ignore: missing_field_in_equatable_props
final SslError? sslError; final SslError? sslError;
@override
final Uint8List? screenshot; final Uint8List? screenshot;
final PageHistory pageHistory; final PageHistory pageHistory;
WebViewPage({ WebViewPage({
String? id,
this.controller, this.controller,
required super.url, required super.url,
this.sslError, this.sslError,
@@ -29,10 +39,12 @@ class WebViewPage extends WebPageInfo with FastEquatable {
super.favicon, super.favicon,
this.screenshot, this.screenshot,
this.pageHistory = (canGoBack: false, canGoForward: false), this.pageHistory = (canGoBack: false, canGoForward: false),
}) : key = GlobalKey(); }) : key = GlobalKey(),
id = id ?? uuid.v7();
WebViewPage._({ WebViewPage._({
required this.key, required this.key,
required this.id,
required this.controller, required this.controller,
required super.url, required super.url,
required this.sslError, required this.sslError,
@@ -48,8 +60,9 @@ class WebViewPage extends WebPageInfo with FastEquatable {
@override @override
List<Object?> get hashParameters => [ List<Object?> get hashParameters => [
key, key,
id,
controller, controller,
url, super.url,
sslError?.toString(), sslError?.toString(),
title, title,
favicon?.toString(), favicon?.toString(),
@@ -86,6 +86,7 @@ class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy {
}) { }) {
return WebViewPage._( return WebViewPage._(
key: _value.key, key: _value.key,
id: _value.id,
controller: controller == const $CopyWithPlaceholder() controller: controller == const $CopyWithPlaceholder()
? _value.controller ? _value.controller
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
@@ -8,35 +8,34 @@ part 'web_view.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class WebViewTabController extends _$WebViewTabController { class WebViewTabController extends _$WebViewTabController {
late Map<Key, WebView> _webViewTabs; late Map<String, WebView> _webViewTabs;
@override @override
WebView? build() { WebView? build() {
_webViewTabs = ref.watch(webViewRepositoryProvider); _webViewTabs = ref.watch(webViewRepositoryProvider);
return (stateOrNull != null) return (stateOrNull != null)
? _webViewTabs[stateOrNull!.page.value.key] ?? ? _webViewTabs[stateOrNull!.tabId] ?? _webViewTabs.values.lastOrNull
_webViewTabs.values.lastOrNull
: null; : null;
} }
void showTab(Key? key) { void showTab(String? id) {
state = (key != null) ? _webViewTabs[key] : null; state = (id != null) ? _webViewTabs[id] : null;
} }
} }
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class WebViewRepository extends _$WebViewRepository { class WebViewRepository extends _$WebViewRepository {
@override @override
Map<Key, WebView> build() { Map<String, WebView> build() {
return stateOrNull ?? {}; return stateOrNull ?? {};
} }
void addTab(WebView webView) { void addTab(WebView webView) {
state = {...state, webView.page.value.key: webView}; state = {...state, webView.tabId: webView};
} }
void closeTab(Key key) { void closeTab(String id) {
state = Map.of(state)..remove(key); state = Map.of(state)..remove(id);
} }
void closeAllTabs() { void closeAllTabs() {
@@ -7,7 +7,7 @@ part of 'web_view.dart';
// ************************************************************************** // **************************************************************************
String _$webViewTabControllerHash() => String _$webViewTabControllerHash() =>
r'45eaa10e6032b04717e8ba08b8dce8ed22764f46'; r'3f1d7460295c6399382881ee56c4ed7f1fd5fe03';
/// See also [WebViewTabController]. /// See also [WebViewTabController].
@ProviderFor(WebViewTabController) @ProviderFor(WebViewTabController)
@@ -23,12 +23,12 @@ final webViewTabControllerProvider =
); );
typedef _$WebViewTabController = Notifier<WebView?>; typedef _$WebViewTabController = Notifier<WebView?>;
String _$webViewRepositoryHash() => r'6732fd1b8a465073b599f64d6710a61cd605c779'; String _$webViewRepositoryHash() => r'dae5e1527ab4ad27f6a882fb63d8e88cd812b20b';
/// See also [WebViewRepository]. /// See also [WebViewRepository].
@ProviderFor(WebViewRepository) @ProviderFor(WebViewRepository)
final webViewRepositoryProvider = final webViewRepositoryProvider =
NotifierProvider<WebViewRepository, Map<Key, WebView>>.internal( NotifierProvider<WebViewRepository, Map<String, WebView>>.internal(
WebViewRepository.new, WebViewRepository.new,
name: r'webViewRepositoryProvider', name: r'webViewRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -38,6 +38,6 @@ final webViewRepositoryProvider =
allTransitiveDependencies: null, allTransitiveDependencies: null,
); );
typedef _$WebViewRepository = Notifier<Map<Key, WebView>>; typedef _$WebViewRepository = Notifier<Map<String, WebView>>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -23,7 +23,7 @@ class SwitchNewTabController extends _$SwitchNewTabController {
.read(webViewRepositoryProvider.notifier) .read(webViewRepositoryProvider.notifier)
.addTab(WebView(tab: newTab)); .addTab(WebView(tab: newTab));
ref.read(webViewTabControllerProvider.notifier).showTab(newTab.key); ref.read(webViewTabControllerProvider.notifier).showTab(newTab.id);
}, },
); );
} }
@@ -7,7 +7,7 @@ part of 'switch_new_tab.dart';
// ************************************************************************** // **************************************************************************
String _$switchNewTabControllerHash() => String _$switchNewTabControllerHash() =>
r'acc20371e95db182cfc5cdc924a289ff56981692'; r'a5b8f7fd71c8cb6cc2023b41504e5cd467c5b063';
/// See also [SwitchNewTabController]. /// See also [SwitchNewTabController].
@ProviderFor(SwitchNewTabController) @ProviderFor(SwitchNewTabController)
@@ -37,6 +37,8 @@ const _webViewSupportedSchemes = [
]; ];
class WebView extends StatefulHookConsumerWidget { class WebView extends StatefulHookConsumerWidget {
final String tabId;
final ValueNotifier<WebViewPage> _pageNotifier; final ValueNotifier<WebViewPage> _pageNotifier;
final ValueNotifier<bool?> _isReaderable; final ValueNotifier<bool?> _isReaderable;
@@ -76,6 +78,7 @@ class WebView extends StatefulHookConsumerWidget {
: _pageNotifier = ValueNotifier(tab), : _pageNotifier = ValueNotifier(tab),
_isReaderable = ValueNotifier(null), _isReaderable = ValueNotifier(null),
_readerableApplied = ValueNotifier(false), _readerableApplied = ValueNotifier(false),
tabId = tab.id,
super(key: tab.key); super(key: tab.key);
@override @override
@@ -1,18 +1,17 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/domain/repositories/web_view.dart';
import 'package:lensai/features/web_view/presentation/widgets/favicon.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 { class WebViewTab extends HookConsumerWidget {
final WebView webView; final ITab tab;
final bool isActive; final bool isActive;
final VoidCallback onClose; final VoidCallback onClose;
const WebViewTab({ const WebViewTab({
required this.webView, required this.tab,
required this.isActive, required this.isActive,
required this.onClose, required this.onClose,
super.key, super.key,
@@ -20,10 +19,8 @@ class WebViewTab extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme; final colorScheme = Theme.of(context).colorScheme;
final page = useValueListenable(webView.page);
return Container( return Container(
key: UniqueKey(),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: isActive ? colorScheme.primary : colorScheme.outline, color: isActive ? colorScheme.primary : colorScheme.outline,
@@ -38,8 +35,9 @@ class WebViewTab extends HookConsumerWidget {
borderRadius: const BorderRadius.all(Radius.circular(16.0)), borderRadius: const BorderRadius.all(Radius.circular(16.0)),
onTap: () { onTap: () {
if (!isActive) { if (!isActive) {
ref.read(webViewTabControllerProvider.notifier).showTab(page.key); //Close first to avoid rebuilds
onClose(); onClose();
ref.read(webViewTabControllerProvider.notifier).showTab(tab.id);
} else { } else {
onClose(); onClose();
} }
@@ -53,7 +51,7 @@ class WebViewTab extends HookConsumerWidget {
padding: const EdgeInsets.only(left: 6.0), padding: const EdgeInsets.only(left: 6.0),
child: Text( child: Text(
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
page.title ?? 'New Tab', tab.title ?? 'New Tab',
maxLines: 2, maxLines: 2,
), ),
), ),
@@ -64,7 +62,7 @@ class WebViewTab extends HookConsumerWidget {
onPressed: () { onPressed: () {
ref ref
.read(webViewRepositoryProvider.notifier) .read(webViewRepositoryProvider.notifier)
.closeTab(page.key); .closeTab(tab.id);
}, },
icon: const Icon(Icons.close), icon: const Icon(Icons.close),
), ),
@@ -76,14 +74,15 @@ class WebViewTab extends HookConsumerWidget {
width: 6.0, width: 6.0,
), ),
FaviconImage( FaviconImage(
webPageInfo: page, url: tab.url,
favicon: tab.favicon,
), ),
const SizedBox( const SizedBox(
width: 6.0, width: 6.0,
), ),
Expanded( Expanded(
child: Text( child: Text(
page.url.authority, tab.url.authority,
style: Theme.of(context).textTheme.bodySmall, style: Theme.of(context).textTheme.bodySmall,
), ),
), ),
@@ -92,7 +91,7 @@ class WebViewTab extends HookConsumerWidget {
const SizedBox( const SizedBox(
height: 6, height: 6,
), ),
if (page.screenshot != null) if (tab.screenshot != null)
Expanded( Expanded(
child: ClipRRect( child: ClipRRect(
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
@@ -103,7 +102,7 @@ class WebViewTab extends HookConsumerWidget {
width: double.infinity, width: double.infinity,
child: Image.memory( child: Image.memory(
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
page.screenshot!, tab.screenshot!,
), ),
), ),
), ),