improved tab & topic handling
This commit is contained in:
@@ -9,6 +9,7 @@ import 'package:lensai/features/content_block/domain/repositories/sync.dart';
|
|||||||
import 'package:lensai/features/search_browser/domain/services/session.dart';
|
import 'package:lensai/features/search_browser/domain/services/session.dart';
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||||
|
import 'package:lensai/features/topics/data/providers.dart';
|
||||||
import 'package:lensai/features/web_view/domain/providers.dart';
|
import 'package:lensai/features/web_view/domain/providers.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -79,6 +80,8 @@ class AppInitializationService extends _$AppInitializationService {
|
|||||||
|
|
||||||
if (settings.incognitoMode) {
|
if (settings.incognitoMode) {
|
||||||
await ref.read(sessionServiceProvider.notifier).clearAllData();
|
await ref.read(sessionServiceProvider.notifier).clearAllData();
|
||||||
|
//Delete all unsasigned tabs
|
||||||
|
await ref.read(tabDatabaseProvider).tabDao.deleteTopicTabs(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.kagiSession case final String session) {
|
if (settings.kagiSession case final String session) {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'app_initialization.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$appInitializationServiceHash() =>
|
String _$appInitializationServiceHash() =>
|
||||||
r'19ec598e2141dd306eee61c1f9edd7f515b56738';
|
r'dadcf205d32efa61eb2a0ff1c9b3d49658b76a56';
|
||||||
|
|
||||||
/// See also [AppInitializationService].
|
/// See also [AppInitializationService].
|
||||||
@ProviderFor(AppInitializationService)
|
@ProviderFor(AppInitializationService)
|
||||||
|
|||||||
@@ -2,12 +2,18 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
import 'package:lensai/features/bangs/data/models/bang_data.dart';
|
||||||
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
import 'package:lensai/features/bangs/domain/repositories/data.dart';
|
||||||
import 'package:lensai/features/search_browser/domain/entities/modes.dart';
|
import 'package:lensai/features/search_browser/domain/entities/modes.dart';
|
||||||
import 'package:lensai/features/search_browser/domain/entities/sheet.dart';
|
import 'package:lensai/features/search_browser/domain/entities/sheet.dart';
|
||||||
import 'package:lensai/features/search_browser/domain/services/create_tab.dart';
|
import 'package:lensai/features/search_browser/domain/services/create_tab.dart';
|
||||||
|
import 'package:lensai/features/topics/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/topics/domain/repositories/tab.dart';
|
||||||
|
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||||
|
import 'package:lensai/features/web_view/presentation/widgets/web_view.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:rxdart/rxdart.dart';
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
|
||||||
@@ -137,3 +143,65 @@ class ShowFindInPage extends _$ShowFindInPage {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class WebViewWidgetCache extends _$WebViewWidgetCache {
|
||||||
|
final _requestedWidgets = <String>{};
|
||||||
|
|
||||||
|
List<TabData> _availableTabs = [];
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<WebView> build() {
|
||||||
|
ref.listen(
|
||||||
|
tabRepositoryProvider,
|
||||||
|
(previous, next) {
|
||||||
|
if (next.hasValue && next.valueOrNull != null) {
|
||||||
|
_availableTabs = next.valueOrNull!;
|
||||||
|
|
||||||
|
_removeClosedWidgets();
|
||||||
|
_requestedWidgets.removeWhere(_ensureWidgetCached);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
webViewTabControllerProvider,
|
||||||
|
(_, tabId) {
|
||||||
|
if (tabId != null && !_ensureWidgetCached(tabId)) {
|
||||||
|
_requestedWidgets.add(tabId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return stateOrNull ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeClosedWidgets() {
|
||||||
|
state = [...state]..removeWhere(
|
||||||
|
(element) => !_availableTabs.any((tab) => tab.id == element.tabId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _ensureWidgetCached(String tabId) {
|
||||||
|
final index = state.indexWhere((tab) => tab.tabId == tabId);
|
||||||
|
if (index < 0) {
|
||||||
|
final tab = _availableTabs.firstWhereOrNull((tab) => tab.id == tabId);
|
||||||
|
if (tab != null) {
|
||||||
|
state = [
|
||||||
|
...state,
|
||||||
|
WebView(
|
||||||
|
key: GlobalKey(),
|
||||||
|
tabId: tabId,
|
||||||
|
initialUrlRequest: URLRequest(url: WebUri.uri(tab.url)),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -411,5 +411,22 @@ final showFindInPageProvider = NotifierProvider<ShowFindInPage, bool>.internal(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef _$ShowFindInPage = Notifier<bool>;
|
typedef _$ShowFindInPage = Notifier<bool>;
|
||||||
|
String _$webViewWidgetCacheHash() =>
|
||||||
|
r'7d6f769e8c314d9dc579d700d36d6d4c944e600f';
|
||||||
|
|
||||||
|
/// See also [WebViewWidgetCache].
|
||||||
|
@ProviderFor(WebViewWidgetCache)
|
||||||
|
final webViewWidgetCacheProvider =
|
||||||
|
AutoDisposeNotifierProvider<WebViewWidgetCache, List<WebView>>.internal(
|
||||||
|
WebViewWidgetCache.new,
|
||||||
|
name: r'webViewWidgetCacheProvider',
|
||||||
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$webViewWidgetCacheHash,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
typedef _$WebViewWidgetCache = AutoDisposeNotifier<List<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
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||||
|
import 'package:lensai/features/web_view/domain/entities/web_view_page.dart';
|
||||||
|
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||||
|
import 'package:lensai/features/web_view/presentation/widgets/favicon.dart';
|
||||||
|
|
||||||
|
class TabActionDialog extends HookConsumerWidget {
|
||||||
|
final WebViewPage initialTab;
|
||||||
|
|
||||||
|
final void Function()? onDismiss;
|
||||||
|
|
||||||
|
const TabActionDialog({
|
||||||
|
required this.initialTab,
|
||||||
|
this.onDismiss,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
|
||||||
|
final topics =
|
||||||
|
ref.watch(topicRepositoryProvider.select((value) => value.valueOrNull));
|
||||||
|
final selectedTopic =
|
||||||
|
topics?.firstWhereOrNull((topic) => topic.id == tabState.topicId);
|
||||||
|
|
||||||
|
final expansionController = useExpansionTileController();
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
ModalBarrier(
|
||||||
|
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
|
||||||
|
onDismiss: onDismiss,
|
||||||
|
),
|
||||||
|
SimpleDialog(
|
||||||
|
titlePadding: const EdgeInsets.fromLTRB(16.0, 12.0, 16.0, 0.0),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
insetPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 20.0,
|
||||||
|
vertical: 24.0,
|
||||||
|
),
|
||||||
|
title: ListTile(
|
||||||
|
leading: FaviconImage(
|
||||||
|
favicon: tabState.favicon,
|
||||||
|
url: tabState.url,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(tabState.title ?? 'Unknown Title'),
|
||||||
|
subtitle: Text(tabState.url.authority),
|
||||||
|
),
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
//We need this to stretch the dialog, then padding from dialog is applied
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: ExpansionTile(
|
||||||
|
controller: expansionController,
|
||||||
|
leading: (selectedTopic != null)
|
||||||
|
? CircleAvatar(backgroundColor: selectedTopic.color)
|
||||||
|
: null,
|
||||||
|
title: (selectedTopic != null)
|
||||||
|
? Text(selectedTopic.name ?? 'New Topic')
|
||||||
|
: const Text('Assign a Topic'),
|
||||||
|
children: topics
|
||||||
|
?.where((topic) => topic.id != tabState.topicId)
|
||||||
|
.map(
|
||||||
|
(topic) => ListTile(
|
||||||
|
leading: CircleAvatar(backgroundColor: topic.color),
|
||||||
|
title: Text(topic.name ?? 'New Topic'),
|
||||||
|
onTap: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabStateProvider(initialTab.id).notifier,
|
||||||
|
)
|
||||||
|
.copyWith(topicId: topic.id);
|
||||||
|
|
||||||
|
expansionController.collapse();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,7 +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/search_browser/presentation/widgets/tabs_action_button.dart';
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.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/topics/domain/repositories/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/readerability.dart';
|
import 'package:lensai/features/web_view/presentation/controllers/readerability.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';
|
||||||
@@ -64,7 +64,7 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final activeWebView = ref.watch(webViewTabControllerProvider);
|
final activeTabId = ref.watch(webViewTabControllerProvider);
|
||||||
|
|
||||||
final menuController = useMemoized(() => MenuController());
|
final menuController = useMemoized(() => MenuController());
|
||||||
|
|
||||||
@@ -102,21 +102,28 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
child: AppBar(
|
child: AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
titleSpacing: 8.0,
|
titleSpacing: 8.0,
|
||||||
title: (activeWebView != null)
|
title: (activeTabId != null)
|
||||||
? AppBarTitle(
|
? Consumer(
|
||||||
activeWebView: activeWebView,
|
builder: (context, ref, child) {
|
||||||
onTap: () {
|
final page = ref.watch(tabStateProvider(activeTabId));
|
||||||
final page = activeWebView.page.value;
|
|
||||||
|
|
||||||
ref.read(overlayDialogProvider.notifier).show(
|
return (page != null)
|
||||||
WebPageDialog(
|
? AppBarTitle(
|
||||||
page: page,
|
page: page,
|
||||||
webViewController: page.controller,
|
onTap: () {
|
||||||
onDismiss: ref
|
ref.read(overlayDialogProvider.notifier).show(
|
||||||
.read(overlayDialogProvider.notifier)
|
WebPageDialog(
|
||||||
.dismiss,
|
url: page.url,
|
||||||
),
|
precachedInfo: page,
|
||||||
);
|
webViewController: page.controller,
|
||||||
|
onDismiss: ref
|
||||||
|
.read(overlayDialogProvider.notifier)
|
||||||
|
.dismiss,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: HookBuilder(
|
: HookBuilder(
|
||||||
@@ -181,17 +188,13 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (activeWebView != null)
|
if (activeTabId != null)
|
||||||
HookConsumer(
|
HookConsumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
final controller =
|
|
||||||
useListenable(activeWebView.page).value.controller;
|
|
||||||
final readerabilityState = ref.watch(
|
final readerabilityState = ref.watch(
|
||||||
readerabilityControllerProvider(
|
readerabilityControllerProvider(activeTabId),
|
||||||
ConsistentController(controller),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final enableReadability = ref.watch(
|
final enableReadability = ref.watch(
|
||||||
@@ -231,24 +234,25 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
data: (_) => Visibility(
|
data: (_) => Visibility(
|
||||||
visible: isReaderable == true || readerableApplied,
|
visible: isReaderable == true || readerableApplied,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: readerabilityState.isLoading
|
onTap: readerabilityState.isLoading
|
||||||
? null
|
? null
|
||||||
: () async {
|
: () async {
|
||||||
final controller =
|
final controller = ref.read(
|
||||||
activeWebView.currentController;
|
webViewControllerProvider(activeTabId),
|
||||||
|
);
|
||||||
|
|
||||||
if (controller != null) {
|
if (controller != null) {
|
||||||
final readabilityNotifier = ref.read(
|
final readabilityNotifier = ref.read(
|
||||||
readerabilityControllerProvider(
|
readerabilityControllerProvider(
|
||||||
ConsistentController(controller),
|
activeTabId,
|
||||||
).notifier,
|
).notifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
await readabilityNotifier
|
await readabilityNotifier
|
||||||
.toggleReaderable();
|
.toggleReaderable();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: icon,
|
child: icon,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
error: (error, stackTrace) => SizedBox.shrink(),
|
error: (error, stackTrace) => SizedBox.shrink(),
|
||||||
@@ -271,7 +275,7 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (activeWebView != null && quickAction != null)
|
if (quickAction != null)
|
||||||
InkWell(
|
InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
var tab = CreateTab(
|
var tab = CreateTab(
|
||||||
@@ -408,86 +412,106 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
child: const Text('Search'),
|
child: const Text('Search'),
|
||||||
),
|
),
|
||||||
const Divider(),
|
const Divider(),
|
||||||
MenuItemButton(
|
if (activeTabId != null)
|
||||||
onPressed: () async {
|
MenuItemButton(
|
||||||
final url =
|
onPressed: () async {
|
||||||
await activeWebView?.currentController?.getUrl();
|
final controller =
|
||||||
if (url != null) {
|
ref.read(webViewControllerProvider(activeTabId));
|
||||||
// ignore: use_build_context_synchronously
|
|
||||||
await ui_helper.launchUrlFeedback(context, url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingIcon: const Icon(Icons.open_in_browser),
|
|
||||||
child: const Text('Launch External'),
|
|
||||||
),
|
|
||||||
MenuItemButton(
|
|
||||||
onPressed: () async {
|
|
||||||
final url =
|
|
||||||
await activeWebView?.currentController?.getUrl();
|
|
||||||
if (url != null) {
|
|
||||||
await Share.shareUri(url);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leadingIcon: const Icon(Icons.share),
|
|
||||||
child: const Text('Share'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
MenuItemButton(
|
|
||||||
onPressed: () async {
|
|
||||||
ref.read(showFindInPageProvider.notifier).update(true);
|
|
||||||
},
|
|
||||||
leadingIcon: const Icon(Icons.search),
|
|
||||||
child: const Text('Find in page'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
MenuItemButton(
|
|
||||||
onPressed: () async {
|
|
||||||
await activeWebView?.currentController?.reload();
|
|
||||||
},
|
|
||||||
leadingIcon: const Icon(Icons.refresh),
|
|
||||||
child: const Text('Reload'),
|
|
||||||
),
|
|
||||||
const Divider(),
|
|
||||||
HookBuilder(
|
|
||||||
builder: (context) {
|
|
||||||
final history = useListenableSelector(
|
|
||||||
activeWebView?.page,
|
|
||||||
() =>
|
|
||||||
activeWebView?.page.value.pageHistory ??
|
|
||||||
(canGoBack: false, canGoForward: false),
|
|
||||||
);
|
|
||||||
|
|
||||||
return Row(
|
final url = await controller?.getUrl();
|
||||||
children: [
|
if (url != null) {
|
||||||
Expanded(
|
// ignore: use_build_context_synchronously
|
||||||
child: IconButton(
|
await ui_helper.launchUrlFeedback(context, url);
|
||||||
onPressed: (history.canGoBack)
|
}
|
||||||
? () async {
|
},
|
||||||
await activeWebView?.currentController
|
leadingIcon: const Icon(Icons.open_in_browser),
|
||||||
?.goBack();
|
child: const Text('Launch External'),
|
||||||
menuController.close();
|
),
|
||||||
}
|
if (activeTabId != null)
|
||||||
: null,
|
MenuItemButton(
|
||||||
icon: const Icon(Icons.arrow_back),
|
onPressed: () async {
|
||||||
|
final controller =
|
||||||
|
ref.read(webViewControllerProvider(activeTabId));
|
||||||
|
|
||||||
|
final url = await controller?.getUrl();
|
||||||
|
if (url != null) {
|
||||||
|
await Share.shareUri(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.share),
|
||||||
|
child: const Text('Share'),
|
||||||
|
),
|
||||||
|
if (activeTabId != null) const Divider(),
|
||||||
|
if (activeTabId != null)
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
ref.read(showFindInPageProvider.notifier).update(true);
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.search),
|
||||||
|
child: const Text('Find in page'),
|
||||||
|
),
|
||||||
|
if (activeTabId != null) const Divider(),
|
||||||
|
if (activeTabId != null)
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final controller =
|
||||||
|
ref.read(webViewControllerProvider(activeTabId));
|
||||||
|
|
||||||
|
await controller?.reload();
|
||||||
|
},
|
||||||
|
leadingIcon: const Icon(Icons.refresh),
|
||||||
|
child: const Text('Reload'),
|
||||||
|
),
|
||||||
|
if (activeTabId != null) const Divider(),
|
||||||
|
if (activeTabId != null)
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final history = ref.watch(
|
||||||
|
tabStateProvider(activeTabId)
|
||||||
|
.select((value) => value?.pageHistory),
|
||||||
|
);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: (history?.canGoBack == true)
|
||||||
|
? () async {
|
||||||
|
final controller = ref.read(
|
||||||
|
webViewControllerProvider(
|
||||||
|
activeTabId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller?.goBack();
|
||||||
|
menuController.close();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 48, child: VerticalDivider()),
|
||||||
const SizedBox(height: 48, child: VerticalDivider()),
|
Expanded(
|
||||||
Expanded(
|
child: IconButton(
|
||||||
child: IconButton(
|
onPressed: (history?.canGoForward == true)
|
||||||
onPressed: (history.canGoForward)
|
? () async {
|
||||||
? () async {
|
final controller = ref.read(
|
||||||
await activeWebView?.currentController
|
webViewControllerProvider(
|
||||||
?.goForward();
|
activeTabId,
|
||||||
menuController.close();
|
),
|
||||||
}
|
);
|
||||||
: null,
|
|
||||||
icon: const Icon(Icons.arrow_forward),
|
await controller?.goForward();
|
||||||
|
menuController.close();
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.arrow_forward),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -506,10 +530,18 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
: null,
|
: null,
|
||||||
child: HookConsumer(
|
child: HookConsumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final webViews = ref.watch(webViewRepositoryProvider);
|
final webViewWidgets = ref.watch(webViewWidgetCacheProvider);
|
||||||
|
final activeWebViewStackIndex = (activeTabId != null)
|
||||||
|
? webViewWidgets
|
||||||
|
.indexWhere((view) => view.tabId == activeTabId)
|
||||||
|
: null;
|
||||||
|
|
||||||
return BackButtonListener(
|
return BackButtonListener(
|
||||||
onBackButtonPressed: () async {
|
onBackButtonPressed: () async {
|
||||||
|
final page = (activeTabId != null)
|
||||||
|
? ref.read(tabStateProvider(activeTabId))
|
||||||
|
: null;
|
||||||
|
|
||||||
//Don't do anything if a child route is active
|
//Don't do anything if a child route is active
|
||||||
if (GoRouterState.of(context).topRoute?.name != 'KagiRoute') {
|
if (GoRouterState.of(context).topRoute?.name != 'KagiRoute') {
|
||||||
return false;
|
return false;
|
||||||
@@ -525,10 +557,10 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activeWebView?.page.value.pageHistory.canGoBack == true) {
|
if (page?.pageHistory.canGoBack == true) {
|
||||||
lastBackButtonPress.value = null;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
await activeWebView?.page.value.controller?.goBack();
|
await page?.controller?.goBack();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -537,10 +569,10 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
const Duration(seconds: 2)) {
|
const Duration(seconds: 2)) {
|
||||||
lastBackButtonPress.value = null;
|
lastBackButtonPress.value = null;
|
||||||
|
|
||||||
if (activeWebView?.key != null && webViews.length > 1) {
|
if (page != null && webViewWidgets.length > 1) {
|
||||||
ref
|
await ref
|
||||||
.read(webViewRepositoryProvider.notifier)
|
.read(tabRepositoryProvider.notifier)
|
||||||
.closeTab(activeWebView!.tabId);
|
.deleteTab(page.id);
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
//Mark back as unhandled and navigator will pop
|
//Mark back as unhandled and navigator will pop
|
||||||
@@ -552,7 +584,7 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
..clearSnackBars()
|
..clearSnackBars()
|
||||||
..showSnackBar(
|
..showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: (webViews.length > 1)
|
content: (webViewWidgets.length > 1)
|
||||||
? const Text(
|
? const Text(
|
||||||
'Please click BACK again to close current tab',
|
'Please click BACK again to close current tab',
|
||||||
)
|
)
|
||||||
@@ -578,20 +610,16 @@ class KagiScreen extends HookConsumerWidget {
|
|||||||
transitionType: SharedAxisTransitionType.horizontal,
|
transitionType: SharedAxisTransitionType.horizontal,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
key: ValueKey(
|
// key: ValueKey(
|
||||||
(activeWebView != null)
|
// (activeTabId != null)
|
||||||
? webViews.keys
|
// ? activeWebViewStackIndex
|
||||||
.toList()
|
// : null,
|
||||||
.indexOf(activeWebView.tabId)
|
// ),
|
||||||
: null,
|
index: (activeWebViewStackIndex != null &&
|
||||||
),
|
activeWebViewStackIndex > -1)
|
||||||
index: (activeWebView != null)
|
? activeWebViewStackIndex + 1
|
||||||
? webViews.keys
|
|
||||||
.toList()
|
|
||||||
.indexOf(activeWebView.tabId) +
|
|
||||||
1
|
|
||||||
: 0,
|
: 0,
|
||||||
children: [const LandingContent(), ...webViews.values],
|
children: [const LandingContent(), ...webViewWidgets],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (displayedSheet != null)
|
if (displayedSheet != null)
|
||||||
|
|||||||
@@ -1,15 +1,21 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
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/search_browser/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/search_browser/presentation/dialogs/tab_action.dart';
|
||||||
|
import 'package:lensai/features/topics/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/topics/domain/repositories/tab.dart';
|
||||||
import 'package:lensai/features/topics/presentation/widgets/topic_chips.dart';
|
import 'package:lensai/features/topics/presentation/widgets/topic_chips.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';
|
||||||
|
|
||||||
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
||||||
|
static const _headerSize = 104.0;
|
||||||
|
|
||||||
final VoidCallback onClose;
|
final VoidCallback onClose;
|
||||||
|
|
||||||
_SliverHeaderDelagate({required this.onClose});
|
_SliverHeaderDelagate({required this.onClose});
|
||||||
@@ -43,10 +49,12 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
|||||||
label: const Text('New Tab'),
|
label: const Text('New Tab'),
|
||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
ref
|
final topic = ref.read(selectedTopicProvider);
|
||||||
.read(webViewRepositoryProvider.notifier)
|
await ref
|
||||||
|
.read(topicTabRepositoryProvider(topic).notifier)
|
||||||
.closeAllTabs();
|
.closeAllTabs();
|
||||||
|
|
||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
@@ -55,6 +63,7 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
TopicChips(),
|
TopicChips(),
|
||||||
|
const SizedBox(height: 8),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -64,10 +73,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double get minExtent => 96;
|
double get minExtent => _headerSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
double get maxExtent => 96;
|
double get maxExtent => _headerSize;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||||
@@ -105,90 +114,135 @@ class ViewTabsSheet extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
return CustomScrollView(
|
return FadingScroll(
|
||||||
|
fadingSize: 25,
|
||||||
|
shaderPadding:
|
||||||
|
const EdgeInsets.only(top: _SliverHeaderDelagate._headerSize),
|
||||||
controller: sheetScrollController,
|
controller: sheetScrollController,
|
||||||
slivers: [
|
builder: (context, controller) {
|
||||||
SliverPersistentHeader(
|
return CustomScrollView(
|
||||||
pinned: true,
|
controller: controller,
|
||||||
delegate: _SliverHeaderDelagate(onClose: onClose),
|
slivers: [
|
||||||
),
|
SliverPersistentHeader(
|
||||||
HookConsumer(
|
pinned: true,
|
||||||
builder: (context, ref, child) {
|
delegate: _SliverHeaderDelagate(onClose: onClose),
|
||||||
final tabs = ref.watch(
|
),
|
||||||
webViewRepositoryProvider.select((tabs) => tabs.values.toList()),
|
HookConsumer(
|
||||||
);
|
builder: (context, ref, child) {
|
||||||
final activeTab = ref.watch(
|
final topic = ref.watch(selectedTopicProvider);
|
||||||
webViewTabControllerProvider.select(
|
final availableTabs = ref.watch(
|
||||||
(webView) => webView?.tabId,
|
topicTabRepositoryProvider(topic)
|
||||||
),
|
.select((value) => value.valueOrNull ?? []),
|
||||||
);
|
);
|
||||||
|
final activeTab = ref.watch(webViewTabControllerProvider);
|
||||||
|
|
||||||
final itemHeight = useMemoized(
|
final itemHeight = useMemoized(
|
||||||
() => _calculateItemHeight(
|
() => _calculateItemHeight(
|
||||||
screenWidth: MediaQuery.of(context).size.width,
|
screenWidth: MediaQuery.of(context).size.width,
|
||||||
childAspectRatio: 0.75,
|
childAspectRatio: 0.75,
|
||||||
horizontalPadding: 4.0,
|
horizontalPadding: 4.0,
|
||||||
mainAxisSpacing: 8.0,
|
mainAxisSpacing: 8.0,
|
||||||
crossAxisSpacing: 8.0,
|
crossAxisSpacing: 8.0,
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 2,
|
||||||
),
|
),
|
||||||
[MediaQuery.of(context).size.width],
|
[MediaQuery.of(context).size.width],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(
|
useEffect(
|
||||||
() {
|
() {
|
||||||
final index =
|
final index = availableTabs
|
||||||
tabs.indexWhere((webView) => webView.tabId == activeTab);
|
.indexWhere((webView) => webView == activeTab);
|
||||||
|
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
final reversedIndex = tabs.length - 1 - index;
|
final reversedIndex = availableTabs.length - 1 - index;
|
||||||
final offset = (reversedIndex ~/ 2) * itemHeight;
|
final offset = (reversedIndex ~/ 2) * itemHeight;
|
||||||
|
|
||||||
if (offset != sheetScrollController.offset) {
|
if (offset != controller.offset) {
|
||||||
unawaited(
|
unawaited(
|
||||||
sheetScrollController.animateTo(
|
controller.animateTo(
|
||||||
offset,
|
offset,
|
||||||
duration: const Duration(milliseconds: 200),
|
duration: const Duration(milliseconds: 200),
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return SliverPadding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
sliver: SliverGrid.count(
|
||||||
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: 2,
|
||||||
|
children: availableTabs.reversed
|
||||||
|
.map(
|
||||||
|
(tabId) => Consumer(
|
||||||
|
key: ValueKey(tabId),
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tab = ref.watch(tabStateProvider(tabId));
|
||||||
|
|
||||||
|
return (tab != null)
|
||||||
|
? WebViewTab(
|
||||||
|
tab: tab,
|
||||||
|
isActive: tabId == activeTab,
|
||||||
|
onTap: () {
|
||||||
|
if (tabId != activeTab) {
|
||||||
|
//Close first to avoid rebuilds
|
||||||
|
onClose();
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
webViewTabControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.showTab(tab.id);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
overlayDialogProvider.notifier,
|
||||||
|
)
|
||||||
|
.show(
|
||||||
|
TabActionDialog(
|
||||||
|
initialTab: tab,
|
||||||
|
onDismiss: ref
|
||||||
|
.read(
|
||||||
|
overlayDialogProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.dismiss,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
onDelete: () async {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabRepositoryProvider.notifier,
|
||||||
|
)
|
||||||
|
.deleteTab(tab.id);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[],
|
),
|
||||||
);
|
],
|
||||||
|
);
|
||||||
return SliverPadding(
|
},
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
||||||
sliver: SliverGrid.count(
|
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: 2,
|
|
||||||
children: tabs.reversed
|
|
||||||
.map(
|
|
||||||
(webView) => HookBuilder(
|
|
||||||
key: ValueKey(webView.tabId),
|
|
||||||
builder: (context) {
|
|
||||||
final tab = useValueListenable(webView.page) as ITab;
|
|
||||||
|
|
||||||
return WebViewTab(
|
|
||||||
tab: tab,
|
|
||||||
isActive: webView.tabId == activeTab,
|
|
||||||
onClose: onClose,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
import 'package:lensai/features/web_view/domain/providers.dart';
|
||||||
|
|
||||||
class TabsActionButton extends HookConsumerWidget {
|
class TabsActionButton extends HookConsumerWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
@@ -17,7 +17,7 @@ class TabsActionButton extends HookConsumerWidget {
|
|||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
|
||||||
final tabCount =
|
final tabCount =
|
||||||
ref.watch(webViewRepositoryProvider.select((tabs) => tabs.length));
|
ref.watch(activeTabsProvider.select((tabs) => tabs.length));
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
|
|||||||
@@ -8,11 +8,42 @@ part 'tab.g.dart';
|
|||||||
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
||||||
TabDao(super.db);
|
TabDao(super.db);
|
||||||
|
|
||||||
Future<void> upsertTab(ITab tab) {
|
Stream<List<TabData>> watchTabs() {
|
||||||
|
return (db.tab.select()..orderBy([(u) => OrderingTerm.asc(u.id)])).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<TabData?> watchTab(String tabId) {
|
||||||
|
return (db.tab.select()..where((t) => t.id.equals(tabId)))
|
||||||
|
.watchSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<bool> watchTabExisiting(String tabId) {
|
||||||
|
final existsStatement =
|
||||||
|
existsQuery(db.tab.select()..where((t) => t.id.equals(tabId)));
|
||||||
|
|
||||||
|
return selectExpressions([existsStatement])
|
||||||
|
.map((row) => row.read(existsStatement)!)
|
||||||
|
.watchSingle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Stream<List<String>> watchTopicTabs(String? topicId) {
|
||||||
|
final query = (selectOnly(db.tab)
|
||||||
|
..addColumns([db.tab.id])
|
||||||
|
..where(
|
||||||
|
(topicId == null)
|
||||||
|
? db.tab.topicId.isNull()
|
||||||
|
: db.tab.topicId.equals(topicId),
|
||||||
|
)
|
||||||
|
..orderBy([OrderingTerm.asc(db.tab.id)]));
|
||||||
|
|
||||||
|
return query.map((row) => row.read(db.tab.id)!).watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> upsertTab(ITab tab, DateTime timestamp) {
|
||||||
return db.tab.insertOne(
|
return db.tab.insertOne(
|
||||||
TabCompanion.insert(
|
TabCompanion.insert(
|
||||||
id: tab.id,
|
id: tab.id,
|
||||||
timestamp: DateTime.now(),
|
timestamp: timestamp,
|
||||||
url: tab.url,
|
url: tab.url,
|
||||||
topicId: Value(tab.topicId),
|
topicId: Value(tab.topicId),
|
||||||
title: Value(tab.title),
|
title: Value(tab.title),
|
||||||
@@ -24,6 +55,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
|
|
||||||
Future<void> updateTab(
|
Future<void> updateTab(
|
||||||
String id, {
|
String id, {
|
||||||
|
required DateTime timestamp,
|
||||||
Value<Uri> url = const Value.absent(),
|
Value<Uri> url = const Value.absent(),
|
||||||
Value<String?> title = const Value.absent(),
|
Value<String?> title = const Value.absent(),
|
||||||
Value<String?> topicId = const Value.absent(),
|
Value<String?> topicId = const Value.absent(),
|
||||||
@@ -33,7 +65,7 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
|
|
||||||
return statement.write(
|
return statement.write(
|
||||||
TabCompanion(
|
TabCompanion(
|
||||||
timestamp: Value(DateTime.now()),
|
timestamp: Value(timestamp),
|
||||||
url: url,
|
url: url,
|
||||||
title: title,
|
title: title,
|
||||||
topicId: topicId,
|
topicId: topicId,
|
||||||
@@ -41,4 +73,18 @@ class TabDao extends DatabaseAccessor<TabDatabase> with _$TabDaoMixin {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTab(String id) {
|
||||||
|
return db.tab.deleteOne(TabCompanion.custom(id: Variable(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTopicTabs(String? topicId) {
|
||||||
|
return (db.tab.delete()
|
||||||
|
..where(
|
||||||
|
(t) => (topicId == null)
|
||||||
|
? t.topicId.isNull()
|
||||||
|
: t.topicId.equals(topicId),
|
||||||
|
))
|
||||||
|
.go();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:ui';
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:lensai/core/uuid.dart';
|
import 'package:lensai/core/uuid.dart';
|
||||||
import 'package:lensai/features/topics/data/database/database.dart';
|
import 'package:lensai/features/topics/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
|
|
||||||
part 'topic.g.dart';
|
part 'topic.g.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:lensai/features/topics/data/database/daos/tab.dart';
|
|||||||
import 'package:lensai/features/topics/data/database/daos/topic.dart';
|
import 'package:lensai/features/topics/data/database/daos/topic.dart';
|
||||||
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
|
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
|
||||||
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
|
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
|
||||||
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
|
import 'package:lensai/features/topics/data/database/drift/converters/color.dart';
|
||||||
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
|
import 'package:lensai/features/topics/data/database/drift/converters/uri.dart';
|
||||||
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
|
|
||||||
CREATE TABLE topic (
|
CREATE TABLE topic (
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`
|
color INTEGER NOT NULL MAPPED BY `const ColorConverter()`
|
||||||
);
|
) WITH TopicData;
|
||||||
|
|
||||||
CREATE TABLE tab (
|
CREATE TABLE tab (
|
||||||
id TEXT PRIMARY KEY NOT NULL,
|
id TEXT PRIMARY KEY NOT NULL,
|
||||||
@@ -16,12 +17,18 @@ CREATE TABLE tab (
|
|||||||
screenshot BLOB
|
screenshot BLOB
|
||||||
);
|
);
|
||||||
|
|
||||||
topics:
|
topicsWithCount WITH TopicDataWithCount:
|
||||||
SELECT topic.*
|
SELECT
|
||||||
|
topic.*,
|
||||||
|
tab_agg.tab_count
|
||||||
FROM topic
|
FROM topic
|
||||||
LEFT JOIN (
|
LEFT JOIN (
|
||||||
SELECT topic_id, MAX(timestamp) AS last_updated
|
SELECT
|
||||||
|
topic_id,
|
||||||
|
COUNT(*) AS tab_count,
|
||||||
|
MAX(timestamp) AS last_updated
|
||||||
FROM tab
|
FROM tab
|
||||||
GROUP BY topic_id
|
GROUP BY topic_id
|
||||||
) AS tab_max ON topic.id = tab_max.topic_id
|
) AS tab_agg ON topic.id = tab_agg.topic_id
|
||||||
ORDER BY tab_max.last_updated DESC NULLS FIRST;
|
ORDER BY tab_agg.last_updated DESC NULLS FIRST;
|
||||||
|
|
||||||
|
|||||||
@@ -56,81 +56,6 @@ class Topic extends Table with TableInfo<Topic, TopicData> {
|
|||||||
bool get dontWriteConstraints => true;
|
bool get dontWriteConstraints => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TopicData extends DataClass implements Insertable<TopicData> {
|
|
||||||
final String id;
|
|
||||||
final String? name;
|
|
||||||
final Color color;
|
|
||||||
const TopicData({required this.id, this.name, required this.color});
|
|
||||||
@override
|
|
||||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
|
||||||
final map = <String, Expression>{};
|
|
||||||
map['id'] = Variable<String>(id);
|
|
||||||
if (!nullToAbsent || name != null) {
|
|
||||||
map['name'] = Variable<String>(name);
|
|
||||||
}
|
|
||||||
{
|
|
||||||
map['color'] = Variable<int>(Topic.$convertercolor.toSql(color));
|
|
||||||
}
|
|
||||||
return map;
|
|
||||||
}
|
|
||||||
|
|
||||||
factory TopicData.fromJson(Map<String, dynamic> json,
|
|
||||||
{ValueSerializer? serializer}) {
|
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
||||||
return TopicData(
|
|
||||||
id: serializer.fromJson<String>(json['id']),
|
|
||||||
name: serializer.fromJson<String?>(json['name']),
|
|
||||||
color: serializer.fromJson<Color>(json['color']),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@override
|
|
||||||
Map<String, dynamic> toJson({ValueSerializer? serializer}) {
|
|
||||||
serializer ??= driftRuntimeOptions.defaultSerializer;
|
|
||||||
return <String, dynamic>{
|
|
||||||
'id': serializer.toJson<String>(id),
|
|
||||||
'name': serializer.toJson<String?>(name),
|
|
||||||
'color': serializer.toJson<Color>(color),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
TopicData copyWith(
|
|
||||||
{String? id,
|
|
||||||
Value<String?> name = const Value.absent(),
|
|
||||||
Color? color}) =>
|
|
||||||
TopicData(
|
|
||||||
id: id ?? this.id,
|
|
||||||
name: name.present ? name.value : this.name,
|
|
||||||
color: color ?? this.color,
|
|
||||||
);
|
|
||||||
TopicData copyWithCompanion(TopicCompanion data) {
|
|
||||||
return TopicData(
|
|
||||||
id: data.id.present ? data.id.value : this.id,
|
|
||||||
name: data.name.present ? data.name.value : this.name,
|
|
||||||
color: data.color.present ? data.color.value : this.color,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return (StringBuffer('TopicData(')
|
|
||||||
..write('id: $id, ')
|
|
||||||
..write('name: $name, ')
|
|
||||||
..write('color: $color')
|
|
||||||
..write(')'))
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get hashCode => Object.hash(id, name, color);
|
|
||||||
@override
|
|
||||||
bool operator ==(Object other) =>
|
|
||||||
identical(this, other) ||
|
|
||||||
(other is TopicData &&
|
|
||||||
other.id == this.id &&
|
|
||||||
other.name == this.name &&
|
|
||||||
other.color == this.color);
|
|
||||||
}
|
|
||||||
|
|
||||||
class TopicCompanion extends UpdateCompanion<TopicData> {
|
class TopicCompanion extends UpdateCompanion<TopicData> {
|
||||||
final Value<String> id;
|
final Value<String> id;
|
||||||
final Value<String?> name;
|
final Value<String?> name;
|
||||||
@@ -511,14 +436,19 @@ abstract class _$TabDatabase extends GeneratedDatabase {
|
|||||||
late final Tab tab = Tab(this);
|
late final Tab tab = Tab(this);
|
||||||
late final TopicDao topicDao = TopicDao(this as TabDatabase);
|
late final TopicDao topicDao = TopicDao(this as TabDatabase);
|
||||||
late final TabDao tabDao = TabDao(this as TabDatabase);
|
late final TabDao tabDao = TabDao(this as TabDatabase);
|
||||||
Selectable<TopicData> topics() {
|
Selectable<TopicDataWithCount> topicsWithCount() {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'SELECT topic.* FROM topic LEFT JOIN (SELECT topic_id, MAX(timestamp) AS last_updated FROM tab GROUP BY topic_id) AS tab_max ON topic.id = tab_max.topic_id ORDER BY tab_max.last_updated DESC NULLS FIRST',
|
'SELECT topic.*, tab_agg.tab_count FROM topic LEFT JOIN (SELECT topic_id, COUNT(*) AS tab_count, MAX(timestamp) AS last_updated FROM tab GROUP BY topic_id) AS tab_agg ON topic.id = tab_agg.topic_id ORDER BY tab_agg.last_updated DESC NULLS FIRST',
|
||||||
variables: [],
|
variables: [],
|
||||||
readsFrom: {
|
readsFrom: {
|
||||||
topic,
|
topic,
|
||||||
tab,
|
tab,
|
||||||
}).asyncMap(topic.mapFromRow);
|
}).map((QueryRow row) => TopicDataWithCount(
|
||||||
|
id: row.read<String>('id'),
|
||||||
|
name: row.readNullable<String>('name'),
|
||||||
|
color: Topic.$convertercolor.fromSql(row.read<int>('color')),
|
||||||
|
tabCount: row.readNullable<int>('tab_count'),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'package:drift/drift.dart';
|
|||||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||||
|
|
||||||
class UriConverter extends TypeConverter<Uri, String> {
|
class UriConverter extends TypeConverter<Uri, String> {
|
||||||
|
const UriConverter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Uri fromSql(String fromDb) {
|
Uri fromSql(String fromDb) {
|
||||||
return uri_parser.tryParseUrl(fromDb, eagerParsing: true)!;
|
return uri_parser.tryParseUrl(fromDb, eagerParsing: true)!;
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
|
||||||
|
class TopicData with FastEquatable {
|
||||||
|
final String id;
|
||||||
|
final String? name;
|
||||||
|
final Color color;
|
||||||
|
|
||||||
|
TopicData({required this.id, this.name, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get cacheHash => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopicDataWithCount extends TopicData {
|
||||||
|
final int? tabCount;
|
||||||
|
|
||||||
|
TopicDataWithCount({
|
||||||
|
required super.id,
|
||||||
|
super.name,
|
||||||
|
required super.color,
|
||||||
|
required this.tabCount,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get cacheHash => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get hashParameters => [
|
||||||
|
...super.hashParameters,
|
||||||
|
tabCount,
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -35,3 +35,15 @@ TabDatabase tabDatabase(TabDatabaseRef ref) {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<bool> isTabExisting(IsTabExistingRef ref, String tabId) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.watchTabExisiting(tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<TabData?> tabData(TabDataRef ref, String tabId) {
|
||||||
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
|
return db.tabDao.watchTab(tabId);
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,5 +20,280 @@ final tabDatabaseProvider = Provider<TabDatabase>.internal(
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef TabDatabaseRef = ProviderRef<TabDatabase>;
|
typedef TabDatabaseRef = ProviderRef<TabDatabase>;
|
||||||
|
String _$isTabExistingHash() => r'98fc7bf073ba8f73c20df6ba7076672ff497246f';
|
||||||
|
|
||||||
|
/// 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 [isTabExisting].
|
||||||
|
@ProviderFor(isTabExisting)
|
||||||
|
const isTabExistingProvider = IsTabExistingFamily();
|
||||||
|
|
||||||
|
/// See also [isTabExisting].
|
||||||
|
class IsTabExistingFamily extends Family<AsyncValue<bool>> {
|
||||||
|
/// See also [isTabExisting].
|
||||||
|
const IsTabExistingFamily();
|
||||||
|
|
||||||
|
/// See also [isTabExisting].
|
||||||
|
IsTabExistingProvider call(
|
||||||
|
String tabId,
|
||||||
|
) {
|
||||||
|
return IsTabExistingProvider(
|
||||||
|
tabId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
IsTabExistingProvider getProviderOverride(
|
||||||
|
covariant IsTabExistingProvider 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'isTabExistingProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [isTabExisting].
|
||||||
|
class IsTabExistingProvider extends AutoDisposeStreamProvider<bool> {
|
||||||
|
/// See also [isTabExisting].
|
||||||
|
IsTabExistingProvider(
|
||||||
|
String tabId,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => isTabExisting(
|
||||||
|
ref as IsTabExistingRef,
|
||||||
|
tabId,
|
||||||
|
),
|
||||||
|
from: isTabExistingProvider,
|
||||||
|
name: r'isTabExistingProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$isTabExistingHash,
|
||||||
|
dependencies: IsTabExistingFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
IsTabExistingFamily._allTransitiveDependencies,
|
||||||
|
tabId: tabId,
|
||||||
|
);
|
||||||
|
|
||||||
|
IsTabExistingProvider._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(
|
||||||
|
Stream<bool> Function(IsTabExistingRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: IsTabExistingProvider._internal(
|
||||||
|
(ref) => create(ref as IsTabExistingRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
tabId: tabId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamProviderElement<bool> createElement() {
|
||||||
|
return _IsTabExistingProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is IsTabExistingProvider && 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 IsTabExistingRef on AutoDisposeStreamProviderRef<bool> {
|
||||||
|
/// The parameter `tabId` of this provider.
|
||||||
|
String get tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IsTabExistingProviderElement
|
||||||
|
extends AutoDisposeStreamProviderElement<bool> with IsTabExistingRef {
|
||||||
|
_IsTabExistingProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tabId => (origin as IsTabExistingProvider).tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$tabDataHash() => r'8b799386305d0c31103bc48a5a467df41d57d065';
|
||||||
|
|
||||||
|
/// See also [tabData].
|
||||||
|
@ProviderFor(tabData)
|
||||||
|
const tabDataProvider = TabDataFamily();
|
||||||
|
|
||||||
|
/// See also [tabData].
|
||||||
|
class TabDataFamily extends Family<AsyncValue<TabData?>> {
|
||||||
|
/// See also [tabData].
|
||||||
|
const TabDataFamily();
|
||||||
|
|
||||||
|
/// See also [tabData].
|
||||||
|
TabDataProvider call(
|
||||||
|
String tabId,
|
||||||
|
) {
|
||||||
|
return TabDataProvider(
|
||||||
|
tabId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TabDataProvider getProviderOverride(
|
||||||
|
covariant TabDataProvider 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'tabDataProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [tabData].
|
||||||
|
class TabDataProvider extends AutoDisposeStreamProvider<TabData?> {
|
||||||
|
/// See also [tabData].
|
||||||
|
TabDataProvider(
|
||||||
|
String tabId,
|
||||||
|
) : this._internal(
|
||||||
|
(ref) => tabData(
|
||||||
|
ref as TabDataRef,
|
||||||
|
tabId,
|
||||||
|
),
|
||||||
|
from: tabDataProvider,
|
||||||
|
name: r'tabDataProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$tabDataHash,
|
||||||
|
dependencies: TabDataFamily._dependencies,
|
||||||
|
allTransitiveDependencies: TabDataFamily._allTransitiveDependencies,
|
||||||
|
tabId: tabId,
|
||||||
|
);
|
||||||
|
|
||||||
|
TabDataProvider._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(
|
||||||
|
Stream<TabData?> Function(TabDataRef provider) create,
|
||||||
|
) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: TabDataProvider._internal(
|
||||||
|
(ref) => create(ref as TabDataRef),
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
tabId: tabId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamProviderElement<TabData?> createElement() {
|
||||||
|
return _TabDataProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is TabDataProvider && 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 TabDataRef on AutoDisposeStreamProviderRef<TabData?> {
|
||||||
|
/// The parameter `tabId` of this provider.
|
||||||
|
String get tabId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabDataProviderElement extends AutoDisposeStreamProviderElement<TabData?>
|
||||||
|
with TabDataRef {
|
||||||
|
_TabDataProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get tabId => (origin as TabDataProvider).tabId;
|
||||||
|
}
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,19 +1,14 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:lensai/features/topics/data/database/database.dart';
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
|
import 'package:lensai/features/topics/data/providers.dart';
|
||||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||||
import 'package:lensai/features/topics/utils/color_palette.dart';
|
import 'package:lensai/features/topics/utils/color_palette.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
|
|
||||||
@Riverpod()
|
|
||||||
Stream<List<TopicData>> topicList(TopicListRef ref) {
|
|
||||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
|
||||||
return repository.watchTopics();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class SelectedTopic extends _$SelectedTopic {
|
class SelectedTopic extends _$SelectedTopic {
|
||||||
void setTopic(String id) {
|
void setTopic(String id) {
|
||||||
@@ -40,21 +35,22 @@ class SelectedTopic extends _$SelectedTopic {
|
|||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<TopicData?> selectedTopicData(SelectedTopicDataRef ref) {
|
Stream<TopicData?> selectedTopicData(SelectedTopicDataRef ref) {
|
||||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
final db = ref.watch(tabDatabaseProvider);
|
||||||
final selectedBangTrigger = ref.watch(selectedTopicProvider);
|
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||||
return repository.watchTopic(selectedBangTrigger);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod()
|
if (selectedTopic != null) {
|
||||||
Future<Set<Color>> distinctTopicColors(DistinctTopicColorsRef ref) {
|
return db.topicDao.getTopicData(selectedTopic).watchSingleOrNull();
|
||||||
final repository = ref.watch(topicRepositoryProvider.notifier);
|
}
|
||||||
return repository.getDistinctColors();
|
|
||||||
|
return Stream.value(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Future<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
|
Future<Color> unusedRandomTopicColor(UnusedRandomTopicColorRef ref) async {
|
||||||
|
final repository = ref.watch(topicRepositoryProvider.notifier);
|
||||||
|
|
||||||
final allColors = colorTypes.flattened.toList();
|
final allColors = colorTypes.flattened.toList();
|
||||||
final usedColors = await ref.read(distinctTopicColorsProvider.future);
|
final usedColors = await repository.getDistinctColors();
|
||||||
|
|
||||||
Color randomColor;
|
Color randomColor;
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -6,21 +6,7 @@ part of 'providers.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$topicListHash() => r'7220aef5653bb3c2b44b016ce4f083f20896b353';
|
String _$selectedTopicDataHash() => r'fa450784052498014f592c356bde4ff5ec675281';
|
||||||
|
|
||||||
/// See also [topicList].
|
|
||||||
@ProviderFor(topicList)
|
|
||||||
final topicListProvider = AutoDisposeStreamProvider<List<TopicData>>.internal(
|
|
||||||
topicList,
|
|
||||||
name: r'topicListProvider',
|
|
||||||
debugGetCreateSourceHash:
|
|
||||||
const bool.fromEnvironment('dart.vm.product') ? null : _$topicListHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef TopicListRef = AutoDisposeStreamProviderRef<List<TopicData>>;
|
|
||||||
String _$selectedTopicDataHash() => r'191ad088ab90ef2348a836593f92b48d095afdc8';
|
|
||||||
|
|
||||||
/// See also [selectedTopicData].
|
/// See also [selectedTopicData].
|
||||||
@ProviderFor(selectedTopicData)
|
@ProviderFor(selectedTopicData)
|
||||||
@@ -36,25 +22,8 @@ final selectedTopicDataProvider =
|
|||||||
);
|
);
|
||||||
|
|
||||||
typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef<TopicData?>;
|
typedef SelectedTopicDataRef = AutoDisposeStreamProviderRef<TopicData?>;
|
||||||
String _$distinctTopicColorsHash() =>
|
|
||||||
r'c671f349615313d3b17e93e9e46fae12889e5e1f';
|
|
||||||
|
|
||||||
/// See also [distinctTopicColors].
|
|
||||||
@ProviderFor(distinctTopicColors)
|
|
||||||
final distinctTopicColorsProvider =
|
|
||||||
AutoDisposeFutureProvider<Set<Color>>.internal(
|
|
||||||
distinctTopicColors,
|
|
||||||
name: r'distinctTopicColorsProvider',
|
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
|
||||||
? null
|
|
||||||
: _$distinctTopicColorsHash,
|
|
||||||
dependencies: null,
|
|
||||||
allTransitiveDependencies: null,
|
|
||||||
);
|
|
||||||
|
|
||||||
typedef DistinctTopicColorsRef = AutoDisposeFutureProviderRef<Set<Color>>;
|
|
||||||
String _$unusedRandomTopicColorHash() =>
|
String _$unusedRandomTopicColorHash() =>
|
||||||
r'8f6907bb50a5bf2ac0320e93531d9e61616337b9';
|
r'099730b0d987cc37bcae4ef5c999cbebfa67d7da';
|
||||||
|
|
||||||
/// See also [unusedRandomTopicColor].
|
/// See also [unusedRandomTopicColor].
|
||||||
@ProviderFor(unusedRandomTopicColor)
|
@ProviderFor(unusedRandomTopicColor)
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
import 'package:lensai/features/topics/data/database/database.dart';
|
import 'package:lensai/features/topics/data/database/database.dart';
|
||||||
import 'package:lensai/features/topics/data/providers.dart';
|
import 'package:lensai/features/topics/data/providers.dart';
|
||||||
|
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'tab.g.dart';
|
part 'tab.g.dart';
|
||||||
@@ -9,7 +11,49 @@ class TabRepository extends _$TabRepository {
|
|||||||
late TabDatabase _db;
|
late TabDatabase _db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
Stream<List<TabData>> build() {
|
||||||
_db = ref.watch(tabDatabaseProvider);
|
_db = ref.watch(tabDatabaseProvider);
|
||||||
|
return _db.tabDao.watchTabs();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updateTab(
|
||||||
|
String id, {
|
||||||
|
Value<Uri> url = const Value.absent(),
|
||||||
|
Value<String?> title = const Value.absent(),
|
||||||
|
Value<String?> topicId = const Value.absent(),
|
||||||
|
Value<Uint8List?> screenshot = const Value.absent(),
|
||||||
|
}) {
|
||||||
|
return _db.tabDao.updateTab(
|
||||||
|
id,
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
url: url,
|
||||||
|
title: title,
|
||||||
|
topicId: topicId,
|
||||||
|
screenshot: screenshot,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTab(String id) {
|
||||||
|
return _db.tabDao.deleteTab(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
class TopicTabRepository extends _$TopicTabRepository {
|
||||||
|
late TabDatabase _db;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<String>> build(String? topicId) {
|
||||||
|
_db = ref.watch(tabDatabaseProvider);
|
||||||
|
return _db.tabDao.watchTopicTabs(topicId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addTab(ITab tab) {
|
||||||
|
assert(tab.topicId == topicId);
|
||||||
|
return _db.tabDao.upsertTab(tab, DateTime.now());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> closeAllTabs() {
|
||||||
|
return _db.tabDao.deleteTopicTabs(topicId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ part of 'tab.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'3f564f553e9586df34e927b20fe4bc860e0814e9';
|
String _$tabRepositoryHash() => r'454e182a99910512be72279f68caa0af125ed165';
|
||||||
|
|
||||||
/// See also [TabRepository].
|
/// See also [TabRepository].
|
||||||
@ProviderFor(TabRepository)
|
@ProviderFor(TabRepository)
|
||||||
final tabRepositoryProvider = NotifierProvider<TabRepository, void>.internal(
|
final tabRepositoryProvider =
|
||||||
|
StreamNotifierProvider<TabRepository, List<TabData>>.internal(
|
||||||
TabRepository.new,
|
TabRepository.new,
|
||||||
name: r'tabRepositoryProvider',
|
name: r'tabRepositoryProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -20,6 +21,172 @@ final tabRepositoryProvider = NotifierProvider<TabRepository, void>.internal(
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$TabRepository = Notifier<void>;
|
typedef _$TabRepository = StreamNotifier<List<TabData>>;
|
||||||
|
String _$topicTabRepositoryHash() =>
|
||||||
|
r'52ceb013e0b31046fdd0a00d031392ba84b9ecda';
|
||||||
|
|
||||||
|
/// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class _$TopicTabRepository
|
||||||
|
extends BuildlessAutoDisposeStreamNotifier<List<String>> {
|
||||||
|
late final String? topicId;
|
||||||
|
|
||||||
|
Stream<List<String>> build(
|
||||||
|
String? topicId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
@ProviderFor(TopicTabRepository)
|
||||||
|
const topicTabRepositoryProvider = TopicTabRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
class TopicTabRepositoryFamily extends Family<AsyncValue<List<String>>> {
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
const TopicTabRepositoryFamily();
|
||||||
|
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
TopicTabRepositoryProvider call(
|
||||||
|
String? topicId,
|
||||||
|
) {
|
||||||
|
return TopicTabRepositoryProvider(
|
||||||
|
topicId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TopicTabRepositoryProvider getProviderOverride(
|
||||||
|
covariant TopicTabRepositoryProvider provider,
|
||||||
|
) {
|
||||||
|
return call(
|
||||||
|
provider.topicId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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'topicTabRepositoryProvider';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
class TopicTabRepositoryProvider extends AutoDisposeStreamNotifierProviderImpl<
|
||||||
|
TopicTabRepository, List<String>> {
|
||||||
|
/// See also [TopicTabRepository].
|
||||||
|
TopicTabRepositoryProvider(
|
||||||
|
String? topicId,
|
||||||
|
) : this._internal(
|
||||||
|
() => TopicTabRepository()..topicId = topicId,
|
||||||
|
from: topicTabRepositoryProvider,
|
||||||
|
name: r'topicTabRepositoryProvider',
|
||||||
|
debugGetCreateSourceHash:
|
||||||
|
const bool.fromEnvironment('dart.vm.product')
|
||||||
|
? null
|
||||||
|
: _$topicTabRepositoryHash,
|
||||||
|
dependencies: TopicTabRepositoryFamily._dependencies,
|
||||||
|
allTransitiveDependencies:
|
||||||
|
TopicTabRepositoryFamily._allTransitiveDependencies,
|
||||||
|
topicId: topicId,
|
||||||
|
);
|
||||||
|
|
||||||
|
TopicTabRepositoryProvider._internal(
|
||||||
|
super._createNotifier, {
|
||||||
|
required super.name,
|
||||||
|
required super.dependencies,
|
||||||
|
required super.allTransitiveDependencies,
|
||||||
|
required super.debugGetCreateSourceHash,
|
||||||
|
required super.from,
|
||||||
|
required this.topicId,
|
||||||
|
}) : super.internal();
|
||||||
|
|
||||||
|
final String? topicId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<String>> runNotifierBuild(
|
||||||
|
covariant TopicTabRepository notifier,
|
||||||
|
) {
|
||||||
|
return notifier.build(
|
||||||
|
topicId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Override overrideWith(TopicTabRepository Function() create) {
|
||||||
|
return ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
override: TopicTabRepositoryProvider._internal(
|
||||||
|
() => create()..topicId = topicId,
|
||||||
|
from: from,
|
||||||
|
name: null,
|
||||||
|
dependencies: null,
|
||||||
|
allTransitiveDependencies: null,
|
||||||
|
debugGetCreateSourceHash: null,
|
||||||
|
topicId: topicId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
AutoDisposeStreamNotifierProviderElement<TopicTabRepository, List<String>>
|
||||||
|
createElement() {
|
||||||
|
return _TopicTabRepositoryProviderElement(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is TopicTabRepositoryProvider && other.topicId == topicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, topicId.hashCode);
|
||||||
|
|
||||||
|
return _SystemHash.finish(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin TopicTabRepositoryRef
|
||||||
|
on AutoDisposeStreamNotifierProviderRef<List<String>> {
|
||||||
|
/// The parameter `topicId` of this provider.
|
||||||
|
String? get topicId;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopicTabRepositoryProviderElement
|
||||||
|
extends AutoDisposeStreamNotifierProviderElement<TopicTabRepository,
|
||||||
|
List<String>> with TopicTabRepositoryRef {
|
||||||
|
_TopicTabRepositoryProviderElement(super.provider);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get topicId => (origin as TopicTabRepositoryProvider).topicId;
|
||||||
|
}
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:lensai/features/topics/data/database/database.dart';
|
import 'package:lensai/features/topics/data/database/database.dart';
|
||||||
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
import 'package:lensai/features/topics/data/providers.dart';
|
import 'package:lensai/features/topics/data/providers.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -11,8 +12,9 @@ class TopicRepository extends _$TopicRepository {
|
|||||||
late TabDatabase _db;
|
late TabDatabase _db;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build() {
|
Stream<List<TopicDataWithCount>> build() {
|
||||||
_db = ref.watch(tabDatabaseProvider);
|
_db = ref.watch(tabDatabaseProvider);
|
||||||
|
return _db.topicsWithCount().watch();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addTopic({required String? name, required Color color}) {
|
Future<void> addTopic({required String? name, required Color color}) {
|
||||||
@@ -31,18 +33,6 @@ class TopicRepository extends _$TopicRepository {
|
|||||||
return _db.topicDao.deleteTopic(id);
|
return _db.topicDao.deleteTopic(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream<List<TopicData>> watchTopics() {
|
|
||||||
return _db.topics().watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<TopicData?> watchTopic(String? id) {
|
|
||||||
if (id != null) {
|
|
||||||
return _db.topicDao.getTopicData(id).watchSingleOrNull();
|
|
||||||
} else {
|
|
||||||
return Stream.value(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Set<Color>> getDistinctColors() {
|
Future<Set<Color>> getDistinctColors() {
|
||||||
return _db.topicDao
|
return _db.topicDao
|
||||||
.getDistinctColors()
|
.getDistinctColors()
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ part of 'topic.dart';
|
|||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$topicRepositoryHash() => r'f0e8d3170d3706a79decfbe4700f20fcdebfae2c';
|
String _$topicRepositoryHash() => r'3b75c5fb1a081ba6d90e2ccb781469d0aa87c415';
|
||||||
|
|
||||||
/// See also [TopicRepository].
|
/// See also [TopicRepository].
|
||||||
@ProviderFor(TopicRepository)
|
@ProviderFor(TopicRepository)
|
||||||
final topicRepositoryProvider =
|
final topicRepositoryProvider =
|
||||||
NotifierProvider<TopicRepository, void>.internal(
|
StreamNotifierProvider<TopicRepository, List<TopicDataWithCount>>.internal(
|
||||||
TopicRepository.new,
|
TopicRepository.new,
|
||||||
name: r'topicRepositoryProvider',
|
name: r'topicRepositoryProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -21,6 +21,6 @@ final topicRepositoryProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$TopicRepository = Notifier<void>;
|
typedef _$TopicRepository = StreamNotifier<List<TopicDataWithCount>>;
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
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/topics/data/database/database.dart';
|
import 'package:lensai/features/topics/data/models/topic_data.dart';
|
||||||
import 'package:lensai/features/topics/domain/providers.dart';
|
import 'package:lensai/features/topics/domain/providers.dart';
|
||||||
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||||
import 'package:lensai/features/topics/presentation/widgets/topic_dialog.dart';
|
import 'package:lensai/features/topics/presentation/widgets/topic_dialog.dart';
|
||||||
@@ -147,38 +148,44 @@ class TopicListScreen extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
body: HookConsumer(
|
body: HookConsumer(
|
||||||
builder: (context, ref, child) {
|
builder: (context, ref, child) {
|
||||||
final topicsAsync = ref.watch(topicListProvider);
|
final topicsAsync = ref.watch(topicRepositoryProvider);
|
||||||
final selectedTopic = ref.watch(selectedTopicProvider);
|
final selectedTopic = ref.watch(selectedTopicProvider);
|
||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
enabled: topicsAsync.isLoading,
|
enabled: topicsAsync.isLoading,
|
||||||
child: topicsAsync.when(
|
child: topicsAsync.when(
|
||||||
data: (topics) => ListView.builder(
|
data: (topics) => FadingScroll(
|
||||||
itemCount: topics.length,
|
fadingSize: 25,
|
||||||
itemBuilder: (context, index) {
|
builder: (context, controller) {
|
||||||
final topic = topics[index];
|
return ListView.builder(
|
||||||
return _TopicTile(
|
controller: controller,
|
||||||
topic,
|
itemCount: topics.length,
|
||||||
key: ValueKey(topic.id),
|
itemBuilder: (context, index) {
|
||||||
isSelected: topic.id == selectedTopic,
|
final topic = topics[index];
|
||||||
onEdit: (edited) async {
|
return _TopicTile(
|
||||||
await ref
|
topic,
|
||||||
.read(topicRepositoryProvider.notifier)
|
key: ValueKey(topic.id),
|
||||||
.replaceTopic(
|
isSelected: topic.id == selectedTopic,
|
||||||
id: topic.id,
|
onEdit: (edited) async {
|
||||||
name: edited.name,
|
await ref
|
||||||
color: edited.color,
|
.read(topicRepositoryProvider.notifier)
|
||||||
);
|
.replaceTopic(
|
||||||
},
|
id: topic.id,
|
||||||
onDelete: () async {
|
name: edited.name,
|
||||||
await ref
|
color: edited.color,
|
||||||
.read(topicRepositoryProvider.notifier)
|
);
|
||||||
.deleteTopic(topic.id);
|
},
|
||||||
},
|
onDelete: () async {
|
||||||
onTap: () {
|
await ref
|
||||||
ref
|
.read(topicRepositoryProvider.notifier)
|
||||||
.read(selectedTopicProvider.notifier)
|
.deleteTopic(topic.id);
|
||||||
.toggleTopic(topic.id);
|
},
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(selectedTopicProvider.notifier)
|
||||||
|
.toggleTopic(topic.id);
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -187,7 +194,7 @@ class TopicListScreen extends HookConsumerWidget {
|
|||||||
loading: () => ListView.builder(
|
loading: () => ListView.builder(
|
||||||
itemCount: 3,
|
itemCount: 3,
|
||||||
itemBuilder: (context, index) => _TopicTile(
|
itemBuilder: (context, index) => _TopicTile(
|
||||||
const TopicData(id: 'null', color: Colors.transparent),
|
TopicData(id: 'null', color: Colors.transparent),
|
||||||
isSelected: false,
|
isSelected: false,
|
||||||
onEdit: (_) {},
|
onEdit: (_) {},
|
||||||
onDelete: () {},
|
onDelete: () {},
|
||||||
|
|||||||
@@ -3,12 +3,13 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:lensai/core/routing/routes.dart';
|
import 'package:lensai/core/routing/routes.dart';
|
||||||
import 'package:lensai/features/topics/domain/providers.dart';
|
import 'package:lensai/features/topics/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/topics/domain/repositories/topic.dart';
|
||||||
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
import 'package:lensai/presentation/widgets/selectable_chips.dart';
|
||||||
|
|
||||||
class TopicChips extends HookConsumerWidget {
|
class TopicChips extends HookConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final topicsAsync = ref.watch(topicListProvider);
|
final topicsAsync = ref.watch(topicRepositoryProvider);
|
||||||
final selectedTopic = ref
|
final selectedTopic = ref
|
||||||
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
.watch(selectedTopicDataProvider.select((value) => value.valueOrNull));
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ class TopicChips extends HookConsumerWidget {
|
|||||||
height: 48,
|
height: 48,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
if (selectedTopic != null || availableTopics.isNotEmpty)
|
if (selectedTopic != null || availableTopics.isNotEmpty)
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SelectableChips(
|
child: SelectableChips(
|
||||||
@@ -31,6 +33,7 @@ class TopicChips extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
itemLabel: (topic) => Text(topic.name ?? 'New Topic'),
|
itemLabel: (topic) => Text(topic.name ?? 'New Topic'),
|
||||||
|
itemBadgeCount: (topic) => topic.tabCount,
|
||||||
availableItems: availableTopics,
|
availableItems: availableTopics,
|
||||||
selectedItem: selectedTopic,
|
selectedItem: selectedTopic,
|
||||||
onSelected: (topic) {
|
onSelected: (topic) {
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import 'package:copy_with_extension/copy_with_extension.dart';
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
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_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:lensai/core/uuid.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';
|
import 'package:lensai/features/web_view/domain/entities/abstract/tab.dart';
|
||||||
|
|
||||||
|
part 'web_view_page.g.dart';
|
||||||
|
|
||||||
typedef PageHistory = ({bool canGoBack, bool canGoForward});
|
typedef PageHistory = ({bool canGoBack, bool canGoForward});
|
||||||
|
|
||||||
|
@CopyWith()
|
||||||
class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
||||||
//@CopyWithField(immutable: true)
|
|
||||||
final Key key;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
//@CopyWithField(immutable: true)
|
@CopyWithField(immutable: true)
|
||||||
final String id;
|
final String id;
|
||||||
|
|
||||||
final InAppWebViewController? controller;
|
final InAppWebViewController? controller;
|
||||||
@@ -30,7 +30,6 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
|||||||
final PageHistory pageHistory;
|
final PageHistory pageHistory;
|
||||||
|
|
||||||
WebViewPage({
|
WebViewPage({
|
||||||
required this.key,
|
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.controller,
|
required this.controller,
|
||||||
required super.url,
|
required super.url,
|
||||||
@@ -52,15 +51,13 @@ class WebViewPage extends WebPageInfo with FastEquatable implements ITab {
|
|||||||
super.favicon,
|
super.favicon,
|
||||||
this.screenshot,
|
this.screenshot,
|
||||||
this.pageHistory = (canGoBack: false, canGoForward: false),
|
this.pageHistory = (canGoBack: false, canGoForward: false),
|
||||||
}) : key = GlobalKey(),
|
}) : id = id ?? uuid.v7();
|
||||||
id = id ?? uuid.v7();
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get cacheHash => true;
|
bool get cacheHash => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
key,
|
|
||||||
id,
|
id,
|
||||||
controller,
|
controller,
|
||||||
super.url,
|
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/content_block/domain/repositories/host.dart';
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.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:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:universal_io/io.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>;
|
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: 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
|
||||||
|
|||||||
@@ -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/domain/entities/consistent_controller.dart';
|
||||||
import 'package:lensai/features/web_view/presentation/controllers/readerability.dart';
|
import 'package:lensai/features/web_view/domain/entities/web_view_page.dart';
|
||||||
import 'package:lensai/features/web_view/presentation/widgets/web_view.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'web_view.g.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 {
|
class WebViewTabController extends _$WebViewTabController {
|
||||||
late Map<String, WebView> _webViewTabs;
|
String? _requestedTab;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
WebView? build() {
|
String? build() {
|
||||||
_webViewTabs = ref.watch(webViewRepositoryProvider);
|
final webViewTabs = ref.watch(
|
||||||
return (stateOrNull != null)
|
tabRepositoryProvider.select(
|
||||||
? _webViewTabs[stateOrNull!.tabId] ?? _webViewTabs.values.lastOrNull
|
(value) => value.value ?? [],
|
||||||
: null;
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
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) {
|
void showTab(String? id) {
|
||||||
state = (id != null) ? _webViewTabs[id] : null;
|
_requestedTab = id;
|
||||||
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod()
|
||||||
class WebViewRepository extends _$WebViewRepository {
|
class TabState extends _$TabState implements _TabStateCopyProxy {
|
||||||
|
KeepAliveLink? _aliveLink;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, WebView> build() {
|
WebViewPage? build(String tabId) {
|
||||||
return stateOrNull ?? {};
|
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) {
|
Future<void> updateScreenshot() async {
|
||||||
final controller = state[id]?.currentController;
|
final screenshot = await state?.controller
|
||||||
if (controller != null) {
|
?.takeScreenshot(
|
||||||
ref
|
screenshotConfiguration: ScreenshotConfiguration(
|
||||||
.read(
|
compressFormat: CompressFormat.JPEG,
|
||||||
readerabilityControllerProvider(ConsistentController(controller))
|
quality: 20,
|
||||||
.notifier,
|
),
|
||||||
)
|
)
|
||||||
.dispose();
|
.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};
|
@Riverpod()
|
||||||
}
|
InAppWebViewController? webViewController(
|
||||||
|
WebViewControllerRef ref,
|
||||||
void closeTab(String id) {
|
String tabId,
|
||||||
_disposeReaderability(id);
|
) {
|
||||||
state = Map.of(state)..remove(id);
|
return ref
|
||||||
}
|
.watch(
|
||||||
|
tabStateProvider(tabId)
|
||||||
void closeAllTabs() {
|
.select((value) => ConsistentController(value?.controller)),
|
||||||
state.keys.forEach(_disposeReaderability);
|
)
|
||||||
state = {};
|
.value;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,164 @@ part of 'web_view.dart';
|
|||||||
// RiverpodGenerator
|
// 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() =>
|
String _$webViewTabControllerHash() =>
|
||||||
r'3f1d7460295c6399382881ee56c4ed7f1fd5fe03';
|
r'cf1b766aa84472040b56a7b4d9ce1fd1606ed5e1';
|
||||||
|
|
||||||
/// See also [WebViewTabController].
|
/// See also [WebViewTabController].
|
||||||
@ProviderFor(WebViewTabController)
|
@ProviderFor(WebViewTabController)
|
||||||
final webViewTabControllerProvider =
|
final webViewTabControllerProvider =
|
||||||
NotifierProvider<WebViewTabController, WebView?>.internal(
|
AutoDisposeNotifierProvider<WebViewTabController, String?>.internal(
|
||||||
WebViewTabController.new,
|
WebViewTabController.new,
|
||||||
name: r'webViewTabControllerProvider',
|
name: r'webViewTabControllerProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -22,22 +173,146 @@ final webViewTabControllerProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$WebViewTabController = Notifier<WebView?>;
|
typedef _$WebViewTabController = AutoDisposeNotifier<String?>;
|
||||||
String _$webViewRepositoryHash() => r'dae5e1527ab4ad27f6a882fb63d8e88cd812b20b';
|
String _$tabStateHash() => r'9ce6b0d99e5032f534c97482d6140dda4b14fb07';
|
||||||
|
|
||||||
/// See also [WebViewRepository].
|
abstract class _$TabState extends BuildlessAutoDisposeNotifier<WebViewPage?> {
|
||||||
@ProviderFor(WebViewRepository)
|
late final String tabId;
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
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: 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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:lensai/features/topics/data/providers.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/services/readerability_script.dart';
|
import 'package:lensai/features/web_view/presentation/services/readerability_script.dart';
|
||||||
import 'package:riverpod/riverpod.dart';
|
import 'package:riverpod/riverpod.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -8,19 +8,25 @@ part 'readerability.g.dart';
|
|||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
class ReaderabilityController extends _$ReaderabilityController {
|
class ReaderabilityController extends _$ReaderabilityController {
|
||||||
late InAppWebViewController? _controller;
|
|
||||||
late ReaderabilityScriptService _service;
|
late ReaderabilityScriptService _service;
|
||||||
late KeepAliveLink _aliveLink;
|
late KeepAliveLink _aliveLink;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncValue<({bool readerable, bool applied})> build(
|
AsyncValue<({bool readerable, bool applied})> build(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
) {
|
) {
|
||||||
_controller = controller.value;
|
_service = ref.watch(readerabilityScriptServiceProvider(tabId).notifier);
|
||||||
_service =
|
|
||||||
ref.watch(readerabilityScriptServiceProvider(controller).notifier);
|
|
||||||
_aliveLink = ref.keepAlive();
|
_aliveLink = ref.keepAlive();
|
||||||
|
|
||||||
|
ref.listen(
|
||||||
|
isTabExistingProvider(tabId),
|
||||||
|
(previous, next) {
|
||||||
|
if (next.valueOrNull == false) {
|
||||||
|
_aliveLink.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
return const AsyncLoading();
|
return const AsyncLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,7 +49,7 @@ class ReaderabilityController extends _$ReaderabilityController {
|
|||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
state = await AsyncValue.guard(() async {
|
state = await AsyncValue.guard(() async {
|
||||||
if (applied) {
|
if (applied) {
|
||||||
await _controller?.reload();
|
await ref.read(webViewControllerProvider(tabId))?.reload();
|
||||||
} else {
|
} else {
|
||||||
await _service.applyReaderable();
|
await _service.applyReaderable();
|
||||||
}
|
}
|
||||||
@@ -55,8 +61,4 @@ class ReaderabilityController extends _$ReaderabilityController {
|
|||||||
void reset() {
|
void reset() {
|
||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
|
||||||
_aliveLink.close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'readerability.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$readerabilityControllerHash() =>
|
String _$readerabilityControllerHash() =>
|
||||||
r'1237e57abce82f71dbc66b6ab46f50dad9e3ddf9';
|
r'a1c8da3a9cf48300089e390a4fcae89862a6ec1a';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -32,10 +32,10 @@ class _SystemHash {
|
|||||||
|
|
||||||
abstract class _$ReaderabilityController extends BuildlessAutoDisposeNotifier<
|
abstract class _$ReaderabilityController extends BuildlessAutoDisposeNotifier<
|
||||||
AsyncValue<({bool readerable, bool applied})>> {
|
AsyncValue<({bool readerable, bool applied})>> {
|
||||||
late final ConsistentController controller;
|
late final String tabId;
|
||||||
|
|
||||||
AsyncValue<({bool readerable, bool applied})> build(
|
AsyncValue<({bool readerable, bool applied})> build(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,10 +51,10 @@ class ReaderabilityControllerFamily
|
|||||||
|
|
||||||
/// See also [ReaderabilityController].
|
/// See also [ReaderabilityController].
|
||||||
ReaderabilityControllerProvider call(
|
ReaderabilityControllerProvider call(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
) {
|
) {
|
||||||
return ReaderabilityControllerProvider(
|
return ReaderabilityControllerProvider(
|
||||||
controller,
|
tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ class ReaderabilityControllerFamily
|
|||||||
covariant ReaderabilityControllerProvider provider,
|
covariant ReaderabilityControllerProvider provider,
|
||||||
) {
|
) {
|
||||||
return call(
|
return call(
|
||||||
provider.controller,
|
provider.tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +87,9 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
ReaderabilityController, AsyncValue<({bool readerable, bool applied})>> {
|
ReaderabilityController, AsyncValue<({bool readerable, bool applied})>> {
|
||||||
/// See also [ReaderabilityController].
|
/// See also [ReaderabilityController].
|
||||||
ReaderabilityControllerProvider(
|
ReaderabilityControllerProvider(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
) : this._internal(
|
) : this._internal(
|
||||||
() => ReaderabilityController()..controller = controller,
|
() => ReaderabilityController()..tabId = tabId,
|
||||||
from: readerabilityControllerProvider,
|
from: readerabilityControllerProvider,
|
||||||
name: r'readerabilityControllerProvider',
|
name: r'readerabilityControllerProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -99,7 +99,7 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
dependencies: ReaderabilityControllerFamily._dependencies,
|
dependencies: ReaderabilityControllerFamily._dependencies,
|
||||||
allTransitiveDependencies:
|
allTransitiveDependencies:
|
||||||
ReaderabilityControllerFamily._allTransitiveDependencies,
|
ReaderabilityControllerFamily._allTransitiveDependencies,
|
||||||
controller: controller,
|
tabId: tabId,
|
||||||
);
|
);
|
||||||
|
|
||||||
ReaderabilityControllerProvider._internal(
|
ReaderabilityControllerProvider._internal(
|
||||||
@@ -109,17 +109,17 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
required super.allTransitiveDependencies,
|
required super.allTransitiveDependencies,
|
||||||
required super.debugGetCreateSourceHash,
|
required super.debugGetCreateSourceHash,
|
||||||
required super.from,
|
required super.from,
|
||||||
required this.controller,
|
required this.tabId,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final ConsistentController controller;
|
final String tabId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
AsyncValue<({bool readerable, bool applied})> runNotifierBuild(
|
AsyncValue<({bool readerable, bool applied})> runNotifierBuild(
|
||||||
covariant ReaderabilityController notifier,
|
covariant ReaderabilityController notifier,
|
||||||
) {
|
) {
|
||||||
return notifier.build(
|
return notifier.build(
|
||||||
controller,
|
tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,13 +128,13 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
return ProviderOverride(
|
return ProviderOverride(
|
||||||
origin: this,
|
origin: this,
|
||||||
override: ReaderabilityControllerProvider._internal(
|
override: ReaderabilityControllerProvider._internal(
|
||||||
() => create()..controller = controller,
|
() => create()..tabId = tabId,
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
debugGetCreateSourceHash: null,
|
debugGetCreateSourceHash: null,
|
||||||
controller: controller,
|
tabId: tabId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,14 +147,13 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return other is ReaderabilityControllerProvider &&
|
return other is ReaderabilityControllerProvider && other.tabId == tabId;
|
||||||
other.controller == controller;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
hash = _SystemHash.combine(hash, controller.hashCode);
|
hash = _SystemHash.combine(hash, tabId.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
@@ -162,8 +161,8 @@ class ReaderabilityControllerProvider extends AutoDisposeNotifierProviderImpl<
|
|||||||
|
|
||||||
mixin ReaderabilityControllerRef on AutoDisposeNotifierProviderRef<
|
mixin ReaderabilityControllerRef on AutoDisposeNotifierProviderRef<
|
||||||
AsyncValue<({bool readerable, bool applied})>> {
|
AsyncValue<({bool readerable, bool applied})>> {
|
||||||
/// The parameter `controller` of this provider.
|
/// The parameter `tabId` of this provider.
|
||||||
ConsistentController get controller;
|
String get tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReaderabilityControllerProviderElement
|
class _ReaderabilityControllerProviderElement
|
||||||
@@ -173,8 +172,7 @@ class _ReaderabilityControllerProviderElement
|
|||||||
_ReaderabilityControllerProviderElement(super.provider);
|
_ReaderabilityControllerProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsistentController get controller =>
|
String get tabId => (origin as ReaderabilityControllerProvider).tabId;
|
||||||
(origin as ReaderabilityControllerProvider).controller;
|
|
||||||
}
|
}
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.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/web_view_page.dart';
|
import 'package:lensai/features/web_view/domain/entities/web_view_page.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/web_view.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'switch_new_tab.g.dart';
|
part 'switch_new_tab.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@Riverpod(keepAlive: true)
|
||||||
class SwitchNewTabController extends _$SwitchNewTabController {
|
class SwitchNewTabController extends _$SwitchNewTabController {
|
||||||
@override
|
@override
|
||||||
FutureOr<void> build() {}
|
FutureOr<void> build() {}
|
||||||
@@ -15,13 +16,15 @@ class SwitchNewTabController extends _$SwitchNewTabController {
|
|||||||
state = const AsyncLoading();
|
state = const AsyncLoading();
|
||||||
state = await AsyncValue.guard(
|
state = await AsyncValue.guard(
|
||||||
() async {
|
() async {
|
||||||
|
final topic = ref.read(selectedTopicProvider);
|
||||||
final newTab = WebViewPage.create(
|
final newTab = WebViewPage.create(
|
||||||
url: WebUri.uri(url),
|
url: WebUri.uri(url),
|
||||||
|
topicId: topic,
|
||||||
);
|
);
|
||||||
|
|
||||||
ref
|
await ref
|
||||||
.read(webViewRepositoryProvider.notifier)
|
.read(topicTabRepositoryProvider(topic).notifier)
|
||||||
.addTab(WebView(tab: newTab));
|
.addTab(newTab);
|
||||||
|
|
||||||
ref.read(webViewTabControllerProvider.notifier).showTab(newTab.id);
|
ref.read(webViewTabControllerProvider.notifier).showTab(newTab.id);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ part of 'switch_new_tab.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$switchNewTabControllerHash() =>
|
String _$switchNewTabControllerHash() =>
|
||||||
r'fb831015e4b99d3ae86bd2a88a3c6f4c7004783d';
|
r'7ebfea953aeb81c279ea11254ff0ad558e6a949f';
|
||||||
|
|
||||||
/// See also [SwitchNewTabController].
|
/// See also [SwitchNewTabController].
|
||||||
@ProviderFor(SwitchNewTabController)
|
@ProviderFor(SwitchNewTabController)
|
||||||
final switchNewTabControllerProvider =
|
final switchNewTabControllerProvider =
|
||||||
AutoDisposeAsyncNotifierProvider<SwitchNewTabController, void>.internal(
|
AsyncNotifierProvider<SwitchNewTabController, void>.internal(
|
||||||
SwitchNewTabController.new,
|
SwitchNewTabController.new,
|
||||||
name: r'switchNewTabControllerProvider',
|
name: r'switchNewTabControllerProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
@@ -22,6 +22,6 @@ final switchNewTabControllerProvider =
|
|||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef _$SwitchNewTabController = AutoDisposeAsyncNotifier<void>;
|
typedef _$SwitchNewTabController = AsyncNotifier<void>;
|
||||||
// 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
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
import 'package:lensai/features/settings/data/models/settings.dart';
|
import 'package:lensai/features/settings/data/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.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:lensai/features/web_view/domain/providers.dart';
|
||||||
|
import 'package:lensai/features/web_view/domain/repositories/web_view.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'readerability_script.g.dart';
|
part 'readerability_script.g.dart';
|
||||||
@@ -14,8 +14,8 @@ class ReaderabilityScriptService extends _$ReaderabilityScriptService {
|
|||||||
late bool _enableReadability;
|
late bool _enableReadability;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> build(ConsistentController controller) async {
|
Future<void> build(String tabId) async {
|
||||||
_controller = controller.value;
|
_controller = ref.watch(webViewControllerProvider(tabId));
|
||||||
_readerabilityScript = ref.watch(readerabilityScriptProvider.future);
|
_readerabilityScript = ref.watch(readerabilityScriptProvider.future);
|
||||||
_enableReadability = ref.watch(
|
_enableReadability = ref.watch(
|
||||||
settingsRepositoryProvider.select(
|
settingsRepositoryProvider.select(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ part of 'readerability_script.dart';
|
|||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$readerabilityScriptServiceHash() =>
|
String _$readerabilityScriptServiceHash() =>
|
||||||
r'bb38e2934ed124502b70faed11b6921c40d0eec2';
|
r'85f0de50516cadb1f2d1efa8161a8cf45868b9d9';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
@@ -32,10 +32,10 @@ class _SystemHash {
|
|||||||
|
|
||||||
abstract class _$ReaderabilityScriptService
|
abstract class _$ReaderabilityScriptService
|
||||||
extends BuildlessAutoDisposeAsyncNotifier<void> {
|
extends BuildlessAutoDisposeAsyncNotifier<void> {
|
||||||
late final ConsistentController controller;
|
late final String tabId;
|
||||||
|
|
||||||
FutureOr<void> build(
|
FutureOr<void> build(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +50,10 @@ class ReaderabilityScriptServiceFamily extends Family<AsyncValue<void>> {
|
|||||||
|
|
||||||
/// See also [ReaderabilityScriptService].
|
/// See also [ReaderabilityScriptService].
|
||||||
ReaderabilityScriptServiceProvider call(
|
ReaderabilityScriptServiceProvider call(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
) {
|
) {
|
||||||
return ReaderabilityScriptServiceProvider(
|
return ReaderabilityScriptServiceProvider(
|
||||||
controller,
|
tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class ReaderabilityScriptServiceFamily extends Family<AsyncValue<void>> {
|
|||||||
covariant ReaderabilityScriptServiceProvider provider,
|
covariant ReaderabilityScriptServiceProvider provider,
|
||||||
) {
|
) {
|
||||||
return call(
|
return call(
|
||||||
provider.controller,
|
provider.tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,9 +87,9 @@ class ReaderabilityScriptServiceProvider
|
|||||||
void> {
|
void> {
|
||||||
/// See also [ReaderabilityScriptService].
|
/// See also [ReaderabilityScriptService].
|
||||||
ReaderabilityScriptServiceProvider(
|
ReaderabilityScriptServiceProvider(
|
||||||
ConsistentController controller,
|
String tabId,
|
||||||
) : this._internal(
|
) : this._internal(
|
||||||
() => ReaderabilityScriptService()..controller = controller,
|
() => ReaderabilityScriptService()..tabId = tabId,
|
||||||
from: readerabilityScriptServiceProvider,
|
from: readerabilityScriptServiceProvider,
|
||||||
name: r'readerabilityScriptServiceProvider',
|
name: r'readerabilityScriptServiceProvider',
|
||||||
debugGetCreateSourceHash:
|
debugGetCreateSourceHash:
|
||||||
@@ -99,7 +99,7 @@ class ReaderabilityScriptServiceProvider
|
|||||||
dependencies: ReaderabilityScriptServiceFamily._dependencies,
|
dependencies: ReaderabilityScriptServiceFamily._dependencies,
|
||||||
allTransitiveDependencies:
|
allTransitiveDependencies:
|
||||||
ReaderabilityScriptServiceFamily._allTransitiveDependencies,
|
ReaderabilityScriptServiceFamily._allTransitiveDependencies,
|
||||||
controller: controller,
|
tabId: tabId,
|
||||||
);
|
);
|
||||||
|
|
||||||
ReaderabilityScriptServiceProvider._internal(
|
ReaderabilityScriptServiceProvider._internal(
|
||||||
@@ -109,17 +109,17 @@ class ReaderabilityScriptServiceProvider
|
|||||||
required super.allTransitiveDependencies,
|
required super.allTransitiveDependencies,
|
||||||
required super.debugGetCreateSourceHash,
|
required super.debugGetCreateSourceHash,
|
||||||
required super.from,
|
required super.from,
|
||||||
required this.controller,
|
required this.tabId,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final ConsistentController controller;
|
final String tabId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FutureOr<void> runNotifierBuild(
|
FutureOr<void> runNotifierBuild(
|
||||||
covariant ReaderabilityScriptService notifier,
|
covariant ReaderabilityScriptService notifier,
|
||||||
) {
|
) {
|
||||||
return notifier.build(
|
return notifier.build(
|
||||||
controller,
|
tabId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,13 +128,13 @@ class ReaderabilityScriptServiceProvider
|
|||||||
return ProviderOverride(
|
return ProviderOverride(
|
||||||
origin: this,
|
origin: this,
|
||||||
override: ReaderabilityScriptServiceProvider._internal(
|
override: ReaderabilityScriptServiceProvider._internal(
|
||||||
() => create()..controller = controller,
|
() => create()..tabId = tabId,
|
||||||
from: from,
|
from: from,
|
||||||
name: null,
|
name: null,
|
||||||
dependencies: null,
|
dependencies: null,
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
debugGetCreateSourceHash: null,
|
debugGetCreateSourceHash: null,
|
||||||
controller: controller,
|
tabId: tabId,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -147,14 +147,13 @@ class ReaderabilityScriptServiceProvider
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return other is ReaderabilityScriptServiceProvider &&
|
return other is ReaderabilityScriptServiceProvider && other.tabId == tabId;
|
||||||
other.controller == controller;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode {
|
int get hashCode {
|
||||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||||
hash = _SystemHash.combine(hash, controller.hashCode);
|
hash = _SystemHash.combine(hash, tabId.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
@@ -162,8 +161,8 @@ class ReaderabilityScriptServiceProvider
|
|||||||
|
|
||||||
mixin ReaderabilityScriptServiceRef
|
mixin ReaderabilityScriptServiceRef
|
||||||
on AutoDisposeAsyncNotifierProviderRef<void> {
|
on AutoDisposeAsyncNotifierProviderRef<void> {
|
||||||
/// The parameter `controller` of this provider.
|
/// The parameter `tabId` of this provider.
|
||||||
ConsistentController get controller;
|
String get tabId;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReaderabilityScriptServiceProviderElement
|
class _ReaderabilityScriptServiceProviderElement
|
||||||
@@ -172,8 +171,7 @@ class _ReaderabilityScriptServiceProviderElement
|
|||||||
_ReaderabilityScriptServiceProviderElement(super.provider);
|
_ReaderabilityScriptServiceProviderElement(super.provider);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsistentController get controller =>
|
String get tabId => (origin as ReaderabilityScriptServiceProvider).tabId;
|
||||||
(origin as ReaderabilityScriptServiceProvider).controller;
|
|
||||||
}
|
}
|
||||||
// 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
|
||||||
|
|||||||
@@ -15,46 +15,22 @@ import 'package:lensai/features/settings/data/models/settings.dart';
|
|||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
import 'package:lensai/features/settings/data/repositories/settings_repository.dart';
|
||||||
import 'package:lensai/features/share_intent/domain/entities/shared_content.dart';
|
import 'package:lensai/features/share_intent/domain/entities/shared_content.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/favicon.dart';
|
|
||||||
import 'package:lensai/presentation/controllers/website_title.dart';
|
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
|
import 'package:lensai/presentation/widgets/website_title_tile.dart';
|
||||||
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
|
import 'package:lensai/utils/ui_helper.dart' as ui_helper;
|
||||||
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
import 'package:lensai/utils/uri_parser.dart' as uri_parser;
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
class LoadingWebPageDialog extends HookConsumerWidget {
|
|
||||||
final Uri url;
|
|
||||||
|
|
||||||
final void Function()? onDismiss;
|
|
||||||
|
|
||||||
const LoadingWebPageDialog(this.url, {this.onDismiss});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final pageInfoAsync = ref.watch(pageInfoProvider(url));
|
|
||||||
|
|
||||||
return pageInfoAsync.when(
|
|
||||||
data: (pageInfo) => WebPageDialog(
|
|
||||||
page: pageInfo,
|
|
||||||
onDismiss: onDismiss,
|
|
||||||
),
|
|
||||||
error: (error, stackTrace) => SizedBox.shrink(),
|
|
||||||
loading: () => ModalBarrier(
|
|
||||||
color: Theme.of(context).dialogTheme.barrierColor ?? Colors.black54,
|
|
||||||
onDismiss: onDismiss,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebPageDialog extends HookConsumerWidget {
|
class WebPageDialog extends HookConsumerWidget {
|
||||||
final WebPageInfo page;
|
final Uri url;
|
||||||
|
final WebPageInfo? precachedInfo;
|
||||||
final InAppWebViewController? webViewController;
|
final InAppWebViewController? webViewController;
|
||||||
|
|
||||||
final void Function()? onDismiss;
|
final void Function()? onDismiss;
|
||||||
|
|
||||||
const WebPageDialog({
|
const WebPageDialog({
|
||||||
required this.page,
|
required this.url,
|
||||||
|
this.precachedInfo,
|
||||||
this.webViewController,
|
this.webViewController,
|
||||||
this.onDismiss,
|
this.onDismiss,
|
||||||
super.key,
|
super.key,
|
||||||
@@ -71,7 +47,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
final availableBangsAsync = ref.watch(
|
final availableBangsAsync = ref.watch(
|
||||||
bangDataListProvider(
|
bangDataListProvider(
|
||||||
filter: (
|
filter: (
|
||||||
domain: page.url.host,
|
domain: url.host,
|
||||||
groups: null,
|
groups: null,
|
||||||
categoryFilter: null,
|
categoryFilter: null,
|
||||||
orderMostFrequentFirst: true,
|
orderMostFrequentFirst: true,
|
||||||
@@ -81,8 +57,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
final availableBangCount = availableBangsAsync.valueOrNull?.length;
|
final availableBangCount = availableBangsAsync.valueOrNull?.length;
|
||||||
|
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
final urlTextController =
|
final urlTextController = useTextEditingController(text: url.toString());
|
||||||
useTextEditingController(text: page.url.toString());
|
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -97,16 +72,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
horizontal: 20.0,
|
horizontal: 20.0,
|
||||||
vertical: 24.0,
|
vertical: 24.0,
|
||||||
),
|
),
|
||||||
title: ListTile(
|
title: WebsiteTitleTile(url, precachedInfo: precachedInfo),
|
||||||
leading: FaviconImage(
|
|
||||||
favicon: page.favicon,
|
|
||||||
url: page.url,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
contentPadding: EdgeInsets.zero,
|
|
||||||
title: Text(page.title ?? 'Unknown Title'),
|
|
||||||
subtitle: Text(page.url.authority),
|
|
||||||
),
|
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
//We need this to stretch the dialog, then padding from dialog is applied
|
//We need this to stretch the dialog, then padding from dialog is applied
|
||||||
@@ -164,7 +130,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return SiteSearch(
|
return SiteSearch(
|
||||||
domain: page.url.host,
|
domain: url.host,
|
||||||
availableBangs: availableBangs,
|
availableBangs: availableBangs,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -173,7 +139,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
exception: error,
|
exception: error,
|
||||||
),
|
),
|
||||||
loading: () => SiteSearch(
|
loading: () => SiteSearch(
|
||||||
domain: page.url.host,
|
domain: url.host,
|
||||||
availableBangs: [
|
availableBangs: [
|
||||||
BangData(
|
BangData(
|
||||||
websiteName: 'websiteName',
|
websiteName: 'websiteName',
|
||||||
@@ -194,14 +160,14 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
title: const Text('Copy address'),
|
title: const Text('Copy address'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(text: page.url.toString()),
|
ClipboardData(text: url.toString()),
|
||||||
);
|
);
|
||||||
onDismiss?.call();
|
onDismiss?.call();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await ui_helper.launchUrlFeedback(context, page.url);
|
await ui_helper.launchUrlFeedback(context, url);
|
||||||
},
|
},
|
||||||
leading: const Icon(Icons.open_in_browser),
|
leading: const Icon(Icons.open_in_browser),
|
||||||
title: const Text('Launch External'),
|
title: const Text('Launch External'),
|
||||||
@@ -212,7 +178,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
await ref
|
await ref
|
||||||
.read(switchNewTabControllerProvider.notifier)
|
.read(switchNewTabControllerProvider.notifier)
|
||||||
.add(page.url);
|
.add(url);
|
||||||
|
|
||||||
onDismiss?.call();
|
onDismiss?.call();
|
||||||
},
|
},
|
||||||
@@ -221,7 +187,7 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
leading: const Icon(Icons.share),
|
leading: const Icon(Icons.share),
|
||||||
title: const Text('Share link'),
|
title: const Text('Share link'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
await Share.shareUri(page.url);
|
await Share.shareUri(url);
|
||||||
|
|
||||||
onDismiss?.call();
|
onDismiss?.call();
|
||||||
},
|
},
|
||||||
@@ -231,14 +197,14 @@ class WebPageDialog extends HookConsumerWidget {
|
|||||||
leading: Icon(KagiTool.summarizer.icon),
|
leading: Icon(KagiTool.summarizer.icon),
|
||||||
title: const Text('Summarize'),
|
title: const Text('Summarize'),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final url = uri_builder.summarizerUri(
|
final summarizerUrl = uri_builder.summarizerUri(
|
||||||
document: SharedUrl(page.url),
|
document: SharedUrl(url),
|
||||||
mode: SummarizerMode.keyMoments,
|
mode: SummarizerMode.keyMoments,
|
||||||
);
|
);
|
||||||
|
|
||||||
await ref
|
await ref
|
||||||
.read(switchNewTabControllerProvider.notifier)
|
.read(switchNewTabControllerProvider.notifier)
|
||||||
.add(url);
|
.add(summarizerUrl);
|
||||||
|
|
||||||
onDismiss?.call();
|
onDismiss?.call();
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:copy_with_extension/copy_with_extension.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
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:flutter_inappwebview/flutter_inappwebview.dart';
|
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||||
@@ -17,9 +15,8 @@ import 'package:lensai/features/search_browser/domain/entities/sheet.dart';
|
|||||||
import 'package:lensai/features/search_browser/domain/providers.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/models/settings.dart';
|
||||||
import 'package:lensai/features/settings/data/repositories/settings_repository.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/domain/providers.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/readerability.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_page_dialog.dart';
|
import 'package:lensai/features/web_view/presentation/widgets/web_page_dialog.dart';
|
||||||
@@ -38,124 +35,15 @@ const _webViewSupportedSchemes = [
|
|||||||
"about",
|
"about",
|
||||||
];
|
];
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
WebViewPage call({
|
|
||||||
InAppWebViewController? controller,
|
|
||||||
Uri? url,
|
|
||||||
SslError? sslError,
|
|
||||||
String? title,
|
|
||||||
String? topicId,
|
|
||||||
Favicon? favicon,
|
|
||||||
Uint8List? screenshot,
|
|
||||||
({bool canGoBack, bool canGoForward})? pageHistory,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
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(
|
|
||||||
key: _value.key,
|
|
||||||
id: _value.id,
|
|
||||||
controller: controller == const $CopyWithPlaceholder()
|
|
||||||
? _value.controller
|
|
||||||
: controller as InAppWebViewController?,
|
|
||||||
url: url == const $CopyWithPlaceholder() || url == null
|
|
||||||
? _value.url
|
|
||||||
: url as Uri,
|
|
||||||
sslError: sslError == const $CopyWithPlaceholder()
|
|
||||||
? _value.sslError
|
|
||||||
: sslError as SslError?,
|
|
||||||
title: title == const $CopyWithPlaceholder()
|
|
||||||
? _value.title
|
|
||||||
: title as String?,
|
|
||||||
topicId: topicId == const $CopyWithPlaceholder()
|
|
||||||
? _value.topicId
|
|
||||||
: topicId as String?,
|
|
||||||
favicon: favicon == const $CopyWithPlaceholder()
|
|
||||||
? _value.favicon
|
|
||||||
: favicon as Favicon?,
|
|
||||||
screenshot: screenshot == const $CopyWithPlaceholder()
|
|
||||||
? _value.screenshot
|
|
||||||
: screenshot as Uint8List?,
|
|
||||||
pageHistory:
|
|
||||||
pageHistory == const $CopyWithPlaceholder() || pageHistory == null
|
|
||||||
? _value.pageHistory
|
|
||||||
: pageHistory as ({bool canGoBack, bool canGoForward}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WebView extends StatefulHookConsumerWidget {
|
class WebView extends StatefulHookConsumerWidget {
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
final URLRequest? initialUrlRequest;
|
||||||
|
|
||||||
final ValueNotifier<WebViewPage> _pageNotifier;
|
const WebView({
|
||||||
|
required this.tabId,
|
||||||
ValueListenable<WebViewPage> get page => _pageNotifier;
|
required super.key,
|
||||||
|
this.initialUrlRequest,
|
||||||
/// Don't cache this value as it depends on current value of a ValueListenable
|
});
|
||||||
InAppWebViewController? get currentController =>
|
|
||||||
_pageNotifier.value.controller;
|
|
||||||
|
|
||||||
WebView({required WebViewPage tab})
|
|
||||||
: _pageNotifier = ValueNotifier(tab),
|
|
||||||
tabId = tab.id,
|
|
||||||
super(key: tab.key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<ConsumerStatefulWidget> createState() => _WebViewState();
|
ConsumerState<ConsumerStatefulWidget> createState() => _WebViewState();
|
||||||
@@ -165,13 +53,6 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
Timer? _onLoadStopDebounce;
|
Timer? _onLoadStopDebounce;
|
||||||
Timer? _periodicScreenshotUpdate;
|
Timer? _periodicScreenshotUpdate;
|
||||||
|
|
||||||
void updatePage(
|
|
||||||
WebViewPage Function(_WebViewPageCWProxyImpl copyWith) update,
|
|
||||||
) {
|
|
||||||
final x = widget._pageNotifier.value =
|
|
||||||
update(_WebViewPageCWProxyImpl(widget._pageNotifier.value));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<bool> _downloadChat(
|
Future<bool> _downloadChat(
|
||||||
DownloadStartRequest downloadStartRequest,
|
DownloadStartRequest downloadStartRequest,
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
@@ -216,25 +97,6 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _updateScreenshot() async {
|
|
||||||
final screenshot = await widget.page.value.controller
|
|
||||||
?.takeScreenshot(
|
|
||||||
screenshotConfiguration: ScreenshotConfiguration(
|
|
||||||
compressFormat: CompressFormat.JPEG,
|
|
||||||
quality: 20,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.timeout(
|
|
||||||
const Duration(milliseconds: 1500),
|
|
||||||
onTimeout: () {
|
|
||||||
logger.w('Screenshot timed out');
|
|
||||||
return null;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
updatePage((copyWith) => copyWith.screenshot(screenshot));
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@@ -242,9 +104,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
_onLoadStopDebounce?.cancel();
|
_onLoadStopDebounce?.cancel();
|
||||||
_periodicScreenshotUpdate?.cancel();
|
_periodicScreenshotUpdate?.cancel();
|
||||||
|
|
||||||
widget._pageNotifier.dispose();
|
logger.i('Disposed ${widget.key} ${widget.tabId}');
|
||||||
|
|
||||||
logger.i('Disposed ${widget.key} (${widget.page.value.title})');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -291,7 +151,8 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
(value.valueOrNull ?? Settings.withDefaults()).enableJavascript,
|
(value.valueOrNull ?? Settings.withDefaults()).enableJavascript,
|
||||||
),
|
),
|
||||||
(previous, next) async {
|
(previous, next) async {
|
||||||
await widget.page.value.controller?.setSettings(
|
final controller = ref.read(webViewControllerProvider(widget.tabId));
|
||||||
|
await controller?.setSettings(
|
||||||
settings: initialSettings.copy()..javaScriptEnabled = next,
|
settings: initialSettings.copy()..javaScriptEnabled = next,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -322,20 +183,22 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
useOnAppLifecycleStateChange((previous, current) async {
|
useOnAppLifecycleStateChange((previous, current) async {
|
||||||
|
final controller = ref.read(webViewControllerProvider(widget.tabId));
|
||||||
|
|
||||||
switch (current) {
|
switch (current) {
|
||||||
case AppLifecycleState.paused:
|
case AppLifecycleState.paused:
|
||||||
if (platform_util.isAndroid()) {
|
if (platform_util.isAndroid()) {
|
||||||
await widget.page.value.controller?.pause();
|
await controller?.pause();
|
||||||
}
|
}
|
||||||
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
||||||
await widget.page.value.controller?.pauseTimers();
|
await controller?.pauseTimers();
|
||||||
}
|
}
|
||||||
case AppLifecycleState.resumed:
|
case AppLifecycleState.resumed:
|
||||||
if (platform_util.isAndroid()) {
|
if (platform_util.isAndroid()) {
|
||||||
await widget.page.value.controller?.resume();
|
await controller?.resume();
|
||||||
}
|
}
|
||||||
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
if (platform_util.isAndroid() || platform_util.isIOS()) {
|
||||||
await widget.page.value.controller?.resumeTimers();
|
await controller?.resumeTimers();
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@@ -344,7 +207,7 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
InAppWebView(
|
InAppWebView(
|
||||||
initialUrlRequest: URLRequest(url: WebUri.uri(widget.page.value.url)),
|
initialUrlRequest: widget.initialUrlRequest,
|
||||||
initialSettings: initialSettings,
|
initialSettings: initialSettings,
|
||||||
findInteractionController: findInteractionController,
|
findInteractionController: findInteractionController,
|
||||||
contextMenu: ContextMenu(
|
contextMenu: ContextMenu(
|
||||||
@@ -353,8 +216,10 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
id: 1,
|
id: 1,
|
||||||
title: "Search",
|
title: "Search",
|
||||||
action: () async {
|
action: () async {
|
||||||
final selectedText =
|
final controller =
|
||||||
await widget.page.value.controller?.getSelectedText();
|
ref.read(webViewControllerProvider(widget.tabId));
|
||||||
|
|
||||||
|
final selectedText = await controller?.getSelectedText();
|
||||||
|
|
||||||
if (selectedText != null && selectedText.isNotEmpty) {
|
if (selectedText != null && selectedText.isNotEmpty) {
|
||||||
final searchBang =
|
final searchBang =
|
||||||
@@ -371,8 +236,10 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
id: 2,
|
id: 2,
|
||||||
title: "Assistant",
|
title: "Assistant",
|
||||||
action: () async {
|
action: () async {
|
||||||
final selectedText =
|
final controller =
|
||||||
await widget.page.value.controller?.getSelectedText();
|
ref.read(webViewControllerProvider(widget.tabId));
|
||||||
|
|
||||||
|
final selectedText = await controller?.getSelectedText();
|
||||||
|
|
||||||
if (selectedText != null && selectedText.isNotEmpty) {
|
if (selectedText != null && selectedText.isNotEmpty) {
|
||||||
ref.read(bottomSheetProvider.notifier).show(
|
ref.read(bottomSheetProvider.notifier).show(
|
||||||
@@ -391,19 +258,24 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
await controller.startSafeBrowsing();
|
await controller.startSafeBrowsing();
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePage((copyWith) => copyWith.controller(controller));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(controller: controller);
|
||||||
},
|
},
|
||||||
onReceivedServerTrustAuthRequest: (controller, challenge) async {
|
onReceivedServerTrustAuthRequest: (controller, challenge) async {
|
||||||
final sslError = challenge.protectionSpace.sslError;
|
final sslError = challenge.protectionSpace.sslError;
|
||||||
|
|
||||||
if (sslError != null && sslError.code != null) {
|
if (sslError != null && sslError.code != null) {
|
||||||
if (challenge.protectionSpace.host ==
|
final url = await controller.getUrl();
|
||||||
await controller.getUrl().then((value) => value?.host)) {
|
if (challenge.protectionSpace.host == url?.host) {
|
||||||
updatePage((copyWith) => copyWith.sslError(sslError));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(sslError: sslError);
|
||||||
|
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ui_helper.showErrorMessage(
|
ui_helper.showErrorMessage(
|
||||||
context,
|
context,
|
||||||
'We detected an security issue and did not continue to ${widget.page.value.url.authority}: ${sslError.message}',
|
'We detected an security issue and did not continue to ${url?.authority}: ${sslError.message}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +286,10 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePage((copyWith) => copyWith.sslError(null));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(sslError: null);
|
||||||
|
|
||||||
return ServerTrustAuthResponse(
|
return ServerTrustAuthResponse(
|
||||||
action: ServerTrustAuthResponseAction.PROCEED,
|
action: ServerTrustAuthResponseAction.PROCEED,
|
||||||
);
|
);
|
||||||
@@ -468,46 +343,48 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
onProgressChanged: (controller, progress) {
|
onProgressChanged: (controller, progress) {
|
||||||
webViewProgress.value = progress;
|
webViewProgress.value = progress;
|
||||||
},
|
},
|
||||||
onLoadStart: (controller, url) {
|
onLoadStart: (controller, url) async {
|
||||||
final readabilityNotifier = ref.read(
|
final readabilityNotifier = ref.read(
|
||||||
readerabilityControllerProvider(
|
readerabilityControllerProvider(widget.tabId).notifier,
|
||||||
ConsistentController(controller),
|
|
||||||
).notifier,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
updatePage(
|
await ref.read(tabStateProvider(widget.tabId).notifier).copyWith(
|
||||||
(copyWith) => copyWith(
|
url: url,
|
||||||
url: url,
|
// ignore: avoid_redundant_argument_values
|
||||||
// ignore: avoid_redundant_argument_values
|
sslError: null,
|
||||||
sslError: null,
|
);
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readabilityNotifier.reset();
|
readabilityNotifier.reset();
|
||||||
},
|
},
|
||||||
onLoadStop: (controller, url) async {
|
onLoadStop: (controller, url) async {
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
updatePage((copyWith) => copyWith.url(url));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(url: url);
|
||||||
}
|
}
|
||||||
|
|
||||||
_onLoadStopDebounce?.cancel();
|
_onLoadStopDebounce?.cancel();
|
||||||
_onLoadStopDebounce =
|
_onLoadStopDebounce =
|
||||||
Timer(const Duration(milliseconds: 150), () async {
|
Timer(const Duration(milliseconds: 150), () async {
|
||||||
final readabilityNotifier = ref.read(
|
final readabilityNotifier = ref.read(
|
||||||
readerabilityControllerProvider(
|
readerabilityControllerProvider(widget.tabId).notifier,
|
||||||
ConsistentController(controller),
|
|
||||||
).notifier,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
await readabilityNotifier.checkReaderable();
|
await readabilityNotifier.checkReaderable();
|
||||||
|
|
||||||
await _updateScreenshot().whenComplete(() {
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.updateScreenshot()
|
||||||
|
.whenComplete(() {
|
||||||
_periodicScreenshotUpdate?.cancel();
|
_periodicScreenshotUpdate?.cancel();
|
||||||
_periodicScreenshotUpdate =
|
_periodicScreenshotUpdate =
|
||||||
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
Timer.periodic(const Duration(seconds: 5), (timer) async {
|
||||||
await _updateScreenshot().onError((error, stackTrace) {
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.updateScreenshot()
|
||||||
|
.onError((error, stackTrace) {
|
||||||
logger.e(error, stackTrace: stackTrace);
|
logger.e(error, stackTrace: stackTrace);
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
});
|
});
|
||||||
@@ -522,7 +399,9 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
canGoForward: await controller.canGoForward()
|
canGoForward: await controller.canGoForward()
|
||||||
);
|
);
|
||||||
|
|
||||||
updatePage((copyWith) => copyWith.pageHistory(history));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(pageHistory: history);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
shouldOverrideUrlLoading: (controller, navigationAction) async {
|
||||||
@@ -582,8 +461,8 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
Uri.tryParse(requestFocusNodeHrefResult?.src ?? '');
|
Uri.tryParse(requestFocusNodeHrefResult?.src ?? '');
|
||||||
if (url?.hasScheme == true && url?.hasAuthority == true) {
|
if (url?.hasScheme == true && url?.hasAuthority == true) {
|
||||||
ref.read(overlayDialogProvider.notifier).show(
|
ref.read(overlayDialogProvider.notifier).show(
|
||||||
LoadingWebPageDialog(
|
WebPageDialog(
|
||||||
url!,
|
url: url!,
|
||||||
onDismiss: () {
|
onDismiss: () {
|
||||||
ref.read(overlayDialogProvider.notifier).dismiss();
|
ref.read(overlayDialogProvider.notifier).dismiss();
|
||||||
},
|
},
|
||||||
@@ -592,8 +471,10 @@ class _WebViewState extends ConsumerState<WebView> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onTitleChanged: (controller, title) {
|
onTitleChanged: (controller, title) async {
|
||||||
updatePage((copyWith) => copyWith.title(title));
|
await ref
|
||||||
|
.read(tabStateProvider(widget.tabId).notifier)
|
||||||
|
.copyWith(title: title);
|
||||||
},
|
},
|
||||||
onDownloadStartRequest: (controller, downloadStartRequest) async {
|
onDownloadStartRequest: (controller, downloadStartRequest) async {
|
||||||
final handled = switch (downloadStartRequest.mimeType) {
|
final handled = switch (downloadStartRequest.mimeType) {
|
||||||
|
|||||||
@@ -1,23 +1,25 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.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/entities/abstract/tab.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';
|
||||||
|
|
||||||
class WebViewTab extends HookConsumerWidget {
|
class WebViewTab extends StatelessWidget {
|
||||||
final ITab tab;
|
final ITab tab;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
|
||||||
final VoidCallback onClose;
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
|
||||||
const WebViewTab({
|
const WebViewTab({
|
||||||
required this.tab,
|
required this.tab,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.onClose,
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onDelete,
|
||||||
super.key,
|
super.key,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
@@ -33,15 +35,8 @@ class WebViewTab extends HookConsumerWidget {
|
|||||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
onTap: () {
|
onTap: onTap,
|
||||||
if (!isActive) {
|
onLongPress: onLongPress,
|
||||||
//Close first to avoid rebuilds
|
|
||||||
onClose();
|
|
||||||
ref.read(webViewTabControllerProvider.notifier).showTab(tab.id);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@@ -59,11 +54,7 @@ class WebViewTab extends HookConsumerWidget {
|
|||||||
IconButton(
|
IconButton(
|
||||||
visualDensity:
|
visualDensity:
|
||||||
const VisualDensity(horizontal: -4.0, vertical: -4.0),
|
const VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||||
onPressed: () {
|
onPressed: onDelete,
|
||||||
ref
|
|
||||||
.read(webViewRepositoryProvider.notifier)
|
|
||||||
.closeTab(tab.id);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.close),
|
icon: const Icon(Icons.close),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,14 +1,33 @@
|
|||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SelectableChips<T, K> extends StatelessWidget {
|
class _BadgeWrapper extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
final int? count;
|
||||||
|
|
||||||
|
const _BadgeWrapper({required this.child, this.count});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return count != null
|
||||||
|
? Badge.count(
|
||||||
|
count: count!,
|
||||||
|
child: child,
|
||||||
|
)
|
||||||
|
: child;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectableChips<T extends S, S, K> extends StatelessWidget {
|
||||||
final List<T> availableItems;
|
final List<T> availableItems;
|
||||||
final T? selectedItem;
|
final S? selectedItem;
|
||||||
final int maxCount;
|
final int maxCount;
|
||||||
final bool deleteIcon;
|
final bool deleteIcon;
|
||||||
|
|
||||||
final K Function(T item) itemId;
|
final K Function(S item) itemId;
|
||||||
final Widget Function(T item) itemLabel;
|
final Widget Function(T item) itemLabel;
|
||||||
final Widget? Function(T item)? itemAvatar;
|
final Widget? Function(T item)? itemAvatar;
|
||||||
|
final int? Function(T item)? itemBadgeCount;
|
||||||
|
|
||||||
final void Function(T item)? onSelected;
|
final void Function(T item)? onSelected;
|
||||||
final void Function(T item)? onDeleted;
|
final void Function(T item)? onDeleted;
|
||||||
@@ -17,6 +36,7 @@ class SelectableChips<T, K> extends StatelessWidget {
|
|||||||
required this.itemId,
|
required this.itemId,
|
||||||
required this.itemLabel,
|
required this.itemLabel,
|
||||||
this.itemAvatar,
|
this.itemAvatar,
|
||||||
|
this.itemBadgeCount,
|
||||||
required this.availableItems,
|
required this.availableItems,
|
||||||
this.selectedItem,
|
this.selectedItem,
|
||||||
this.maxCount = 25,
|
this.maxCount = 25,
|
||||||
@@ -46,33 +66,42 @@ class SelectableChips<T, K> extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ListView.builder(
|
return FadingScroll(
|
||||||
scrollDirection: Axis.horizontal,
|
fadingSize: 15,
|
||||||
shrinkWrap: true,
|
builder: (context, controller) {
|
||||||
itemCount: items.length,
|
return ListView.builder(
|
||||||
itemBuilder: (context, index) {
|
controller: controller,
|
||||||
final item = items[index];
|
scrollDirection: Axis.horizontal,
|
||||||
return Padding(
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.only(right: 8.0),
|
itemCount: items.length,
|
||||||
child: FilterChip(
|
itemBuilder: (context, index) {
|
||||||
selected: selectedItem != null &&
|
final item = items[index];
|
||||||
itemId(item) == itemId(selectedItem as T),
|
return Padding(
|
||||||
showCheckmark: false,
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
onSelected: (value) {
|
child: _BadgeWrapper(
|
||||||
if (value) {
|
count: itemBadgeCount?.call(item),
|
||||||
onSelected?.call(item);
|
child: FilterChip(
|
||||||
} else {
|
selected: selectedItem != null &&
|
||||||
onDeleted?.call(item);
|
itemId(item) == itemId(selectedItem as S),
|
||||||
}
|
showCheckmark: false,
|
||||||
},
|
onSelected: (value) {
|
||||||
onDeleted: deleteIcon
|
if (value) {
|
||||||
? () {
|
onSelected?.call(item);
|
||||||
onDeleted?.call(item);
|
} else {
|
||||||
}
|
onDeleted?.call(item);
|
||||||
: null,
|
}
|
||||||
label: itemLabel.call(item),
|
},
|
||||||
avatar: itemAvatar?.call(item),
|
onDeleted: deleteIcon
|
||||||
),
|
? () {
|
||||||
|
onDeleted?.call(item);
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
label: itemLabel.call(item),
|
||||||
|
avatar: itemAvatar?.call(item),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:lensai/domain/entities/web_page_info.dart';
|
||||||
import 'package:lensai/features/web_view/presentation/widgets/favicon.dart';
|
import 'package:lensai/features/web_view/presentation/widgets/favicon.dart';
|
||||||
import 'package:lensai/presentation/controllers/website_title.dart';
|
import 'package:lensai/presentation/controllers/website_title.dart';
|
||||||
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
import 'package:lensai/presentation/widgets/failure_widget.dart';
|
||||||
@@ -7,16 +8,17 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
|
|
||||||
class WebsiteTitleTile extends HookConsumerWidget {
|
class WebsiteTitleTile extends HookConsumerWidget {
|
||||||
final Uri url;
|
final Uri url;
|
||||||
|
final WebPageInfo? precachedInfo;
|
||||||
|
|
||||||
const WebsiteTitleTile(this.url, {super.key});
|
const WebsiteTitleTile(this.url, {this.precachedInfo, super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final websiteTileAsync = ref.watch(pageInfoProvider(url));
|
final pageInfoAsync = ref.watch(pageInfoProvider(url));
|
||||||
|
|
||||||
return Skeletonizer(
|
return Skeletonizer(
|
||||||
enabled: websiteTileAsync.isLoading,
|
enabled: pageInfoAsync.isLoading && precachedInfo == null,
|
||||||
child: websiteTileAsync.when(
|
child: pageInfoAsync.when(
|
||||||
data: (info) {
|
data: (info) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: FaviconImage(
|
leading: FaviconImage(
|
||||||
@@ -35,11 +37,22 @@ class WebsiteTitleTile extends HookConsumerWidget {
|
|||||||
onRetry: () => ref.refresh(pageInfoProvider(url)),
|
onRetry: () => ref.refresh(pageInfoProvider(url)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const ListTile(
|
loading: () => (precachedInfo != null)
|
||||||
contentPadding: EdgeInsets.zero,
|
? ListTile(
|
||||||
title: Bone.text(),
|
leading: FaviconImage(
|
||||||
subtitle: Bone.text(),
|
favicon: precachedInfo!.favicon,
|
||||||
),
|
url: precachedInfo!.url,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Text(precachedInfo!.title ?? 'Unknown Title'),
|
||||||
|
subtitle: Text(url.authority),
|
||||||
|
)
|
||||||
|
: const ListTile(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: Bone.text(),
|
||||||
|
subtitle: Bone.text(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-13
@@ -330,10 +330,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: fading_scroll
|
name: fading_scroll
|
||||||
sha256: "0eeb846385950dfb03415bf76cebf087e7ec21b4fe28fd0c864ce334b6f56f02"
|
sha256: "70d13b031dc075d2cf395d599f381be7ea6759577cdfd88daa923d375522a9a2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.0"
|
version: "0.9.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -495,10 +495,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de"
|
sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.21"
|
version: "2.0.22"
|
||||||
flutter_riverpod:
|
flutter_riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -601,10 +601,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: go_router
|
name: go_router
|
||||||
sha256: ddc16d34b0d74cb313986918c0f0885a7ba2fc24d8fb8419de75f0015144ccfe
|
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.3"
|
version: "14.2.7"
|
||||||
go_router_builder:
|
go_router_builder:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
@@ -825,10 +825,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: mime
|
name: mime
|
||||||
sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
|
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.5"
|
version: "1.0.6"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1041,10 +1041,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974
|
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.3.2"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1294,10 +1294,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79
|
sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.9"
|
version: "6.3.10"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1436,4 +1436,4 @@ packages:
|
|||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.5.0 <4.0.0"
|
dart: ">=3.5.0 <4.0.0"
|
||||||
flutter: ">=3.22.0"
|
flutter: ">=3.24.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user