diff --git a/app/lib/features/search_browser/presentation/screens/browser.dart b/app/lib/features/search_browser/presentation/screens/browser.dart index cffabfe3..d2a9fefd 100644 --- a/app/lib/features/search_browser/presentation/screens/browser.dart +++ b/app/lib/features/search_browser/presentation/screens/browser.dart @@ -19,6 +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/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'; @@ -188,7 +189,9 @@ class KagiScreen extends HookConsumerWidget { final controller = useListenable(activeWebView.page).value.controller; final readerabilityState = ref.watch( - readerabilityControllerProvider(controller), + readerabilityControllerProvider( + ConsistentController(controller), + ), ); final enableReadability = ref.watch( @@ -198,13 +201,11 @@ class KagiScreen extends HookConsumerWidget { ), ); - final isReaderable = useValueListenable( - activeWebView.isReaderable, - ); + final isReaderable = + readerabilityState.valueOrNull?.readerable ?? false; - final readerableApplied = useValueListenable( - activeWebView.readerableApplied, - ); + final readerableApplied = + readerabilityState.valueOrNull?.applied ?? false; final icon = useMemoized( () => readerableApplied @@ -220,54 +221,49 @@ class KagiScreen extends HookConsumerWidget { ); return Visibility( - visible: enableReadability && - (isReaderable == true || readerableApplied), - child: InkWell( - onTap: readerabilityState.isLoading - ? null - : () async { - final controller = - activeWebView.currentController; + visible: enableReadability, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 15.0, + horizontal: 8.0, + ), + child: readerabilityState.when( + data: (_) => (isReaderable == true || readerableApplied) + ? InkWell( + onTap: readerabilityState.isLoading + ? null + : () async { + final controller = + activeWebView.currentController; - if (controller != null) { - final readabilityNotifier = ref.read( - readerabilityControllerProvider( - controller, - ).notifier, - ); + if (controller != null) { + final readabilityNotifier = ref.read( + readerabilityControllerProvider( + ConsistentController(controller), + ).notifier, + ); - if (readerableApplied) { - await activeWebView - .updateReaderableApplied(false); - } else { - await readabilityNotifier.applyReaderable(); - await activeWebView - .updateReaderableApplied(true); - } - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 15.0, - horizontal: 8.0, - ), - child: readerabilityState.when( - data: (_) => icon, - error: (error, stackTrace) => SizedBox.shrink(), - loading: () => AnimateGradientShader( - duration: const Duration(milliseconds: 500), - primaryEnd: Alignment.bottomLeft, - secondaryEnd: Alignment.topRight, - primaryColors: [ - colorScheme.primary, - colorScheme.primaryContainer, - ], - secondaryColors: [ - colorScheme.secondary, - colorScheme.secondaryContainer, - ], - child: icon, - ), + await readabilityNotifier + .toggleReaderable(); + } + }, + child: icon, + ) + : const SizedBox.shrink(), + error: (error, stackTrace) => SizedBox.shrink(), + loading: () => AnimateGradientShader( + duration: const Duration(milliseconds: 500), + primaryEnd: Alignment.bottomLeft, + secondaryEnd: Alignment.topRight, + primaryColors: [ + colorScheme.primary, + colorScheme.primaryContainer, + ], + secondaryColors: [ + colorScheme.secondary, + colorScheme.secondaryContainer, + ], + child: icon, ), ), ), diff --git a/app/lib/features/web_view/domain/entities/consistent_controller.dart b/app/lib/features/web_view/domain/entities/consistent_controller.dart new file mode 100644 index 00000000..eca5ed21 --- /dev/null +++ b/app/lib/features/web_view/domain/entities/consistent_controller.dart @@ -0,0 +1,18 @@ +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class ConsistentController { + final InAppWebViewController? value; + + const ConsistentController(this.value); + + @override + // ignore: avoid_dynamic_calls + int get hashCode => value?.platform.id.hashCode ?? -1; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + if (other is! ConsistentController) return false; + return hashCode == other.hashCode; + } +} 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 faba38fe..e51644ce 100644 --- a/app/lib/features/web_view/domain/repositories/web_view.dart +++ b/app/lib/features/web_view/domain/repositories/web_view.dart @@ -1,6 +1,7 @@ import 'dart:collection'; -import 'package:flutter/foundation.dart'; +import 'package:lensai/features/web_view/domain/entities/consistent_controller.dart'; +import 'package:lensai/features/web_view/presentation/controllers/readerability.dart'; import 'package:lensai/features/web_view/presentation/widgets/web_view.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -30,15 +31,29 @@ class WebViewRepository extends _$WebViewRepository { return stateOrNull ?? {}; } + void _disposeReaderability(String id) { + final controller = state[id]?.currentController; + if (controller != null) { + ref + .read( + readerabilityControllerProvider(ConsistentController(controller)) + .notifier, + ) + .dispose(); + } + } + void addTab(WebView webView) { state = {...state, webView.tabId: webView}; } void closeTab(String id) { + _disposeReaderability(id); state = Map.of(state)..remove(id); } void closeAllTabs() { + state.keys.forEach(_disposeReaderability); state = {}; } } diff --git a/app/lib/features/web_view/presentation/controllers/readerability.dart b/app/lib/features/web_view/presentation/controllers/readerability.dart index 28510b49..75e986be 100644 --- a/app/lib/features/web_view/presentation/controllers/readerability.dart +++ b/app/lib/features/web_view/presentation/controllers/readerability.dart @@ -1,33 +1,62 @@ import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:lensai/features/web_view/domain/entities/consistent_controller.dart'; import 'package:lensai/features/web_view/presentation/services/readerability_script.dart'; +import 'package:riverpod/riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'readerability.g.dart'; @Riverpod() class ReaderabilityController extends _$ReaderabilityController { + late InAppWebViewController? _controller; late ReaderabilityScriptService _service; + late KeepAliveLink _aliveLink; @override - FutureOr build(InAppWebViewController? controller) async { + AsyncValue<({bool readerable, bool applied})> build( + ConsistentController controller, + ) { + _controller = controller.value; _service = ref.watch(readerabilityScriptServiceProvider(controller).notifier); + _aliveLink = ref.keepAlive(); + + return const AsyncLoading(); } - Future isReaderable() async { + Future checkReaderable() async { + final applied = state.valueOrNull?.applied ?? false; + state = const AsyncLoading(); final result = await AsyncValue.guard(() async { - return await _service.isReaderable(); + return (readerable: await _service.isReaderable(), applied: applied); }); state = result; - return result.valueOrNull ?? false; + return result.valueOrNull?.readerable ?? false; } - Future applyReaderable() async { + Future toggleReaderable() async { + final applied = state.valueOrNull?.applied ?? false; + final readerable = state.valueOrNull?.readerable ?? false; + state = const AsyncLoading(); state = await AsyncValue.guard(() async { - await _service.applyReaderable(); + if (applied) { + await _controller?.reload(); + } else { + await _service.applyReaderable(); + } + + return (readerable: readerable, applied: !applied); }); } + + void reset() { + state = const AsyncLoading(); + } + + void dispose() { + _aliveLink.close(); + } } diff --git a/app/lib/features/web_view/presentation/controllers/readerability.g.dart b/app/lib/features/web_view/presentation/controllers/readerability.g.dart index 08e77ac4..6af3bf74 100644 --- a/app/lib/features/web_view/presentation/controllers/readerability.g.dart +++ b/app/lib/features/web_view/presentation/controllers/readerability.g.dart @@ -7,7 +7,7 @@ part of 'readerability.dart'; // ************************************************************************** String _$readerabilityControllerHash() => - r'1260623c58d23511c3e4f58f97e18663be354ced'; + r'1237e57abce82f71dbc66b6ab46f50dad9e3ddf9'; /// Copied from Dart SDK class _SystemHash { @@ -30,12 +30,12 @@ class _SystemHash { } } -abstract class _$ReaderabilityController - extends BuildlessAutoDisposeAsyncNotifier { - late final InAppWebViewController? controller; +abstract class _$ReaderabilityController extends BuildlessAutoDisposeNotifier< + AsyncValue<({bool readerable, bool applied})>> { + late final ConsistentController controller; - FutureOr build( - InAppWebViewController? controller, + AsyncValue<({bool readerable, bool applied})> build( + ConsistentController controller, ); } @@ -44,13 +44,14 @@ abstract class _$ReaderabilityController const readerabilityControllerProvider = ReaderabilityControllerFamily(); /// See also [ReaderabilityController]. -class ReaderabilityControllerFamily extends Family> { +class ReaderabilityControllerFamily + extends Family> { /// See also [ReaderabilityController]. const ReaderabilityControllerFamily(); /// See also [ReaderabilityController]. ReaderabilityControllerProvider call( - InAppWebViewController? controller, + ConsistentController controller, ) { return ReaderabilityControllerProvider( controller, @@ -82,12 +83,11 @@ class ReaderabilityControllerFamily extends Family> { } /// See also [ReaderabilityController]. -class ReaderabilityControllerProvider - extends AutoDisposeAsyncNotifierProviderImpl { +class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl< + ReaderabilityController, AsyncValue<({bool readerable, bool applied})>> { /// See also [ReaderabilityController]. ReaderabilityControllerProvider( - InAppWebViewController? controller, + ConsistentController controller, ) : this._internal( () => ReaderabilityController()..controller = controller, from: readerabilityControllerProvider, @@ -112,10 +112,10 @@ class ReaderabilityControllerProvider required this.controller, }) : super.internal(); - final InAppWebViewController? controller; + final ConsistentController controller; @override - FutureOr runNotifierBuild( + AsyncValue<({bool readerable, bool applied})> runNotifierBuild( covariant ReaderabilityController notifier, ) { return notifier.build( @@ -140,8 +140,8 @@ class ReaderabilityControllerProvider } @override - AutoDisposeAsyncNotifierProviderElement - createElement() { + AutoDisposeNotifierProviderElement> createElement() { return _ReaderabilityControllerProviderElement(this); } @@ -160,18 +160,20 @@ class ReaderabilityControllerProvider } } -mixin ReaderabilityControllerRef on AutoDisposeAsyncNotifierProviderRef { +mixin ReaderabilityControllerRef on AutoDisposeNotifierProviderRef< + AsyncValue<({bool readerable, bool applied})>> { /// The parameter `controller` of this provider. - InAppWebViewController? get controller; + ConsistentController get controller; } class _ReaderabilityControllerProviderElement - extends AutoDisposeAsyncNotifierProviderElement with ReaderabilityControllerRef { + extends AutoDisposeNotifierProviderElement> + with ReaderabilityControllerRef { _ReaderabilityControllerProviderElement(super.provider); @override - InAppWebViewController? get controller => + ConsistentController get controller => (origin as ReaderabilityControllerProvider).controller; } // ignore_for_file: type=lint diff --git a/app/lib/features/web_view/presentation/services/readerability_script.dart b/app/lib/features/web_view/presentation/services/readerability_script.dart index 223eabd9..22827934 100644 --- a/app/lib/features/web_view/presentation/services/readerability_script.dart +++ b/app/lib/features/web_view/presentation/services/readerability_script.dart @@ -1,6 +1,7 @@ import 'package:flutter_inappwebview/flutter_inappwebview.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/web_view/domain/providers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -8,11 +9,13 @@ part 'readerability_script.g.dart'; @Riverpod() class ReaderabilityScriptService extends _$ReaderabilityScriptService { + late InAppWebViewController? _controller; late Future _readerabilityScript; late bool _enableReadability; @override - Future build(InAppWebViewController? controller) async { + Future build(ConsistentController controller) async { + _controller = controller.value; _readerabilityScript = ref.watch(readerabilityScriptProvider.future); _enableReadability = ref.watch( settingsRepositoryProvider.select( @@ -23,15 +26,15 @@ class ReaderabilityScriptService extends _$ReaderabilityScriptService { } Future _injectScript() async { - if (controller != null) { + if (_controller != null) { await _readerabilityScript - .then((script) => controller!.evaluateJavascript(source: script)); + .then((script) => _controller!.evaluateJavascript(source: script)); } } Future _ensureScriptInjected() async { - if (controller != null) { - final injected = await controller!.evaluateJavascript( + if (_controller != null) { + final injected = await _controller!.evaluateJavascript( source: "(typeof window !== 'undefined' && typeof window.isReaderable === 'function')", ) as bool; @@ -46,8 +49,8 @@ class ReaderabilityScriptService extends _$ReaderabilityScriptService { if (_enableReadability) { await _ensureScriptInjected(); - if (controller != null) { - return await controller!.evaluateJavascript( + if (_controller != null) { + return await _controller!.evaluateJavascript( source: 'isReaderable();', ) as bool; } @@ -60,8 +63,8 @@ class ReaderabilityScriptService extends _$ReaderabilityScriptService { if (_enableReadability) { await _ensureScriptInjected(); - if (controller != null) { - await controller!.evaluateJavascript(source: 'applyReaderable();'); + if (_controller != null) { + await _controller!.evaluateJavascript(source: 'applyReaderable();'); } } } diff --git a/app/lib/features/web_view/presentation/services/readerability_script.g.dart b/app/lib/features/web_view/presentation/services/readerability_script.g.dart index cb6a94ea..e8d710eb 100644 --- a/app/lib/features/web_view/presentation/services/readerability_script.g.dart +++ b/app/lib/features/web_view/presentation/services/readerability_script.g.dart @@ -7,7 +7,7 @@ part of 'readerability_script.dart'; // ************************************************************************** String _$readerabilityScriptServiceHash() => - r'7a4fe65f683c31545b2f2bcb3ea199619be4bf4b'; + r'bb38e2934ed124502b70faed11b6921c40d0eec2'; /// Copied from Dart SDK class _SystemHash { @@ -32,10 +32,10 @@ class _SystemHash { abstract class _$ReaderabilityScriptService extends BuildlessAutoDisposeAsyncNotifier { - late final InAppWebViewController? controller; + late final ConsistentController controller; FutureOr build( - InAppWebViewController? controller, + ConsistentController controller, ); } @@ -50,7 +50,7 @@ class ReaderabilityScriptServiceFamily extends Family> { /// See also [ReaderabilityScriptService]. ReaderabilityScriptServiceProvider call( - InAppWebViewController? controller, + ConsistentController controller, ) { return ReaderabilityScriptServiceProvider( controller, @@ -87,7 +87,7 @@ class ReaderabilityScriptServiceProvider void> { /// See also [ReaderabilityScriptService]. ReaderabilityScriptServiceProvider( - InAppWebViewController? controller, + ConsistentController controller, ) : this._internal( () => ReaderabilityScriptService()..controller = controller, from: readerabilityScriptServiceProvider, @@ -112,7 +112,7 @@ class ReaderabilityScriptServiceProvider required this.controller, }) : super.internal(); - final InAppWebViewController? controller; + final ConsistentController controller; @override FutureOr runNotifierBuild( @@ -163,7 +163,7 @@ class ReaderabilityScriptServiceProvider mixin ReaderabilityScriptServiceRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `controller` of this provider. - InAppWebViewController? get controller; + ConsistentController get controller; } class _ReaderabilityScriptServiceProviderElement @@ -172,7 +172,7 @@ class _ReaderabilityScriptServiceProviderElement _ReaderabilityScriptServiceProviderElement(super.provider); @override - InAppWebViewController? get controller => + ConsistentController get controller => (origin as ReaderabilityScriptServiceProvider).controller; } // ignore_for_file: type=lint 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 b9964b70..bae7c614 100644 --- a/app/lib/features/web_view/presentation/widgets/web_view.dart +++ b/app/lib/features/web_view/presentation/widgets/web_view.dart @@ -16,6 +16,7 @@ import 'package:lensai/features/search_browser/domain/entities/sheet.dart'; import 'package:lensai/features/search_browser/domain/providers.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/web_view/domain/entities/web_view_page.dart'; import 'package:lensai/features/web_view/domain/providers.dart'; import 'package:lensai/features/web_view/presentation/controllers/readerability.dart'; @@ -41,43 +42,18 @@ class WebView extends StatefulHookConsumerWidget { final ValueNotifier _pageNotifier; - final ValueNotifier _isReaderable; - final ValueNotifier _readerableApplied; - ValueListenable get page => _pageNotifier; /// Don't cache this value as it depends on current value of a ValueListenable InAppWebViewController? get currentController => _pageNotifier.value.controller; - ValueListenable get isReaderable => _isReaderable; - ValueListenable get readerableApplied => _readerableApplied; - void updatePage(WebViewPage Function(WebViewPage page) update) { _pageNotifier.value = update(_pageNotifier.value); } - void resetReaderable() { - _isReaderable.value = null; - _readerableApplied.value = false; - } - - Future updateReaderableApplied(bool value) async { - if (value == false) { - await _pageNotifier.value.controller?.reload(); - } - - _readerableApplied.value = value; - } - - void updateIsReaderable(bool value) { - _isReaderable.value = value; - } - WebView({required WebViewPage tab}) : _pageNotifier = ValueNotifier(tab), - _isReaderable = ValueNotifier(null), - _readerableApplied = ValueNotifier(false), tabId = tab.id, super(key: tab.key); @@ -162,8 +138,6 @@ class _WebViewState extends ConsumerState { _periodicScreenshotUpdate?.cancel(); widget._pageNotifier.dispose(); - widget._isReaderable.dispose(); - widget._readerableApplied.dispose(); logger.i('Disposed ${widget.key} (${widget.page.value.title})'); } @@ -390,6 +364,12 @@ class _WebViewState extends ConsumerState { webViewProgress.value = progress; }, onLoadStart: (controller, url) { + final readabilityNotifier = ref.read( + readerabilityControllerProvider( + ConsistentController(controller), + ).notifier, + ); + if (url != null) { widget.updatePage( (page) => page.copyWith( @@ -400,7 +380,7 @@ class _WebViewState extends ConsumerState { ); } - widget.resetReaderable(); + readabilityNotifier.reset(); }, onLoadStop: (controller, url) async { if (url != null) { @@ -410,10 +390,13 @@ class _WebViewState extends ConsumerState { _onLoadStopDebounce?.cancel(); _onLoadStopDebounce = Timer(const Duration(milliseconds: 150), () async { - // final favicon = await widget.page.value.controller - // ?.getFavicons() - // .then((icons) => choseFavicon(icons)); - // widget.updatePage((page) => page.copyWith.favicon(favicon)); + final readabilityNotifier = ref.read( + readerabilityControllerProvider( + ConsistentController(controller), + ).notifier, + ); + + await readabilityNotifier.checkReaderable(); await _updateScreenshot().whenComplete(() { _periodicScreenshotUpdate?.cancel(); @@ -426,12 +409,6 @@ class _WebViewState extends ConsumerState { }); }); }); - - final readabilityNotifier = ref.read( - readerabilityControllerProvider(controller).notifier, - ); - - widget.updateIsReaderable(await readabilityNotifier.isReaderable()); }, onUpdateVisitedHistory: (controller, url, isReload) async { if (isReload != true) {