getting there

This commit is contained in:
Fabian Freund
2024-10-15 11:13:32 +02:00
parent 10d6a3de1d
commit c4d93e8fe7
56 changed files with 1830 additions and 762 deletions
@@ -2,8 +2,11 @@
import 'dart:async';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/repositories/data.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/topics/data/providers.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -80,3 +83,29 @@ class ShowFindInPage extends _$ShowFindInPage {
return false;
}
}
@Riverpod()
Stream<List<String>> availableTabIds(AvailableTabIdsRef ref, String? topicId) {
final db = ref.watch(tabDatabaseProvider);
final openTabs = ref
.watch(
tabStatesProvider.select(
(states) =>
EquatableCollection(states.keys.toSet(), immutable: false),
),
)
.collection;
if (topicId != null) {
return db.tabLinkDao.topicTabIds(topicId).watch().map(
(tabIds) =>
tabIds.where((tabId) => openTabs.contains(tabId)).toList(),
);
} else {
return db.tabLinkDao.allTabIds().watch().map(
(assignedTabIds) => openTabs
.where((tabId) => !assignedTabIds.contains(tabId))
.toList(),
);
}
}
@@ -156,6 +156,135 @@ class _SelectedBangDataProviderElement
String? get domain => (origin as SelectedBangDataProvider).domain;
}
String _$availableTabIdsHash() => r'bea231cdf6211fc876b8031b5d72999eae5d7b15';
/// See also [availableTabIds].
@ProviderFor(availableTabIds)
const availableTabIdsProvider = AvailableTabIdsFamily();
/// See also [availableTabIds].
class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
/// See also [availableTabIds].
const AvailableTabIdsFamily();
/// See also [availableTabIds].
AvailableTabIdsProvider call(
String? topicId,
) {
return AvailableTabIdsProvider(
topicId,
);
}
@override
AvailableTabIdsProvider getProviderOverride(
covariant AvailableTabIdsProvider 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'availableTabIdsProvider';
}
/// See also [availableTabIds].
class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
/// See also [availableTabIds].
AvailableTabIdsProvider(
String? topicId,
) : this._internal(
(ref) => availableTabIds(
ref as AvailableTabIdsRef,
topicId,
),
from: availableTabIdsProvider,
name: r'availableTabIdsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$availableTabIdsHash,
dependencies: AvailableTabIdsFamily._dependencies,
allTransitiveDependencies:
AvailableTabIdsFamily._allTransitiveDependencies,
topicId: topicId,
);
AvailableTabIdsProvider._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
Override overrideWith(
Stream<List<String>> Function(AvailableTabIdsRef provider) create,
) {
return ProviderOverride(
origin: this,
override: AvailableTabIdsProvider._internal(
(ref) => create(ref as AvailableTabIdsRef),
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
topicId: topicId,
),
);
}
@override
AutoDisposeStreamProviderElement<List<String>> createElement() {
return _AvailableTabIdsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AvailableTabIdsProvider && 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 AvailableTabIdsRef on AutoDisposeStreamProviderRef<List<String>> {
/// The parameter `topicId` of this provider.
String? get topicId;
}
class _AvailableTabIdsProviderElement
extends AutoDisposeStreamProviderElement<List<String>>
with AvailableTabIdsRef {
_AvailableTabIdsProviderElement(super.provider);
@override
String? get topicId => (origin as AvailableTabIdsProvider).topicId;
}
String _$selectedBangTriggerHash() =>
r'b0e12bc95e93d50d04f1c658230cea9f15ff0385';
@@ -2,6 +2,7 @@ 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/data/models/equatable_iterable.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers.dart';
@@ -25,8 +26,14 @@ class TabActionDialog extends HookConsumerWidget {
tabTopicIdProvider(initialTab.id).select((value) => value.valueOrNull),
);
final topics =
ref.watch(topicsWithCountProvider.select((value) => value.valueOrNull));
final topics = ref
.watch(
topicsWithCountProvider.select(
(value) => EquatableCollection(value.valueOrNull, immutable: true),
),
)
.collection;
final selectedTopic =
topics?.firstWhereOrNull((topic) => topic.id == tabTopicId);
@@ -1,3 +1,5 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@@ -7,6 +9,7 @@ import 'package:lensai/data/models/web_page_info.dart';
import 'package:lensai/features/bangs/data/models/bang_data.dart';
import 'package:lensai/features/bangs/domain/providers.dart';
import 'package:lensai/features/bangs/presentation/widgets/site_search.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
@@ -55,6 +58,7 @@ class WebPageDialog extends HookConsumerWidget {
final formKey = useMemoized(() => GlobalKey<FormState>());
final urlTextController = useTextEditingController(text: url.toString());
final urlTextFocusNode = useFocusNode();
return Stack(
children: [
@@ -80,6 +84,7 @@ class WebPageDialog extends HookConsumerWidget {
key: formKey,
child: TextFormField(
controller: urlTextController,
focusNode: urlTextFocusNode,
enableIMEPersonalizedLearning: !incognitoEnabled,
decoration: InputDecoration(
labelText: 'URL',
@@ -109,6 +114,15 @@ class WebPageDialog extends HookConsumerWidget {
return 'Invalid URL';
},
onTap: () {
if (!urlTextFocusNode.hasFocus) {
// Select all text when the field is tapped
urlTextController.selection = TextSelection(
baseOffset: 0,
extentOffset: urlTextController.text.length,
);
}
},
),
),
),
@@ -183,6 +197,40 @@ class WebPageDialog extends HookConsumerWidget {
onDismiss?.call();
},
),
ListTile(
leading: const Icon(Icons.mobile_screen_share),
title: const Text('Share screenshot'),
onTap: () async {
final screenshot = await ref
.read(selectedTabSessionNotifierProvider)
.requestScreenshot();
if (screenshot != null) {
ui.decodeImageFromList(
screenshot,
(result) async {
final png = await result.toByteData(
format: ui.ImageByteFormat.png,
);
if (png != null) {
final file = XFile.fromData(
png.buffer.asUint8List(),
mimeType: 'image/png',
);
await Share.shareXFiles(
[file],
subject: precachedInfo?.title,
);
}
},
);
}
onDismiss?.call();
},
),
const Divider(),
ListTile(
leading: Icon(KagiTool.summarizer.icon),
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart' show rootBundle;
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/domain/services/app_initialization.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/error_container.dart';
@@ -51,10 +52,16 @@ class LandingContent extends HookConsumerWidget {
const LandingAction(),
Consumer(
builder: (context, ref, child) {
final errors = ref.watch(
appInitializationServiceProvider
.select((result) => result.valueOrNull?.errors),
);
final errors = ref
.watch(
appInitializationServiceProvider.select(
(result) => EquatableCollection(
result.valueOrNull?.errors,
immutable: false,
),
),
)
.collection;
if (errors == null || errors.isEmpty) {
return const SizedBox.shrink();
@@ -1,16 +1,16 @@
import 'dart:async';
import 'package:fading_scroll/fading_scroll.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/data/models/equatable_iterable.dart';
import 'package:lensai/features/geckoview/domain/providers/selected_tab.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/browser/domain/providers.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/dialogs/tab_action.dart';
import 'package:lensai/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers.dart';
import 'package:lensai/features/geckoview/features/topics/domain/providers/selected_topic.dart';
import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart';
import 'package:lensai/features/geckoview/features/topics/presentation/widgets/topic_chips.dart';
@@ -116,135 +116,133 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
return FadingScroll(
fadingSize: 25,
shaderPadding:
const EdgeInsets.only(top: _SliverHeaderDelagate._headerSize),
return CustomScrollView(
controller: sheetScrollController,
builder: (context, controller) {
return CustomScrollView(
controller: controller,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeaderDelagate(onClose: onClose),
),
HookConsumer(
builder: (context, ref, child) {
final topic = ref.watch(selectedTopicProvider);
final availableTabs = ref.watch(
topicTabIdsProvider(topic)
.select((value) => value.valueOrNull ?? []),
);
final activeTab = ref.watch(selectedTabProvider);
final itemHeight = useMemoized(
() => _calculateItemHeight(
screenWidth: MediaQuery.of(context).size.width,
childAspectRatio: 0.75,
horizontalPadding: 4.0,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
slivers: [
SliverPersistentHeader(
pinned: true,
delegate: _SliverHeaderDelagate(onClose: onClose),
),
HookConsumer(
builder: (context, ref, child) {
final topic = ref.watch(selectedTopicProvider);
final availableTabs = ref
.watch(
availableTabIdsProvider(topic).select(
(value) => EquatableCollection(
value.valueOrNull ?? [],
immutable: true,
),
),
[MediaQuery.of(context).size.width],
);
)
.collection;
final activeTab = ref.watch(selectedTabProvider);
useEffect(
() {
final index = availableTabs
.indexWhere((webView) => webView == activeTab);
final itemHeight = useMemoized(
() => _calculateItemHeight(
screenWidth: MediaQuery.of(context).size.width,
childAspectRatio: 0.75,
horizontalPadding: 4.0,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
[MediaQuery.of(context).size.width],
);
if (index > -1) {
final reversedIndex = availableTabs.length - 1 - index;
final offset = (reversedIndex ~/ 2) * itemHeight;
useEffect(
() {
final index =
availableTabs.indexWhere((webView) => webView == activeTab);
if (offset != controller.offset) {
unawaited(
controller.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
if (index > -1) {
final reversedIndex = availableTabs.length - 1 - index;
final offset = (reversedIndex ~/ 2) * itemHeight;
return null;
},
[],
);
if (offset != sheetScrollController.offset) {
unawaited(
sheetScrollController.animateTo(
offset,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
),
);
}
}
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)
? TabPreview(
tab: tab,
isActive: tabId == activeTab,
onTap: () async {
if (tabId != activeTab) {
//Close first to avoid rebuilds
onClose();
await ref
.read(
tabRepositoryProvider.notifier,
)
.selectTab(tab.id);
} else {
onClose();
}
},
onLongPress: () {
ref
.read(
overlayDialogControllerProvider
.notifier,
)
.show(
TabActionDialog(
initialTab: tab,
onDismiss: ref
.read(
overlayDialogControllerProvider
.notifier,
)
.dismiss,
),
);
},
onDelete: () async {
await ref
.read(
tabRepositoryProvider.notifier,
)
.closeTab(tab.id);
},
)
: const SizedBox.shrink();
},
),
)
.toList(),
),
);
return null;
},
),
],
);
},
[availableTabs, activeTab],
);
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)
? TabPreview(
tab: tab,
isActive: tabId == activeTab,
onTap: () async {
if (tabId != activeTab) {
//Close first to avoid rebuilds
onClose();
await ref
.read(
tabRepositoryProvider.notifier,
)
.selectTab(tab.id);
} else {
onClose();
}
},
onLongPress: () {
ref
.read(
overlayDialogControllerProvider
.notifier,
)
.show(
TabActionDialog(
initialTab: tab,
onDismiss: ref
.read(
overlayDialogControllerProvider
.notifier,
)
.dismiss,
),
);
},
onDelete: () async {
await ref
.read(
tabRepositoryProvider.notifier,
)
.closeTab(tab.id);
},
)
: const SizedBox.shrink();
},
),
)
.toList(),
),
);
},
),
],
);
}
}
@@ -6,7 +6,9 @@ import 'package:flutter_material_design_icons/flutter_material_design_icons.dart
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/core/logger.dart';
import 'package:lensai/core/routing/routes.dart';
import 'package:lensai/features/geckoview/domain/providers.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_session.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/repositories/tab.dart';
@@ -22,6 +24,7 @@ import 'package:lensai/features/geckoview/features/controllers/bottom_sheet.dart
import 'package:lensai/features/geckoview/features/controllers/overlay_dialog.dart';
import 'package:lensai/features/geckoview/features/find_in_page/presentation/controllers/find_in_page_visibility.dart';
import 'package:lensai/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.dart';
import 'package:lensai/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:lensai/features/kagi/data/services/session.dart';
@@ -546,7 +549,23 @@ class BrowserScreen extends HookConsumerWidget {
SafeArea(
child: Stack(
children: [
const GeckoView(),
GeckoView(
preInitializationStep: () async {
await ref
.read(eventServiceProvider)
.fragmentReadyStateEvents
.firstWhere((state) => state == true)
.timeout(
const Duration(seconds: 3),
onTimeout: () {
logger.e(
'Browser fragement not reported ready, trying to intitialize anyways',
);
return true;
},
);
},
),
Positioned(
bottom: 0,
left: 0,
@@ -604,6 +623,7 @@ class BrowserScreen extends HookConsumerWidget {
),
),
),
floatingActionButton: ReaderAppearanceButton(),
bottomSheet: (displayedSheet != null)
? NotificationListener<DraggableScrollableNotification>(
onNotification: (notification) {