improved tab & topic handling

This commit is contained in:
Fabian Freund
2024-08-28 16:00:39 +02:00
parent 95dd254484
commit 0a7e84236f
43 changed files with 2035 additions and 847 deletions
@@ -1,19 +1,19 @@
import 'package:copy_with_extension/copy_with_extension.dart';
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()
class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
//@CopyWithField(immutable: true)
final Key key;
@override
//@CopyWithField(immutable: true)
@CopyWithField(immutable: true)
final String id;
final InAppWebViewController? controller;
@@ -30,7 +30,6 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
final PageHistory pageHistory;
WebViewPage({
required this.key,
required this.id,
required this.controller,
required super.url,
@@ -52,15 +51,13 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
super.favicon,
this.screenshot,
this.pageHistory = (canGoBack: false, canGoForward: false),
}) : key = GlobalKey(),
id = id ?? uuid.v7();
}) : id = id ?? uuid.v7();
@override
bool get cacheHash => true;
@override
List<Object?> get hashParameters => [
key,
id,
controller,
super.url,
@@ -0,0 +1,137 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'web_view_page.dart';
// **************************************************************************
// CopyWithGenerator
// **************************************************************************
abstract class _$WebViewPageCWProxy {
WebViewPage controller(InAppWebViewController? controller);
WebViewPage url(Uri url);
WebViewPage sslError(SslError? sslError);
WebViewPage title(String? title);
WebViewPage topicId(String? topicId);
WebViewPage favicon(Favicon? favicon);
WebViewPage screenshot(Uint8List? screenshot);
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory);
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// WebViewPage(...).copyWith(id: 12, name: "My name")
/// ````
WebViewPage call({
InAppWebViewController? controller,
Uri? url,
SslError? sslError,
String? title,
String? topicId,
Favicon? favicon,
Uint8List? screenshot,
({bool canGoBack, bool canGoForward})? pageHistory,
});
}
/// Proxy class for `copyWith` functionality. This is a callable class and can be used as follows: `instanceOfWebViewPage.copyWith(...)`. Additionally contains functions for specific fields e.g. `instanceOfWebViewPage.copyWith.fieldName(...)`
class _$WebViewPageCWProxyImpl implements _$WebViewPageCWProxy {
const _$WebViewPageCWProxyImpl(this._value);
final WebViewPage _value;
@override
WebViewPage controller(InAppWebViewController? controller) =>
this(controller: controller);
@override
WebViewPage url(Uri url) => this(url: url);
@override
WebViewPage sslError(SslError? sslError) => this(sslError: sslError);
@override
WebViewPage title(String? title) => this(title: title);
@override
WebViewPage topicId(String? topicId) => this(topicId: topicId);
@override
WebViewPage favicon(Favicon? favicon) => this(favicon: favicon);
@override
WebViewPage screenshot(Uint8List? screenshot) => this(screenshot: screenshot);
@override
WebViewPage pageHistory(({bool canGoBack, bool canGoForward}) pageHistory) =>
this(pageHistory: pageHistory);
@override
/// This function **does support** nullification of nullable fields. All `null` values passed to `non-nullable` fields will be ignored. You can also use `WebViewPage(...).copyWith.fieldName(...)` to override fields one at a time with nullification support.
///
/// Usage
/// ```dart
/// WebViewPage(...).copyWith(id: 12, name: "My name")
/// ````
WebViewPage call({
Object? controller = const $CopyWithPlaceholder(),
Object? url = const $CopyWithPlaceholder(),
Object? sslError = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(),
Object? topicId = const $CopyWithPlaceholder(),
Object? favicon = const $CopyWithPlaceholder(),
Object? screenshot = const $CopyWithPlaceholder(),
Object? pageHistory = const $CopyWithPlaceholder(),
}) {
return WebViewPage(
id: _value.id,
controller: controller == const $CopyWithPlaceholder()
? _value.controller
// ignore: cast_nullable_to_non_nullable
: controller as InAppWebViewController?,
url: url == const $CopyWithPlaceholder() || url == null
? _value.url
// ignore: cast_nullable_to_non_nullable
: url as Uri,
sslError: sslError == const $CopyWithPlaceholder()
? _value.sslError
// ignore: cast_nullable_to_non_nullable
: sslError as SslError?,
title: title == const $CopyWithPlaceholder()
? _value.title
// ignore: cast_nullable_to_non_nullable
: title as String?,
topicId: topicId == const $CopyWithPlaceholder()
? _value.topicId
// ignore: cast_nullable_to_non_nullable
: topicId as String?,
favicon: favicon == const $CopyWithPlaceholder()
? _value.favicon
// ignore: cast_nullable_to_non_nullable
: favicon as Favicon?,
screenshot: screenshot == const $CopyWithPlaceholder()
? _value.screenshot
// ignore: cast_nullable_to_non_nullable
: screenshot as Uint8List?,
pageHistory:
pageHistory == const $CopyWithPlaceholder() || pageHistory == null
? _value.pageHistory
// ignore: cast_nullable_to_non_nullable
: pageHistory as ({bool canGoBack, bool canGoForward}),
);
}
}
extension $WebViewPageCopyWith on WebViewPage {
/// Returns a callable class that can be used as follows: `instanceOfWebViewPage.copyWith(...)` or like so:`instanceOfWebViewPage.copyWith.fieldName(...)`.
// ignore: library_private_types_in_public_api
_$WebViewPageCWProxy get copyWith => _$WebViewPageCWProxyImpl(this);
}
@@ -5,6 +5,8 @@ import 'package:flutter/services.dart' show rootBundle;
import 'package:lensai/features/content_block/domain/repositories/host.dart';
import 'package:lensai/features/settings/data/models/settings.dart';
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
import 'package:lensai/features/topics/domain/providers.dart';
import 'package:lensai/features/topics/domain/repositories/tab.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:universal_io/io.dart';
@@ -43,3 +45,12 @@ Future<String> readerabilityScript(ReaderabilityScriptRef ref) {
),
);
}
@Riverpod()
List<String> activeTabs(ActiveTabsRef ref) {
final topic = ref.watch(selectedTopicProvider);
return ref.watch(
topicTabRepositoryProvider(topic)
.select((value) => value.valueOrNull ?? []),
);
}
@@ -37,5 +37,19 @@ final readerabilityScriptProvider = FutureProvider<String>.internal(
);
typedef ReaderabilityScriptRef = FutureProviderRef<String>;
String _$activeTabsHash() => r'd1ef2ae32d8707f7cc0902dd031e8302a63f50d4';
/// See also [activeTabs].
@ProviderFor(activeTabs)
final activeTabsProvider = AutoDisposeProvider<List<String>>.internal(
activeTabs,
name: r'activeTabsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$activeTabsHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ActiveTabsRef = AutoDisposeProviderRef<List<String>>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -1,59 +1,179 @@
import 'dart:collection';
import 'package:copy_with_extension/copy_with_extension.dart';
import 'package:drift/drift.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/logger.dart';
import 'package:lensai/features/topics/data/providers.dart';
import 'package:lensai/features/topics/domain/providers.dart';
import 'package:lensai/features/topics/domain/repositories/tab.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:lensai/features/web_view/domain/entities/web_view_page.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'web_view.g.dart';
@Riverpod(keepAlive: true)
abstract class _TabStateCopyProxy {
Future<void> copyWith({
InAppWebViewController? controller,
Uri? url,
SslError? sslError,
String? title,
String? topicId,
Favicon? favicon,
Uint8List? screenshot,
({bool canGoBack, bool canGoForward})? pageHistory,
});
}
@Riverpod()
class WebViewTabController extends _$WebViewTabController {
late Map<String, WebView> _webViewTabs;
String? _requestedTab;
@override
WebView? build() {
_webViewTabs = ref.watch(webViewRepositoryProvider);
return (stateOrNull != null)
? _webViewTabs[stateOrNull!.tabId] ?? _webViewTabs.values.lastOrNull
: null;
String? build() {
final webViewTabs = ref.watch(
tabRepositoryProvider.select(
(value) => value.value ?? [],
),
);
if (_requestedTab != null) {
if (webViewTabs.any((tab) => tab.id == _requestedTab)) {
return _requestedTab;
} else if (webViewTabs.isNotEmpty) {
final topic = ref.read(selectedTopicProvider);
return webViewTabs.where((tab) => tab.topicId == topic).lastOrNull?.id;
}
}
return null;
}
void showTab(String? id) {
state = (id != null) ? _webViewTabs[id] : null;
_requestedTab = id;
ref.invalidateSelf();
}
}
@Riverpod(keepAlive: true)
class WebViewRepository extends _$WebViewRepository {
@Riverpod()
class TabState extends _$TabState implements _TabStateCopyProxy {
KeepAliveLink? _aliveLink;
@override
Map<String, WebView> build() {
return stateOrNull ?? {};
WebViewPage? build(String tabId) {
final tabAsync = ref.watch(tabDataProvider(tabId));
if (tabAsync.hasValue) {
final tab = tabAsync.valueOrNull;
final current = stateOrNull ??
WebViewPage.create(
id: tabId,
url: tab?.url ?? Uri.https('localhost'),
);
if (tab != null) {
_aliveLink ??= ref.keepAlive();
return current.copyWith(
topicId: tab.topicId,
url: tab.url,
title: tab.title,
screenshot: tab.screenshot,
);
} else {
_aliveLink?.close();
_aliveLink = null;
return current;
}
}
return null;
}
void _disposeReaderability(String id) {
final controller = state[id]?.currentController;
if (controller != null) {
ref
.read(
readerabilityControllerProvider(ConsistentController(controller))
.notifier,
)
.dispose();
Future<void> updateScreenshot() async {
final screenshot = await state?.controller
?.takeScreenshot(
screenshotConfiguration: ScreenshotConfiguration(
compressFormat: CompressFormat.JPEG,
quality: 20,
),
)
.timeout(
const Duration(milliseconds: 1500),
onTimeout: () {
logger.w('Screenshot timed out');
return null;
},
);
await copyWith(screenshot: screenshot);
}
@override
Future<void> copyWith({
Object? controller = const $CopyWithPlaceholder(),
Object? url = const $CopyWithPlaceholder(),
Object? sslError = const $CopyWithPlaceholder(),
Object? title = const $CopyWithPlaceholder(),
Object? topicId = const $CopyWithPlaceholder(),
Object? favicon = const $CopyWithPlaceholder(),
Object? screenshot = const $CopyWithPlaceholder(),
Object? pageHistory = const $CopyWithPlaceholder(),
}) async {
if (state != null) {
final current = state!;
state = WebViewPage(
id: current.id,
controller: controller == const $CopyWithPlaceholder()
? current.controller
: controller as InAppWebViewController?,
url: current.url,
sslError: sslError == const $CopyWithPlaceholder()
? current.sslError
: sslError as SslError?,
title: current.title,
topicId: current.topicId,
favicon: favicon == const $CopyWithPlaceholder()
? current.favicon
: favicon as Favicon?,
screenshot: current.screenshot,
pageHistory:
pageHistory == const $CopyWithPlaceholder() || pageHistory == null
? current.pageHistory
: pageHistory as ({bool canGoBack, bool canGoForward}),
);
await ref.read(tabRepositoryProvider.notifier).updateTab(
current.id,
url: url == const $CopyWithPlaceholder() || url == null
? const Value.absent()
: Value(url as Uri),
title: title == const $CopyWithPlaceholder()
? const Value.absent()
: Value(title as String?),
topicId: topicId == const $CopyWithPlaceholder()
? const Value.absent()
: Value(topicId as String?),
screenshot: screenshot == const $CopyWithPlaceholder()
? const Value.absent()
: Value(screenshot as Uint8List?),
);
}
}
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 = {};
}
}
@Riverpod()
InAppWebViewController? webViewController(
WebViewControllerRef ref,
String tabId,
) {
return ref
.watch(
tabStateProvider(tabId)
.select((value) => ConsistentController(value?.controller)),
)
.value;
}
@@ -6,13 +6,164 @@ part of 'web_view.dart';
// RiverpodGenerator
// **************************************************************************
String _$webViewControllerHash() => r'f35fb13d46fcb04cee5676089f0cb8d5ca21e89b';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
/// See also [webViewController].
@ProviderFor(webViewController)
const webViewControllerProvider = WebViewControllerFamily();
/// See also [webViewController].
class WebViewControllerFamily extends Family<InAppWebViewController?> {
/// See also [webViewController].
const WebViewControllerFamily();
/// See also [webViewController].
WebViewControllerProvider call(
String tabId,
) {
return WebViewControllerProvider(
tabId,
);
}
@override
WebViewControllerProvider getProviderOverride(
covariant WebViewControllerProvider provider,
) {
return call(
provider.tabId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'webViewControllerProvider';
}
/// See also [webViewController].
class WebViewControllerProvider
extends AutoDisposeProvider<InAppWebViewController?> {
/// See also [webViewController].
WebViewControllerProvider(
String tabId,
) : this._internal(
(ref) => webViewController(
ref as WebViewControllerRef,
tabId,
),
from: webViewControllerProvider,
name: r'webViewControllerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$webViewControllerHash,
dependencies: WebViewControllerFamily._dependencies,
allTransitiveDependencies:
WebViewControllerFamily._allTransitiveDependencies,
tabId: tabId,
);
WebViewControllerProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
Override overrideWith(
InAppWebViewController? Function(WebViewControllerRef provider) create,
) {
return ProviderOverride(
origin: this,
override: WebViewControllerProvider._internal(
(ref) => create(ref as WebViewControllerRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeProviderElement<InAppWebViewController?> createElement() {
return _WebViewControllerProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is WebViewControllerProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin WebViewControllerRef on AutoDisposeProviderRef<InAppWebViewController?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _WebViewControllerProviderElement
extends AutoDisposeProviderElement<InAppWebViewController?>
with WebViewControllerRef {
_WebViewControllerProviderElement(super.provider);
@override
String get tabId => (origin as WebViewControllerProvider).tabId;
}
String _$webViewTabControllerHash() =>
r'3f1d7460295c6399382881ee56c4ed7f1fd5fe03';
r'cf1b766aa84472040b56a7b4d9ce1fd1606ed5e1';
/// See also [WebViewTabController].
@ProviderFor(WebViewTabController)
final webViewTabControllerProvider =
NotifierProvider<WebViewTabController, WebView?>.internal(
AutoDisposeNotifierProvider<WebViewTabController, String?>.internal(
WebViewTabController.new,
name: r'webViewTabControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@@ -22,22 +173,146 @@ final webViewTabControllerProvider =
allTransitiveDependencies: null,
);
typedef _$WebViewTabController = Notifier<WebView?>;
String _$webViewRepositoryHash() => r'dae5e1527ab4ad27f6a882fb63d8e88cd812b20b';
typedef _$WebViewTabController = AutoDisposeNotifier<String?>;
String _$tabStateHash() => r'9ce6b0d99e5032f534c97482d6140dda4b14fb07';
/// See also [WebViewRepository].
@ProviderFor(WebViewRepository)
final webViewRepositoryProvider =
NotifierProvider<WebViewRepository, Map<String, WebView>>.internal(
WebViewRepository.new,
name: r'webViewRepositoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$webViewRepositoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
abstract class _$TabState extends BuildlessAutoDisposeNotifier<WebViewPage?> {
late final String tabId;
typedef _$WebViewRepository = Notifier<Map<String, WebView>>;
WebViewPage? build(
String tabId,
);
}
/// See also [TabState].
@ProviderFor(TabState)
const tabStateProvider = TabStateFamily();
/// See also [TabState].
class TabStateFamily extends Family<WebViewPage?> {
/// See also [TabState].
const TabStateFamily();
/// See also [TabState].
TabStateProvider call(
String tabId,
) {
return TabStateProvider(
tabId,
);
}
@override
TabStateProvider getProviderOverride(
covariant TabStateProvider provider,
) {
return call(
provider.tabId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'tabStateProvider';
}
/// See also [TabState].
class TabStateProvider
extends AutoDisposeNotifierProviderImpl<TabState, WebViewPage?> {
/// See also [TabState].
TabStateProvider(
String tabId,
) : this._internal(
() => TabState()..tabId = tabId,
from: tabStateProvider,
name: r'tabStateProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$tabStateHash,
dependencies: TabStateFamily._dependencies,
allTransitiveDependencies: TabStateFamily._allTransitiveDependencies,
tabId: tabId,
);
TabStateProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.tabId,
}) : super.internal();
final String tabId;
@override
WebViewPage? runNotifierBuild(
covariant TabState notifier,
) {
return notifier.build(
tabId,
);
}
@override
Override overrideWith(TabState Function() create) {
return ProviderOverride(
origin: this,
override: TabStateProvider._internal(
() => create()..tabId = tabId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
tabId: tabId,
),
);
}
@override
AutoDisposeNotifierProviderElement<TabState, WebViewPage?> createElement() {
return _TabStateProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is TabStateProvider && other.tabId == tabId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, tabId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin TabStateRef on AutoDisposeNotifierProviderRef<WebViewPage?> {
/// The parameter `tabId` of this provider.
String get tabId;
}
class _TabStateProviderElement
extends AutoDisposeNotifierProviderElement<TabState, WebViewPage?>
with TabStateRef {
_TabStateProviderElement(super.provider);
@override
String get tabId => (origin as TabStateProvider).tabId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member