improved tab & topic handling
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user