one step closer

This commit is contained in:
Fabian Freund
2024-10-22 07:27:37 +02:00
parent c4d93e8fe7
commit 8d351e55c0
92 changed files with 3829 additions and 2694 deletions
@@ -2,11 +2,10 @@
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/geckoview/domain/providers/tab_list.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/kagi/data/entities/modes.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -85,27 +84,15 @@ class ShowFindInPage extends _$ShowFindInPage {
}
@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;
List<String> availableTabIds(
AvailableTabIdsRef ref,
String? containerId,
) {
final containerTabs = ref.watch(
containerTabIdsProvider(containerId).select((value) => value.valueOrNull),
);
final tabStates = ref.watch(tabListProvider);
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(),
);
}
return containerTabs?.where((tabId) => tabStates.contains(tabId)).toList() ??
[];
}
@@ -156,23 +156,23 @@ class _SelectedBangDataProviderElement
String? get domain => (origin as SelectedBangDataProvider).domain;
}
String _$availableTabIdsHash() => r'bea231cdf6211fc876b8031b5d72999eae5d7b15';
String _$availableTabIdsHash() => r'427ea2e58c6bc27e2f16cffb850dc6621875e247';
/// See also [availableTabIds].
@ProviderFor(availableTabIds)
const availableTabIdsProvider = AvailableTabIdsFamily();
/// See also [availableTabIds].
class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
class AvailableTabIdsFamily extends Family<List<String>> {
/// See also [availableTabIds].
const AvailableTabIdsFamily();
/// See also [availableTabIds].
AvailableTabIdsProvider call(
String? topicId,
String? containerId,
) {
return AvailableTabIdsProvider(
topicId,
containerId,
);
}
@@ -181,7 +181,7 @@ class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
covariant AvailableTabIdsProvider provider,
) {
return call(
provider.topicId,
provider.containerId,
);
}
@@ -201,14 +201,14 @@ class AvailableTabIdsFamily extends Family<AsyncValue<List<String>>> {
}
/// See also [availableTabIds].
class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
class AvailableTabIdsProvider extends AutoDisposeProvider<List<String>> {
/// See also [availableTabIds].
AvailableTabIdsProvider(
String? topicId,
String? containerId,
) : this._internal(
(ref) => availableTabIds(
ref as AvailableTabIdsRef,
topicId,
containerId,
),
from: availableTabIdsProvider,
name: r'availableTabIdsProvider',
@@ -219,7 +219,7 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: AvailableTabIdsFamily._dependencies,
allTransitiveDependencies:
AvailableTabIdsFamily._allTransitiveDependencies,
topicId: topicId,
containerId: containerId,
);
AvailableTabIdsProvider._internal(
@@ -229,14 +229,14 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.topicId,
required this.containerId,
}) : super.internal();
final String? topicId;
final String? containerId;
@override
Override overrideWith(
Stream<List<String>> Function(AvailableTabIdsRef provider) create,
List<String> Function(AvailableTabIdsRef provider) create,
) {
return ProviderOverride(
origin: this,
@@ -247,42 +247,41 @@ class AvailableTabIdsProvider extends AutoDisposeStreamProvider<List<String>> {
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
topicId: topicId,
containerId: containerId,
),
);
}
@override
AutoDisposeStreamProviderElement<List<String>> createElement() {
AutoDisposeProviderElement<List<String>> createElement() {
return _AvailableTabIdsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AvailableTabIdsProvider && other.topicId == topicId;
return other is AvailableTabIdsProvider && other.containerId == containerId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, topicId.hashCode);
hash = _SystemHash.combine(hash, containerId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin AvailableTabIdsRef on AutoDisposeStreamProviderRef<List<String>> {
/// The parameter `topicId` of this provider.
String? get topicId;
mixin AvailableTabIdsRef on AutoDisposeProviderRef<List<String>> {
/// The parameter `containerId` of this provider.
String? get containerId;
}
class _AvailableTabIdsProviderElement
extends AutoDisposeStreamProviderElement<List<String>>
with AvailableTabIdsRef {
extends AutoDisposeProviderElement<List<String>> with AvailableTabIdsRef {
_AvailableTabIdsProviderElement(super.provider);
@override
String? get topicId => (origin as AvailableTabIdsProvider).topicId;
String? get containerId => (origin as AvailableTabIdsProvider).containerId;
}
String _$selectedBangTriggerHash() =>
@@ -5,8 +5,8 @@ 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';
import 'package:lensai/features/geckoview/features/topics/domain/repositories/tab_link.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/providers.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
class TabActionDialog extends HookConsumerWidget {
final TabState initialTab;
@@ -22,20 +22,21 @@ class TabActionDialog extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final tabState = ref.watch(tabStateProvider(initialTab.id)) ?? initialTab;
final tabTopicId = ref.watch(
tabTopicIdProvider(initialTab.id).select((value) => value.valueOrNull),
final tabContainerId = ref.watch(
tabContainerIdProvider(initialTab.id)
.select((value) => value.valueOrNull),
);
final topics = ref
final containers = ref
.watch(
topicsWithCountProvider.select(
containersWithCountProvider.select(
(value) => EquatableCollection(value.valueOrNull, immutable: true),
),
)
.collection;
final selectedTopic =
topics?.firstWhereOrNull((topic) => topic.id == tabTopicId);
final selectedContainer = containers
?.firstWhereOrNull((container) => container.id == tabContainerId);
final expansionController = useExpansionTileController();
@@ -68,22 +69,23 @@ class TabActionDialog extends HookConsumerWidget {
width: double.maxFinite,
child: ExpansionTile(
controller: expansionController,
leading: (selectedTopic != null)
? CircleAvatar(backgroundColor: selectedTopic.color)
leading: (selectedContainer != null)
? CircleAvatar(backgroundColor: selectedContainer.color)
: null,
title: (selectedTopic != null)
? Text(selectedTopic.name ?? 'New Topic')
: const Text('Assign a Topic'),
children: topics
?.where((topic) => topic.id != tabTopicId)
title: (selectedContainer != null)
? Text(selectedContainer.name ?? 'New Container')
: const Text('Assign a Container'),
children: containers
?.where((container) => container.id != tabContainerId)
.map(
(topic) => ListTile(
leading: CircleAvatar(backgroundColor: topic.color),
title: Text(topic.name ?? 'New Topic'),
(container) => ListTile(
leading:
CircleAvatar(backgroundColor: container.color),
title: Text(container.name ?? 'New Container'),
onTap: () async {
await ref
.read(tabLinkRepositoryProvider.notifier)
.assignTab(initialTab.id, topic.id);
.read(tabDataRepositoryProvider.notifier)
.assignContainer(initialTab.id, container.id);
expansionController.collapse();
},
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:lensai/features/geckoview/domain/entities/tab_state.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:text_scroll/text_scroll.dart';
class AppBarTitle extends StatelessWidget {
@@ -23,11 +24,16 @@ class AppBarTitle extends StatelessWidget {
color: Theme.of(context).colorScheme.errorContainer,
size: 14,
);
} else {
} else if (!tab.isLoading) {
return const Icon(
MdiIcons.lock,
size: 14,
);
} else {
return const Icon(
MdiIcons.timerSand,
size: 14,
);
}
}
@@ -38,10 +44,18 @@ class AppBarTitle extends StatelessWidget {
onTap: onTap,
child: Row(
children: [
RawImage(
image: tab.icon?.value,
height: 16,
width: 16,
Skeletonizer(
enabled: tab.icon == null,
child: Skeleton.replace(
replacement: const Bone.icon(
size: 16,
),
child: RawImage(
image: tab.icon?.value,
height: 16,
width: 16,
),
),
),
const SizedBox(
width: 8,
@@ -51,20 +65,29 @@ class AppBarTitle extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
TextScroll(
key: ValueKey(tab.title),
tab.title,
style: theme.textTheme.bodyLarge
?.copyWith(color: theme.colorScheme.onSurface),
// mode: TextScrollMode.bouncing,
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 5000),
fadedBorder: true,
fadeBorderSide: FadeBorderSide.right,
fadedBorderWidth: 0.05,
intervalSpaces: 4,
numberOfReps: 2,
Skeletonizer(
enabled: tab.title.isEmpty,
child: Skeleton.replace(
replacement: const Padding(
padding: EdgeInsets.only(right: 4, top: 1, bottom: 1),
child: Bone.text(),
),
child: TextScroll(
key: ValueKey(tab.title),
tab.title,
style: theme.textTheme.bodyLarge
?.copyWith(color: theme.colorScheme.onSurface),
// mode: TextScrollMode.bouncing,
velocity: const Velocity(pixelsPerSecond: Offset(75, 0)),
delayBefore: const Duration(milliseconds: 500),
pauseBetween: const Duration(milliseconds: 5000),
fadedBorder: true,
fadeBorderSide: FadeBorderSide.right,
fadedBorderWidth: 0.05,
intervalSpaces: 4,
numberOfReps: 2,
),
),
),
Row(
children: [
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -11,9 +12,11 @@ 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/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';
import 'package:lensai/features/geckoview/features/tabs/domain/providers/selected_container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/container.dart';
import 'package:lensai/features/geckoview/features/tabs/domain/repositories/tab.dart';
import 'package:lensai/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
import 'package:reorderable_grid/reorderable_grid.dart';
class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
static const _headerSize = 104.0;
@@ -52,10 +55,10 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
),
TextButton.icon(
onPressed: () async {
final topic = ref.read(selectedTopicProvider);
final container = ref.read(selectedContainerProvider);
await ref
.read(tabLinkRepositoryProvider.notifier)
.closeAllTabs(topic);
.read(tabDataRepositoryProvider.notifier)
.closeAllTabs(container);
onClose();
},
@@ -64,7 +67,7 @@ class _SliverHeaderDelagate extends SliverPersistentHeaderDelegate {
),
],
),
TopicChips(),
ContainerChips(),
const SizedBox(height: 8),
],
),
@@ -125,14 +128,11 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
),
HookConsumer(
builder: (context, ref, child) {
final topic = ref.watch(selectedTopicProvider);
final container = ref.watch(selectedContainerProvider);
final availableTabs = ref
.watch(
availableTabIdsProvider(topic).select(
(value) => EquatableCollection(
value.valueOrNull ?? [],
immutable: true,
),
availableTabIdsProvider(container).select(
(value) => EquatableCollection(value, immutable: true),
),
)
.collection;
@@ -156,8 +156,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
availableTabs.indexWhere((webView) => webView == activeTab);
if (index > -1) {
final reversedIndex = availableTabs.length - 1 - index;
final offset = (reversedIndex ~/ 2) * itemHeight;
final offset = (index ~/ 2) * itemHeight;
if (offset != sheetScrollController.offset) {
unawaited(
@@ -175,21 +174,15 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
[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),
final tabs = useMemoized(
() => availableTabs
.mapIndexed(
(index, tabId) => ReorderableGridDelayedDragStartListener(
key: ValueKey(tabId),
index: index,
child: Consumer(
builder: (context, ref, child) {
final tab = ref.watch(tabStateProvider(tabId));
return (tab != null)
? TabPreview(
tab: tab,
@@ -207,7 +200,7 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
onClose();
}
},
onLongPress: () {
onDoubleTap: () {
ref
.read(
overlayDialogControllerProvider
@@ -236,8 +229,52 @@ class ViewTabsSheetWidget extends HookConsumerWidget {
: const SizedBox.shrink();
},
),
)
.toList(),
),
)
.toList(),
[availableTabs, activeTab],
);
return SliverPadding(
padding: const EdgeInsets.symmetric(horizontal: 4.0),
sliver: SliverReorderableGrid(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: 2,
),
itemCount: tabs.length,
itemBuilder: (context, index) => tabs[index],
onReorder: (oldIndex, newIndex) async {
final containerRepository =
ref.read(containerRepositoryProvider.notifier);
final tabId = availableTabs[oldIndex];
final containerId = await ref
.read(tabDataRepositoryProvider.notifier)
.containerTabId(tabId);
final String key;
if (newIndex <= 0) {
key = await containerRepository
.getLeadingOrderKey(containerId);
} else if (newIndex >= availableTabs.length - 1) {
key = await containerRepository
.getTrailingOrderKey(containerId);
} else {
final orderAfterIndex = newIndex - 1;
key = await containerRepository.getOrderKeyAfterTab(
availableTabs[orderAfterIndex],
containerId,
);
}
await ref
.read(tabDataRepositoryProvider.notifier)
.assignOrderKey(tabId, key);
},
),
);
},
@@ -6,14 +6,14 @@ class TabPreview extends StatelessWidget {
final bool isActive;
final VoidCallback? onTap;
final VoidCallback? onLongPress;
final VoidCallback? onDoubleTap;
final VoidCallback? onDelete;
const TabPreview({
required this.tab,
required this.isActive,
this.onTap,
this.onLongPress,
this.onDoubleTap,
this.onDelete,
super.key,
});
@@ -35,7 +35,7 @@ class TabPreview extends StatelessWidget {
child: InkWell(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
onTap: onTap,
onLongPress: onLongPress,
onDoubleTap: onDoubleTap,
child: Column(
children: [
Row(
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_state.dart';
import 'package:lensai/features/geckoview/domain/providers/tab_list.dart';
class TabsActionButton extends HookConsumerWidget {
final bool isActive;
@@ -16,7 +16,7 @@ class TabsActionButton extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final theme = Theme.of(context);
final tabCount = ref.watch(tabStatesProvider.select((tabs) => tabs.length));
final tabCount = ref.watch(tabListProvider.select((tabs) => tabs.length));
return InkWell(
onTap: onTap,
File diff suppressed because it is too large Load Diff