rework tab bar; added contextual bar and quick switcher; tab abr dismissable; reorder now opt in; consolidated controllers and menu buttons;

This commit is contained in:
Fabian Freund
2025-12-22 10:05:17 +01:00
parent 2b1400eb76
commit e549375178
26 changed files with 1903 additions and 893 deletions
@@ -31,6 +31,7 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_preview.dart'; import 'package:weblibre/features/geckoview/features/search/domain/entities/tab_preview.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/entities/container_filter.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/data/entities/tab_entity.dart';
import 'package:weblibre/features/geckoview/features/tabs/data/models/container_data.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/geckoview/features/tabs/domain/repositories/gecko_inference.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/gecko_inference.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab_search.dart';
@@ -38,6 +39,8 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
part 'providers.g.dart'; part 'providers.g.dart';
typedef FifoTab = (TabState, ContainerData?);
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SelectedBangTrigger extends _$SelectedBangTrigger { class SelectedBangTrigger extends _$SelectedBangTrigger {
// ignore: document_ignores api decision // ignore: document_ignores api decision
@@ -111,6 +114,33 @@ EquatableValue<Map<String, TabState>> availableTabStates(
}); });
} }
@Riverpod(keepAlive: true)
EquatableValue<List<FifoTab>> fifoTabStates(Ref ref) {
final containerData = ref
.watch(watchContainersWithCountProvider.select((value) => value.value))
.mapNotNull(
(value) => Map.fromEntries(value.map((c) => MapEntry(c.id, c))),
);
final sortedTabs = ref.watch(
watchgetTabsFifoProvider.select((value) => value.value),
);
final tabStates = ref.watch(tabStatesProvider);
return EquatableValue([
if (sortedTabs != null)
for (final tab in sortedTabs)
if (tabStates.containsKey(tab.id))
(
tabStates[tab.id]!,
tab.containerId.mapNotNull(
(containerId) => containerData?[containerId],
),
),
]);
}
@Riverpod() @Riverpod()
EquatableValue<List<TabEntity>> suggestedTabEntities( EquatableValue<List<TabEntity>> suggestedTabEntities(
Ref ref, Ref ref,
@@ -382,6 +382,55 @@ final class AvailableTabStatesFamily extends $Family
String toString() => r'availableTabStatesProvider'; String toString() => r'availableTabStatesProvider';
} }
@ProviderFor(fifoTabStates)
const fifoTabStatesProvider = FifoTabStatesProvider._();
final class FifoTabStatesProvider
extends
$FunctionalProvider<
EquatableValue<List<FifoTab>>,
EquatableValue<List<FifoTab>>,
EquatableValue<List<FifoTab>>
>
with $Provider<EquatableValue<List<FifoTab>>> {
const FifoTabStatesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'fifoTabStatesProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$fifoTabStatesHash();
@$internal
@override
$ProviderElement<EquatableValue<List<FifoTab>>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
EquatableValue<List<FifoTab>> create(Ref ref) {
return fifoTabStates(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(EquatableValue<List<FifoTab>> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<EquatableValue<List<FifoTab>>>(
value,
),
);
}
}
String _$fifoTabStatesHash() => r'0320c28b328d814ed358ec6a10ec770a1ed634ad';
@ProviderFor(suggestedTabEntities) @ProviderFor(suggestedTabEntities)
const suggestedTabEntitiesProvider = SuggestedTabEntitiesFamily._(); const suggestedTabEntitiesProvider = SuggestedTabEntitiesFamily._();
@@ -0,0 +1,21 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab_bar_dismissable.g.dart';
@Riverpod()
class TabBarDismissableController extends _$TabBarDismissableController {
void show() {
state = false;
}
void dismiss() {
if (!state) {
state = true;
}
}
@override
bool build() {
return false;
}
}
@@ -1,6 +1,6 @@
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_suggestions.dart'; part of 'tab_bar_dismissable.dart';
// ************************************************************************** // **************************************************************************
// RiverpodGenerator // RiverpodGenerator
@@ -9,28 +9,29 @@ part of 'tab_suggestions.dart';
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning // ignore_for_file: type=lint, type=warning
@ProviderFor(TabSuggestionsController) @ProviderFor(TabBarDismissableController)
const tabSuggestionsControllerProvider = TabSuggestionsControllerProvider._(); const tabBarDismissableControllerProvider =
TabBarDismissableControllerProvider._();
final class TabSuggestionsControllerProvider final class TabBarDismissableControllerProvider
extends $NotifierProvider<TabSuggestionsController, bool> { extends $NotifierProvider<TabBarDismissableController, bool> {
const TabSuggestionsControllerProvider._() const TabBarDismissableControllerProvider._()
: super( : super(
from: null, from: null,
argument: null, argument: null,
retry: null, retry: null,
name: r'tabSuggestionsControllerProvider', name: r'tabBarDismissableControllerProvider',
isAutoDispose: false, isAutoDispose: true,
dependencies: null, dependencies: null,
$allTransitiveDependencies: null, $allTransitiveDependencies: null,
); );
@override @override
String debugGetCreateSourceHash() => _$tabSuggestionsControllerHash(); String debugGetCreateSourceHash() => _$tabBarDismissableControllerHash();
@$internal @$internal
@override @override
TabSuggestionsController create() => TabSuggestionsController(); TabBarDismissableController create() => TabBarDismissableController();
/// {@macro riverpod.override_with_value} /// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) { Override overrideWithValue(bool value) {
@@ -41,10 +42,10 @@ final class TabSuggestionsControllerProvider
} }
} }
String _$tabSuggestionsControllerHash() => String _$tabBarDismissableControllerHash() =>
r'5ce7f385b8aba14d432912cf0acb06aad04433fc'; r'2bb0055f5525a21982ba3d7cb6443bcad823b64f';
abstract class _$TabSuggestionsController extends $Notifier<bool> { abstract class _$TabBarDismissableController extends $Notifier<bool> {
bool build(); bool build();
@$mustCallSuper @$mustCallSuper
@override @override
@@ -17,9 +17,11 @@
* 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:flutter/material.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tab_suggestions.g.dart'; part 'tab_view_controllers.g.dart';
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class TabSuggestionsController extends _$TabSuggestionsController { class TabSuggestionsController extends _$TabSuggestionsController {
@@ -38,3 +40,45 @@ class TabSuggestionsController extends _$TabSuggestionsController {
return false; return false;
} }
} }
enum TabsViewMode {
grid(MdiIcons.table, 'Grid'),
tree(MdiIcons.familyTree, 'Tree');
final IconData icon;
final String label;
const TabsViewMode(this.icon, this.label);
}
@Riverpod(keepAlive: true)
class TabsViewModeController extends _$TabsViewModeController {
void set(TabsViewMode mode) {
if (mode != state) {
state = mode;
}
}
@override
TabsViewMode build() {
return TabsViewMode.grid;
}
}
@Riverpod()
class TabsReorderableController extends _$TabsReorderableController {
void toggle() {
state = !state;
}
void hide() {
if (state) {
state = false;
}
}
@override
bool build() {
return false;
}
}
@@ -0,0 +1,172 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tab_view_controllers.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TabSuggestionsController)
const tabSuggestionsControllerProvider = TabSuggestionsControllerProvider._();
final class TabSuggestionsControllerProvider
extends $NotifierProvider<TabSuggestionsController, bool> {
const TabSuggestionsControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabSuggestionsControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabSuggestionsControllerHash();
@$internal
@override
TabSuggestionsController create() => TabSuggestionsController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$tabSuggestionsControllerHash() =>
r'5ce7f385b8aba14d432912cf0acb06aad04433fc';
abstract class _$TabSuggestionsController extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(TabsViewModeController)
const tabsViewModeControllerProvider = TabsViewModeControllerProvider._();
final class TabsViewModeControllerProvider
extends $NotifierProvider<TabsViewModeController, TabsViewMode> {
const TabsViewModeControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabsViewModeControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabsViewModeControllerHash();
@$internal
@override
TabsViewModeController create() => TabsViewModeController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TabsViewMode value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TabsViewMode>(value),
);
}
}
String _$tabsViewModeControllerHash() =>
r'9c574458f6d5a4c6a8b5aabf96ad7f5dcd043dd0';
abstract class _$TabsViewModeController extends $Notifier<TabsViewMode> {
TabsViewMode build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<TabsViewMode, TabsViewMode>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<TabsViewMode, TabsViewMode>,
TabsViewMode,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@ProviderFor(TabsReorderableController)
const tabsReorderableControllerProvider = TabsReorderableControllerProvider._();
final class TabsReorderableControllerProvider
extends $NotifierProvider<TabsReorderableController, bool> {
const TabsReorderableControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'tabsReorderableControllerProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$tabsReorderableControllerHash();
@$internal
@override
TabsReorderableController create() => TabsReorderableController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$tabsReorderableControllerHash() =>
r'f169e9dc04055ef611f17ebe121f0f51f6a294cb';
abstract class _$TabsReorderableController extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@@ -1,40 +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 'package:riverpod_annotation/riverpod_annotation.dart';
part 'tree_view.g.dart';
@Riverpod(keepAlive: true)
class TreeViewController extends _$TreeViewController {
void toggle() {
state = !state;
}
void hide() {
if (state) {
state = false;
}
}
@override
bool build() {
return false;
}
}
@@ -1,64 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'tree_view.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(TreeViewController)
const treeViewControllerProvider = TreeViewControllerProvider._();
final class TreeViewControllerProvider
extends $NotifierProvider<TreeViewController, bool> {
const TreeViewControllerProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'treeViewControllerProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$treeViewControllerHash();
@$internal
@override
TreeViewController create() => TreeViewController();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(bool value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<bool>(value),
);
}
}
String _$treeViewControllerHash() =>
r'1fac2a133d369e3bd3f8f75cdd2d0c862a6ab942';
abstract class _$TreeViewController extends $Notifier<bool> {
bool build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<bool, bool>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<bool, bool>,
bool,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
@@ -40,23 +40,31 @@ import 'package:weblibre/features/geckoview/domain/providers/tab_session.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/domain/entities/sheet.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_bar_dismissable.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/bottom_app_bar.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/view_tabs.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';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_appearance_button.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' as ui_helper; import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class _TabBar extends HookConsumerWidget { class _TabBar extends HookConsumerWidget {
final bool showContextualToolbar;
final bool showQuickTabSwitcherBar;
final ValueNotifier<bool> showAppBar; final ValueNotifier<bool> showAppBar;
final ValueNotifier<PersistentBottomSheetController?> sheetController; final ValueNotifier<PersistentBottomSheetController?> sheetController;
const _TabBar({required this.showAppBar, required this.sheetController}); const _TabBar({
required this.showContextualToolbar,
required this.showQuickTabSwitcherBar,
required this.showAppBar,
required this.sheetController,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -76,7 +84,11 @@ class _TabBar extends HookConsumerWidget {
if (!autoHideTabBar) { if (!autoHideTabBar) {
return Visibility( return Visibility(
visible: !tabInFullScreen, visible: !tabInFullScreen,
child: BrowserBottomAppBar(displayedSheet: displayedSheet), child: BrowserBottomAppBar(
displayedSheet: displayedSheet,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
); );
} }
@@ -84,7 +96,10 @@ class _TabBar extends HookConsumerWidget {
final diffAcc = useRef(0.0); final diffAcc = useRef(0.0);
void resetHiddenState() { void resetHiddenState() {
showAppBar.value = true; if (!ref.read(tabBarDismissableControllerProvider)) {
showAppBar.value = true;
}
diffAcc.value = 0.0; diffAcc.value = 0.0;
} }
@@ -153,7 +168,12 @@ class _TabBar extends HookConsumerWidget {
return Visibility( return Visibility(
visible: visible:
sheetController.value != null || (!tabInFullScreen && appBarVisible), sheetController.value != null || (!tabInFullScreen && appBarVisible),
child: BrowserBottomAppBar(displayedSheet: displayedSheet), maintainState: true,
child: BrowserBottomAppBar(
displayedSheet: displayedSheet,
showContextualToolbar: showContextualToolbar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
); );
} }
} }
@@ -171,8 +191,24 @@ class BrowserScreen extends HookConsumerWidget {
final overlayController = useOverlayPortalController(); final overlayController = useOverlayPortalController();
final showContextualAppBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarShowContextualBar,
),
);
final showQuickTabSwitcherBar = ref.watch(
generalSettingsWithDefaultsProvider.select(
(value) => value.tabBarShowQuickTabSwitcherBar,
),
);
final showAppBar = useValueNotifier(true); final showAppBar = useValueNotifier(true);
ref.listen(tabBarDismissableControllerProvider, (previous, next) {
showAppBar.value = !next;
});
ref.listen(overlayControllerProvider, (previous, next) { ref.listen(overlayControllerProvider, (previous, next) {
if (next != null) { if (next != null) {
overlayController.show(); overlayController.show();
@@ -220,17 +256,25 @@ class BrowserScreen extends HookConsumerWidget {
//This causes issues with a non dismissable barrier pushed, we ahve our own barrier and this does seem to have issues when dismissing, so disable it completely //This causes issues with a non dismissable barrier pushed, we ahve our own barrier and this does seem to have issues when dismissing, so disable it completely
return null; return null;
}, },
bottomNavigationBar: _TabBar( body: Column(
showAppBar: showAppBar, children: [
sheetController: sheetController, _TabBar(
showAppBar: showAppBar,
sheetController: sheetController,
showContextualToolbar: showContextualAppBar,
showQuickTabSwitcherBar: showQuickTabSwitcherBar,
),
Expanded(
child: _Browser(
overlayController: overlayController,
sheetController: sheetController,
showAppBar: showAppBar,
tabInFullScreen: tabInFullScreen,
),
),
],
), ),
body: _Browser( floatingActionButton: BrowserFab(),
overlayController: overlayController,
sheetController: sheetController,
showAppBar: showAppBar,
tabInFullScreen: tabInFullScreen,
),
floatingActionButton: ReaderAppearanceButton(),
), ),
), ),
); );
@@ -408,7 +452,9 @@ class _Browser extends HookConsumerWidget {
} }
//Make sure app bar is visible //Make sure app bar is visible
showAppBar.value = true; if (!ref.read(tabBarDismissableControllerProvider)) {
showAppBar.value = true;
}
if (tabState?.isLoading == true) { if (tabState?.isLoading == true) {
lastBackButtonPress.value = null; lastBackButtonPress.value = null;
@@ -634,11 +680,15 @@ class _ViewTabsSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final draggableScrollableController = useDraggableScrollableController(); final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
final treeModeEnabled = ref.watch(treeViewControllerProvider); final draggableScrollableController = useDraggableScrollableController(
keys: [tabsReorderable],
);
return DraggableScrollableSheet( return DraggableScrollableSheet(
key: ValueKey(tabsReorderable),
controller: draggableScrollableController, controller: draggableScrollableController,
expand: false, expand: false,
minChildSize: 0.1, minChildSize: 0.1,
@@ -649,26 +699,28 @@ class _ViewTabsSheet extends HookConsumerWidget {
topLeft: Radius.circular(28), topLeft: Radius.circular(28),
topRight: Radius.circular(28), topRight: Radius.circular(28),
), ),
child: treeModeEnabled child: switch (tabsViewMode) {
? ViewTabTreesWidget( TabsViewMode.grid => ViewTabsWidget(
scrollController: scrollController, scrollController: scrollController,
showNewTabFab: true, showNewTabFab: true,
onClose: () { tabsReorderable: tabsReorderable,
ref draggableScrollableController: draggableScrollableController,
.read(bottomSheetControllerProvider.notifier) onClose: () {
.requestDismiss(); ref
}, .read(bottomSheetControllerProvider.notifier)
) .requestDismiss();
: ViewTabsWidget( },
scrollController: scrollController, ),
showNewTabFab: true, TabsViewMode.tree => ViewTabTreesWidget(
draggableScrollableController: draggableScrollableController, scrollController: scrollController,
onClose: () { showNewTabFab: true,
ref onClose: () {
.read(bottomSheetControllerProvider.notifier) ref
.requestDismiss(); .read(bottomSheetControllerProvider.notifier)
}, .requestDismiss();
), },
),
},
); );
}, },
); );
@@ -22,7 +22,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; 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/tree_view.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/view_tabs.dart';
import 'package:weblibre/features/user/domain/repositories/general_settings.dart'; import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
@@ -31,28 +31,32 @@ class TabViewScreen extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final treeModeEnabled = ref.watch(treeViewControllerProvider); final tabsViewMode = ref.watch(tabsViewModeControllerProvider);
final tabsReorderable = ref.watch(tabsReorderableControllerProvider);
final scrollController = useScrollController(); final scrollController = useScrollController(keys: [tabsReorderable]);
return Dialog.fullscreen( return Dialog.fullscreen(
child: Scaffold( child: Scaffold(
body: SafeArea( body: SafeArea(
child: treeModeEnabled child: switch (tabsViewMode) {
? ViewTabTreesWidget( TabsViewMode.grid => ViewTabsWidget(
scrollController: scrollController, key: ValueKey(tabsReorderable),
showNewTabFab: false, scrollController: scrollController,
onClose: () { tabsReorderable: tabsReorderable,
const BrowserRoute().go(context); showNewTabFab: false,
}, onClose: () {
) const BrowserRoute().go(context);
: ViewTabsWidget( },
scrollController: scrollController, ),
showNewTabFab: false, TabsViewMode.tree => ViewTabTreesWidget(
onClose: () { scrollController: scrollController,
const BrowserRoute().go(context); showNewTabFab: false,
}, onClose: () {
), const BrowserRoute().go(context);
},
),
},
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: () async { onPressed: () async {
@@ -22,12 +22,15 @@ 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:weblibre/features/geckoview/domain/entities/states/readerable.dart'; import 'package:weblibre/features/geckoview/domain/entities/states/readerable.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_bar_dismissable.dart';
import 'package:weblibre/features/geckoview/features/readerview/domain/providers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/domain/providers/readerable.dart';
class ReaderAppearanceButton extends HookConsumerWidget { class BrowserFab extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final buttonVisible = ref.watch( final tabBarDismissed = ref.watch(tabBarDismissableControllerProvider);
final appearanceButtonVisible = ref.watch(
appearanceButtonVisibilityProvider.select( appearanceButtonVisibilityProvider.select(
(value) => value.value ?? false, (value) => value.value ?? false,
), ),
@@ -40,7 +43,15 @@ class ReaderAppearanceButton extends HookConsumerWidget {
); );
return Visibility( return Visibility(
visible: readerabilityState.active && buttonVisible, visible: readerabilityState.active && appearanceButtonVisible,
replacement: tabBarDismissed
? FloatingActionButton(
child: const Icon(MdiIcons.dockBottom),
onPressed: () {
ref.read(tabBarDismissableControllerProvider.notifier).show();
},
)
: const SizedBox.shrink(),
child: FloatingActionButton( child: FloatingActionButton(
onPressed: () async { onPressed: () async {
await ref.read(readerableServiceProvider).onAppearanceButtonTap(); await ref.read(readerableServiceProvider).onAppearanceButtonTap();
@@ -0,0 +1,150 @@
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:share_plus/share_plus.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/dialogs/qr_code.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
class ShareMenuItemButton extends HookConsumerWidget {
const ShareMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(Icons.share),
closeOnActivate: false,
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await SharePlus.instance.share(ShareParams(uri: tabState.url));
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
child: const Text('Share Link'),
);
}
}
class ShowQrCodeMenuItemButton extends HookConsumerWidget {
const ShowQrCodeMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(Icons.qr_code),
closeOnActivate: false,
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await showQrCode(context, tabState.url.toString());
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
child: const Text('Show QR Code'),
);
}
}
class ShareScreenshotMenuItemButton extends HookConsumerWidget {
const ShareScreenshotMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(Icons.mobile_screen_share),
closeOnActivate: false,
child: const Text('Share Screenshot'),
onPressed: () async {
final screenshot = await ref
.read(selectedTabSessionProvider)
.requestScreenshot();
final tabState = ref.read(tabStateProvider(selectedTabId))!;
if (screenshot != null) {
ui.decodeImageFromList(screenshot, (result) async {
final png = await result.toByteData(format: ui.ImageByteFormat.png);
if (png != null) {
final file = XFile.fromData(
png.buffer.asUint8List(),
mimeType: 'image/png',
);
await SharePlus.instance.share(
ShareParams(files: [file], subject: tabState.titleOrAuthority),
);
}
});
}
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
);
}
}
class LaunchExternalMenuItemButton extends HookConsumerWidget {
const LaunchExternalMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(Icons.open_in_browser),
closeOnActivate: false,
child: const Text('Launch External'),
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await ui_helper.launchUrlFeedback(context, tabState.url);
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
);
}
}
class CopyAddressMenuItemButton extends HookConsumerWidget {
const CopyAddressMenuItemButton({super.key, required this.selectedTabId});
final String? selectedTabId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return MenuItemButton(
leadingIcon: const Icon(MdiIcons.contentCopy),
closeOnActivate: false,
child: const Text('Copy Address'),
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await Clipboard.setData(ClipboardData(text: tabState.url.toString()));
if (context.mounted) {
MenuController.maybeOf(context)?.close();
}
},
);
}
}
@@ -26,13 +26,11 @@ import 'package:weblibre/features/user/domain/repositories/general_settings.dart
class TabCreationMenu extends HookConsumerWidget { class TabCreationMenu extends HookConsumerWidget {
final Widget child; final Widget child;
final MenuController controller; final MenuController controller;
final String? selectedTabId;
const TabCreationMenu({ const TabCreationMenu({
super.key, super.key,
required this.child, required this.child,
required this.controller, required this.controller,
required this.selectedTabId,
}); });
@override @override
@@ -18,23 +18,19 @@
* 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 'dart:convert';
import 'dart:ui' as ui;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; 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:flutter_mozilla_components/flutter_mozilla_components.dart'; import 'package:flutter_mozilla_components/flutter_mozilla_components.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:share_plus/share_plus.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/desktop_mode.dart'; import 'package:weblibre/features/geckoview/domain/providers/desktop_mode.dart';
import 'package:weblibre/features/geckoview/domain/providers/tab_session.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/dialogs/qr_code.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/menu_item_buttons.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/readerview/presentation/controllers/readerable.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart'; import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
@@ -315,75 +311,11 @@ class TabMenu extends HookConsumerWidget {
), ),
SubmenuButton( SubmenuButton(
menuChildren: [ menuChildren: [
MenuItemButton( CopyAddressMenuItemButton(selectedTabId: selectedTabId),
leadingIcon: const Icon(MdiIcons.contentCopy), LaunchExternalMenuItemButton(selectedTabId: selectedTabId),
child: const Text('Copy Address'), ShareScreenshotMenuItemButton(selectedTabId: selectedTabId),
onPressed: () async { ShareMenuItemButton(selectedTabId: selectedTabId),
final tabState = ref.read(tabStateProvider(selectedTabId))!; ShowQrCodeMenuItemButton(selectedTabId: selectedTabId),
await Clipboard.setData(
ClipboardData(text: tabState.url.toString()),
);
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.open_in_browser),
child: const Text('Launch External'),
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await ui_helper.launchUrlFeedback(context, tabState.url);
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.mobile_screen_share),
child: const Text('Share Screenshot'),
onPressed: () async {
final screenshot = await ref
.read(selectedTabSessionProvider)
.requestScreenshot();
final tabState = ref.read(tabStateProvider(selectedTabId))!;
if (screenshot != null) {
ui.decodeImageFromList(screenshot, (result) async {
final png = await result.toByteData(
format: ui.ImageByteFormat.png,
);
if (png != null) {
final file = XFile.fromData(
png.buffer.asUint8List(),
mimeType: 'image/png',
);
await SharePlus.instance.share(
ShareParams(files: [file], subject: tabState.title),
);
}
});
}
},
),
MenuItemButton(
leadingIcon: const Icon(Icons.share),
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await SharePlus.instance.share(ShareParams(uri: tabState.url));
},
child: const Text('Share Link'),
),
MenuItemButton(
leadingIcon: const Icon(Icons.qr_code),
onPressed: () async {
final tabState = ref.read(tabStateProvider(selectedTabId))!;
await showQrCode(context, tabState.url.toString());
controller.close();
},
child: const Text('Show QR Code'),
),
], ],
leadingIcon: const Icon(Icons.share), leadingIcon: const Icon(Icons.share),
child: const Text('Share'), child: const Text('Share'),
@@ -26,6 +26,7 @@ import 'package:fast_equatable/fast_equatable.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:flutter_material_design_icons/flutter_material_design_icons.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:flutter_reorderable_grid_view/widgets/widgets.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';
@@ -36,8 +37,7 @@ 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/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/domain/providers.dart'; import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_suggestions.dart'; import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tab_view_controllers.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/tree_view.dart';
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/draggable_scrollable_header.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/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/container_filter.dart';
@@ -50,6 +50,7 @@ import 'package:weblibre/features/geckoview/features/tabs/presentation/widgets/c
import 'package:weblibre/features/tor/presentation/controllers/start_tor_proxy.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/features/user/domain/repositories/general_settings.dart';
import 'package:weblibre/presentation/hooks/listenable_callback.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/presentation/widgets/speech_to_text_button.dart';
import 'package:weblibre/utils/ui_helper.dart' as ui_helper; import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
@@ -67,7 +68,7 @@ int calculateCrossAxisItemCount({
return crossAxisCount; return crossAxisCount;
} }
double calculateItemHeight({ Size calculateItemSize({
required double screenWidth, required double screenWidth,
required double childAspectRatio, required double childAspectRatio,
required double horizontalPadding, required double horizontalPadding,
@@ -81,9 +82,8 @@ double calculateItemHeight({
screenWidth - totalHorizontalPadding - totalCrossAxisSpacing; screenWidth - totalHorizontalPadding - totalCrossAxisSpacing;
final itemWidth = availableWidth / crossAxisCount; final itemWidth = availableWidth / crossAxisCount;
final itemHeight = itemWidth / childAspectRatio; final itemHeight = itemWidth / childAspectRatio;
final totalItemHeight = itemHeight + mainAxisSpacing;
return totalItemHeight; return Size(itemWidth, itemHeight + mainAxisSpacing);
} }
class _TabDraggable extends HookConsumerWidget { class _TabDraggable extends HookConsumerWidget {
@@ -165,10 +165,10 @@ class _TabDraggable extends HookConsumerWidget {
class _TabViewHeader extends HookConsumerWidget { class _TabViewHeader extends HookConsumerWidget {
static const headerSize = 124.0; static const headerSize = 124.0;
final bool treeViewEnabled; final TabsViewMode tabsViewMode;
final VoidCallback onClose; final VoidCallback onClose;
const _TabViewHeader({required this.onClose, required this.treeViewEnabled}); const _TabViewHeader({required this.onClose, required this.tabsViewMode});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -176,6 +176,9 @@ class _TabViewHeader extends HookConsumerWidget {
final searchTextFocus = useFocusNode(); final searchTextFocus = useFocusNode();
final searchTextController = useTextEditingController(); final searchTextController = useTextEditingController();
final viewModeMenuController = useMenuController();
final tabsActionMenuController = useMenuController();
final hasSearchText = useListenableSelector( final hasSearchText = useListenableSelector(
searchTextController, searchTextController,
() => searchTextController.text.isNotEmpty, () => searchTextController.text.isNotEmpty,
@@ -218,8 +221,8 @@ class _TabViewHeader extends HookConsumerWidget {
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
onPressed: () { onPressed: () {
ref ref
.read(treeViewControllerProvider.notifier) .read(tabsViewModeControllerProvider.notifier)
.hide(); .set(TabsViewMode.grid);
searchMode.value = true; searchMode.value = true;
searchTextFocus.requestFocus(); searchTextFocus.requestFocus();
@@ -229,17 +232,40 @@ class _TabViewHeader extends HookConsumerWidget {
height: 32, height: 32,
child: VerticalDivider(indent: 4, endIndent: 4), child: VerticalDivider(indent: 4, endIndent: 4),
), ),
IconButton( MenuAnchor(
icon: const Icon(MdiIcons.familyTree), controller: viewModeMenuController,
selectedIcon: const Icon(MdiIcons.table), menuChildren: TabsViewMode.values
isSelected: treeViewEnabled, .map(
iconSize: 18, (mode) => MenuItemButton(
padding: EdgeInsets.zero, leadingIcon: Icon(mode.icon),
onPressed: () { child: Text(mode.label),
ref onPressed: () {
.read(treeViewControllerProvider.notifier) ref
.toggle(); .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) if (enableAiFeatures)
Consumer( Consumer(
@@ -264,26 +290,122 @@ class _TabViewHeader extends HookConsumerWidget {
); );
}, },
), ),
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();
},
);
},
),
], ],
), ),
TextButton.icon( MenuAnchor(
onPressed: () async { controller: tabsActionMenuController,
final container = ref.read(selectedContainerProvider); menuChildren: [
MenuItemButton(
leadingIcon: const Icon(MdiIcons.bookmarkPlusOutline),
child: const Text('Bookmark all'),
onPressed: () async {
final containerId = ref.read(
selectedContainerProvider,
);
final count = await ref final tabData = await ref
.read(tabDataRepositoryProvider.notifier) .read(tabDataRepositoryProvider.notifier)
.closeAllTabsByContainer(container); .getContainerTabsData(containerId);
if (context.mounted) { for (final tab in tabData) {
ui_helper.showTabUndoClose( if (context.mounted) {
context, await BookmarkEntryAddRoute(
ref.read(tabRepositoryProvider.notifier).undoClose, bookmarkInfo: jsonEncode(
count: count, BookmarkInfo(
); title: tab.title,
} url: tab.url.toString(),
}, ).encode(),
icon: const Icon(MdiIcons.closeBoxMultiple), ),
label: const Text('Close All'), ).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),
),
), ),
], ],
) )
@@ -318,7 +440,7 @@ class _TabViewHeader extends HookConsumerWidget {
), ),
), ),
), ),
if (!treeViewEnabled) if (tabsViewMode == TabsViewMode.grid)
Consumer( Consumer(
builder: (context, ref, child) { builder: (context, ref, child) {
final selectedContainer = ref.watch( final selectedContainer = ref.watch(
@@ -372,9 +494,14 @@ class _TabViewHeader extends HookConsumerWidget {
class _TabView extends HookConsumerWidget { class _TabView extends HookConsumerWidget {
final ScrollController scrollController; final ScrollController scrollController;
final bool tabsReorderable;
final VoidCallback onClose; final VoidCallback onClose;
const _TabView({required this.scrollController, required this.onClose}); const _TabView({
required this.scrollController,
required this.tabsReorderable,
required this.onClose,
});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@@ -415,8 +542,8 @@ class _TabView extends HookConsumerWidget {
return math.max(math.min(calculatedCount, itemCount), 2); return math.max(math.min(calculatedCount, itemCount), 2);
}, [screenWidth, itemCount]); }, [screenWidth, itemCount]);
final itemHeight = useMemoized( final itemSize = useMemoized(
() => calculateItemHeight( () => calculateItemSize(
screenWidth: screenWidth, screenWidth: screenWidth,
childAspectRatio: 0.75, childAspectRatio: 0.75,
horizontalPadding: 4.0, horizontalPadding: 4.0,
@@ -438,7 +565,7 @@ class _TabView extends HookConsumerWidget {
); );
if (index > -1) { if (index > -1) {
final offset = (index ~/ 2) * itemHeight; final offset = (index ~/ 2) * itemSize.height;
if (offset != scrollController.offset) { if (offset != scrollController.offset) {
lastScroll.value = activeTab; lastScroll.value = activeTab;
@@ -465,117 +592,192 @@ class _TabView extends HookConsumerWidget {
fadingSize: 5, fadingSize: 5,
controller: scrollController, controller: scrollController,
builder: (context, controller) { builder: (context, controller) {
return ReorderableBuilder.builder( return !tabsReorderable
//Rebuild when cross axis count changes ? _TabGrid(
key: ValueKey(crossAxisCount), 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 GridView.builder(
controller: controller,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
//Sync values for itemHeight calculation _calculateItemHeight
childAspectRatio: 0.75,
mainAxisSpacing: 8.0,
crossAxisSpacing: 8.0,
crossAxisCount: crossAxisCount, crossAxisCount: crossAxisCount,
), itemCount: itemCount,
itemCount: itemCount, scrollController: controller,
itemBuilder: (context, index) { itemBuilder: (widget, _) {
final Widget tab; if (widget is CustomDraggable) {
if (index < filteredTabEntities.value.length) { return LongPressDraggable(
final entity = filteredTabEntities.value[index]; feedback: Material(
tab = CustomDraggable( color: Colors
key: Key(entity.tabId), .transparent, // removes white corners when having shadow
data: TabDragData(entity.tabId), child: Transform.scale(
child: _TabDraggable(entity: entity, onClose: onClose), scale: 1.05,
); child: SizedBox(
} else { height: itemSize.height,
final suggestedIndex = width: itemSize.width,
index - filteredTabEntities.value.length; child: widget.child,
final entity = suggestedTabEntities.value[suggestedIndex]; ),
),
tab = CustomDraggable(
key: Key('suggested_${entity.tabId}'),
child: _TabDraggable(
entity: entity,
onClose: onClose,
suggestedContainerId: ref.watch(
selectedContainerProvider,
), ),
), data: widget.data,
); child: widget.child,
} );
}
return itemBuilder(tab, index); 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 { class ViewTabsWidget extends HookConsumerWidget {
final ScrollController scrollController; final ScrollController scrollController;
final DraggableScrollableController? draggableScrollableController; final DraggableScrollableController? draggableScrollableController;
final VoidCallback onClose;
final bool showNewTabFab; final bool showNewTabFab;
final bool tabsReorderable;
final VoidCallback onClose;
const ViewTabsWidget({ const ViewTabsWidget({
required this.onClose, required this.onClose,
required this.scrollController, required this.scrollController,
this.draggableScrollableController, this.draggableScrollableController,
required this.tabsReorderable,
required this.showNewTabFab, required this.showNewTabFab,
super.key, super.key,
}); });
@@ -596,14 +798,21 @@ class ViewTabsWidget extends HookConsumerWidget {
controller: draggableScrollableController, controller: draggableScrollableController,
child: _TabViewHeader( child: _TabViewHeader(
onClose: onClose, onClose: onClose,
treeViewEnabled: false, tabsViewMode: TabsViewMode.grid,
), ),
), ),
) ?? ) ??
_TabViewHeader(onClose: onClose, treeViewEnabled: false), _TabViewHeader(
onClose: onClose,
tabsViewMode: TabsViewMode.grid,
),
), ),
], ],
body: _TabView(scrollController: scrollController, onClose: onClose), body: _TabView(
scrollController: scrollController,
tabsReorderable: tabsReorderable,
onClose: onClose,
),
), ),
if (showNewTabFab) if (showNewTabFab)
Padding( Padding(
@@ -650,7 +859,7 @@ class ViewTabTreesWidget extends HookConsumerWidget {
children: [ children: [
Column( Column(
children: [ children: [
_TabViewHeader(onClose: onClose, treeViewEnabled: true), _TabViewHeader(onClose: onClose, tabsViewMode: TabsViewMode.tree),
Expanded( Expanded(
child: HookConsumer( child: HookConsumer(
builder: (context, ref, child) { builder: (context, ref, child) {
@@ -684,8 +893,8 @@ class ViewTabTreesWidget extends HookConsumerWidget {
); );
}, [screenWidth, filteredTabEntities.value.length]); }, [screenWidth, filteredTabEntities.value.length]);
final itemHeight = useMemoized( final itemSize = useMemoized(
() => calculateItemHeight( () => calculateItemSize(
screenWidth: screenWidth, screenWidth: screenWidth,
childAspectRatio: 0.75, childAspectRatio: 0.75,
horizontalPadding: 4.0, horizontalPadding: 4.0,
@@ -702,7 +911,7 @@ class ViewTabTreesWidget extends HookConsumerWidget {
); );
if (index > -1) { if (index > -1) {
final offset = (index ~/ 2) * itemHeight; final offset = (index ~/ 2) * itemSize.height;
if (offset != scrollController.offset) { if (offset != scrollController.offset) {
unawaited( unawaited(
@@ -51,6 +51,12 @@ class TabDao extends DatabaseAccessor<TabDatabase> with $TabDaoMixin {
return query.map((row) => row.read(db.tab.id)!); return query.map((row) => row.read(db.tab.id)!);
} }
Selectable<TabData> getTabsFifo({int limit = 25}) {
return select(db.tab)
..limit(limit)
..orderBy([(t) => OrderingTerm.desc(t.timestamp)]);
}
SingleOrNullSelectable<String?> getTabContainerId(String tabId) { SingleOrNullSelectable<String?> getTabContainerId(String tabId) {
final query = selectOnly(db.tab) final query = selectOnly(db.tab)
..addColumns([db.tab.containerId]) ..addColumns([db.tab.containerId])
@@ -74,6 +74,13 @@ Stream<List<String>> watchContainerTabIds(
} }
} }
@Riverpod(keepAlive: true)
Stream<List<TabData>> watchgetTabsFifo(Ref ref) {
final db = ref.watch(tabDatabaseProvider);
return db.tabDao.getTabsFifo().watch();
}
@Riverpod() @Riverpod()
Future<int> containerTabCount(Ref ref, ContainerFilter containerFilter) { Future<int> containerTabCount(Ref ref, ContainerFilter containerFilter) {
return ref.watch( return ref.watch(
@@ -221,6 +221,45 @@ final class WatchContainerTabIdsFamily extends $Family
String toString() => r'watchContainerTabIdsProvider'; String toString() => r'watchContainerTabIdsProvider';
} }
@ProviderFor(watchgetTabsFifo)
const watchgetTabsFifoProvider = WatchgetTabsFifoProvider._();
final class WatchgetTabsFifoProvider
extends
$FunctionalProvider<
AsyncValue<List<TabData>>,
List<TabData>,
Stream<List<TabData>>
>
with $FutureModifier<List<TabData>>, $StreamProvider<List<TabData>> {
const WatchgetTabsFifoProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'watchgetTabsFifoProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$watchgetTabsFifoHash();
@$internal
@override
$StreamProviderElement<List<TabData>> $createElement(
$ProviderPointer pointer,
) => $StreamProviderElement(pointer);
@override
Stream<List<TabData>> create(Ref ref) {
return watchgetTabsFifo(ref);
}
}
String _$watchgetTabsFifoHash() => r'b0f58ca1688003ba46a55de1ca29d8253fc3de3a';
@ProviderFor(containerTabCount) @ProviderFor(containerTabCount)
const containerTabCountProvider = ContainerTabCountFamily._(); const containerTabCountProvider = ContainerTabCountFamily._();
@@ -20,6 +20,7 @@
import 'package:nullability/nullability.dart'; import 'package:nullability/nullability.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.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/tabs/data/database/definitions.drift.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/data/providers.dart'; import 'package:weblibre/features/geckoview/features/tabs/data/providers.dart';
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart'; import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/container.dart';
@@ -87,11 +88,7 @@ class TabDataRepository extends _$TabDataRepository {
} }
Future<int> closeAllTabsByHost(String? containerId, String host) async { Future<int> closeAllTabsByHost(String? containerId, String host) async {
final tabs = await ref final tabs = await getContainerTabsData(containerId);
.read(tabDatabaseProvider)
.containerDao
.getContainerTabsData(containerId)
.get();
final filtered = tabs final filtered = tabs
.where((tab) => tab.url?.host == host) .where((tab) => tab.url?.host == host)
@@ -105,6 +102,14 @@ class TabDataRepository extends _$TabDataRepository {
return filtered.length; return filtered.length;
} }
Future<List<TabData>> getContainerTabsData(String? containerId) {
return ref
.read(tabDatabaseProvider)
.containerDao
.getContainerTabsData(containerId)
.get();
}
Future<String?> getContainerTabId(String tabId) { Future<String?> getContainerTabId(String tabId) {
return ref return ref
.read(tabDatabaseProvider) .read(tabDatabaseProvider)
@@ -41,7 +41,7 @@ final class TabDataRepositoryProvider
} }
} }
String _$tabDataRepositoryHash() => r'8f8a1dff5e5be5cbe59562c86b2e57c229b97f8a'; String _$tabDataRepositoryHash() => r'e12750bc1962878a5bf5140d97521d642001994f';
abstract class _$TabDataRepository extends $Notifier<void> { abstract class _$TabDataRepository extends $Notifier<void> {
void build(); void build();
@@ -362,6 +362,38 @@ class GeneralSettingsScreen extends HookConsumerWidget {
); );
}, },
), ),
SwitchListTile.adaptive(
title: const Text('Show Contextual Tab Bar'),
subtitle: const Text(
'Show additional bottom toolbar for navigation and actions',
),
secondary: const Icon(MdiIcons.dockBottom),
value: generalSettings.tabBarShowContextualBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.tabBarShowContextualBar(value),
);
},
),
SwitchListTile.adaptive(
title: const Text('Show Quick Tab Switcher Bar'),
subtitle: const Text(
'Show additional toolbar to quickly switch to recently used tabs',
),
secondary: const Icon(MdiIcons.dockBottom),
value: generalSettings.tabBarShowQuickTabSwitcherBar,
onChanged: (value) async {
await ref
.read(saveGeneralSettingsControllerProvider.notifier)
.save(
(currentSettings) => currentSettings.copyWith
.tabBarShowQuickTabSwitcherBar(value),
);
},
),
SwitchListTile.adaptive( SwitchListTile.adaptive(
title: const Text('Create Child Tabs'), title: const Text('Create Child Tabs'),
subtitle: const Text( subtitle: const Text(
@@ -72,6 +72,8 @@ class GeneralSettings with FastEquatable {
final Duration historyAutoCleanInterval; final Duration historyAutoCleanInterval;
final bool tabViewBottomSheet; final bool tabViewBottomSheet;
final bool tabBarReaderView; final bool tabBarReaderView;
final bool tabBarShowContextualBar;
final bool tabBarShowQuickTabSwitcherBar;
GeneralSettings({ GeneralSettings({
required this.themeMode, required this.themeMode,
@@ -90,6 +92,8 @@ class GeneralSettings with FastEquatable {
required this.historyAutoCleanInterval, required this.historyAutoCleanInterval,
required this.tabViewBottomSheet, required this.tabViewBottomSheet,
required this.tabBarReaderView, required this.tabBarReaderView,
required this.tabBarShowContextualBar,
required this.tabBarShowQuickTabSwitcherBar,
}); });
GeneralSettings.withDefaults({ GeneralSettings.withDefaults({
@@ -109,6 +113,8 @@ class GeneralSettings with FastEquatable {
Duration? historyAutoCleanInterval, Duration? historyAutoCleanInterval,
bool? tabViewBottomSheet, bool? tabViewBottomSheet,
bool? tabBarReaderView, bool? tabBarReaderView,
bool? tabBarShowContextualBar,
bool? tabBarShowQuickTabSwitcherBar,
}) : themeMode = themeMode ?? ThemeMode.dark, }) : themeMode = themeMode ?? ThemeMode.dark,
enableReadability = enableReadability ?? true, enableReadability = enableReadability ?? true,
enforceReadability = enforceReadability ?? false, enforceReadability = enforceReadability ?? false,
@@ -126,7 +132,9 @@ class GeneralSettings with FastEquatable {
historyAutoCleanInterval = historyAutoCleanInterval =
historyAutoCleanInterval ?? const Duration(days: 90), historyAutoCleanInterval ?? const Duration(days: 90),
tabViewBottomSheet = tabViewBottomSheet ?? false, tabViewBottomSheet = tabViewBottomSheet ?? false,
tabBarReaderView = tabBarReaderView ?? false; tabBarReaderView = tabBarReaderView ?? false,
tabBarShowContextualBar = tabBarShowContextualBar ?? true,
tabBarShowQuickTabSwitcherBar = tabBarShowQuickTabSwitcherBar ?? true;
factory GeneralSettings.fromJson(Map<String, dynamic> json) => factory GeneralSettings.fromJson(Map<String, dynamic> json) =>
_$GeneralSettingsFromJson(json); _$GeneralSettingsFromJson(json);
@@ -151,5 +159,7 @@ class GeneralSettings with FastEquatable {
historyAutoCleanInterval, historyAutoCleanInterval,
tabViewBottomSheet, tabViewBottomSheet,
tabBarReaderView, tabBarReaderView,
tabBarShowContextualBar,
tabBarShowQuickTabSwitcherBar,
]; ];
} }
@@ -45,6 +45,12 @@ abstract class _$GeneralSettingsCWProxy {
GeneralSettings tabBarReaderView(bool tabBarReaderView); GeneralSettings tabBarReaderView(bool tabBarReaderView);
GeneralSettings tabBarShowContextualBar(bool tabBarShowContextualBar);
GeneralSettings tabBarShowQuickTabSwitcherBar(
bool tabBarShowQuickTabSwitcherBar,
);
/// Creates a new instance with the provided field values. /// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
/// ///
@@ -69,6 +75,8 @@ abstract class _$GeneralSettingsCWProxy {
Duration historyAutoCleanInterval, Duration historyAutoCleanInterval,
bool tabViewBottomSheet, bool tabViewBottomSheet,
bool tabBarReaderView, bool tabBarReaderView,
bool tabBarShowContextualBar,
bool tabBarShowQuickTabSwitcherBar,
}); });
} }
@@ -145,6 +153,15 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
GeneralSettings tabBarReaderView(bool tabBarReaderView) => GeneralSettings tabBarReaderView(bool tabBarReaderView) =>
call(tabBarReaderView: tabBarReaderView); call(tabBarReaderView: tabBarReaderView);
@override
GeneralSettings tabBarShowContextualBar(bool tabBarShowContextualBar) =>
call(tabBarShowContextualBar: tabBarShowContextualBar);
@override
GeneralSettings tabBarShowQuickTabSwitcherBar(
bool tabBarShowQuickTabSwitcherBar,
) => call(tabBarShowQuickTabSwitcherBar: tabBarShowQuickTabSwitcherBar);
@override @override
/// Creates a new instance with the provided field values. /// Creates a new instance with the provided field values.
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`. /// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GeneralSettings(...).copyWith.fieldName(value)`.
@@ -170,6 +187,8 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
Object? historyAutoCleanInterval = const $CopyWithPlaceholder(), Object? historyAutoCleanInterval = const $CopyWithPlaceholder(),
Object? tabViewBottomSheet = const $CopyWithPlaceholder(), Object? tabViewBottomSheet = const $CopyWithPlaceholder(),
Object? tabBarReaderView = const $CopyWithPlaceholder(), Object? tabBarReaderView = const $CopyWithPlaceholder(),
Object? tabBarShowContextualBar = const $CopyWithPlaceholder(),
Object? tabBarShowQuickTabSwitcherBar = const $CopyWithPlaceholder(),
}) { }) {
return GeneralSettings( return GeneralSettings(
themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null themeMode: themeMode == const $CopyWithPlaceholder() || themeMode == null
@@ -264,6 +283,18 @@ class _$GeneralSettingsCWProxyImpl implements _$GeneralSettingsCWProxy {
? _value.tabBarReaderView ? _value.tabBarReaderView
// ignore: cast_nullable_to_non_nullable // ignore: cast_nullable_to_non_nullable
: tabBarReaderView as bool, : tabBarReaderView as bool,
tabBarShowContextualBar:
tabBarShowContextualBar == const $CopyWithPlaceholder() ||
tabBarShowContextualBar == null
? _value.tabBarShowContextualBar
// ignore: cast_nullable_to_non_nullable
: tabBarShowContextualBar as bool,
tabBarShowQuickTabSwitcherBar:
tabBarShowQuickTabSwitcherBar == const $CopyWithPlaceholder() ||
tabBarShowQuickTabSwitcherBar == null
? _value.tabBarShowQuickTabSwitcherBar
// ignore: cast_nullable_to_non_nullable
: tabBarShowQuickTabSwitcherBar as bool,
); );
} }
} }
@@ -279,46 +310,48 @@ extension $GeneralSettingsCopyWith on GeneralSettings {
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
GeneralSettings _$GeneralSettingsFromJson(Map<String, dynamic> json) => GeneralSettings _$GeneralSettingsFromJson(
GeneralSettings.withDefaults( Map<String, dynamic> json,
themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']), ) => GeneralSettings.withDefaults(
enableReadability: json['enableReadability'] as bool?, themeMode: $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']),
enforceReadability: json['enforceReadability'] as bool?, enableReadability: json['enableReadability'] as bool?,
deleteBrowsingDataOnQuit: enforceReadability: json['enforceReadability'] as bool?,
(json['deleteBrowsingDataOnQuit'] as List<dynamic>?) deleteBrowsingDataOnQuit: (json['deleteBrowsingDataOnQuit'] as List<dynamic>?)
?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e)) ?.map((e) => $enumDecode(_$DeleteBrowsingDataTypeEnumMap, e))
.toSet(), .toSet(),
defaultSearchProvider: const BangKeyConverter().fromJson( defaultSearchProvider: const BangKeyConverter().fromJson(
json['defaultSearchProvider'] as String?, json['defaultSearchProvider'] as String?,
), ),
defaultSearchSuggestionsProvider: $enumDecodeNullable( defaultSearchSuggestionsProvider: $enumDecodeNullable(
_$SearchSuggestionProvidersEnumMap, _$SearchSuggestionProvidersEnumMap,
json['defaultSearchSuggestionsProvider'], json['defaultSearchSuggestionsProvider'],
), ),
createChildTabsOption: json['createChildTabsOption'] as bool?, createChildTabsOption: json['createChildTabsOption'] as bool?,
showExtensionShortcut: json['showExtensionShortcut'] as bool?, showExtensionShortcut: json['showExtensionShortcut'] as bool?,
enableLocalAiFeatures: json['enableLocalAiFeatures'] as bool?, enableLocalAiFeatures: json['enableLocalAiFeatures'] as bool?,
defaultCreateTabType: $enumDecodeNullable( defaultCreateTabType: $enumDecodeNullable(
_$TabTypeEnumMap, _$TabTypeEnumMap,
json['defaultCreateTabType'], json['defaultCreateTabType'],
), ),
tabIntentOpenSetting: $enumDecodeNullable( tabIntentOpenSetting: $enumDecodeNullable(
_$TabIntentOpenSettingEnumMap, _$TabIntentOpenSettingEnumMap,
json['tabIntentOpenSetting'], json['tabIntentOpenSetting'],
), ),
autoHideTabBar: json['autoHideTabBar'] as bool?, autoHideTabBar: json['autoHideTabBar'] as bool?,
tabBarSwipeAction: $enumDecodeNullable( tabBarSwipeAction: $enumDecodeNullable(
_$TabBarSwipeActionEnumMap, _$TabBarSwipeActionEnumMap,
json['tabBarSwipeAction'], json['tabBarSwipeAction'],
), ),
historyAutoCleanInterval: json['historyAutoCleanInterval'] == null historyAutoCleanInterval: json['historyAutoCleanInterval'] == null
? null ? null
: Duration( : Duration(
microseconds: (json['historyAutoCleanInterval'] as num).toInt(), microseconds: (json['historyAutoCleanInterval'] as num).toInt(),
), ),
tabViewBottomSheet: json['tabViewBottomSheet'] as bool?, tabViewBottomSheet: json['tabViewBottomSheet'] as bool?,
tabBarReaderView: json['tabBarReaderView'] as bool?, tabBarReaderView: json['tabBarReaderView'] as bool?,
); tabBarShowContextualBar: json['tabBarShowContextualBar'] as bool?,
tabBarShowQuickTabSwitcherBar: json['tabBarShowQuickTabSwitcherBar'] as bool?,
);
Map<String, dynamic> _$GeneralSettingsToJson( Map<String, dynamic> _$GeneralSettingsToJson(
GeneralSettings instance, GeneralSettings instance,
@@ -346,6 +379,8 @@ Map<String, dynamic> _$GeneralSettingsToJson(
'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds, 'historyAutoCleanInterval': instance.historyAutoCleanInterval.inMicroseconds,
'tabViewBottomSheet': instance.tabViewBottomSheet, 'tabViewBottomSheet': instance.tabViewBottomSheet,
'tabBarReaderView': instance.tabBarReaderView, 'tabBarReaderView': instance.tabBarReaderView,
'tabBarShowContextualBar': instance.tabBarShowContextualBar,
'tabBarShowQuickTabSwitcherBar': instance.tabBarShowQuickTabSwitcherBar,
}; };
const _$ThemeModeEnumMap = { const _$ThemeModeEnumMap = {
@@ -108,6 +108,12 @@ class GeneralSettingsRepository extends _$GeneralSettingsRepository {
DriftSqlType.bool, DriftSqlType.bool,
db.typeMapping, db.typeMapping,
), ),
'tabBarShowContextualBar': settings['tabBarShowContextualBar']?.readAs(
DriftSqlType.bool,
db.typeMapping,
),
'tabBarShowQuickTabSwitcherBar': settings['tabBarShowQuickTabSwitcherBar']
?.readAs(DriftSqlType.bool, db.typeMapping),
}); });
} }
@@ -35,7 +35,7 @@ final class GeneralSettingsRepositoryProvider
} }
String _$generalSettingsRepositoryHash() => String _$generalSettingsRepositoryHash() =>
r'e929fd7a8bf315b08ea7f5a4df2bd5bf066bc84f'; r'4014986c9510d6d7210c5c16d814f6b018a1f93c';
abstract class _$GeneralSettingsRepository abstract class _$GeneralSettingsRepository
extends $StreamNotifier<GeneralSettings> { extends $StreamNotifier<GeneralSettings> {