improve readerability handling

This commit is contained in:
Fabian Freund
2024-08-19 14:07:57 +02:00
parent 5ca9595b42
commit 8dc858af38
8 changed files with 176 additions and 136 deletions
@@ -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,
),
),
),
@@ -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;
}
}
@@ -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 = {};
}
}
@@ -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<void> 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<bool> isReaderable() async {
Future<bool> 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<void> applyReaderable() async {
Future<void> 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();
}
}
@@ -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<void> {
late final InAppWebViewController? controller;
abstract class _$ReaderabilityController extends BuildlessAutoDisposeNotifier<
AsyncValue<({bool readerable, bool applied})>> {
late final ConsistentController controller;
FutureOr<void> 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<AsyncValue<void>> {
class ReaderabilityControllerFamily
extends Family<AsyncValue<({bool readerable, bool applied})>> {
/// 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<AsyncValue<void>> {
}
/// See also [ReaderabilityController].
class ReaderabilityControllerProvider
extends AutoDisposeAsyncNotifierProviderImpl<ReaderabilityController,
void> {
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<void> runNotifierBuild(
AsyncValue<({bool readerable, bool applied})> runNotifierBuild(
covariant ReaderabilityController notifier,
) {
return notifier.build(
@@ -140,8 +140,8 @@ class ReaderabilityControllerProvider
}
@override
AutoDisposeAsyncNotifierProviderElement<ReaderabilityController, void>
createElement() {
AutoDisposeNotifierProviderElement<ReaderabilityController,
AsyncValue<({bool readerable, bool applied})>> createElement() {
return _ReaderabilityControllerProviderElement(this);
}
@@ -160,18 +160,20 @@ class ReaderabilityControllerProvider
}
}
mixin ReaderabilityControllerRef on AutoDisposeAsyncNotifierProviderRef<void> {
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<ReaderabilityController,
void> with ReaderabilityControllerRef {
extends AutoDisposeNotifierProviderElement<ReaderabilityController,
AsyncValue<({bool readerable, bool applied})>>
with ReaderabilityControllerRef {
_ReaderabilityControllerProviderElement(super.provider);
@override
InAppWebViewController? get controller =>
ConsistentController get controller =>
(origin as ReaderabilityControllerProvider).controller;
}
// ignore_for_file: type=lint
@@ -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<String> _readerabilityScript;
late bool _enableReadability;
@override
Future<void> build(InAppWebViewController? controller) async {
Future<void> 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<void> _injectScript() async {
if (controller != null) {
if (_controller != null) {
await _readerabilityScript
.then((script) => controller!.evaluateJavascript(source: script));
.then((script) => _controller!.evaluateJavascript(source: script));
}
}
Future<void> _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();');
}
}
}
@@ -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<void> {
late final InAppWebViewController? controller;
late final ConsistentController controller;
FutureOr<void> build(
InAppWebViewController? controller,
ConsistentController controller,
);
}
@@ -50,7 +50,7 @@ class ReaderabilityScriptServiceFamily extends Family<AsyncValue<void>> {
/// 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<void> runNotifierBuild(
@@ -163,7 +163,7 @@ class ReaderabilityScriptServiceProvider
mixin ReaderabilityScriptServiceRef
on AutoDisposeAsyncNotifierProviderRef<void> {
/// 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
@@ -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<WebViewPage> _pageNotifier;
final ValueNotifier<bool?> _isReaderable;
final ValueNotifier<bool> _readerableApplied;
ValueListenable<WebViewPage> get page => _pageNotifier;
/// Don't cache this value as it depends on current value of a ValueListenable
InAppWebViewController? get currentController =>
_pageNotifier.value.controller;
ValueListenable<bool?> get isReaderable => _isReaderable;
ValueListenable<bool> get readerableApplied => _readerableApplied;
void updatePage(WebViewPage Function(WebViewPage page) update) {
_pageNotifier.value = update(_pageNotifier.value);
}
void resetReaderable() {
_isReaderable.value = null;
_readerableApplied.value = false;
}
Future<void> 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<WebView> {
_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<WebView> {
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<WebView> {
);
}
widget.resetReaderable();
readabilityNotifier.reset();
},
onLoadStop: (controller, url) async {
if (url != null) {
@@ -410,10 +390,13 @@ class _WebViewState extends ConsumerState<WebView> {
_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<WebView> {
});
});
});
final readabilityNotifier = ref.read(
readerabilityControllerProvider(controller).notifier,
);
widget.updateIsReaderable(await readabilityNotifier.isReaderable());
},
onUpdateVisitedHistory: (controller, url, isReload) async {
if (isReload != true) {