improved tab & topic handling

This commit is contained in:
Fabian Freund
2024-08-28 16:00:39 +02:00
parent 95dd254484
commit 0a7e84236f
43 changed files with 2035 additions and 847 deletions
@@ -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
+12 -16
View File
@@ -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
View File
@@ -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"