added list view; persist tab view ui settings;
This commit is contained in:
@@ -57,7 +57,7 @@ final class TabSessionProvider extends $NotifierProvider<TabSession, void> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabSessionHash() => r'f0a3aca70719afd37cad260c5d36621455252bfb';
|
String _$tabSessionHash() => r'4b7206863e205dfeee6201ba6072e742c43ab6f3';
|
||||||
|
|
||||||
final class TabSessionFamily extends $Family
|
final class TabSessionFamily extends $Family
|
||||||
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
|
with $ClassFamilyOverride<TabSession, void, void, void, String?> {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ Future<bool> isTabTunneled(Ref ref, String? tabId) async {
|
|||||||
case TorRegularTabProxyMode.container:
|
case TorRegularTabProxyMode.container:
|
||||||
final containerId = await ref
|
final containerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabState.id);
|
.getTabContainerId(tabState.id);
|
||||||
|
|
||||||
final containerData = await containerId.mapNotNull(
|
final containerData = await containerId.mapNotNull(
|
||||||
(containerId) => ref
|
(containerId) => ref
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ final class IsTabTunneledProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$isTabTunneledHash() => r'fd8ca366b0835eef8aea7ad64453e6a6f2e69e82';
|
String _$isTabTunneledHash() => r'2e1a911c26345951cf259ca4547d236a71607667';
|
||||||
|
|
||||||
final class IsTabTunneledFamily extends $Family
|
final class IsTabTunneledFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
with $FunctionalFamilyOverride<FutureOr<bool>, String?> {
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ class TabRepository extends _$TabRepository {
|
|||||||
Future<bool> selectTab(String tabId) async {
|
Future<bool> selectTab(String tabId) async {
|
||||||
final containerId = await ref
|
final containerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabId);
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
if (!ref.mounted) return false;
|
if (!ref.mounted) return false;
|
||||||
|
|
||||||
@@ -234,7 +234,7 @@ class TabRepository extends _$TabRepository {
|
|||||||
|
|
||||||
final currentContainerId = await ref
|
final currentContainerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabId);
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
if (!ref.mounted) return;
|
if (!ref.mounted) return;
|
||||||
|
|
||||||
@@ -419,7 +419,7 @@ class TabRepository extends _$TabRepository {
|
|||||||
} else {
|
} else {
|
||||||
final tabContainerId = await ref
|
final tabContainerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabState.id);
|
.getTabContainerId(tabState.id);
|
||||||
|
|
||||||
if (targetContainerId != tabContainerId) {
|
if (targetContainerId != tabContainerId) {
|
||||||
if (originUri == null) {
|
if (originUri == null) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabRepositoryHash() => r'ef1116cbf09f456deae7a7d3f5cee3ba843d7430';
|
String _$tabRepositoryHash() => r'7af24e759044d1b1b471cdf90a3eb38eef06f61b';
|
||||||
|
|
||||||
abstract class _$TabRepository extends $Notifier<void> {
|
abstract class _$TabRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ class SelectedBangData extends _$SelectedBangData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
EquatableValue<List<String>> availableTabIds(
|
EquatableValue<List<DefaultTabEntity>> availableTabIds(
|
||||||
Ref ref,
|
Ref ref,
|
||||||
ContainerFilter containerFilter,
|
ContainerFilter containerFilter,
|
||||||
) {
|
) {
|
||||||
@@ -94,10 +94,37 @@ EquatableValue<List<String>> availableTabIds(
|
|||||||
);
|
);
|
||||||
final tabList = ref.watch(tabListProvider);
|
final tabList = ref.watch(tabListProvider);
|
||||||
|
|
||||||
return EquatableValue(
|
final availableTabs =
|
||||||
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
containerTabs?.where((tabId) => tabList.value.contains(tabId)).toList() ??
|
||||||
[],
|
[];
|
||||||
);
|
|
||||||
|
switch (containerFilter) {
|
||||||
|
case ContainerFilterById():
|
||||||
|
return EquatableValue(
|
||||||
|
availableTabs
|
||||||
|
.map(
|
||||||
|
(t) => DefaultTabEntity(
|
||||||
|
tabId: t,
|
||||||
|
containerId: containerFilter.containerId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
case ContainerFilterDisabled():
|
||||||
|
return ref.watch(
|
||||||
|
watchTabsContainerIdProvider(EquatableValue(availableTabs)).select(
|
||||||
|
(value) => EquatableValue(
|
||||||
|
value.value?.entries
|
||||||
|
.map(
|
||||||
|
(e) =>
|
||||||
|
DefaultTabEntity(tabId: e.key, containerId: e.value),
|
||||||
|
)
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
@@ -109,8 +136,9 @@ EquatableValue<Map<String, TabState>> availableTabStates(
|
|||||||
final tabStates = ref.watch(tabStatesProvider);
|
final tabStates = ref.watch(tabStatesProvider);
|
||||||
|
|
||||||
return EquatableValue({
|
return EquatableValue({
|
||||||
for (final tabId in availableTabs.value)
|
for (final tabEntity in availableTabs.value)
|
||||||
if (tabStates.containsKey(tabId)) tabId: tabStates[tabId]!,
|
if (tabStates.containsKey(tabEntity.tabId))
|
||||||
|
tabEntity.tabId: tabStates[tabEntity.tabId]!,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +199,12 @@ EquatableValue<List<TabEntity>> suggestedTabEntities(
|
|||||||
.whereNot(
|
.whereNot(
|
||||||
(tabId) => excludedTabIds.value?.contains(tabId) ?? false,
|
(tabId) => excludedTabIds.value?.contains(tabId) ?? false,
|
||||||
)
|
)
|
||||||
.map((tabId) => DefaultTabEntity(tabId: tabId))
|
.map(
|
||||||
|
(tabId) => DefaultTabEntity(
|
||||||
|
tabId: tabId,
|
||||||
|
containerId: containerId,
|
||||||
|
),
|
||||||
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
) ??
|
) ??
|
||||||
const [],
|
const [],
|
||||||
@@ -198,6 +231,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
|||||||
.map(
|
.map(
|
||||||
(tab) => SearchResultTabEntity(
|
(tab) => SearchResultTabEntity(
|
||||||
tabId: tab.id,
|
tabId: tab.id,
|
||||||
|
containerId: tab.containerId,
|
||||||
searchQuery: result.query,
|
searchQuery: result.query,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -208,13 +242,7 @@ EquatableValue<List<TabEntity>> seamlessFilteredTabEntities(
|
|||||||
)
|
)
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
final availableTabs = ref.watch(
|
final availableTabs = ref.watch(availableTabIdsProvider(containerFilter));
|
||||||
availableTabIdsProvider(containerFilter).select(
|
|
||||||
(value) => EquatableValue(
|
|
||||||
value.value.map((tab) => DefaultTabEntity(tabId: tab)).toList(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (tabSearchResults == null) {
|
if (tabSearchResults == null) {
|
||||||
if (groupTrees) {
|
if (groupTrees) {
|
||||||
|
|||||||
@@ -212,11 +212,11 @@ const availableTabIdsProvider = AvailableTabIdsFamily._();
|
|||||||
final class AvailableTabIdsProvider
|
final class AvailableTabIdsProvider
|
||||||
extends
|
extends
|
||||||
$FunctionalProvider<
|
$FunctionalProvider<
|
||||||
EquatableValue<List<String>>,
|
EquatableValue<List<DefaultTabEntity>>,
|
||||||
EquatableValue<List<String>>,
|
EquatableValue<List<DefaultTabEntity>>,
|
||||||
EquatableValue<List<String>>
|
EquatableValue<List<DefaultTabEntity>>
|
||||||
>
|
>
|
||||||
with $Provider<EquatableValue<List<String>>> {
|
with $Provider<EquatableValue<List<DefaultTabEntity>>> {
|
||||||
const AvailableTabIdsProvider._({
|
const AvailableTabIdsProvider._({
|
||||||
required AvailableTabIdsFamily super.from,
|
required AvailableTabIdsFamily super.from,
|
||||||
required ContainerFilter super.argument,
|
required ContainerFilter super.argument,
|
||||||
@@ -240,21 +240,22 @@ final class AvailableTabIdsProvider
|
|||||||
|
|
||||||
@$internal
|
@$internal
|
||||||
@override
|
@override
|
||||||
$ProviderElement<EquatableValue<List<String>>> $createElement(
|
$ProviderElement<EquatableValue<List<DefaultTabEntity>>> $createElement(
|
||||||
$ProviderPointer pointer,
|
$ProviderPointer pointer,
|
||||||
) => $ProviderElement(pointer);
|
) => $ProviderElement(pointer);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
EquatableValue<List<String>> create(Ref ref) {
|
EquatableValue<List<DefaultTabEntity>> create(Ref ref) {
|
||||||
final argument = this.argument as ContainerFilter;
|
final argument = this.argument as ContainerFilter;
|
||||||
return availableTabIds(ref, argument);
|
return availableTabIds(ref, argument);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// {@macro riverpod.override_with_value}
|
/// {@macro riverpod.override_with_value}
|
||||||
Override overrideWithValue(EquatableValue<List<String>> value) {
|
Override overrideWithValue(EquatableValue<List<DefaultTabEntity>> value) {
|
||||||
return $ProviderOverride(
|
return $ProviderOverride(
|
||||||
origin: this,
|
origin: this,
|
||||||
providerOverride: $SyncValueProvider<EquatableValue<List<String>>>(value),
|
providerOverride:
|
||||||
|
$SyncValueProvider<EquatableValue<List<DefaultTabEntity>>>(value),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,12 +270,12 @@ final class AvailableTabIdsProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$availableTabIdsHash() => r'aa166b471d45913df5166a3126abeb7c0d791412';
|
String _$availableTabIdsHash() => r'fa7280a7a5f9c277faa71a30f5dd50afa30a20b1';
|
||||||
|
|
||||||
final class AvailableTabIdsFamily extends $Family
|
final class AvailableTabIdsFamily extends $Family
|
||||||
with
|
with
|
||||||
$FunctionalFamilyOverride<
|
$FunctionalFamilyOverride<
|
||||||
EquatableValue<List<String>>,
|
EquatableValue<List<DefaultTabEntity>>,
|
||||||
ContainerFilter
|
ContainerFilter
|
||||||
> {
|
> {
|
||||||
const AvailableTabIdsFamily._()
|
const AvailableTabIdsFamily._()
|
||||||
@@ -358,7 +359,7 @@ final class AvailableTabStatesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$availableTabStatesHash() =>
|
String _$availableTabStatesHash() =>
|
||||||
r'f506b050e1dc4f50fbd2699d97af5a16a037bc4b';
|
r'32f15b0f739495e11025bdc10d324764465b542d';
|
||||||
|
|
||||||
final class AvailableTabStatesFamily extends $Family
|
final class AvailableTabStatesFamily extends $Family
|
||||||
with
|
with
|
||||||
@@ -497,7 +498,7 @@ final class SuggestedTabEntitiesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$suggestedTabEntitiesHash() =>
|
String _$suggestedTabEntitiesHash() =>
|
||||||
r'6fb58bf4b7377f826377c88110c8e4aca9f6d6f9';
|
r'1c3d9a4f85db60301fe5c5b55fb55512d1b59696';
|
||||||
|
|
||||||
final class SuggestedTabEntitiesFamily extends $Family
|
final class SuggestedTabEntitiesFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<EquatableValue<List<TabEntity>>, String?> {
|
with $FunctionalFamilyOverride<EquatableValue<List<TabEntity>>, String?> {
|
||||||
@@ -601,7 +602,7 @@ final class SeamlessFilteredTabEntitiesProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$seamlessFilteredTabEntitiesHash() =>
|
String _$seamlessFilteredTabEntitiesHash() =>
|
||||||
r'eb6573514f55b598fbc1a91370079b4b8372c55e';
|
r'f2bf14737bc03f71fc61252359979b4e901e5df7';
|
||||||
|
|
||||||
final class SeamlessFilteredTabEntitiesFamily extends $Family
|
final class SeamlessFilteredTabEntitiesFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
+25
-2
@@ -17,9 +17,14 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:riverpod/experimental/persist.dart';
|
||||||
|
import 'package:riverpod_annotation/experimental/persist.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
import 'package:weblibre/features/user/data/providers.dart';
|
||||||
|
|
||||||
part 'tab_view_controllers.g.dart';
|
part 'tab_view_controllers.g.dart';
|
||||||
|
|
||||||
@@ -37,11 +42,19 @@ class TabSuggestionsController extends _$TabSuggestionsController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool build() {
|
bool build() {
|
||||||
return false;
|
persist(
|
||||||
|
ref.watch(riverpodDatabaseStorageProvider),
|
||||||
|
key: 'TabSuggestions',
|
||||||
|
encode: (state) => jsonEncode([state]),
|
||||||
|
decode: (encoded) => (jsonDecode(encoded) as List<dynamic>).first as bool,
|
||||||
|
);
|
||||||
|
|
||||||
|
return stateOrNull ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum TabsViewMode {
|
enum TabsViewMode {
|
||||||
|
list(MdiIcons.folderTable, 'List'),
|
||||||
grid(MdiIcons.table, 'Grid'),
|
grid(MdiIcons.table, 'Grid'),
|
||||||
tree(MdiIcons.familyTree, 'Tree');
|
tree(MdiIcons.familyTree, 'Tree');
|
||||||
|
|
||||||
@@ -61,7 +74,17 @@ class TabsViewModeController extends _$TabsViewModeController {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
TabsViewMode build() {
|
TabsViewMode build() {
|
||||||
return TabsViewMode.grid;
|
persist(
|
||||||
|
ref.watch(riverpodDatabaseStorageProvider),
|
||||||
|
key: 'TabsViewMode',
|
||||||
|
encode: (state) => jsonEncode([state.name]),
|
||||||
|
decode: (encoded) {
|
||||||
|
final name = (jsonDecode(encoded) as List<dynamic>).first as String;
|
||||||
|
return TabsViewMode.values.firstWhere((e) => e.name == name);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return stateOrNull ?? TabsViewMode.list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -42,7 +42,7 @@ final class TabSuggestionsControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$tabSuggestionsControllerHash() =>
|
String _$tabSuggestionsControllerHash() =>
|
||||||
r'5ce7f385b8aba14d432912cf0acb06aad04433fc';
|
r'c60912e474177f2a0f15167743db439d157cd852';
|
||||||
|
|
||||||
abstract class _$TabSuggestionsController extends $Notifier<bool> {
|
abstract class _$TabSuggestionsController extends $Notifier<bool> {
|
||||||
bool build();
|
bool build();
|
||||||
@@ -96,7 +96,7 @@ final class TabsViewModeControllerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$tabsViewModeControllerHash() =>
|
String _$tabsViewModeControllerHash() =>
|
||||||
r'9c574458f6d5a4c6a8b5aabf96ad7f5dcd043dd0';
|
r'e013b174218fcad81981f7d939cf161c3b002adc';
|
||||||
|
|
||||||
abstract class _$TabsViewModeController extends $Notifier<TabsViewMode> {
|
abstract class _$TabsViewModeController extends $Notifier<TabsViewMode> {
|
||||||
TabsViewMode build();
|
TabsViewMode build();
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart';
|
import 'package:weblibre/utils/ui_helper.dart';
|
||||||
@@ -123,7 +123,7 @@ class TabTreeDialog extends HookConsumerWidget {
|
|||||||
final id = node.key!.value as String;
|
final id = node.key!.value as String;
|
||||||
return SizedBox.fromSize(
|
return SizedBox.fromSize(
|
||||||
size: childSize,
|
size: childSize,
|
||||||
child: SingleTabPreview(
|
child: SingleGridTabPreview(
|
||||||
key: ValueKey(id),
|
key: ValueKey(id),
|
||||||
tabId: id,
|
tabId: id,
|
||||||
activeTabId: selectedTabId,
|
activeTabId: selectedTabId,
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ import 'package:weblibre/features/geckoview/features/browser/presentation/widget
|
|||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_fab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/browser_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/sheets/view_tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
import 'package:weblibre/features/geckoview/features/contextmenu/extensions/hit_result.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/widgets/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||||
@@ -780,7 +782,18 @@ class _ViewTabsSheet extends HookConsumerWidget {
|
|||||||
topRight: Radius.circular(28),
|
topRight: Radius.circular(28),
|
||||||
),
|
),
|
||||||
child: switch (tabsViewMode) {
|
child: switch (tabsViewMode) {
|
||||||
TabsViewMode.grid => ViewTabsWidget(
|
TabsViewMode.list => ViewTabListWidget(
|
||||||
|
scrollController: scrollController,
|
||||||
|
showNewTabFab: true,
|
||||||
|
tabsReorderable: tabsReorderable,
|
||||||
|
draggableScrollableController: draggableScrollableController,
|
||||||
|
onClose: () {
|
||||||
|
ref
|
||||||
|
.read(bottomSheetControllerProvider.notifier)
|
||||||
|
.requestDismiss();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TabsViewMode.grid => ViewTabGridWidget(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
showNewTabFab: true,
|
showNewTabFab: true,
|
||||||
tabsReorderable: tabsReorderable,
|
tabsReorderable: tabsReorderable,
|
||||||
|
|||||||
@@ -23,7 +23,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_grid_view.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_list_view.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_tree_view.dart';
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
class TabViewScreen extends HookConsumerWidget {
|
class TabViewScreen extends HookConsumerWidget {
|
||||||
@@ -40,7 +42,16 @@ class TabViewScreen extends HookConsumerWidget {
|
|||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: switch (tabsViewMode) {
|
child: switch (tabsViewMode) {
|
||||||
TabsViewMode.grid => ViewTabsWidget(
|
TabsViewMode.list => ViewTabListWidget(
|
||||||
|
key: ValueKey(tabsReorderable),
|
||||||
|
scrollController: scrollController,
|
||||||
|
tabsReorderable: tabsReorderable,
|
||||||
|
showNewTabFab: false,
|
||||||
|
onClose: () {
|
||||||
|
const BrowserRoute().go(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
TabsViewMode.grid => ViewTabGridWidget(
|
||||||
key: ValueKey(tabsReorderable),
|
key: ValueKey(tabsReorderable),
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
tabsReorderable: tabsReorderable,
|
tabsReorderable: tabsReorderable,
|
||||||
|
|||||||
+457
@@ -0,0 +1,457 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_reorderable_grid_view/widgets/custom_draggable.dart';
|
||||||
|
import 'package:flutter_reorderable_grid_view/widgets/reorderable_builder.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:weblibre/core/providers/global_drop.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/data/models/drag_data.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
|
final TabEntity entity;
|
||||||
|
final String? suggestedContainerId;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const _TabDraggable({
|
||||||
|
required this.entity,
|
||||||
|
required this.onClose,
|
||||||
|
this.suggestedContainerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
final dragData = ref.watch(
|
||||||
|
willAcceptDropProvider.select((value) {
|
||||||
|
final dragTabId = switch (value) {
|
||||||
|
ContainerDropData() => value.tabId,
|
||||||
|
DeleteDropData() => value.tabId,
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (dragTabId == entity.tabId) ? value : null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache the tab widget to avoid rebuilding
|
||||||
|
final tab = useMemoized(() {
|
||||||
|
return (suggestedContainerId != null)
|
||||||
|
? SuggestedSingleGridTabPreview(
|
||||||
|
key: ValueKey(entity.tabId),
|
||||||
|
tabId: entity.tabId,
|
||||||
|
activeTabId: activeTab,
|
||||||
|
onTap: () async {
|
||||||
|
final containerData = await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.getContainerData(suggestedContainerId!);
|
||||||
|
|
||||||
|
if (containerData != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignContainer(entity.tabId, containerData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SingleGridTabPreview(
|
||||||
|
key: ValueKey(entity.tabId),
|
||||||
|
tabId: entity.tabId,
|
||||||
|
activeTabId: activeTab,
|
||||||
|
onClose: onClose,
|
||||||
|
sourceSearchQuery: switch (entity) {
|
||||||
|
DefaultTabEntity _ => null,
|
||||||
|
final SearchResultTabEntity entity => entity.searchQuery,
|
||||||
|
TabTreeEntity _ => throw UnimplementedError(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [entity.tabId, activeTab, suggestedContainerId]);
|
||||||
|
|
||||||
|
return switch (dragData) {
|
||||||
|
ContainerDropData() => Opacity(
|
||||||
|
opacity: 0.3,
|
||||||
|
child: Transform.scale(scale: 0.9, child: tab),
|
||||||
|
),
|
||||||
|
DeleteDropData() => Opacity(
|
||||||
|
opacity: 0.3,
|
||||||
|
child: ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
||||||
|
child: tab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null => tab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabGridView extends HookConsumerWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final bool tabsReorderable;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const _TabGridView({
|
||||||
|
required this.scrollController,
|
||||||
|
required this.tabsReorderable,
|
||||||
|
required this.onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
|
final filteredTabEntities = ref.watch(
|
||||||
|
seamlessFilteredTabEntitiesProvider(
|
||||||
|
searchPartition: TabSearchPartition.preview,
|
||||||
|
// ignore: document_ignores using fast equatable
|
||||||
|
// ignore: provider_parameters
|
||||||
|
containerFilter: ContainerFilterById(containerId: containerId),
|
||||||
|
groupTrees: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
||||||
|
|
||||||
|
final suggestedTabEntities = tabSuggestionsEnabled
|
||||||
|
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||||
|
: EquatableValue(<TabEntity>[]);
|
||||||
|
|
||||||
|
final itemCount =
|
||||||
|
filteredTabEntities.value.length +
|
||||||
|
//Limit to 3 sugegstions for now
|
||||||
|
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||||
|
|
||||||
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
final crossAxisCount = useMemoized(() {
|
||||||
|
final calculatedCount = calculateCrossAxisItemCount(
|
||||||
|
screenWidth: screenWidth,
|
||||||
|
horizontalPadding: 4.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return math.max(math.min(calculatedCount, itemCount), 2);
|
||||||
|
}, [screenWidth, itemCount]);
|
||||||
|
|
||||||
|
final itemSize = useMemoized(
|
||||||
|
() => calculateItemSize(
|
||||||
|
screenWidth: screenWidth,
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
horizontalPadding: 4.0,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
),
|
||||||
|
[screenWidth, crossAxisCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
final lastScroll = useRef<String?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
if (lastScroll.value != activeTab) {
|
||||||
|
final index = filteredTabEntities.value.indexWhere(
|
||||||
|
(entity) => entity.tabId == activeTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
final offset = (index ~/ 2) * itemSize.height;
|
||||||
|
|
||||||
|
if (offset != scrollController.offset) {
|
||||||
|
lastScroll.value = activeTab;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
scrollController.animateTo(
|
||||||
|
offset,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [filteredTabEntities, activeTab]);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: FadingScroll(
|
||||||
|
fadingSize: 5,
|
||||||
|
controller: scrollController,
|
||||||
|
builder: (context, controller) {
|
||||||
|
return !tabsReorderable
|
||||||
|
? _TabGrid(
|
||||||
|
key: ValueKey(crossAxisCount),
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
itemCount: itemCount,
|
||||||
|
scrollController: controller,
|
||||||
|
itemBuilder: (widget, _) {
|
||||||
|
if (widget is CustomDraggable) {
|
||||||
|
return LongPressDraggable(
|
||||||
|
feedback: Material(
|
||||||
|
color: Colors
|
||||||
|
.transparent, // removes white corners when having shadow
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.05,
|
||||||
|
child: SizedBox(
|
||||||
|
height: itemSize.height,
|
||||||
|
width: itemSize.width,
|
||||||
|
child: widget.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: widget.data,
|
||||||
|
childWhenDragging: SizedBox(
|
||||||
|
height: itemSize.height,
|
||||||
|
width: itemSize.width,
|
||||||
|
),
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget;
|
||||||
|
},
|
||||||
|
suggestedContainerId: containerId,
|
||||||
|
filteredTabEntities: filteredTabEntities,
|
||||||
|
suggestedTabEntities: suggestedTabEntities,
|
||||||
|
onClose: onClose,
|
||||||
|
)
|
||||||
|
: ReorderableBuilder.builder(
|
||||||
|
//Rebuild when cross axis count changes
|
||||||
|
key: ValueKey(crossAxisCount),
|
||||||
|
scrollController: controller,
|
||||||
|
itemCount: itemCount,
|
||||||
|
onDragStarted: (index) {
|
||||||
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
|
},
|
||||||
|
onReorderPositions: (positions) async {
|
||||||
|
assert(
|
||||||
|
positions.length == 1,
|
||||||
|
'Not ready for multiple reorders',
|
||||||
|
);
|
||||||
|
|
||||||
|
final oldIndex = positions.first.oldIndex;
|
||||||
|
final newIndex = positions.first.newIndex;
|
||||||
|
|
||||||
|
final containerRepository = ref.read(
|
||||||
|
containerRepositoryProvider.notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
//Suggestions are at the end and not reorderable, so skip
|
||||||
|
if (oldIndex >= filteredTabEntities.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
||||||
|
final containerId = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
if (newIndex <= 0) {
|
||||||
|
key = await containerRepository.getLeadingOrderKey(
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
} else if (newIndex >=
|
||||||
|
filteredTabEntities.value.length - 1) {
|
||||||
|
key = await containerRepository.getTrailingOrderKey(
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (newIndex < oldIndex) {
|
||||||
|
key = (await containerRepository.getOrderKeyAfterTab(
|
||||||
|
filteredTabEntities.value[newIndex - 1].tabId,
|
||||||
|
containerId,
|
||||||
|
))!;
|
||||||
|
} else {
|
||||||
|
key = await containerRepository.getOrderKeyBeforeTab(
|
||||||
|
filteredTabEntities.value[newIndex + 1].tabId,
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignOrderKey(tabId, key);
|
||||||
|
},
|
||||||
|
childBuilder: (itemBuilder) {
|
||||||
|
return _TabGrid(
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
itemCount: itemCount,
|
||||||
|
scrollController: controller,
|
||||||
|
itemBuilder: itemBuilder,
|
||||||
|
suggestedContainerId: containerId,
|
||||||
|
filteredTabEntities: filteredTabEntities,
|
||||||
|
suggestedTabEntities: suggestedTabEntities,
|
||||||
|
onClose: onClose,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabGrid extends StatelessWidget {
|
||||||
|
const _TabGrid({
|
||||||
|
super.key,
|
||||||
|
required this.crossAxisCount,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.itemCount,
|
||||||
|
required this.itemBuilder,
|
||||||
|
required this.suggestedContainerId,
|
||||||
|
required this.filteredTabEntities,
|
||||||
|
required this.suggestedTabEntities,
|
||||||
|
required this.onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
final int crossAxisCount;
|
||||||
|
final int itemCount;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
final String? suggestedContainerId;
|
||||||
|
final EquatableValue<List<TabEntity>> filteredTabEntities;
|
||||||
|
final EquatableValue<List<TabEntity>> suggestedTabEntities;
|
||||||
|
final Widget Function(Widget, int)? itemBuilder;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GridView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
),
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final Widget tab;
|
||||||
|
if (index < filteredTabEntities.value.length) {
|
||||||
|
final entity = filteredTabEntities.value[index];
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key(entity.tabId),
|
||||||
|
data: TabDragData(entity.tabId),
|
||||||
|
child: _TabDraggable(entity: entity, onClose: onClose),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final suggestedIndex = index - filteredTabEntities.value.length;
|
||||||
|
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key('suggested_${entity.tabId}'),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
suggestedContainerId: suggestedContainerId,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewTabGridWidget extends HookConsumerWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final DraggableScrollableController? draggableScrollableController;
|
||||||
|
final bool showNewTabFab;
|
||||||
|
final bool tabsReorderable;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const ViewTabGridWidget({
|
||||||
|
required this.onClose,
|
||||||
|
required this.scrollController,
|
||||||
|
this.draggableScrollableController,
|
||||||
|
required this.tabsReorderable,
|
||||||
|
required this.showNewTabFab,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
children: [
|
||||||
|
NestedScrollView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child:
|
||||||
|
draggableScrollableController.mapNotNull(
|
||||||
|
(draggableScrollableController) =>
|
||||||
|
DraggableScrollableHeader(
|
||||||
|
controller: draggableScrollableController,
|
||||||
|
child: TabViewHeader(
|
||||||
|
onClose: onClose,
|
||||||
|
tabsViewMode: TabsViewMode.grid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
TabViewHeader(
|
||||||
|
onClose: onClose,
|
||||||
|
tabsViewMode: TabsViewMode.grid,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: _TabGridView(
|
||||||
|
scrollController: scrollController,
|
||||||
|
tabsReorderable: tabsReorderable,
|
||||||
|
onClose: onClose,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showNewTabFab)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: TabViewHeader.headerSize + 4,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
onPressed: () async {
|
||||||
|
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
await SearchRoute(
|
||||||
|
tabType:
|
||||||
|
ref.read(selectedTabTypeProvider) ??
|
||||||
|
settings.defaultCreateTabType,
|
||||||
|
).push(context);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+425
@@ -0,0 +1,425 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:fading_scroll/fading_scroll.dart';
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_reorderable_grid_view/widgets/custom_draggable.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:nullability/nullability.dart';
|
||||||
|
import 'package:weblibre/core/providers/global_drop.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/data/models/drag_data.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
|
||||||
|
class _TabDraggable extends HookConsumerWidget {
|
||||||
|
final TabEntity entity;
|
||||||
|
final String? suggestedContainerId;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
final double height;
|
||||||
|
|
||||||
|
const _TabDraggable({
|
||||||
|
required this.entity,
|
||||||
|
required this.onClose,
|
||||||
|
required this.height,
|
||||||
|
this.suggestedContainerId,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
final dragData = ref.watch(
|
||||||
|
willAcceptDropProvider.select((value) {
|
||||||
|
final dragTabId = switch (value) {
|
||||||
|
ContainerDropData() => value.tabId,
|
||||||
|
DeleteDropData() => value.tabId,
|
||||||
|
null => null,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (dragTabId == entity.tabId) ? value : null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cache the tab widget to avoid rebuilding
|
||||||
|
final tab = useMemoized(() {
|
||||||
|
return (suggestedContainerId != null)
|
||||||
|
? SuggestedSingleListTabPreview(
|
||||||
|
key: ValueKey(entity.tabId),
|
||||||
|
tabId: entity.tabId,
|
||||||
|
activeTabId: activeTab,
|
||||||
|
onTap: () async {
|
||||||
|
final containerData = await ref
|
||||||
|
.read(containerRepositoryProvider.notifier)
|
||||||
|
.getContainerData(suggestedContainerId!);
|
||||||
|
|
||||||
|
if (containerData != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignContainer(entity.tabId, containerData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SingleListTabPreview(
|
||||||
|
key: ValueKey(entity.tabId),
|
||||||
|
tabId: entity.tabId,
|
||||||
|
activeTabId: activeTab,
|
||||||
|
onClose: onClose,
|
||||||
|
sourceSearchQuery: switch (entity) {
|
||||||
|
DefaultTabEntity _ => null,
|
||||||
|
final SearchResultTabEntity entity => entity.searchQuery,
|
||||||
|
TabTreeEntity _ => throw UnimplementedError(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, [entity.tabId, activeTab, suggestedContainerId]);
|
||||||
|
|
||||||
|
return switch (dragData) {
|
||||||
|
ContainerDropData() => Opacity(
|
||||||
|
opacity: 0.3,
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 0.9,
|
||||||
|
child: SizedBox(
|
||||||
|
height: height,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: tab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
DeleteDropData() => Opacity(
|
||||||
|
opacity: 0.3,
|
||||||
|
child: ColorFiltered(
|
||||||
|
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
||||||
|
child: tab,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null => tab,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TabListView extends HookConsumerWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final bool tabsReorderable;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const _TabListView({
|
||||||
|
required this.scrollController,
|
||||||
|
required this.tabsReorderable,
|
||||||
|
required this.onClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
static const _itemHeight = 80.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
// final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
final containerId = ref.watch(selectedContainerProvider);
|
||||||
|
|
||||||
|
final filteredTabEntities = ref.watch(
|
||||||
|
seamlessFilteredTabEntitiesProvider(
|
||||||
|
searchPartition: TabSearchPartition.preview,
|
||||||
|
// ignore: document_ignores using fast equatable
|
||||||
|
// ignore: provider_parameters
|
||||||
|
containerFilter: ContainerFilterById(containerId: containerId),
|
||||||
|
groupTrees: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
||||||
|
|
||||||
|
final suggestedTabEntities = tabSuggestionsEnabled
|
||||||
|
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
||||||
|
: EquatableValue(<TabEntity>[]);
|
||||||
|
|
||||||
|
final itemCount =
|
||||||
|
filteredTabEntities.value.length +
|
||||||
|
//Limit to 3 sugegstions for now
|
||||||
|
math.min<int>(suggestedTabEntities.value.length, 3);
|
||||||
|
|
||||||
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
final lastScroll = useRef<String?>(null);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (scrollController.hasClients) {
|
||||||
|
if (lastScroll.value != activeTab) {
|
||||||
|
final index = filteredTabEntities.value.indexWhere(
|
||||||
|
(entity) => entity.tabId == activeTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
final offset = index * _itemHeight;
|
||||||
|
|
||||||
|
if (offset != scrollController.offset) {
|
||||||
|
lastScroll.value = activeTab;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
scrollController.animateTo(
|
||||||
|
offset,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [filteredTabEntities, activeTab]);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: FadingScroll(
|
||||||
|
fadingSize: 5,
|
||||||
|
controller: scrollController,
|
||||||
|
builder: (context, controller) {
|
||||||
|
return !tabsReorderable
|
||||||
|
? ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
itemCount: itemCount,
|
||||||
|
itemExtent: _itemHeight,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final CustomDraggable tab;
|
||||||
|
|
||||||
|
if (index < filteredTabEntities.value.length) {
|
||||||
|
final entity = filteredTabEntities.value[index];
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key(entity.tabId),
|
||||||
|
data: TabDragData(entity.tabId),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final suggestedIndex =
|
||||||
|
index - filteredTabEntities.value.length;
|
||||||
|
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key('suggested_${entity.tabId}'),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
suggestedContainerId: containerId,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return LongPressDraggable(
|
||||||
|
feedback: Material(
|
||||||
|
color: Colors
|
||||||
|
.transparent, // removes white corners when having shadow
|
||||||
|
child: Transform.scale(
|
||||||
|
scale: 1.05,
|
||||||
|
child: SizedBox(
|
||||||
|
height: _itemHeight,
|
||||||
|
width: MediaQuery.of(context).size.width,
|
||||||
|
child: tab.child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
data: tab.data,
|
||||||
|
childWhenDragging: const SizedBox(height: _itemHeight),
|
||||||
|
child: tab.child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: ReorderableListView.builder(
|
||||||
|
scrollController: controller,
|
||||||
|
itemCount: itemCount,
|
||||||
|
onReorderStart: (index) {
|
||||||
|
ref.read(willAcceptDropProvider.notifier).clear();
|
||||||
|
},
|
||||||
|
onReorder: (oldIndex, newIndex) async {
|
||||||
|
final containerRepository = ref.read(
|
||||||
|
containerRepositoryProvider.notifier,
|
||||||
|
);
|
||||||
|
|
||||||
|
//Suggestions are at the end and not reorderable, so skip
|
||||||
|
if (oldIndex >= filteredTabEntities.value.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
||||||
|
final containerId = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
|
final String key;
|
||||||
|
if (newIndex <= 0) {
|
||||||
|
key = await containerRepository.getLeadingOrderKey(
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
} else if (newIndex >=
|
||||||
|
filteredTabEntities.value.length - 1) {
|
||||||
|
key = await containerRepository.getTrailingOrderKey(
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
if (newIndex < oldIndex) {
|
||||||
|
key = (await containerRepository.getOrderKeyAfterTab(
|
||||||
|
filteredTabEntities.value[newIndex - 1].tabId,
|
||||||
|
containerId,
|
||||||
|
))!;
|
||||||
|
} else {
|
||||||
|
key = await containerRepository.getOrderKeyBeforeTab(
|
||||||
|
filteredTabEntities.value[newIndex + 1].tabId,
|
||||||
|
containerId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.assignOrderKey(tabId, key);
|
||||||
|
},
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final CustomDraggable tab;
|
||||||
|
|
||||||
|
if (index < filteredTabEntities.value.length) {
|
||||||
|
final entity = filteredTabEntities.value[index];
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key(entity.tabId),
|
||||||
|
data: TabDragData(entity.tabId),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
final suggestedIndex =
|
||||||
|
index - filteredTabEntities.value.length;
|
||||||
|
final entity = suggestedTabEntities.value[suggestedIndex];
|
||||||
|
|
||||||
|
tab = CustomDraggable(
|
||||||
|
key: Key('suggested_${entity.tabId}'),
|
||||||
|
child: _TabDraggable(
|
||||||
|
entity: entity,
|
||||||
|
onClose: onClose,
|
||||||
|
suggestedContainerId: containerId,
|
||||||
|
height: _itemHeight,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// return LongPressDraggable(
|
||||||
|
// key: tab.key,
|
||||||
|
// feedback: Material(
|
||||||
|
// color: Colors
|
||||||
|
// .transparent, // removes white corners when having shadow
|
||||||
|
// child: Transform.scale(
|
||||||
|
// scale: 1.05,
|
||||||
|
// child: SizedBox(
|
||||||
|
// height: _itemHeight,
|
||||||
|
// width: MediaQuery.of(context).size.width,
|
||||||
|
// child: tab.child,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
// data: tab.data,
|
||||||
|
// child: tab.child,
|
||||||
|
// );
|
||||||
|
return tab;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewTabListWidget extends HookConsumerWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final DraggableScrollableController? draggableScrollableController;
|
||||||
|
final bool showNewTabFab;
|
||||||
|
final bool tabsReorderable;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const ViewTabListWidget({
|
||||||
|
required this.onClose,
|
||||||
|
required this.scrollController,
|
||||||
|
this.draggableScrollableController,
|
||||||
|
required this.tabsReorderable,
|
||||||
|
required this.showNewTabFab,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
children: [
|
||||||
|
NestedScrollView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
||||||
|
SliverToBoxAdapter(
|
||||||
|
child:
|
||||||
|
draggableScrollableController.mapNotNull(
|
||||||
|
(draggableScrollableController) =>
|
||||||
|
DraggableScrollableHeader(
|
||||||
|
controller: draggableScrollableController,
|
||||||
|
child: TabViewHeader(
|
||||||
|
onClose: onClose,
|
||||||
|
tabsViewMode: TabsViewMode.list,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
TabViewHeader(
|
||||||
|
onClose: onClose,
|
||||||
|
tabsViewMode: TabsViewMode.list,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
body: _TabListView(
|
||||||
|
scrollController: scrollController,
|
||||||
|
tabsReorderable: tabsReorderable,
|
||||||
|
onClose: onClose,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (showNewTabFab)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: TabViewHeader.headerSize + 4,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
onPressed: () async {
|
||||||
|
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
await SearchRoute(
|
||||||
|
tabType:
|
||||||
|
ref.read(selectedTabTypeProvider) ??
|
||||||
|
settings.defaultCreateTabType,
|
||||||
|
).push(context);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+296
-166
@@ -24,24 +24,22 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/domain/entities/find_in_page_state.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
import 'package:weblibre/features/geckoview/features/find_in_page/presentation/controllers/find_in_page.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/uri_breadcrumb.dart';
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class TabContainer extends StatelessWidget {
|
class GridTabItemContainer extends StatelessWidget {
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final bool isPrivate;
|
final bool isPrivate;
|
||||||
final Widget? child;
|
final Widget? child;
|
||||||
|
|
||||||
const TabContainer({
|
const GridTabItemContainer({
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.isPrivate,
|
required this.isPrivate,
|
||||||
this.child,
|
this.child,
|
||||||
@@ -71,38 +69,7 @@ class TabContainer extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabMiniPreview extends HookConsumerWidget {
|
class GridTabPreview extends HookConsumerWidget {
|
||||||
final String tabId;
|
|
||||||
|
|
||||||
const TabMiniPreview({super.key, required this.tabId});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final thumbnail = ref.watch(
|
|
||||||
tabStateProvider(tabId).select((value) => value?.thumbnail),
|
|
||||||
);
|
|
||||||
|
|
||||||
return TabContainer(
|
|
||||||
isActive: false,
|
|
||||||
isPrivate: false,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.all(Radius.circular(14.0)),
|
|
||||||
child: Skeleton.replace(
|
|
||||||
replace: thumbnail == null,
|
|
||||||
replacement: const Bone.square(size: double.infinity),
|
|
||||||
child: SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: RepaintBoundary(
|
|
||||||
child: RawImage(image: thumbnail?.value, fit: BoxFit.fitWidth),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class TabPreview extends HookConsumerWidget {
|
|
||||||
final String tabId;
|
final String tabId;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
|
||||||
@@ -114,7 +81,7 @@ class TabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final Widget? trailingChild;
|
final Widget? trailingChild;
|
||||||
|
|
||||||
const TabPreview({
|
const GridTabPreview({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -135,7 +102,7 @@ class TabPreview extends HookConsumerWidget {
|
|||||||
|
|
||||||
final extendedDeleteMenuController = useMenuController();
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
return TabContainer(
|
return GridTabItemContainer(
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
isPrivate: tabState.isPrivate,
|
isPrivate: tabState.isPrivate,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -258,7 +225,145 @@ class TabPreview extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingleTabPreview extends HookConsumerWidget {
|
class ListTabPreview extends HookConsumerWidget {
|
||||||
|
final String tabId;
|
||||||
|
final bool isActive;
|
||||||
|
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
final VoidCallback? onDelete;
|
||||||
|
final void Function(String host)? onDeleteAll;
|
||||||
|
|
||||||
|
final Widget? trailingChild;
|
||||||
|
|
||||||
|
const ListTabPreview({
|
||||||
|
required this.tabId,
|
||||||
|
required this.isActive,
|
||||||
|
this.onTap,
|
||||||
|
this.onLongPress,
|
||||||
|
this.onDelete,
|
||||||
|
this.onDeleteAll,
|
||||||
|
this.trailingChild,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
|
final tabState = ref.watch(tabStateProvider(tabId));
|
||||||
|
if (tabState == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final extendedDeleteMenuController = useMenuController();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: tabState.isPrivate ? const Color(0xFF25003E) : null,
|
||||||
|
border: isActive ? Border.all(color: colorScheme.primary) : null,
|
||||||
|
// borderRadius: const BorderRadius.all(Radius.circular(16.0)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListTile(
|
||||||
|
key: ValueKey(tabState.id),
|
||||||
|
onTap: onTap,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
contentPadding: const EdgeInsets.only(left: 4),
|
||||||
|
leading: (tabState.thumbnail != null)
|
||||||
|
? RepaintBoundary(
|
||||||
|
child: RawImage(
|
||||||
|
image: tabState.thumbnail!.value,
|
||||||
|
fit: BoxFit.fitHeight,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: TabIcon(tabState: tabState, iconSize: 32),
|
||||||
|
title: Text(
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
tabState.titleOrAuthority,
|
||||||
|
maxLines: 2,
|
||||||
|
style: tabState.isPrivate
|
||||||
|
? const TextStyle(color: Colors.white)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
subtitle: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: UriBreadcrumb(
|
||||||
|
uri: tabState.url,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: tabState.isPrivate ? Colors.white : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (tabState.isPrivate) ...[
|
||||||
|
const SizedBox(width: 6.0),
|
||||||
|
const SizedBox(
|
||||||
|
height: 16,
|
||||||
|
width: 24,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
top: -4,
|
||||||
|
child: Icon(
|
||||||
|
MdiIcons.dominoMask,
|
||||||
|
color: Color(0xFF8000D7),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (onDelete != null || onDeleteAll != null)
|
||||||
|
MenuAnchor(
|
||||||
|
controller: extendedDeleteMenuController,
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
onPressed: () {
|
||||||
|
onDeleteAll?.call(tabState.url.host);
|
||||||
|
},
|
||||||
|
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||||
|
child: Text('Close all from ${tabState.url.host}'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: onDelete,
|
||||||
|
onLongPress: onDeleteAll != null
|
||||||
|
? () {
|
||||||
|
if (extendedDeleteMenuController.isOpen) {
|
||||||
|
extendedDeleteMenuController.close();
|
||||||
|
} else {
|
||||||
|
extendedDeleteMenuController.open();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
?trailingChild,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SingleGridTabPreview extends HookConsumerWidget {
|
||||||
final String tabId;
|
final String tabId;
|
||||||
final String? activeTabId;
|
final String? activeTabId;
|
||||||
final String? sourceSearchQuery;
|
final String? sourceSearchQuery;
|
||||||
@@ -267,7 +372,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
|||||||
final void Function() onClose;
|
final void Function() onClose;
|
||||||
final void Function()? onBeforeDelete;
|
final void Function()? onBeforeDelete;
|
||||||
|
|
||||||
const SingleTabPreview({
|
const SingleGridTabPreview({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
required this.activeTabId,
|
required this.activeTabId,
|
||||||
required this.onClose,
|
required this.onClose,
|
||||||
@@ -319,7 +424,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
child: Opacity(
|
child: Opacity(
|
||||||
opacity: 1.0 - draggedDistance.value / deleteThreshold,
|
opacity: 1.0 - draggedDistance.value / deleteThreshold,
|
||||||
child: TabPreview(
|
child: GridTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: tabId == activeTabId,
|
isActive: tabId == activeTabId,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -341,7 +446,7 @@ class SingleTabPreview extends HookConsumerWidget {
|
|||||||
onDeleteAll: (host) async {
|
onDeleteAll: (host) async {
|
||||||
final containerId = await ref
|
final containerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabId);
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
final count = await ref
|
final count = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
@@ -382,14 +487,138 @@ class SingleTabPreview extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SuggestedSingleTabPreview extends StatelessWidget {
|
class SingleListTabPreview extends HookConsumerWidget {
|
||||||
|
final String tabId;
|
||||||
|
final String? activeTabId;
|
||||||
|
final String? sourceSearchQuery;
|
||||||
|
final double deleteThreshold;
|
||||||
|
|
||||||
|
final void Function() onClose;
|
||||||
|
final void Function()? onBeforeDelete;
|
||||||
|
|
||||||
|
const SingleListTabPreview({
|
||||||
|
required this.tabId,
|
||||||
|
required this.activeTabId,
|
||||||
|
required this.onClose,
|
||||||
|
required this.sourceSearchQuery,
|
||||||
|
this.deleteThreshold = 100,
|
||||||
|
this.onBeforeDelete,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final hasTabState = ref.watch(
|
||||||
|
tabStateProvider(tabId).select((value) => value != null),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasTabState) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final dragStartPosition = useRef(Offset.zero);
|
||||||
|
final draggedDistance = useState(0.0);
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onHorizontalDragStart: (details) {
|
||||||
|
dragStartPosition.value = details.globalPosition;
|
||||||
|
draggedDistance.value = 0.0;
|
||||||
|
},
|
||||||
|
onHorizontalDragUpdate: (details) {
|
||||||
|
final cappedDistance = math.min(
|
||||||
|
(dragStartPosition.value - details.globalPosition).dx.abs(),
|
||||||
|
deleteThreshold,
|
||||||
|
);
|
||||||
|
|
||||||
|
draggedDistance.value = cappedDistance;
|
||||||
|
},
|
||||||
|
onHorizontalDragEnd: (details) async {
|
||||||
|
if (draggedDistance.value >= deleteThreshold) {
|
||||||
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
draggedDistance.value = 0.0;
|
||||||
|
},
|
||||||
|
child: Opacity(
|
||||||
|
opacity: 1.0 - draggedDistance.value / deleteThreshold,
|
||||||
|
child: ListTabPreview(
|
||||||
|
tabId: tabId,
|
||||||
|
isActive: tabId == activeTabId,
|
||||||
|
onTap: () async {
|
||||||
|
if (tabId != activeTabId) {
|
||||||
|
//Close first to avoid rebuilds
|
||||||
|
onClose();
|
||||||
|
await ref.read(tabRepositoryProvider.notifier).selectTab(tabId);
|
||||||
|
if (sourceSearchQuery.isNotEmpty &&
|
||||||
|
ref.read(findInPageControllerProvider(tabId)) ==
|
||||||
|
FindInPageState.hidden()) {
|
||||||
|
await ref
|
||||||
|
.read(findInPageControllerProvider(tabId).notifier)
|
||||||
|
.findAll(text: sourceSearchQuery!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleteAll: (host) async {
|
||||||
|
final containerId = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerId(tabId);
|
||||||
|
|
||||||
|
final count = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.closeAllTabsByHost(containerId, host);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||||
|
count: count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// onDoubleTap: () {
|
||||||
|
// ref.read(overlayDialogControllerProvider.notifier).show(
|
||||||
|
// TabActionDialog(
|
||||||
|
// initialTab: tab,
|
||||||
|
// onDismiss:
|
||||||
|
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
onDelete: () async {
|
||||||
|
onBeforeDelete?.call();
|
||||||
|
|
||||||
|
await ref.read(tabRepositoryProvider.notifier).closeTab(tabId);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SuggestedSingleGridTabPreview extends StatelessWidget {
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
|
||||||
final String? activeTabId;
|
final String? activeTabId;
|
||||||
|
|
||||||
final void Function()? onTap;
|
final void Function()? onTap;
|
||||||
|
|
||||||
const SuggestedSingleTabPreview({
|
const SuggestedSingleGridTabPreview({
|
||||||
required this.tabId,
|
required this.tabId,
|
||||||
required this.activeTabId,
|
required this.activeTabId,
|
||||||
this.onTap,
|
this.onTap,
|
||||||
@@ -400,7 +629,7 @@ class SuggestedSingleTabPreview extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Opacity(
|
return Opacity(
|
||||||
opacity: 0.5,
|
opacity: 0.5,
|
||||||
child: TabPreview(
|
child: GridTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: tabId == activeTabId,
|
isActive: tabId == activeTabId,
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
@@ -414,133 +643,34 @@ class SuggestedSingleTabPreview extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabTreePreview extends HookConsumerWidget {
|
class SuggestedSingleListTabPreview extends StatelessWidget {
|
||||||
final TabTreeEntity entity;
|
final String tabId;
|
||||||
|
|
||||||
final String? activeTabId;
|
final String? activeTabId;
|
||||||
|
|
||||||
final Offset stackPadding;
|
final void Function()? onTap;
|
||||||
|
|
||||||
final void Function() onClose;
|
const SuggestedSingleListTabPreview({
|
||||||
|
required this.tabId,
|
||||||
TabTreePreview({
|
|
||||||
required this.entity,
|
|
||||||
required this.activeTabId,
|
required this.activeTabId,
|
||||||
required this.onClose,
|
this.onTap,
|
||||||
required this.stackPadding,
|
super.key,
|
||||||
}) : super(key: ValueKey(entity));
|
});
|
||||||
|
|
||||||
Widget _addPadding(int stackCount, int totalCount, Widget child) {
|
|
||||||
final topPadding = stackPadding * stackCount.toDouble();
|
|
||||||
final bottomPadding = stackPadding * (totalCount - stackCount).toDouble();
|
|
||||||
|
|
||||||
return Positioned(
|
|
||||||
top: topPadding.dy,
|
|
||||||
left: topPadding.dx,
|
|
||||||
right: bottomPadding.dx,
|
|
||||||
bottom: bottomPadding.dy,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context) {
|
||||||
final isTabPrivate = ref.watch(
|
return Opacity(
|
||||||
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
|
opacity: 0.5,
|
||||||
);
|
child: ListTabPreview(
|
||||||
|
tabId: tabId,
|
||||||
if (isTabPrivate == null) {
|
isActive: tabId == activeTabId,
|
||||||
return const SizedBox.shrink();
|
onTap: onTap,
|
||||||
}
|
trailingChild: const IconButton(
|
||||||
|
visualDensity: VisualDensity(horizontal: -4.0, vertical: -4.0),
|
||||||
final stackCount = math.min(entity.totalTabs, 3) - 1;
|
icon: Icon(MdiIcons.creation),
|
||||||
|
onPressed: null,
|
||||||
return Stack(
|
|
||||||
children: [
|
|
||||||
for (var index = 0; index < stackCount; index++)
|
|
||||||
_addPadding(
|
|
||||||
index,
|
|
||||||
stackCount,
|
|
||||||
TabContainer(
|
|
||||||
isActive: entity.tabId == activeTabId,
|
|
||||||
isPrivate: isTabPrivate == true,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
_addPadding(
|
|
||||||
stackCount,
|
|
||||||
stackCount,
|
|
||||||
Badge.count(
|
|
||||||
isLabelVisible: entity.totalTabs > 1,
|
|
||||||
count: entity.totalTabs,
|
|
||||||
alignment: AlignmentDirectional.bottomEnd,
|
|
||||||
offset: const Offset(-8, -24),
|
|
||||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
|
||||||
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
|
||||||
child: TabPreview(
|
|
||||||
tabId: entity.tabId,
|
|
||||||
isActive: entity.tabId == activeTabId,
|
|
||||||
onLongPress: () async {
|
|
||||||
if (entity.tabId != activeTabId) {
|
|
||||||
//Close first to avoid rebuilds
|
|
||||||
onClose();
|
|
||||||
await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.selectTab(entity.tabId);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onTap: () async {
|
|
||||||
if (entity.totalTabs > 1) {
|
|
||||||
await TabTreeRoute(entity.rootId).push(context);
|
|
||||||
} else if (entity.tabId != activeTabId) {
|
|
||||||
//Close first to avoid rebuilds
|
|
||||||
onClose();
|
|
||||||
await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.selectTab(entity.tabId);
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// onDeleteAll: (host) async {
|
|
||||||
// final containerId = await ref
|
|
||||||
// .read(tabDataRepositoryProvider.notifier)
|
|
||||||
// .containerTabId(tab.id);
|
|
||||||
|
|
||||||
// await ref
|
|
||||||
// .read(tabDataRepositoryProvider.notifier)
|
|
||||||
// .closeAllTabsByHost(containerId, host);
|
|
||||||
// },
|
|
||||||
// onDoubleTap: () {
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).show(
|
|
||||||
// TabActionDialog(
|
|
||||||
// initialTab: tab,
|
|
||||||
// onDismiss:
|
|
||||||
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
|
||||||
// ),
|
|
||||||
// );
|
|
||||||
// },
|
|
||||||
onDelete: () async {
|
|
||||||
final tabs = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.getTabDescendants(entity.rootId);
|
|
||||||
|
|
||||||
await ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.closeTabs(tabs.keys.toList());
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref.read(tabRepositoryProvider.notifier).undoClose,
|
|
||||||
count: tabs.length,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+299
@@ -0,0 +1,299 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_view_header.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
class _TabTreePreview extends HookConsumerWidget {
|
||||||
|
final TabTreeEntity entity;
|
||||||
|
final String? activeTabId;
|
||||||
|
|
||||||
|
final Offset stackPadding;
|
||||||
|
|
||||||
|
final void Function() onClose;
|
||||||
|
|
||||||
|
_TabTreePreview({
|
||||||
|
required this.entity,
|
||||||
|
required this.activeTabId,
|
||||||
|
required this.onClose,
|
||||||
|
required this.stackPadding,
|
||||||
|
}) : super(key: ValueKey(entity));
|
||||||
|
|
||||||
|
Widget _addPadding(int stackCount, int totalCount, Widget child) {
|
||||||
|
final topPadding = stackPadding * stackCount.toDouble();
|
||||||
|
final bottomPadding = stackPadding * (totalCount - stackCount).toDouble();
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
top: topPadding.dy,
|
||||||
|
left: topPadding.dx,
|
||||||
|
right: bottomPadding.dx,
|
||||||
|
bottom: bottomPadding.dy,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isTabPrivate = ref.watch(
|
||||||
|
tabStateProvider(entity.tabId).select((value) => value?.isPrivate),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isTabPrivate == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
|
||||||
|
final stackCount = math.min(entity.totalTabs, 3) - 1;
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
for (var index = 0; index < stackCount; index++)
|
||||||
|
_addPadding(
|
||||||
|
index,
|
||||||
|
stackCount,
|
||||||
|
GridTabItemContainer(
|
||||||
|
isActive: entity.tabId == activeTabId,
|
||||||
|
isPrivate: isTabPrivate == true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_addPadding(
|
||||||
|
stackCount,
|
||||||
|
stackCount,
|
||||||
|
Badge.count(
|
||||||
|
isLabelVisible: entity.totalTabs > 1,
|
||||||
|
count: entity.totalTabs,
|
||||||
|
alignment: AlignmentDirectional.bottomEnd,
|
||||||
|
offset: const Offset(-8, -24),
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||||
|
textColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||||
|
child: GridTabPreview(
|
||||||
|
tabId: entity.tabId,
|
||||||
|
isActive: entity.tabId == activeTabId,
|
||||||
|
onLongPress: () async {
|
||||||
|
if (entity.tabId != activeTabId) {
|
||||||
|
//Close first to avoid rebuilds
|
||||||
|
onClose();
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.selectTab(entity.tabId);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTap: () async {
|
||||||
|
if (entity.totalTabs > 1) {
|
||||||
|
await TabTreeRoute(entity.rootId).push(context);
|
||||||
|
} else if (entity.tabId != activeTabId) {
|
||||||
|
//Close first to avoid rebuilds
|
||||||
|
onClose();
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.selectTab(entity.tabId);
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// onDeleteAll: (host) async {
|
||||||
|
// final containerId = await ref
|
||||||
|
// .read(tabDataRepositoryProvider.notifier)
|
||||||
|
// .containerTabId(tab.id);
|
||||||
|
|
||||||
|
// await ref
|
||||||
|
// .read(tabDataRepositoryProvider.notifier)
|
||||||
|
// .closeAllTabsByHost(containerId, host);
|
||||||
|
// },
|
||||||
|
// onDoubleTap: () {
|
||||||
|
// ref.read(overlayDialogControllerProvider.notifier).show(
|
||||||
|
// TabActionDialog(
|
||||||
|
// initialTab: tab,
|
||||||
|
// onDismiss:
|
||||||
|
// ref.read(overlayDialogControllerProvider.notifier).dismiss,
|
||||||
|
// ),
|
||||||
|
// );
|
||||||
|
// },
|
||||||
|
onDelete: () async {
|
||||||
|
final tabs = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabDescendants(entity.rootId);
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.closeTabs(tabs.keys.toList());
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||||
|
count: tabs.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewTabTreesWidget extends HookConsumerWidget {
|
||||||
|
final ScrollController scrollController;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
final bool showNewTabFab;
|
||||||
|
|
||||||
|
const ViewTabTreesWidget({
|
||||||
|
required this.onClose,
|
||||||
|
required this.scrollController,
|
||||||
|
required this.showNewTabFab,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
|
||||||
|
Expanded(
|
||||||
|
child: HookConsumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final screenWidth = MediaQuery.of(context).size.width;
|
||||||
|
|
||||||
|
final filteredTabEntities = ref.watch(
|
||||||
|
seamlessFilteredTabEntitiesProvider(
|
||||||
|
searchPartition: TabSearchPartition.preview,
|
||||||
|
// ignore: document_ignores using fast equatable
|
||||||
|
// ignore: provider_parameters
|
||||||
|
containerFilter: ContainerFilterDisabled(),
|
||||||
|
groupTrees: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
final activeTab = ref.watch(selectedTabProvider);
|
||||||
|
|
||||||
|
final crossAxisCount = useMemoized(() {
|
||||||
|
final calculatedCount = calculateCrossAxisItemCount(
|
||||||
|
screenWidth: screenWidth,
|
||||||
|
horizontalPadding: 4.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
);
|
||||||
|
|
||||||
|
return math.max(
|
||||||
|
math.min(
|
||||||
|
calculatedCount,
|
||||||
|
filteredTabEntities.value.length,
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
);
|
||||||
|
}, [screenWidth, filteredTabEntities.value.length]);
|
||||||
|
|
||||||
|
final itemSize = useMemoized(
|
||||||
|
() => calculateItemSize(
|
||||||
|
screenWidth: screenWidth,
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
horizontalPadding: 4.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
),
|
||||||
|
[screenWidth, crossAxisCount],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() {
|
||||||
|
final index = filteredTabEntities.value.indexWhere(
|
||||||
|
(entity) => entity.tabId == activeTab,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
final offset = (index ~/ 2) * itemSize.height;
|
||||||
|
|
||||||
|
if (offset != scrollController.offset) {
|
||||||
|
unawaited(
|
||||||
|
scrollController.animateTo(
|
||||||
|
offset,
|
||||||
|
duration: const Duration(milliseconds: 200),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}, [filteredTabEntities, activeTab]);
|
||||||
|
|
||||||
|
final tabs = useMemoized(() {
|
||||||
|
return filteredTabEntities.value
|
||||||
|
.whereType<TabTreeEntity>()
|
||||||
|
.map((entity) {
|
||||||
|
return _TabTreePreview(
|
||||||
|
entity: entity,
|
||||||
|
activeTabId: activeTab,
|
||||||
|
onClose: onClose,
|
||||||
|
stackPadding: const Offset(8, 8),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.toList();
|
||||||
|
}, [filteredTabEntities, activeTab]);
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||||
|
child: GridView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
//Sync values for itemHeight calculation _calculateItemHeight
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
mainAxisSpacing: 8.0,
|
||||||
|
crossAxisSpacing: 8.0,
|
||||||
|
crossAxisCount: crossAxisCount,
|
||||||
|
),
|
||||||
|
itemCount: tabs.length,
|
||||||
|
itemBuilder: (context, index) => tabs[index],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (showNewTabFab)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(
|
||||||
|
top: TabViewHeader.headerSize + 4,
|
||||||
|
right: 4,
|
||||||
|
),
|
||||||
|
child: FloatingActionButton.small(
|
||||||
|
onPressed: () async {
|
||||||
|
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
await SearchRoute(
|
||||||
|
tabType:
|
||||||
|
ref.read(selectedTabTypeProvider) ??
|
||||||
|
settings.defaultCreateTabType,
|
||||||
|
).push(context);
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
},
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+371
@@ -0,0 +1,371 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
||||||
|
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
|
||||||
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
|
class TabViewHeader extends HookConsumerWidget {
|
||||||
|
static const headerSize = 124.0;
|
||||||
|
|
||||||
|
final TabsViewMode tabsViewMode;
|
||||||
|
final VoidCallback onClose;
|
||||||
|
|
||||||
|
const TabViewHeader({required this.onClose, required this.tabsViewMode});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final searchMode = useState(false);
|
||||||
|
final searchTextFocus = useFocusNode();
|
||||||
|
final searchTextController = useTextEditingController();
|
||||||
|
|
||||||
|
final viewModeMenuController = useMenuController();
|
||||||
|
final tabsActionMenuController = useMenuController();
|
||||||
|
|
||||||
|
final hasSearchText = useListenableSelector(
|
||||||
|
searchTextController,
|
||||||
|
() => searchTextController.text.isNotEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
|
final enableAiFeatures = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(settings) => settings.enableLocalAiFeatures,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useListenableCallback(searchTextController, () async {
|
||||||
|
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
||||||
|
await ref
|
||||||
|
.read(
|
||||||
|
tabSearchRepositoryProvider(TabSearchPartition.preview).notifier,
|
||||||
|
)
|
||||||
|
.addQuery(searchTextController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
|
||||||
|
child: Align(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0, left: 8.0, top: 8.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!searchMode.value)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(MdiIcons.tabSearch),
|
||||||
|
iconSize: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
switch (ref.read(tabsViewModeControllerProvider)) {
|
||||||
|
case TabsViewMode.tree:
|
||||||
|
case TabsViewMode.list:
|
||||||
|
break;
|
||||||
|
case TabsViewMode.grid:
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabsViewModeControllerProvider.notifier,
|
||||||
|
)
|
||||||
|
.set(TabsViewMode.list);
|
||||||
|
}
|
||||||
|
|
||||||
|
searchMode.value = true;
|
||||||
|
searchTextFocus.requestFocus();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(
|
||||||
|
height: 32,
|
||||||
|
child: VerticalDivider(indent: 4, endIndent: 4),
|
||||||
|
),
|
||||||
|
MenuAnchor(
|
||||||
|
controller: viewModeMenuController,
|
||||||
|
menuChildren: TabsViewMode.values
|
||||||
|
.map(
|
||||||
|
(mode) => MenuItemButton(
|
||||||
|
leadingIcon: Icon(mode.icon),
|
||||||
|
child: Text(mode.label),
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabsViewModeControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.set(mode);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (viewModeMenuController.isOpen) {
|
||||||
|
viewModeMenuController.close();
|
||||||
|
} else {
|
||||||
|
viewModeMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(tabsViewMode.icon, size: 18),
|
||||||
|
const Icon(Icons.arrow_drop_down, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (enableAiFeatures &&
|
||||||
|
switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
})
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tabSuggestionsEnabled = ref.watch(
|
||||||
|
tabSuggestionsControllerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton.filledTonal(
|
||||||
|
icon: const Icon(MdiIcons.imageAutoAdjust),
|
||||||
|
isSelected: tabSuggestionsEnabled,
|
||||||
|
iconSize: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabSuggestionsControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.toggle();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
if (switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
})
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final tabsReorderabe = ref.watch(
|
||||||
|
tabsReorderableControllerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton.filledTonal(
|
||||||
|
icon: const Icon(
|
||||||
|
MdiIcons.orderNumericAscending,
|
||||||
|
),
|
||||||
|
isSelected: tabsReorderabe,
|
||||||
|
iconSize: 18,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
tabsReorderableControllerProvider
|
||||||
|
.notifier,
|
||||||
|
)
|
||||||
|
.toggle();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
MenuAnchor(
|
||||||
|
controller: tabsActionMenuController,
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
||||||
|
child: const Text('Bookmark all'),
|
||||||
|
onPressed: () async {
|
||||||
|
final containerId = ref.read(
|
||||||
|
selectedContainerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
final tabData = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.getContainerTabsData(containerId);
|
||||||
|
|
||||||
|
for (final tab in tabData) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await BookmarkEntryAddRoute(
|
||||||
|
bookmarkInfo: jsonEncode(
|
||||||
|
BookmarkInfo(
|
||||||
|
title: tab.title,
|
||||||
|
url: tab.url.toString(),
|
||||||
|
).encode(),
|
||||||
|
),
|
||||||
|
).push(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
||||||
|
child: const Text('Close All'),
|
||||||
|
onPressed: () async {
|
||||||
|
final result = await showDialog<bool?>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
icon: const Icon(Icons.warning),
|
||||||
|
title: const Text('Close All Tabs'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to close all displayed tabs?',
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, false);
|
||||||
|
},
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, true);
|
||||||
|
},
|
||||||
|
child: const Text('Close'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result == true) {
|
||||||
|
final container = ref.read(
|
||||||
|
selectedContainerProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
final count = await ref
|
||||||
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
|
.closeAllTabsByContainer(container);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.undoClose,
|
||||||
|
count: count,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (tabsActionMenuController.isOpen) {
|
||||||
|
tabsActionMenuController.close();
|
||||||
|
} else {
|
||||||
|
tabsActionMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.dotsVertical),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
TextField(
|
||||||
|
controller: searchTextController,
|
||||||
|
focusNode: searchTextFocus,
|
||||||
|
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
// border: InputBorder.none,
|
||||||
|
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
||||||
|
hintText: 'Search inside tabs...',
|
||||||
|
floatingLabelBehavior: FloatingLabelBehavior.always,
|
||||||
|
suffixIcon: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (!hasSearchText)
|
||||||
|
SpeechToTextButton(
|
||||||
|
onTextReceived: (data) {
|
||||||
|
searchTextController.text = data.toString();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
searchTextController.clear();
|
||||||
|
searchTextFocus.requestFocus();
|
||||||
|
searchMode.value = false;
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (switch (tabsViewMode) {
|
||||||
|
TabsViewMode.list || TabsViewMode.grid => true,
|
||||||
|
TabsViewMode.tree => false,
|
||||||
|
})
|
||||||
|
Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final selectedContainer = ref.watch(
|
||||||
|
selectedContainerDataProvider.select(
|
||||||
|
(value) => value.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ContainerChips(
|
||||||
|
showGroupSuggestions: true,
|
||||||
|
selectedContainer: selectedContainer,
|
||||||
|
onSelected: (container) async {
|
||||||
|
if (container != null) {
|
||||||
|
final result = await ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.setContainerId(container.id);
|
||||||
|
|
||||||
|
if (context.mounted &&
|
||||||
|
result == SetContainerResult.successHasProxy) {
|
||||||
|
await ref
|
||||||
|
.read(startProxyControllerProvider.notifier)
|
||||||
|
.maybeStartProxy(context);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.clearContainer();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleted: (container) {
|
||||||
|
ref
|
||||||
|
.read(selectedContainerProvider.notifier)
|
||||||
|
.clearContainer();
|
||||||
|
},
|
||||||
|
onLongPress: (container) async {
|
||||||
|
await ContainerEditRoute(
|
||||||
|
containerData: jsonEncode(container.toJson()),
|
||||||
|
).push(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,988 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2024-2025 Fabian Freund.
|
|
||||||
*
|
|
||||||
* This file is part of WebLibre
|
|
||||||
* (see https://weblibre.eu).
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as
|
|
||||||
* published by the Free Software Foundation, either version 3 of the
|
|
||||||
* License, or (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
import 'dart:async';
|
|
||||||
import 'dart:convert';
|
|
||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:fading_scroll/fading_scroll.dart';
|
|
||||||
import 'package:fast_equatable/fast_equatable.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
|
||||||
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:nullability/nullability.dart';
|
|
||||||
import 'package:weblibre/core/providers/global_drop.dart';
|
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
|
||||||
import 'package:weblibre/data/models/drag_data.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/domain/repositories/tab.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_entity.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/providers/selected_container.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
|
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/container_chips.dart';
|
|
||||||
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.dart';
|
|
||||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
|
||||||
import 'package:weblibre/presentation/hooks/listenable_callback.dart';
|
|
||||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
|
||||||
import 'package:weblibre/presentation/widgets/speech_to_text_button.dart';
|
|
||||||
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
|
||||||
|
|
||||||
int calculateCrossAxisItemCount({
|
|
||||||
required double screenWidth,
|
|
||||||
required double horizontalPadding,
|
|
||||||
required double crossAxisSpacing,
|
|
||||||
}) {
|
|
||||||
final totalHorizontalPadding = horizontalPadding * 2;
|
|
||||||
final availableWidth =
|
|
||||||
screenWidth - totalHorizontalPadding - crossAxisSpacing;
|
|
||||||
|
|
||||||
final crossAxisCount = availableWidth ~/ 180.0;
|
|
||||||
|
|
||||||
return crossAxisCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
Size calculateItemSize({
|
|
||||||
required double screenWidth,
|
|
||||||
required double childAspectRatio,
|
|
||||||
required double horizontalPadding,
|
|
||||||
required double mainAxisSpacing,
|
|
||||||
required double crossAxisSpacing,
|
|
||||||
required int crossAxisCount,
|
|
||||||
}) {
|
|
||||||
final totalHorizontalPadding = horizontalPadding * 2;
|
|
||||||
final totalCrossAxisSpacing = crossAxisSpacing * (crossAxisCount - 1);
|
|
||||||
final availableWidth =
|
|
||||||
screenWidth - totalHorizontalPadding - totalCrossAxisSpacing;
|
|
||||||
final itemWidth = availableWidth / crossAxisCount;
|
|
||||||
final itemHeight = itemWidth / childAspectRatio;
|
|
||||||
|
|
||||||
return Size(itemWidth, itemHeight + mainAxisSpacing);
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabDraggable extends HookConsumerWidget {
|
|
||||||
final TabEntity entity;
|
|
||||||
final String? suggestedContainerId;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const _TabDraggable({
|
|
||||||
required this.entity,
|
|
||||||
required this.onClose,
|
|
||||||
this.suggestedContainerId,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
|
||||||
|
|
||||||
final dragData = ref.watch(
|
|
||||||
willAcceptDropProvider.select((value) {
|
|
||||||
final dragTabId = switch (value) {
|
|
||||||
ContainerDropData() => value.tabId,
|
|
||||||
DeleteDropData() => value.tabId,
|
|
||||||
null => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return (dragTabId == entity.tabId) ? value : null;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cache the tab widget to avoid rebuilding
|
|
||||||
final tab = useMemoized(() {
|
|
||||||
return (suggestedContainerId != null)
|
|
||||||
? SuggestedSingleTabPreview(
|
|
||||||
key: ValueKey(entity.tabId),
|
|
||||||
tabId: entity.tabId,
|
|
||||||
activeTabId: activeTab,
|
|
||||||
onTap: () async {
|
|
||||||
final containerData = await ref
|
|
||||||
.read(containerRepositoryProvider.notifier)
|
|
||||||
.getContainerData(suggestedContainerId!);
|
|
||||||
|
|
||||||
if (containerData != null) {
|
|
||||||
await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.assignContainer(entity.tabId, containerData);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
: SingleTabPreview(
|
|
||||||
key: ValueKey(entity.tabId),
|
|
||||||
tabId: entity.tabId,
|
|
||||||
activeTabId: activeTab,
|
|
||||||
onClose: onClose,
|
|
||||||
sourceSearchQuery: switch (entity) {
|
|
||||||
DefaultTabEntity _ => null,
|
|
||||||
final SearchResultTabEntity entity => entity.searchQuery,
|
|
||||||
TabTreeEntity _ => throw UnimplementedError(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, [entity.tabId, activeTab, suggestedContainerId]);
|
|
||||||
|
|
||||||
return switch (dragData) {
|
|
||||||
ContainerDropData() => Opacity(
|
|
||||||
opacity: 0.3,
|
|
||||||
child: Transform.scale(scale: 0.9, child: tab),
|
|
||||||
),
|
|
||||||
DeleteDropData() => Opacity(
|
|
||||||
opacity: 0.3,
|
|
||||||
child: ColorFiltered(
|
|
||||||
colorFilter: const ColorFilter.mode(Colors.red, BlendMode.modulate),
|
|
||||||
child: tab,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
null => tab,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabViewHeader extends HookConsumerWidget {
|
|
||||||
static const headerSize = 124.0;
|
|
||||||
|
|
||||||
final TabsViewMode tabsViewMode;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const _TabViewHeader({required this.onClose, required this.tabsViewMode});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final searchMode = useState(false);
|
|
||||||
final searchTextFocus = useFocusNode();
|
|
||||||
final searchTextController = useTextEditingController();
|
|
||||||
|
|
||||||
final viewModeMenuController = useMenuController();
|
|
||||||
final tabsActionMenuController = useMenuController();
|
|
||||||
|
|
||||||
final hasSearchText = useListenableSelector(
|
|
||||||
searchTextController,
|
|
||||||
() => searchTextController.text.isNotEmpty,
|
|
||||||
);
|
|
||||||
|
|
||||||
final enableAiFeatures = ref.watch(
|
|
||||||
generalSettingsWithDefaultsProvider.select(
|
|
||||||
(settings) => settings.enableLocalAiFeatures,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
useListenableCallback(searchTextController, () async {
|
|
||||||
if (ref.exists(tabSearchRepositoryProvider(TabSearchPartition.preview))) {
|
|
||||||
await ref
|
|
||||||
.read(
|
|
||||||
tabSearchRepositoryProvider(TabSearchPartition.preview).notifier,
|
|
||||||
)
|
|
||||||
.addQuery(searchTextController.text);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return Material(
|
|
||||||
//Fix layout issue https://github.com/flutter/flutter/issues/78748#issuecomment-1194680555
|
|
||||||
child: Align(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(right: 8.0, left: 8.0, top: 8.0),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (!searchMode.value)
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(MdiIcons.tabSearch),
|
|
||||||
iconSize: 18,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(tabsViewModeControllerProvider.notifier)
|
|
||||||
.set(TabsViewMode.grid);
|
|
||||||
|
|
||||||
searchMode.value = true;
|
|
||||||
searchTextFocus.requestFocus();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(
|
|
||||||
height: 32,
|
|
||||||
child: VerticalDivider(indent: 4, endIndent: 4),
|
|
||||||
),
|
|
||||||
MenuAnchor(
|
|
||||||
controller: viewModeMenuController,
|
|
||||||
menuChildren: TabsViewMode.values
|
|
||||||
.map(
|
|
||||||
(mode) => MenuItemButton(
|
|
||||||
leadingIcon: Icon(mode.icon),
|
|
||||||
child: Text(mode.label),
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabsViewModeControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.set(mode);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.toList(),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (viewModeMenuController.isOpen) {
|
|
||||||
viewModeMenuController.close();
|
|
||||||
} else {
|
|
||||||
viewModeMenuController.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(tabsViewMode.icon, size: 18),
|
|
||||||
const Icon(Icons.arrow_drop_down, size: 18),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (enableAiFeatures)
|
|
||||||
Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final tabSuggestionsEnabled = ref.watch(
|
|
||||||
tabSuggestionsControllerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
return IconButton.filledTonal(
|
|
||||||
icon: const Icon(MdiIcons.imageAutoAdjust),
|
|
||||||
isSelected: tabSuggestionsEnabled,
|
|
||||||
iconSize: 18,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabSuggestionsControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.toggle();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final tabsReorderabe = ref.watch(
|
|
||||||
tabsReorderableControllerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
return IconButton.filledTonal(
|
|
||||||
icon: const Icon(MdiIcons.orderNumericAscending),
|
|
||||||
isSelected: tabsReorderabe,
|
|
||||||
iconSize: 18,
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: () {
|
|
||||||
ref
|
|
||||||
.read(
|
|
||||||
tabsReorderableControllerProvider
|
|
||||||
.notifier,
|
|
||||||
)
|
|
||||||
.toggle();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
MenuAnchor(
|
|
||||||
controller: tabsActionMenuController,
|
|
||||||
menuChildren: [
|
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
|
|
||||||
child: const Text('Bookmark all'),
|
|
||||||
onPressed: () async {
|
|
||||||
final containerId = ref.read(
|
|
||||||
selectedContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
final tabData = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.getContainerTabsData(containerId);
|
|
||||||
|
|
||||||
for (final tab in tabData) {
|
|
||||||
if (context.mounted) {
|
|
||||||
await BookmarkEntryAddRoute(
|
|
||||||
bookmarkInfo: jsonEncode(
|
|
||||||
BookmarkInfo(
|
|
||||||
title: tab.title,
|
|
||||||
url: tab.url.toString(),
|
|
||||||
).encode(),
|
|
||||||
),
|
|
||||||
).push(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
MenuItemButton(
|
|
||||||
leadingIcon: const Icon(MdiIcons.closeBoxMultiple),
|
|
||||||
child: const Text('Close All'),
|
|
||||||
onPressed: () async {
|
|
||||||
final result = await showDialog<bool?>(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
icon: const Icon(Icons.warning),
|
|
||||||
title: const Text('Close All Tabs'),
|
|
||||||
content: const Text(
|
|
||||||
'Are you sure you want to close all displayed tabs?',
|
|
||||||
),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, false);
|
|
||||||
},
|
|
||||||
child: const Text('Cancel'),
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context, true);
|
|
||||||
},
|
|
||||||
child: const Text('Close'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
final container = ref.read(
|
|
||||||
selectedContainerProvider,
|
|
||||||
);
|
|
||||||
|
|
||||||
final count = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.closeAllTabsByContainer(container);
|
|
||||||
|
|
||||||
if (context.mounted) {
|
|
||||||
ui_helper.showTabUndoClose(
|
|
||||||
context,
|
|
||||||
ref
|
|
||||||
.read(tabRepositoryProvider.notifier)
|
|
||||||
.undoClose,
|
|
||||||
count: count,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
if (tabsActionMenuController.isOpen) {
|
|
||||||
tabsActionMenuController.close();
|
|
||||||
} else {
|
|
||||||
tabsActionMenuController.open();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
icon: const Icon(MdiIcons.dotsVertical),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
else
|
|
||||||
TextField(
|
|
||||||
controller: searchTextController,
|
|
||||||
focusNode: searchTextFocus,
|
|
||||||
// enableIMEPersonalizedLearning: !incognitoEnabled,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
// border: InputBorder.none,
|
|
||||||
prefixIcon: const Icon(MdiIcons.tabSearch, size: 18),
|
|
||||||
hintText: 'Search inside tabs...',
|
|
||||||
floatingLabelBehavior: FloatingLabelBehavior.always,
|
|
||||||
suffixIcon: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
if (!hasSearchText)
|
|
||||||
SpeechToTextButton(
|
|
||||||
onTextReceived: (data) {
|
|
||||||
searchTextController.text = data.toString();
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {
|
|
||||||
searchTextController.clear();
|
|
||||||
searchTextFocus.requestFocus();
|
|
||||||
searchMode.value = false;
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.clear),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (tabsViewMode == TabsViewMode.grid)
|
|
||||||
Consumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final selectedContainer = ref.watch(
|
|
||||||
selectedContainerDataProvider.select(
|
|
||||||
(value) => value.value,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ContainerChips(
|
|
||||||
showGroupSuggestions: true,
|
|
||||||
selectedContainer: selectedContainer,
|
|
||||||
onSelected: (container) async {
|
|
||||||
if (container != null) {
|
|
||||||
final result = await ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.setContainerId(container.id);
|
|
||||||
|
|
||||||
if (context.mounted &&
|
|
||||||
result == SetContainerResult.successHasProxy) {
|
|
||||||
await ref
|
|
||||||
.read(startProxyControllerProvider.notifier)
|
|
||||||
.maybeStartProxy(context);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.clearContainer();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDeleted: (container) {
|
|
||||||
ref
|
|
||||||
.read(selectedContainerProvider.notifier)
|
|
||||||
.clearContainer();
|
|
||||||
},
|
|
||||||
onLongPress: (container) async {
|
|
||||||
await ContainerEditRoute(
|
|
||||||
containerData: jsonEncode(container.toJson()),
|
|
||||||
).push(context);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabView extends HookConsumerWidget {
|
|
||||||
final ScrollController scrollController;
|
|
||||||
final bool tabsReorderable;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const _TabView({
|
|
||||||
required this.scrollController,
|
|
||||||
required this.tabsReorderable,
|
|
||||||
required this.onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
|
|
||||||
final containerId = ref.watch(selectedContainerProvider);
|
|
||||||
|
|
||||||
final filteredTabEntities = ref.watch(
|
|
||||||
seamlessFilteredTabEntitiesProvider(
|
|
||||||
searchPartition: TabSearchPartition.preview,
|
|
||||||
// ignore: document_ignores using fast equatable
|
|
||||||
// ignore: provider_parameters
|
|
||||||
containerFilter: ContainerFilterById(containerId: containerId),
|
|
||||||
groupTrees: false,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final tabSuggestionsEnabled = ref.watch(tabSuggestionsControllerProvider);
|
|
||||||
|
|
||||||
final suggestedTabEntities = tabSuggestionsEnabled
|
|
||||||
? ref.watch(suggestedTabEntitiesProvider(containerId))
|
|
||||||
: EquatableValue(<TabEntity>[]);
|
|
||||||
|
|
||||||
final itemCount =
|
|
||||||
filteredTabEntities.value.length +
|
|
||||||
//Limit to 3 sugegstions for now
|
|
||||||
math.min<int>(suggestedTabEntities.value.length, 3);
|
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
|
||||||
|
|
||||||
final crossAxisCount = useMemoized(() {
|
|
||||||
final calculatedCount = calculateCrossAxisItemCount(
|
|
||||||
screenWidth: screenWidth,
|
|
||||||
horizontalPadding: 4.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return math.max(math.min(calculatedCount, itemCount), 2);
|
|
||||||
}, [screenWidth, itemCount]);
|
|
||||||
|
|
||||||
final itemSize = useMemoized(
|
|
||||||
() => calculateItemSize(
|
|
||||||
screenWidth: screenWidth,
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
horizontalPadding: 4.0,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
),
|
|
||||||
[screenWidth, crossAxisCount],
|
|
||||||
);
|
|
||||||
|
|
||||||
final lastScroll = useRef<String?>(null);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
if (scrollController.hasClients) {
|
|
||||||
if (lastScroll.value != activeTab) {
|
|
||||||
final index = filteredTabEntities.value.indexWhere(
|
|
||||||
(entity) => entity.tabId == activeTab,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
final offset = (index ~/ 2) * itemSize.height;
|
|
||||||
|
|
||||||
if (offset != scrollController.offset) {
|
|
||||||
lastScroll.value = activeTab;
|
|
||||||
|
|
||||||
unawaited(
|
|
||||||
scrollController.animateTo(
|
|
||||||
offset,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [filteredTabEntities, activeTab]);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
||||||
child: FadingScroll(
|
|
||||||
fadingSize: 5,
|
|
||||||
controller: scrollController,
|
|
||||||
builder: (context, controller) {
|
|
||||||
return !tabsReorderable
|
|
||||||
? _TabGrid(
|
|
||||||
key: ValueKey(crossAxisCount),
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
itemCount: itemCount,
|
|
||||||
scrollController: controller,
|
|
||||||
itemBuilder: (widget, _) {
|
|
||||||
if (widget is CustomDraggable) {
|
|
||||||
return LongPressDraggable(
|
|
||||||
feedback: Material(
|
|
||||||
color: Colors
|
|
||||||
.transparent, // removes white corners when having shadow
|
|
||||||
child: Transform.scale(
|
|
||||||
scale: 1.05,
|
|
||||||
child: SizedBox(
|
|
||||||
height: itemSize.height,
|
|
||||||
width: itemSize.width,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
data: widget.data,
|
|
||||||
child: widget.child,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return widget;
|
|
||||||
},
|
|
||||||
suggestedContainerId: containerId,
|
|
||||||
filteredTabEntities: filteredTabEntities,
|
|
||||||
suggestedTabEntities: suggestedTabEntities,
|
|
||||||
onClose: onClose,
|
|
||||||
)
|
|
||||||
: ReorderableBuilder.builder(
|
|
||||||
//Rebuild when cross axis count changes
|
|
||||||
key: ValueKey(crossAxisCount),
|
|
||||||
scrollController: controller,
|
|
||||||
itemCount: itemCount,
|
|
||||||
onDragStarted: (index) {
|
|
||||||
ref.read(willAcceptDropProvider.notifier).clear();
|
|
||||||
},
|
|
||||||
onReorderPositions: (positions) async {
|
|
||||||
assert(
|
|
||||||
positions.length == 1,
|
|
||||||
'Not ready for multiple reorders',
|
|
||||||
);
|
|
||||||
|
|
||||||
final oldIndex = positions.first.oldIndex;
|
|
||||||
final newIndex = positions.first.newIndex;
|
|
||||||
|
|
||||||
final containerRepository = ref.read(
|
|
||||||
containerRepositoryProvider.notifier,
|
|
||||||
);
|
|
||||||
|
|
||||||
//Suggestions are at the end and not reorderable, so skip
|
|
||||||
if (oldIndex >= filteredTabEntities.value.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final tabId = filteredTabEntities.value[oldIndex].tabId;
|
|
||||||
final containerId = await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.getContainerTabId(tabId);
|
|
||||||
|
|
||||||
final String key;
|
|
||||||
if (newIndex <= 0) {
|
|
||||||
key = await containerRepository.getLeadingOrderKey(
|
|
||||||
containerId,
|
|
||||||
);
|
|
||||||
} else if (newIndex >=
|
|
||||||
filteredTabEntities.value.length - 1) {
|
|
||||||
key = await containerRepository.getTrailingOrderKey(
|
|
||||||
containerId,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
if (newIndex < oldIndex) {
|
|
||||||
key = (await containerRepository.getOrderKeyAfterTab(
|
|
||||||
filteredTabEntities.value[newIndex - 1].tabId,
|
|
||||||
containerId,
|
|
||||||
))!;
|
|
||||||
} else {
|
|
||||||
key = await containerRepository.getOrderKeyBeforeTab(
|
|
||||||
filteredTabEntities.value[newIndex + 1].tabId,
|
|
||||||
containerId,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await ref
|
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
|
||||||
.assignOrderKey(tabId, key);
|
|
||||||
},
|
|
||||||
childBuilder: (itemBuilder) {
|
|
||||||
return _TabGrid(
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
itemCount: itemCount,
|
|
||||||
scrollController: controller,
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
suggestedContainerId: containerId,
|
|
||||||
filteredTabEntities: filteredTabEntities,
|
|
||||||
suggestedTabEntities: suggestedTabEntities,
|
|
||||||
onClose: onClose,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _TabGrid extends StatelessWidget {
|
|
||||||
const _TabGrid({
|
|
||||||
super.key,
|
|
||||||
required this.crossAxisCount,
|
|
||||||
required this.scrollController,
|
|
||||||
required this.itemCount,
|
|
||||||
required this.itemBuilder,
|
|
||||||
required this.suggestedContainerId,
|
|
||||||
required this.filteredTabEntities,
|
|
||||||
required this.suggestedTabEntities,
|
|
||||||
required this.onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
final int crossAxisCount;
|
|
||||||
final int itemCount;
|
|
||||||
final ScrollController? scrollController;
|
|
||||||
final String? suggestedContainerId;
|
|
||||||
final EquatableValue<List<TabEntity>> filteredTabEntities;
|
|
||||||
final EquatableValue<List<TabEntity>> suggestedTabEntities;
|
|
||||||
final Widget Function(Widget, int)? itemBuilder;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return GridView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
),
|
|
||||||
itemCount: itemCount,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final Widget tab;
|
|
||||||
if (index < filteredTabEntities.value.length) {
|
|
||||||
final entity = filteredTabEntities.value[index];
|
|
||||||
tab = CustomDraggable(
|
|
||||||
key: Key(entity.tabId),
|
|
||||||
data: TabDragData(entity.tabId),
|
|
||||||
child: _TabDraggable(entity: entity, onClose: onClose),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
final suggestedIndex = index - filteredTabEntities.value.length;
|
|
||||||
final entity = suggestedTabEntities.value[suggestedIndex];
|
|
||||||
|
|
||||||
tab = CustomDraggable(
|
|
||||||
key: Key('suggested_${entity.tabId}'),
|
|
||||||
child: _TabDraggable(
|
|
||||||
entity: entity,
|
|
||||||
onClose: onClose,
|
|
||||||
suggestedContainerId: suggestedContainerId,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (itemBuilder != null) ? itemBuilder!(tab, index) : tab;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewTabsWidget extends HookConsumerWidget {
|
|
||||||
final ScrollController scrollController;
|
|
||||||
final DraggableScrollableController? draggableScrollableController;
|
|
||||||
final bool showNewTabFab;
|
|
||||||
final bool tabsReorderable;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
|
|
||||||
const ViewTabsWidget({
|
|
||||||
required this.onClose,
|
|
||||||
required this.scrollController,
|
|
||||||
this.draggableScrollableController,
|
|
||||||
required this.tabsReorderable,
|
|
||||||
required this.showNewTabFab,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
children: [
|
|
||||||
NestedScrollView(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
headerSliverBuilder: (context, innerBoxIsScrolled) => [
|
|
||||||
SliverToBoxAdapter(
|
|
||||||
child:
|
|
||||||
draggableScrollableController.mapNotNull(
|
|
||||||
(draggableScrollableController) =>
|
|
||||||
DraggableScrollableHeader(
|
|
||||||
controller: draggableScrollableController,
|
|
||||||
child: _TabViewHeader(
|
|
||||||
onClose: onClose,
|
|
||||||
tabsViewMode: TabsViewMode.grid,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
) ??
|
|
||||||
_TabViewHeader(
|
|
||||||
onClose: onClose,
|
|
||||||
tabsViewMode: TabsViewMode.grid,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
body: _TabView(
|
|
||||||
scrollController: scrollController,
|
|
||||||
tabsReorderable: tabsReorderable,
|
|
||||||
onClose: onClose,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showNewTabFab)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: _TabViewHeader.headerSize + 4,
|
|
||||||
right: 4,
|
|
||||||
),
|
|
||||||
child: FloatingActionButton.small(
|
|
||||||
onPressed: () async {
|
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
|
||||||
|
|
||||||
await SearchRoute(
|
|
||||||
tabType:
|
|
||||||
ref.read(selectedTabTypeProvider) ??
|
|
||||||
settings.defaultCreateTabType,
|
|
||||||
).push(context);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ViewTabTreesWidget extends HookConsumerWidget {
|
|
||||||
final ScrollController scrollController;
|
|
||||||
final VoidCallback onClose;
|
|
||||||
final bool showNewTabFab;
|
|
||||||
|
|
||||||
const ViewTabTreesWidget({
|
|
||||||
required this.onClose,
|
|
||||||
required this.scrollController,
|
|
||||||
required this.showNewTabFab,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
|
||||||
return Stack(
|
|
||||||
alignment: Alignment.bottomRight,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
_TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
|
|
||||||
Expanded(
|
|
||||||
child: HookConsumer(
|
|
||||||
builder: (context, ref, child) {
|
|
||||||
final screenWidth = MediaQuery.of(context).size.width;
|
|
||||||
|
|
||||||
final filteredTabEntities = ref.watch(
|
|
||||||
seamlessFilteredTabEntitiesProvider(
|
|
||||||
searchPartition: TabSearchPartition.preview,
|
|
||||||
// ignore: document_ignores using fast equatable
|
|
||||||
// ignore: provider_parameters
|
|
||||||
containerFilter: ContainerFilterDisabled(),
|
|
||||||
groupTrees: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
final activeTab = ref.watch(selectedTabProvider);
|
|
||||||
|
|
||||||
final crossAxisCount = useMemoized(() {
|
|
||||||
final calculatedCount = calculateCrossAxisItemCount(
|
|
||||||
screenWidth: screenWidth,
|
|
||||||
horizontalPadding: 4.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return math.max(
|
|
||||||
math.min(
|
|
||||||
calculatedCount,
|
|
||||||
filteredTabEntities.value.length,
|
|
||||||
),
|
|
||||||
2,
|
|
||||||
);
|
|
||||||
}, [screenWidth, filteredTabEntities.value.length]);
|
|
||||||
|
|
||||||
final itemSize = useMemoized(
|
|
||||||
() => calculateItemSize(
|
|
||||||
screenWidth: screenWidth,
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
horizontalPadding: 4.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
),
|
|
||||||
[screenWidth, crossAxisCount],
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() {
|
|
||||||
final index = filteredTabEntities.value.indexWhere(
|
|
||||||
(entity) => entity.tabId == activeTab,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (index > -1) {
|
|
||||||
final offset = (index ~/ 2) * itemSize.height;
|
|
||||||
|
|
||||||
if (offset != scrollController.offset) {
|
|
||||||
unawaited(
|
|
||||||
scrollController.animateTo(
|
|
||||||
offset,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}, [filteredTabEntities, activeTab]);
|
|
||||||
|
|
||||||
final tabs = useMemoized(() {
|
|
||||||
return filteredTabEntities.value
|
|
||||||
.whereType<TabTreeEntity>()
|
|
||||||
.map((entity) {
|
|
||||||
return TabTreePreview(
|
|
||||||
entity: entity,
|
|
||||||
activeTabId: activeTab,
|
|
||||||
onClose: onClose,
|
|
||||||
stackPadding: const Offset(8, 8),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
}, [filteredTabEntities, activeTab]);
|
|
||||||
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
|
||||||
child: GridView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
||||||
//Sync values for itemHeight calculation _calculateItemHeight
|
|
||||||
childAspectRatio: 0.75,
|
|
||||||
mainAxisSpacing: 8.0,
|
|
||||||
crossAxisSpacing: 8.0,
|
|
||||||
crossAxisCount: crossAxisCount,
|
|
||||||
),
|
|
||||||
itemCount: tabs.length,
|
|
||||||
itemBuilder: (context, index) => tabs[index],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (showNewTabFab)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(
|
|
||||||
top: _TabViewHeader.headerSize + 4,
|
|
||||||
right: 4,
|
|
||||||
),
|
|
||||||
child: FloatingActionButton.small(
|
|
||||||
onPressed: () async {
|
|
||||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
|
||||||
|
|
||||||
await SearchRoute(
|
|
||||||
tabType:
|
|
||||||
ref.read(selectedTabTypeProvider) ??
|
|
||||||
settings.defaultCreateTabType,
|
|
||||||
).push(context);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
},
|
|
||||||
child: const Icon(Icons.add),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
int calculateCrossAxisItemCount({
|
||||||
|
required double screenWidth,
|
||||||
|
required double horizontalPadding,
|
||||||
|
required double crossAxisSpacing,
|
||||||
|
}) {
|
||||||
|
final totalHorizontalPadding = horizontalPadding * 2;
|
||||||
|
final availableWidth =
|
||||||
|
screenWidth - totalHorizontalPadding - crossAxisSpacing;
|
||||||
|
|
||||||
|
final crossAxisCount = availableWidth ~/ 180.0;
|
||||||
|
|
||||||
|
return crossAxisCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
Size calculateItemSize({
|
||||||
|
required double screenWidth,
|
||||||
|
required double childAspectRatio,
|
||||||
|
required double horizontalPadding,
|
||||||
|
required double mainAxisSpacing,
|
||||||
|
required double crossAxisSpacing,
|
||||||
|
required int crossAxisCount,
|
||||||
|
}) {
|
||||||
|
final totalHorizontalPadding = horizontalPadding * 2;
|
||||||
|
final totalCrossAxisSpacing = crossAxisSpacing * (crossAxisCount - 1);
|
||||||
|
final availableWidth =
|
||||||
|
screenWidth - totalHorizontalPadding - totalCrossAxisSpacing;
|
||||||
|
final itemWidth = availableWidth / crossAxisCount;
|
||||||
|
final itemHeight = itemWidth / childAspectRatio;
|
||||||
|
|
||||||
|
return Size(itemWidth, itemHeight + mainAxisSpacing);
|
||||||
|
}
|
||||||
@@ -17,11 +17,10 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
import 'package:flutter_mozilla_components/flutter_mozilla_components.dart';
|
||||||
import 'package:riverpod/experimental/persist.dart';
|
import 'package:riverpod/experimental/persist.dart';
|
||||||
|
import 'package:riverpod_annotation/experimental/json_persist.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/entities/history_filter_options.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
import 'package:weblibre/features/geckoview/features/history/domain/repositories/history.dart';
|
||||||
@@ -30,6 +29,7 @@ import 'package:weblibre/features/user/data/providers.dart';
|
|||||||
part 'providers.g.dart';
|
part 'providers.g.dart';
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
|
@JsonPersist()
|
||||||
class HistoryFilter extends _$HistoryFilter {
|
class HistoryFilter extends _$HistoryFilter {
|
||||||
void updateVisitType(VisitType type, bool value) {
|
void updateVisitType(VisitType type, bool value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -49,15 +49,9 @@ class HistoryFilter extends _$HistoryFilter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
HistoryFilterOptions build() {
|
HistoryFilterOptions build() {
|
||||||
final riverpodStorage = ref.watch(riverpodDatabaseStorageProvider);
|
|
||||||
|
|
||||||
persist(
|
persist(
|
||||||
riverpodStorage,
|
ref.watch(riverpodDatabaseStorageProvider),
|
||||||
key: 'HistoryFilterOptions',
|
key: 'HistoryFilterOptions',
|
||||||
decode: (encoded) => HistoryFilterOptions.fromJson(
|
|
||||||
jsonDecode(encoded) as Map<String, dynamic>,
|
|
||||||
),
|
|
||||||
encode: (state) => jsonEncode(state.toJson()),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return stateOrNull ?? HistoryFilterOptions.withDefaults();
|
return stateOrNull ?? HistoryFilterOptions.withDefaults();
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ part of 'providers.dart';
|
|||||||
// ignore_for_file: type=lint, type=warning
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
|
||||||
@ProviderFor(HistoryFilter)
|
@ProviderFor(HistoryFilter)
|
||||||
|
@JsonPersist()
|
||||||
const historyFilterProvider = HistoryFilterProvider._();
|
const historyFilterProvider = HistoryFilterProvider._();
|
||||||
|
|
||||||
|
@JsonPersist()
|
||||||
final class HistoryFilterProvider
|
final class HistoryFilterProvider
|
||||||
extends $NotifierProvider<HistoryFilter, HistoryFilterOptions> {
|
extends $NotifierProvider<HistoryFilter, HistoryFilterOptions> {
|
||||||
const HistoryFilterProvider._()
|
const HistoryFilterProvider._()
|
||||||
@@ -41,9 +43,10 @@ final class HistoryFilterProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$historyFilterHash() => r'1f8269e5f1227d6efde194e00c3d79a268ab1e4d';
|
String _$historyFilterHash() => r'685b92590011ec450ef0c19820e4a6750d26b79d';
|
||||||
|
|
||||||
abstract class _$HistoryFilter extends $Notifier<HistoryFilterOptions> {
|
@JsonPersist()
|
||||||
|
abstract class _$HistoryFilterBase extends $Notifier<HistoryFilterOptions> {
|
||||||
HistoryFilterOptions build();
|
HistoryFilterOptions build();
|
||||||
@$mustCallSuper
|
@$mustCallSuper
|
||||||
@override
|
@override
|
||||||
@@ -100,3 +103,40 @@ final class BrowsingHistoryProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$browsingHistoryHash() => r'3b6ee5853387481d7544bc9733741a5519f84f40';
|
String _$browsingHistoryHash() => r'3b6ee5853387481d7544bc9733741a5519f84f40';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
abstract class _$HistoryFilter extends _$HistoryFilterBase {
|
||||||
|
/// The default key used by [persist].
|
||||||
|
String get key {
|
||||||
|
const resolvedKey = "HistoryFilter";
|
||||||
|
return resolvedKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A variant of [persist], for JSON-specific encoding.
|
||||||
|
///
|
||||||
|
/// You can override [key] to customize the key used for storage.
|
||||||
|
PersistResult persist(
|
||||||
|
FutureOr<Storage<String, String>> storage, {
|
||||||
|
String? key,
|
||||||
|
String Function(HistoryFilterOptions state)? encode,
|
||||||
|
HistoryFilterOptions Function(String encoded)? decode,
|
||||||
|
StorageOptions options = const StorageOptions(),
|
||||||
|
}) {
|
||||||
|
return NotifierPersistX(this).persist<String, String>(
|
||||||
|
storage,
|
||||||
|
key: key ?? this.key,
|
||||||
|
encode: encode ?? $jsonCodex.encode,
|
||||||
|
decode:
|
||||||
|
decode ??
|
||||||
|
(encoded) {
|
||||||
|
final e = $jsonCodex.decode(encoded);
|
||||||
|
return HistoryFilterOptions.fromJson(e as Map<String, Object?>);
|
||||||
|
},
|
||||||
|
options: options,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -65,6 +65,18 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
|
|||||||
return query.map((row) => row.read(db.tab.containerId));
|
return query.map((row) => row.read(db.tab.containerId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Selectable<MapEntry<String, String?>> getTabsContainerId(
|
||||||
|
Iterable<String> tabIds,
|
||||||
|
) {
|
||||||
|
final query = selectOnly(db.tab)
|
||||||
|
..addColumns([db.tab.id, db.tab.containerId])
|
||||||
|
..where(db.tab.id.isIn(tabIds));
|
||||||
|
|
||||||
|
return query.map(
|
||||||
|
(row) => MapEntry(row.read(db.tab.id)!, row.read(db.tab.containerId)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<String> _generateOrderKey({
|
Future<String> _generateOrderKey({
|
||||||
required Value<String?> parentId,
|
required Value<String?> parentId,
|
||||||
required Value<String?> containerId,
|
required Value<String?> containerId,
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ queryTabsBasic WITH TabQueryResult:
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
|
t.container_id,
|
||||||
t.title,
|
t.title,
|
||||||
CAST(t.url AS TEXT) AS url,
|
CAST(t.url AS TEXT) AS url,
|
||||||
t.url AS clean_url,
|
t.url AS clean_url,
|
||||||
@@ -166,6 +167,7 @@ queryTabsFullContent WITH TabQueryResult:
|
|||||||
)
|
)
|
||||||
SELECT
|
SELECT
|
||||||
t.id,
|
t.id,
|
||||||
|
t.container_id,
|
||||||
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
highlight(tab_fts, 0, :beforeMatch, :afterMatch) AS title,
|
||||||
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
highlight(tab_fts, 1, :beforeMatch, :afterMatch) AS url,
|
||||||
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
snippet(tab_fts, 2, :beforeMatch, :afterMatch, :ellipsis, :snippetLength) AS extracted_content,
|
||||||
|
|||||||
@@ -2161,12 +2161,13 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
|||||||
|
|
||||||
i0.Selectable<i8.TabQueryResult> queryTabsBasic({required String query}) {
|
i0.Selectable<i8.TabQueryResult> queryTabsBasic({required String query}) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC',
|
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight) SELECT t.id, t.container_id, t.title, CAST(t.url AS TEXT) AS url, t.url AS clean_url, bm25(tab_fts, weights.title_weight, weights.url_weight) AS weighted_rank FROM tab_fts AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights WHERE fts.title LIKE ?1 OR fts.url LIKE ?1 ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||||
variables: [i0.Variable<String>(query)],
|
variables: [i0.Variable<String>(query)],
|
||||||
readsFrom: {tab, tabFts},
|
readsFrom: {tab, tabFts},
|
||||||
).map(
|
).map(
|
||||||
(i0.QueryRow row) => i8.TabQueryResult(
|
(i0.QueryRow row) => i8.TabQueryResult(
|
||||||
id: row.read<String>('id'),
|
id: row.read<String>('id'),
|
||||||
|
containerId: row.readNullable<String>('container_id'),
|
||||||
title: row.readNullable<String>('title'),
|
title: row.readNullable<String>('title'),
|
||||||
url: row.readNullable<String>('url'),
|
url: row.readNullable<String>('url'),
|
||||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
cleanUrl: i3.Tab.$converterurl.fromSql(
|
||||||
@@ -2185,7 +2186,7 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
|||||||
required String query,
|
required String query,
|
||||||
}) {
|
}) {
|
||||||
return customSelect(
|
return customSelect(
|
||||||
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
|
'WITH weights AS (SELECT 10.0 AS title_weight, 5.0 AS url_weight, 3.0 AS extracted_weight, 1.0 AS full_weight) SELECT t.id, t.container_id, highlight(tab_fts, 0, ?1, ?2) AS title, highlight(tab_fts, 1, ?1, ?2) AS url, snippet(tab_fts, 2, ?1, ?2, ?3, ?4) AS extracted_content, snippet(tab_fts, 3, ?1, ?2, ?3, ?4) AS full_content, t.url AS clean_url,(bm25(tab_fts, weights.title_weight, weights.url_weight, weights.extracted_weight, weights.full_weight))AS weighted_rank FROM tab_fts(?5)AS fts INNER JOIN tab AS t ON t."rowid" = fts."rowid" CROSS JOIN weights ORDER BY weighted_rank ASC, t.timestamp DESC',
|
||||||
variables: [
|
variables: [
|
||||||
i0.Variable<String>(beforeMatch),
|
i0.Variable<String>(beforeMatch),
|
||||||
i0.Variable<String>(afterMatch),
|
i0.Variable<String>(afterMatch),
|
||||||
@@ -2197,6 +2198,7 @@ class DefinitionsDrift extends i7.ModularAccessor {
|
|||||||
).map(
|
).map(
|
||||||
(i0.QueryRow row) => i8.TabQueryResult(
|
(i0.QueryRow row) => i8.TabQueryResult(
|
||||||
id: row.read<String>('id'),
|
id: row.read<String>('id'),
|
||||||
|
containerId: row.readNullable<String>('container_id'),
|
||||||
title: row.readNullable<String>('title'),
|
title: row.readNullable<String>('title'),
|
||||||
url: row.readNullable<String>('url'),
|
url: row.readNullable<String>('url'),
|
||||||
cleanUrl: i3.Tab.$converterurl.fromSql(
|
cleanUrl: i3.Tab.$converterurl.fromSql(
|
||||||
|
|||||||
@@ -26,23 +26,29 @@ sealed class TabEntity with FastEquatable {
|
|||||||
class DefaultTabEntity extends TabEntity {
|
class DefaultTabEntity extends TabEntity {
|
||||||
@override
|
@override
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
final String? containerId;
|
||||||
|
|
||||||
DefaultTabEntity({required this.tabId});
|
DefaultTabEntity({required this.tabId, required this.containerId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabId];
|
List<Object?> get hashParameters => [tabId, containerId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchResultTabEntity extends TabEntity {
|
class SearchResultTabEntity extends TabEntity {
|
||||||
@override
|
@override
|
||||||
final String tabId;
|
final String tabId;
|
||||||
|
final String? containerId;
|
||||||
|
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
|
|
||||||
SearchResultTabEntity({required this.tabId, required this.searchQuery});
|
SearchResultTabEntity({
|
||||||
|
required this.tabId,
|
||||||
|
required this.searchQuery,
|
||||||
|
required this.containerId,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [tabId, searchQuery];
|
List<Object?> get hashParameters => [tabId, searchQuery, containerId];
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabTreeEntity extends TabEntity {
|
class TabTreeEntity extends TabEntity {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import 'package:fast_equatable/fast_equatable.dart';
|
|||||||
|
|
||||||
class TabQueryResult with FastEquatable {
|
class TabQueryResult with FastEquatable {
|
||||||
final String id;
|
final String id;
|
||||||
|
final String? containerId;
|
||||||
|
|
||||||
final String? title;
|
final String? title;
|
||||||
final Uri? cleanUrl;
|
final Uri? cleanUrl;
|
||||||
@@ -33,6 +34,7 @@ class TabQueryResult with FastEquatable {
|
|||||||
|
|
||||||
TabQueryResult({
|
TabQueryResult({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
required this.containerId,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.url,
|
required this.url,
|
||||||
required this.cleanUrl,
|
required this.cleanUrl,
|
||||||
@@ -44,6 +46,7 @@ class TabQueryResult with FastEquatable {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get hashParameters => [
|
List<Object?> get hashParameters => [
|
||||||
id,
|
id,
|
||||||
|
containerId,
|
||||||
title,
|
title,
|
||||||
cleanUrl,
|
cleanUrl,
|
||||||
url,
|
url,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
* You should have received a copy of the GNU Affero General Public License
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
import 'package:fast_equatable/fast_equatable.dart';
|
||||||
import 'package:nullability/nullability.dart';
|
import 'package:nullability/nullability.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';
|
||||||
@@ -129,6 +130,19 @@ Stream<String?> watchContainerTabId(Ref ref, String tabId) {
|
|||||||
.watchSingleOrNull();
|
.watchSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Riverpod()
|
||||||
|
Stream<Map<String, String?>> watchTabsContainerId(
|
||||||
|
Ref ref,
|
||||||
|
EquatableValue<List<String>> tabIds,
|
||||||
|
) {
|
||||||
|
return ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.getTabsContainerId(tabIds.value)
|
||||||
|
.watch()
|
||||||
|
.map(Map.fromEntries);
|
||||||
|
}
|
||||||
|
|
||||||
@Riverpod()
|
@Riverpod()
|
||||||
Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
Stream<List<SiteAssignment>> watchAllAssignedSites(Ref ref) {
|
||||||
return ref.read(tabDatabaseProvider).containerDao.allAssignedSites().watch();
|
return ref.read(tabDatabaseProvider).containerDao.allAssignedSites().watch();
|
||||||
|
|||||||
@@ -671,6 +671,88 @@ final class WatchContainerTabIdFamily extends $Family
|
|||||||
String toString() => r'watchContainerTabIdProvider';
|
String toString() => r'watchContainerTabIdProvider';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ProviderFor(watchTabsContainerId)
|
||||||
|
const watchTabsContainerIdProvider = WatchTabsContainerIdFamily._();
|
||||||
|
|
||||||
|
final class WatchTabsContainerIdProvider
|
||||||
|
extends
|
||||||
|
$FunctionalProvider<
|
||||||
|
AsyncValue<Map<String, String?>>,
|
||||||
|
Map<String, String?>,
|
||||||
|
Stream<Map<String, String?>>
|
||||||
|
>
|
||||||
|
with
|
||||||
|
$FutureModifier<Map<String, String?>>,
|
||||||
|
$StreamProvider<Map<String, String?>> {
|
||||||
|
const WatchTabsContainerIdProvider._({
|
||||||
|
required WatchTabsContainerIdFamily super.from,
|
||||||
|
required EquatableValue<List<String>> super.argument,
|
||||||
|
}) : super(
|
||||||
|
retry: null,
|
||||||
|
name: r'watchTabsContainerIdProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$watchTabsContainerIdHash();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return r'watchTabsContainerIdProvider'
|
||||||
|
''
|
||||||
|
'($argument)';
|
||||||
|
}
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$StreamProviderElement<Map<String, String?>> $createElement(
|
||||||
|
$ProviderPointer pointer,
|
||||||
|
) => $StreamProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Map<String, String?>> create(Ref ref) {
|
||||||
|
final argument = this.argument as EquatableValue<List<String>>;
|
||||||
|
return watchTabsContainerId(ref, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is WatchTabsContainerIdProvider && other.argument == argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return argument.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$watchTabsContainerIdHash() =>
|
||||||
|
r'ffdebb10f7118cdff93695c821ef5fbb0d434de1';
|
||||||
|
|
||||||
|
final class WatchTabsContainerIdFamily extends $Family
|
||||||
|
with
|
||||||
|
$FunctionalFamilyOverride<
|
||||||
|
Stream<Map<String, String?>>,
|
||||||
|
EquatableValue<List<String>>
|
||||||
|
> {
|
||||||
|
const WatchTabsContainerIdFamily._()
|
||||||
|
: super(
|
||||||
|
retry: null,
|
||||||
|
name: r'watchTabsContainerIdProvider',
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
isAutoDispose: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
WatchTabsContainerIdProvider call(EquatableValue<List<String>> tabIds) =>
|
||||||
|
WatchTabsContainerIdProvider._(argument: tabIds, from: this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => r'watchTabsContainerIdProvider';
|
||||||
|
}
|
||||||
|
|
||||||
@ProviderFor(watchAllAssignedSites)
|
@ProviderFor(watchAllAssignedSites)
|
||||||
const watchAllAssignedSitesProvider = WatchAllAssignedSitesProvider._();
|
const watchAllAssignedSitesProvider = WatchAllAssignedSitesProvider._();
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ class SelectedContainer extends _$SelectedContainer {
|
|||||||
if (next != null) {
|
if (next != null) {
|
||||||
final tabContainerId = await ref
|
final tabContainerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(next);
|
.getTabContainerId(next);
|
||||||
|
|
||||||
if (ref.mounted && tabContainerId != stateOrNull) {
|
if (ref.mounted && tabContainerId != stateOrNull) {
|
||||||
if (tabContainerId != null) {
|
if (tabContainerId != null) {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class SelectedContainerProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$selectedContainerHash() => r'fd41c7d7ff0cfaff2829fd93e2afd41d883b8317';
|
String _$selectedContainerHash() => r'35a8fc409e563758156a197bb8b114387d31b389';
|
||||||
|
|
||||||
abstract class _$SelectedContainer extends $Notifier<String?> {
|
abstract class _$SelectedContainer extends $Notifier<String?> {
|
||||||
String? build();
|
String? build();
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
ContainerData targetContainer, {
|
ContainerData targetContainer, {
|
||||||
bool closeOldTab = true,
|
bool closeOldTab = true,
|
||||||
}) async {
|
}) async {
|
||||||
final currentContainerId = await getContainerTabId(tabId);
|
final currentContainerId = await getTabContainerId(tabId);
|
||||||
|
|
||||||
final currentContainerData = await currentContainerId.mapNotNull(
|
final currentContainerData = await currentContainerId.mapNotNull(
|
||||||
(containerId) => ref
|
(containerId) => ref
|
||||||
@@ -118,7 +118,7 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.get();
|
.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<String?> getContainerTabId(String tabId) {
|
Future<String?> getTabContainerId(String tabId) {
|
||||||
return ref
|
return ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
.tabDao
|
.tabDao
|
||||||
@@ -126,6 +126,15 @@ class TabDataRepository extends _$TabDataRepository {
|
|||||||
.getSingleOrNull();
|
.getSingleOrNull();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Map<String, String?>> getTabsContainerId(Iterable<String> tabIds) {
|
||||||
|
return ref
|
||||||
|
.read(tabDatabaseProvider)
|
||||||
|
.tabDao
|
||||||
|
.getTabsContainerId(tabIds)
|
||||||
|
.get()
|
||||||
|
.then(Map.fromEntries);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Map<String, String?>> getTabDescendants(String tabId) async {
|
Future<Map<String, String?>> getTabDescendants(String tabId) async {
|
||||||
final results = await ref
|
final results = await ref
|
||||||
.read(tabDatabaseProvider)
|
.read(tabDatabaseProvider)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$tabDataRepositoryHash() => r'e12750bc1962878a5bf5140d97521d642001994f';
|
String _$tabDataRepositoryHash() => r'3d9149efb88f256cc826eb538f13331b49438a7c';
|
||||||
|
|
||||||
abstract class _$TabDataRepository extends $Notifier<void> {
|
abstract class _$TabDataRepository extends $Notifier<void> {
|
||||||
void build();
|
void build();
|
||||||
|
|||||||
+4
-4
@@ -30,8 +30,8 @@ import 'package:skeletonizer/skeletonizer.dart';
|
|||||||
import 'package:weblibre/core/logger.dart';
|
import 'package:weblibre/core/logger.dart';
|
||||||
import 'package:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
import 'package:weblibre/core/uuid.dart';
|
import 'package:weblibre/core/uuid.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_preview.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_view/tab_preview.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/view_tabs.dart';
|
import 'package:weblibre/features/geckoview/features/browser/utils/grid_calculations.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
|
||||||
@@ -147,7 +147,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
|||||||
return (selectedContainerTabs.value[equatable]
|
return (selectedContainerTabs.value[equatable]
|
||||||
?.contains(tabId) ==
|
?.contains(tabId) ==
|
||||||
true)
|
true)
|
||||||
? TabPreview(
|
? GridTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
onDelete: () {
|
onDelete: () {
|
||||||
@@ -160,7 +160,7 @@ class ContainerDraftSuggestionsScreen extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: SuggestedSingleTabPreview(
|
: SuggestedSingleGridTabPreview(
|
||||||
tabId: tabId,
|
tabId: tabId,
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ Future<WebPageInfo> pageInfo(
|
|||||||
if (tabState?.id != null) {
|
if (tabState?.id != null) {
|
||||||
final containerId = await ref
|
final containerId = await ref
|
||||||
.read(tabDataRepositoryProvider.notifier)
|
.read(tabDataRepositoryProvider.notifier)
|
||||||
.getContainerTabId(tabState!.id);
|
.getTabContainerId(tabState!.id);
|
||||||
|
|
||||||
final containerData = await containerId.mapNotNull(
|
final containerData = await containerId.mapNotNull(
|
||||||
(containerId) => ref
|
(containerId) => ref
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ final class PageInfoProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$pageInfoHash() => r'87d23da9b25c3557e7270d90c2dee6f37eaaf8e0';
|
String _$pageInfoHash() => r'b756ee75e155bcee1e54658556eb8f4ef417d118';
|
||||||
|
|
||||||
final class PageInfoFamily extends $Family
|
final class PageInfoFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|||||||
Reference in New Issue
Block a user