added feature to configure contextual tab bar buttons
This commit is contained in:
+82
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:fast_equatable/fast_equatable.dart';
|
||||
import 'package:lexo_rank/lexo_rank.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/repositories/contextual_toolbar_config_repository.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart'
|
||||
show ToolbarButtonConfig;
|
||||
|
||||
part 'toolbar_button_configs.g.dart';
|
||||
|
||||
List<ToolbarButtonConfig> _buildDefaultToolbarButtonConfigs() {
|
||||
String? lastKey;
|
||||
|
||||
return toolbarButtonSpecs.map((spec) {
|
||||
final key = lastKey == null
|
||||
? LexoRank.middle().value
|
||||
: LexoRank.parse(lastKey!).genNext().value;
|
||||
|
||||
lastKey = key;
|
||||
|
||||
return ToolbarButtonConfig(
|
||||
buttonId: spec.id.name,
|
||||
orderKey: key,
|
||||
isVisible: spec.defaultVisible,
|
||||
fallbackId: spec.defaultFallback?.name,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
final defaultToolbarButtonConfigs = EquatableValue(
|
||||
_buildDefaultToolbarButtonConfigs(),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
Stream<List<ToolbarButtonConfig>> toolbarButtonConfigs(Ref ref) async* {
|
||||
final repository = ref.watch(contextualToolbarConfigRepositoryProvider);
|
||||
await repository.seedMissingDefaults();
|
||||
yield* repository.watchAll();
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
EquatableValue<List<ToolbarButtonConfig>> effectiveToolbarButtonConfigs(
|
||||
Ref ref,
|
||||
) {
|
||||
final configsAsync = ref.watch(toolbarButtonConfigsProvider);
|
||||
|
||||
return configsAsync.when(
|
||||
data: (configs) {
|
||||
final filtered = configs
|
||||
.where((config) => knownToolbarButtonIds.contains(config.buttonId))
|
||||
.toList();
|
||||
|
||||
if (filtered.isEmpty) {
|
||||
return defaultToolbarButtonConfigs;
|
||||
}
|
||||
|
||||
return EquatableValue(filtered);
|
||||
},
|
||||
loading: () => defaultToolbarButtonConfigs,
|
||||
error: (_, _) => defaultToolbarButtonConfigs,
|
||||
);
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'toolbar_button_configs.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(toolbarButtonConfigs)
|
||||
final toolbarButtonConfigsProvider = ToolbarButtonConfigsProvider._();
|
||||
|
||||
final class ToolbarButtonConfigsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<ToolbarButtonConfig>>,
|
||||
List<ToolbarButtonConfig>,
|
||||
Stream<List<ToolbarButtonConfig>>
|
||||
>
|
||||
with
|
||||
$FutureModifier<List<ToolbarButtonConfig>>,
|
||||
$StreamProvider<List<ToolbarButtonConfig>> {
|
||||
ToolbarButtonConfigsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'toolbarButtonConfigsProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$toolbarButtonConfigsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$StreamProviderElement<List<ToolbarButtonConfig>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $StreamProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Stream<List<ToolbarButtonConfig>> create(Ref ref) {
|
||||
return toolbarButtonConfigs(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$toolbarButtonConfigsHash() =>
|
||||
r'02f79c2087a3a5413fe879405c05f4a4d1eaffeb';
|
||||
|
||||
@ProviderFor(effectiveToolbarButtonConfigs)
|
||||
final effectiveToolbarButtonConfigsProvider =
|
||||
EffectiveToolbarButtonConfigsProvider._();
|
||||
|
||||
final class EffectiveToolbarButtonConfigsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
EquatableValue<List<ToolbarButtonConfig>>,
|
||||
EquatableValue<List<ToolbarButtonConfig>>,
|
||||
EquatableValue<List<ToolbarButtonConfig>>
|
||||
>
|
||||
with $Provider<EquatableValue<List<ToolbarButtonConfig>>> {
|
||||
EffectiveToolbarButtonConfigsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'effectiveToolbarButtonConfigsProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$effectiveToolbarButtonConfigsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<EquatableValue<List<ToolbarButtonConfig>>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
EquatableValue<List<ToolbarButtonConfig>> create(Ref ref) {
|
||||
return effectiveToolbarButtonConfigs(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(EquatableValue<List<ToolbarButtonConfig>> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride:
|
||||
$SyncValueProvider<EquatableValue<List<ToolbarButtonConfig>>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$effectiveToolbarButtonConfigsHash() =>
|
||||
r'dd876160f586c64237a42a29eb63cb119f962b02';
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||
import 'package:weblibre/features/user/data/database/daos/toolbar_button_config.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart'
|
||||
show ToolbarButtonConfig;
|
||||
import 'package:weblibre/features/user/data/providers.dart';
|
||||
|
||||
part 'contextual_toolbar_config_repository.g.dart';
|
||||
|
||||
class ContextualToolbarConfigRepository {
|
||||
ContextualToolbarConfigRepository(this._dao);
|
||||
|
||||
final ToolbarButtonConfigDao _dao;
|
||||
|
||||
Stream<List<ToolbarButtonConfig>> watchAll() => _dao.watchAll();
|
||||
|
||||
Future<void> replaceAll(List<ToolbarButtonConfig> configs) {
|
||||
return _dao.replaceAll(configs);
|
||||
}
|
||||
|
||||
Future<void> assignOrderKey(String buttonId, {required String orderKey}) {
|
||||
return _dao.assignOrderKey(buttonId, orderKey: orderKey);
|
||||
}
|
||||
|
||||
Future<void> assignVisibility(String buttonId, {required bool visible}) {
|
||||
return _dao.assignVisibility(buttonId, visible: visible);
|
||||
}
|
||||
|
||||
Future<void> assignFallback(String buttonId, String? fallbackId) {
|
||||
return _dao.assignFallback(buttonId, fallbackId);
|
||||
}
|
||||
|
||||
Future<String> generateLeadingOrderKey() {
|
||||
return _dao.generateLeadingOrderKey().getSingle();
|
||||
}
|
||||
|
||||
Future<String> generateTrailingOrderKey() {
|
||||
return _dao.generateTrailingOrderKey().getSingle();
|
||||
}
|
||||
|
||||
Future<String?> generateOrderKeyAfterButtonId(String buttonId) {
|
||||
return _dao.generateOrderKeyAfterButtonId(buttonId).getSingleOrNull();
|
||||
}
|
||||
|
||||
Future<String> generateOrderKeyBeforeButtonId(String buttonId) {
|
||||
return _dao.generateOrderKeyBeforeButtonId(buttonId).getSingle();
|
||||
}
|
||||
|
||||
Future<void> seedMissingDefaults() {
|
||||
return _dao.seedMissing(
|
||||
toolbarButtonSpecs
|
||||
.map(
|
||||
(spec) => (
|
||||
buttonId: spec.id.name,
|
||||
defaultVisible: spec.defaultVisible,
|
||||
defaultFallback: spec.defaultFallback?.name,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ContextualToolbarConfigRepository contextualToolbarConfigRepository(Ref ref) {
|
||||
return ContextualToolbarConfigRepository(
|
||||
ref.watch(userDatabaseProvider).toolbarButtonConfigDao,
|
||||
);
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'contextual_toolbar_config_repository.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(contextualToolbarConfigRepository)
|
||||
final contextualToolbarConfigRepositoryProvider =
|
||||
ContextualToolbarConfigRepositoryProvider._();
|
||||
|
||||
final class ContextualToolbarConfigRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
ContextualToolbarConfigRepository,
|
||||
ContextualToolbarConfigRepository,
|
||||
ContextualToolbarConfigRepository
|
||||
>
|
||||
with $Provider<ContextualToolbarConfigRepository> {
|
||||
ContextualToolbarConfigRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'contextualToolbarConfigRepositoryProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() =>
|
||||
_$contextualToolbarConfigRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<ContextualToolbarConfigRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
ContextualToolbarConfigRepository create(Ref ref) {
|
||||
return contextualToolbarConfigRepository(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(ContextualToolbarConfigRepository value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<ContextualToolbarConfigRepository>(
|
||||
value,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$contextualToolbarConfigRepositoryHash() =>
|
||||
r'96bb0c00019e076ef65d3e901ba850de0fccbb1b';
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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/>.
|
||||
*/
|
||||
|
||||
enum ToolbarButtonId {
|
||||
back,
|
||||
forward,
|
||||
bookmarks,
|
||||
share,
|
||||
addTab,
|
||||
tabsCount,
|
||||
navigationMenu,
|
||||
}
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_id.dart';
|
||||
|
||||
class ToolbarButtonSpec {
|
||||
final ToolbarButtonId id;
|
||||
final bool defaultVisible;
|
||||
final ToolbarButtonId? defaultFallback;
|
||||
final bool canBeFallbackTarget;
|
||||
|
||||
const ToolbarButtonSpec({
|
||||
required this.id,
|
||||
required this.defaultVisible,
|
||||
this.defaultFallback,
|
||||
this.canBeFallbackTarget = true,
|
||||
});
|
||||
}
|
||||
|
||||
const backToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.back,
|
||||
defaultVisible: true,
|
||||
defaultFallback: ToolbarButtonId.bookmarks,
|
||||
);
|
||||
|
||||
const forwardToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.forward,
|
||||
defaultVisible: true,
|
||||
defaultFallback: ToolbarButtonId.share,
|
||||
);
|
||||
|
||||
const bookmarksToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.bookmarks,
|
||||
defaultVisible: false,
|
||||
);
|
||||
|
||||
const shareToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.share,
|
||||
defaultVisible: false,
|
||||
);
|
||||
|
||||
const addTabToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.addTab,
|
||||
defaultVisible: true,
|
||||
);
|
||||
|
||||
const tabsCountToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.tabsCount,
|
||||
defaultVisible: true,
|
||||
);
|
||||
|
||||
const navigationMenuToolbarButtonSpec = ToolbarButtonSpec(
|
||||
id: ToolbarButtonId.navigationMenu,
|
||||
defaultVisible: true,
|
||||
);
|
||||
|
||||
const toolbarButtonSpecs = [
|
||||
backToolbarButtonSpec,
|
||||
forwardToolbarButtonSpec,
|
||||
bookmarksToolbarButtonSpec,
|
||||
shareToolbarButtonSpec,
|
||||
addTabToolbarButtonSpec,
|
||||
tabsCountToolbarButtonSpec,
|
||||
navigationMenuToolbarButtonSpec,
|
||||
];
|
||||
|
||||
final Map<String, ToolbarButtonSpec> toolbarButtonSpecsById = {
|
||||
for (final spec in toolbarButtonSpecs) spec.id.name: spec,
|
||||
};
|
||||
|
||||
final Set<String> knownToolbarButtonIds = toolbarButtonSpecsById.keys.toSet();
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:fast_equatable/fast_equatable.dart';
|
||||
|
||||
sealed class ToolbarFallbackChoice with FastEquatable {
|
||||
ToolbarFallbackChoice();
|
||||
|
||||
factory ToolbarFallbackChoice.fromStored(String? storedFallbackId) {
|
||||
if (storedFallbackId != null) {
|
||||
return ToolbarFallbackButton(buttonId: storedFallbackId);
|
||||
}
|
||||
return ToolbarFallbackNone();
|
||||
}
|
||||
|
||||
String? resolveRuntimeFallbackId();
|
||||
|
||||
String? toStoredFallbackId() => resolveRuntimeFallbackId();
|
||||
}
|
||||
|
||||
class ToolbarFallbackButton extends ToolbarFallbackChoice {
|
||||
final String buttonId;
|
||||
|
||||
ToolbarFallbackButton({required this.buttonId});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [buttonId];
|
||||
|
||||
@override
|
||||
String resolveRuntimeFallbackId() => buttonId;
|
||||
}
|
||||
|
||||
class ToolbarFallbackNone extends ToolbarFallbackChoice {
|
||||
ToolbarFallbackNone();
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [null];
|
||||
|
||||
@override
|
||||
String? resolveRuntimeFallbackId() => null;
|
||||
}
|
||||
+116
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_fallback_choice.dart';
|
||||
import 'package:weblibre/features/user/data/database/definitions.drift.dart';
|
||||
|
||||
class ContextualToolbarButtonResolution {
|
||||
const ContextualToolbarButtonResolution({
|
||||
required this.buttonId,
|
||||
required this.isEnabled,
|
||||
});
|
||||
|
||||
final String buttonId;
|
||||
final bool isEnabled;
|
||||
}
|
||||
|
||||
List<ContextualToolbarButtonResolution> resolveVisibleContextualToolbarButtons({
|
||||
required List<ToolbarButtonConfig> configs,
|
||||
required Set<String> knownButtonIds,
|
||||
required bool Function(String buttonId) isPrimaryAvailable,
|
||||
}) {
|
||||
final configById = {for (final config in configs) config.buttonId: config};
|
||||
final resolvedButtons = <ContextualToolbarButtonResolution>[];
|
||||
final seenIds = <String>{};
|
||||
|
||||
for (final config in configs.where((config) => config.isVisible)) {
|
||||
final resolvedButton = resolveContextualToolbarButton(
|
||||
config: config,
|
||||
configById: configById,
|
||||
knownButtonIds: knownButtonIds,
|
||||
isPrimaryAvailable: isPrimaryAvailable,
|
||||
);
|
||||
if (resolvedButton == null) {
|
||||
continue;
|
||||
}
|
||||
if (seenIds.add(resolvedButton.buttonId)) {
|
||||
resolvedButtons.add(resolvedButton);
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedButtons;
|
||||
}
|
||||
|
||||
ContextualToolbarButtonResolution? resolveContextualToolbarButton({
|
||||
required ToolbarButtonConfig config,
|
||||
required Map<String, ToolbarButtonConfig> configById,
|
||||
required Set<String> knownButtonIds,
|
||||
required bool Function(String buttonId) isPrimaryAvailable,
|
||||
Set<String> visited = const {},
|
||||
}) {
|
||||
if (visited.contains(config.buttonId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!knownButtonIds.contains(config.buttonId)) {
|
||||
return ContextualToolbarButtonResolution(
|
||||
buttonId: config.buttonId,
|
||||
isEnabled: true,
|
||||
);
|
||||
}
|
||||
|
||||
if (isPrimaryAvailable(config.buttonId)) {
|
||||
return ContextualToolbarButtonResolution(
|
||||
buttonId: config.buttonId,
|
||||
isEnabled: true,
|
||||
);
|
||||
}
|
||||
|
||||
final fallbackId = ToolbarFallbackChoice.fromStored(
|
||||
config.fallbackId,
|
||||
).resolveRuntimeFallbackId();
|
||||
if (fallbackId == null) {
|
||||
return ContextualToolbarButtonResolution(
|
||||
buttonId: config.buttonId,
|
||||
isEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
final fallbackConfig = configById[fallbackId];
|
||||
if (fallbackConfig == null) {
|
||||
return knownButtonIds.contains(fallbackId)
|
||||
? ContextualToolbarButtonResolution(
|
||||
buttonId: fallbackId,
|
||||
isEnabled: true,
|
||||
)
|
||||
: ContextualToolbarButtonResolution(
|
||||
buttonId: config.buttonId,
|
||||
isEnabled: false,
|
||||
);
|
||||
}
|
||||
|
||||
return resolveContextualToolbarButton(
|
||||
config: fallbackConfig,
|
||||
configById: configById,
|
||||
knownButtonIds: knownButtonIds,
|
||||
isPrimaryAvailable: isPrimaryAvailable,
|
||||
visited: {...visited, config.buttonId},
|
||||
);
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:fast_equatable/fast_equatable.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/tab.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
|
||||
class ContextualToolbarScope with FastEquatable {
|
||||
final String? selectedTabId;
|
||||
final Sheet? displayedSheet;
|
||||
final TabState? tabState;
|
||||
final bool isPreview;
|
||||
|
||||
ContextualToolbarScope({
|
||||
required this.selectedTabId,
|
||||
required this.displayedSheet,
|
||||
required this.tabState,
|
||||
required this.isPreview,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
selectedTabId,
|
||||
displayedSheet,
|
||||
tabState,
|
||||
isPreview,
|
||||
];
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:flutter/material.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/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||
|
||||
class ToolbarButtonDefinition {
|
||||
final ToolbarButtonSpec spec;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final bool Function(ContextualToolbarScope scope, WidgetRef ref)?
|
||||
isPrimaryAvailable;
|
||||
final Widget Function(
|
||||
ContextualToolbarScope scope,
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
)
|
||||
builder;
|
||||
|
||||
const ToolbarButtonDefinition({
|
||||
required this.spec,
|
||||
required this.label,
|
||||
required this.icon,
|
||||
this.isPrimaryAvailable,
|
||||
required this.builder,
|
||||
});
|
||||
}
|
||||
|
||||
final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
|
||||
ToolbarButtonDefinition(
|
||||
spec: backToolbarButtonSpec,
|
||||
label: 'Back',
|
||||
icon: Icons.arrow_back,
|
||||
isPrimaryAvailable: (scope, ref) =>
|
||||
scope.tabState?.historyState.canGoBack == true ||
|
||||
scope.tabState?.isLoading == true,
|
||||
builder: (scope, context, ref) {
|
||||
if (scope.isPreview) {
|
||||
return NavigateBackButtonView(
|
||||
canGoBack: true,
|
||||
isLoading: false,
|
||||
onPressed: () {},
|
||||
onLongPress: () {},
|
||||
);
|
||||
}
|
||||
return NavigateBackButton(
|
||||
selectedTabId: scope.selectedTabId,
|
||||
isLoading: scope.tabState?.isLoading ?? false,
|
||||
);
|
||||
},
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: forwardToolbarButtonSpec,
|
||||
label: 'Forward',
|
||||
icon: Icons.arrow_forward,
|
||||
isPrimaryAvailable: (scope, ref) =>
|
||||
scope.tabState?.historyState.canGoForward == true,
|
||||
builder: (scope, context, ref) {
|
||||
if (scope.isPreview) {
|
||||
return NavigateForwardButtonView(
|
||||
canGoForward: true,
|
||||
onPressed: () {},
|
||||
onLongPress: () {},
|
||||
);
|
||||
}
|
||||
return NavigateForwardButton(selectedTabId: scope.selectedTabId);
|
||||
},
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: bookmarksToolbarButtonSpec,
|
||||
label: 'Bookmarks',
|
||||
icon: MdiIcons.bookmarkMultiple,
|
||||
builder: (scope, context, ref) {
|
||||
return IconButton(
|
||||
onPressed: scope.isPreview
|
||||
? () {}
|
||||
: () async {
|
||||
await BookmarkListRoute(
|
||||
entryGuid: BookmarkRoot.root.id,
|
||||
).push(context);
|
||||
},
|
||||
icon: const Icon(MdiIcons.bookmarkMultiple),
|
||||
);
|
||||
},
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: shareToolbarButtonSpec,
|
||||
label: 'Share',
|
||||
icon: Icons.share,
|
||||
builder: (scope, context, ref) => scope.isPreview
|
||||
? ShareMenuButtonView(onPressed: () {})
|
||||
: ShareMenuButton(selectedTabId: scope.selectedTabId),
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: addTabToolbarButtonSpec,
|
||||
label: 'New Tab',
|
||||
icon: MdiIcons.tabPlus,
|
||||
builder: (scope, context, ref) => scope.isPreview
|
||||
? AddTabButtonView(onPressed: () {}, onLongPress: () {})
|
||||
: const AddTabButton(),
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: tabsCountToolbarButtonSpec,
|
||||
label: 'Tabs',
|
||||
icon: MdiIcons.tab,
|
||||
builder: (scope, context, ref) => scope.isPreview
|
||||
? TabsCountButtonView(
|
||||
isActive: false,
|
||||
onTap: () {},
|
||||
onLongPress: () {},
|
||||
buttonBuilder: (isActive, onTap, onLongPress) {
|
||||
return TabsActionButtonView(
|
||||
isActive: isActive,
|
||||
tabCountText: '5',
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
},
|
||||
)
|
||||
: TabsCountButton(
|
||||
selectedTabId: scope.selectedTabId,
|
||||
displayedSheet: scope.displayedSheet,
|
||||
showLongPressMenu: false,
|
||||
),
|
||||
),
|
||||
ToolbarButtonDefinition(
|
||||
spec: navigationMenuToolbarButtonSpec,
|
||||
label: 'Menu',
|
||||
icon: Icons.more_vert,
|
||||
builder: (scope, context, ref) => scope.isPreview
|
||||
? NavigationMenuButtonView(onTap: () {})
|
||||
: NavigationMenuButton(selectedTabId: scope.selectedTabId),
|
||||
),
|
||||
];
|
||||
|
||||
final Map<String, ToolbarButtonDefinition> toolbarButtonRegistryById = {
|
||||
for (final def in toolbarButtonRegistry) def.spec.id.name: def,
|
||||
};
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:flutter/material.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/share_bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||
|
||||
class ShareMenuButton extends StatelessWidget {
|
||||
final String? selectedTabId;
|
||||
|
||||
const ShareMenuButton({super.key, required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShareMenuButtonView(
|
||||
onPressed: () async {
|
||||
final tabId = selectedTabId;
|
||||
if (tabId != null) {
|
||||
await showShareBottomSheet(context, selectedTabId: tabId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenuButtonView extends StatelessWidget {
|
||||
const ShareMenuButtonView({super.key, this.onPressed});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(onPressed: onPressed, icon: const Icon(Icons.share));
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationMenuButton extends StatelessWidget {
|
||||
final String? selectedTabId;
|
||||
|
||||
const NavigationMenuButton({super.key, required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigationMenuButtonView(
|
||||
onTap: () async {
|
||||
await showBrowserMenuSheet(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationMenuButtonView extends StatelessWidget {
|
||||
const NavigationMenuButtonView({super.key, this.onTap});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarButton(onTap: onTap, child: const Icon(Icons.more_vert));
|
||||
}
|
||||
}
|
||||
|
||||
class AddTabButton extends HookConsumerWidget {
|
||||
const AddTabButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabMenuController = useMenuController();
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: AddTabButtonView(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.effectiveDefaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (tabMenuController.isOpen) {
|
||||
tabMenuController.close();
|
||||
} else {
|
||||
tabMenuController.open();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddTabButtonView extends StatelessWidget {
|
||||
const AddTabButtonView({super.key, this.onPressed, this.onLongPress});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(MdiIcons.tabPlus),
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsCountButtonView extends StatelessWidget {
|
||||
const TabsCountButtonView({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.buttonBuilder,
|
||||
});
|
||||
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final Widget Function(
|
||||
bool isActive,
|
||||
VoidCallback onTap,
|
||||
VoidCallback? onLongPress,
|
||||
)?
|
||||
buttonBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (buttonBuilder != null)
|
||||
? buttonBuilder!(isActive, onTap, onLongPress)
|
||||
: TabsActionButton(
|
||||
isActive: isActive,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsCountButton extends HookConsumerWidget {
|
||||
const TabsCountButton({
|
||||
super.key,
|
||||
required this.selectedTabId,
|
||||
required this.displayedSheet,
|
||||
required this.showLongPressMenu,
|
||||
});
|
||||
|
||||
final String? selectedTabId;
|
||||
final Sheet? displayedSheet;
|
||||
final bool showLongPressMenu;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabMenuController = useMenuController();
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: TabsCountButtonView(
|
||||
isActive: displayedSheet is ViewTabsSheet,
|
||||
onTap: () async {
|
||||
final tabViewBottomSheet = ref
|
||||
.read(generalSettingsWithDefaultsProvider)
|
||||
.tabViewBottomSheet;
|
||||
|
||||
if (tabViewBottomSheet) {
|
||||
if (displayedSheet case ViewTabsSheet()) {
|
||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||
} else {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(ViewTabsSheet());
|
||||
}
|
||||
} else {
|
||||
await const TabViewRoute().push(context);
|
||||
}
|
||||
},
|
||||
onLongPress: showLongPressMenu
|
||||
? () {
|
||||
if (tabMenuController.isOpen) {
|
||||
tabMenuController.close();
|
||||
} else {
|
||||
tabMenuController.open();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2024-2026 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:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/tab_state.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/data/providers/toolbar_button_configs.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/entities/toolbar_button_spec.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/domain/services/toolbar_button_resolution.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/models/contextual_toolbar_scope.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/toolbar_button_registry.dart';
|
||||
|
||||
class ContextualToolbar extends HookConsumerWidget {
|
||||
const ContextualToolbar({
|
||||
super.key,
|
||||
required this.selectedTabId,
|
||||
required this.displayedSheet,
|
||||
});
|
||||
|
||||
final String? selectedTabId;
|
||||
final Sheet? displayedSheet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||
final configs = ref.watch(effectiveToolbarButtonConfigsProvider);
|
||||
|
||||
final scope = ContextualToolbarScope(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
tabState: tabState,
|
||||
isPreview: false,
|
||||
);
|
||||
|
||||
final resolvedButtons = useMemoized(
|
||||
() => resolveVisibleContextualToolbarButtons(
|
||||
configs: configs.value,
|
||||
knownButtonIds: knownToolbarButtonIds,
|
||||
isPrimaryAvailable: (buttonId) {
|
||||
final def = toolbarButtonRegistryById[buttonId];
|
||||
return def?.isPrimaryAvailable?.call(scope, ref) ?? true;
|
||||
},
|
||||
),
|
||||
[configs, scope],
|
||||
);
|
||||
|
||||
final buttons = resolvedButtons
|
||||
.map((button) => _buildButton(scope, context, ref, button))
|
||||
.toList();
|
||||
|
||||
return ContextualToolbarView(buttons: buttons);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
ContextualToolbarScope scope,
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
ContextualToolbarButtonResolution button,
|
||||
) {
|
||||
final def = toolbarButtonRegistryById[button.buttonId];
|
||||
if (def == null) return const SizedBox.shrink();
|
||||
|
||||
final child = def.builder(scope, context, ref);
|
||||
|
||||
if (button.isEnabled) {
|
||||
return child;
|
||||
}
|
||||
|
||||
return Opacity(opacity: 0.38, child: IgnorePointer(child: child));
|
||||
}
|
||||
}
|
||||
|
||||
class ContextualToolbarView extends StatelessWidget {
|
||||
const ContextualToolbarView({super.key, required this.buttons});
|
||||
|
||||
final List<Widget> buttons;
|
||||
|
||||
static const _minButtonWidth = 48.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (buttons.isEmpty) return const SizedBox.shrink();
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final fitsEvenly =
|
||||
constraints.maxWidth >= _minButtonWidth * buttons.length;
|
||||
|
||||
if (fitsEvenly) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: buttons,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: buttons
|
||||
.map(
|
||||
(button) => SizedBox(width: _minButtonWidth, child: button),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+2
-279
@@ -25,10 +25,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/design/app_colors.dart';
|
||||
import 'package:weblibre/core/routing/routes.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/controllers/bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/domain/providers/selected_tab.dart';
|
||||
@@ -36,16 +34,13 @@ 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/entities/sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/domain/providers.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/features/contextual_toolbar/presentation/widgets/contextual_toolbar.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/controllers/toolbar_visibility.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_menu_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/browser_modules/app_bar_title.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/extension_shortcut_menu.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_buttons.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/share_bottom_sheet.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_creation_menu.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_icon.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tab_menu.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/controllers/readerable.dart';
|
||||
import 'package:weblibre/features/geckoview/features/readerview/presentation/widgets/reader_button.dart';
|
||||
@@ -460,90 +455,6 @@ class QuickTabSwitcherItem with FastEquatable {
|
||||
];
|
||||
}
|
||||
|
||||
class ContextualToolbar extends HookConsumerWidget {
|
||||
const ContextualToolbar({
|
||||
super.key,
|
||||
required this.selectedTabId,
|
||||
required this.displayedSheet,
|
||||
});
|
||||
|
||||
final String? selectedTabId;
|
||||
final Sheet? displayedSheet;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabState = ref.watch(tabStateProvider(selectedTabId));
|
||||
|
||||
return ContextualToolbarView(
|
||||
canGoBack:
|
||||
tabState?.historyState.canGoBack == true ||
|
||||
tabState?.isLoading == true,
|
||||
canGoForward: tabState?.historyState.canGoForward == true,
|
||||
onBookmarksTap: () async {
|
||||
await BookmarkListRoute(entryGuid: BookmarkRoot.root.id).push(context);
|
||||
},
|
||||
backButton: NavigateBackButton(
|
||||
selectedTabId: selectedTabId,
|
||||
isLoading: tabState?.isLoading ?? false,
|
||||
),
|
||||
forwardButton: NavigateForwardButton(selectedTabId: selectedTabId),
|
||||
shareButton: ShareMenuButton(selectedTabId: selectedTabId),
|
||||
addTabButton: const AddTabButton(),
|
||||
tabsCountButton: TabsCountButton(
|
||||
selectedTabId: selectedTabId,
|
||||
displayedSheet: displayedSheet,
|
||||
showLongPressMenu: false,
|
||||
),
|
||||
navigationButton: NavigationMenuButton(selectedTabId: selectedTabId),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ContextualToolbarView extends StatelessWidget {
|
||||
const ContextualToolbarView({
|
||||
super.key,
|
||||
required this.canGoBack,
|
||||
required this.canGoForward,
|
||||
required this.onBookmarksTap,
|
||||
required this.backButton,
|
||||
required this.forwardButton,
|
||||
required this.shareButton,
|
||||
required this.addTabButton,
|
||||
required this.tabsCountButton,
|
||||
required this.navigationButton,
|
||||
});
|
||||
|
||||
final bool canGoBack;
|
||||
final bool canGoForward;
|
||||
final VoidCallback onBookmarksTap;
|
||||
final Widget backButton;
|
||||
final Widget forwardButton;
|
||||
final Widget shareButton;
|
||||
final Widget addTabButton;
|
||||
final Widget tabsCountButton;
|
||||
final Widget navigationButton;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
if (canGoBack)
|
||||
backButton
|
||||
else
|
||||
IconButton(
|
||||
onPressed: onBookmarksTap,
|
||||
icon: const Icon(MdiIcons.bookmarkMultiple),
|
||||
),
|
||||
if (canGoForward) forwardButton else shareButton,
|
||||
addTabButton,
|
||||
tabsCountButton,
|
||||
navigationButton,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class QuickTabSwitcher extends HookConsumerWidget {
|
||||
final QuickTabSwitcherMode quickTabSwitcherMode;
|
||||
|
||||
@@ -760,191 +671,3 @@ class QuickTabSwitcherView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenuButton extends StatelessWidget {
|
||||
final String? selectedTabId;
|
||||
|
||||
const ShareMenuButton({super.key, required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ShareMenuButtonView(
|
||||
onPressed: () async {
|
||||
final tabId = selectedTabId;
|
||||
if (tabId != null) {
|
||||
await showShareBottomSheet(context, selectedTabId: tabId);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenuButtonView extends StatelessWidget {
|
||||
const ShareMenuButtonView({super.key, this.onPressed});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(onPressed: onPressed, icon: const Icon(Icons.share));
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationMenuButton extends StatelessWidget {
|
||||
final String? selectedTabId;
|
||||
|
||||
const NavigationMenuButton({super.key, required this.selectedTabId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigationMenuButtonView(
|
||||
onTap: () async {
|
||||
await showBrowserMenuSheet(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationMenuButtonView extends StatelessWidget {
|
||||
const NavigationMenuButtonView({super.key, this.onTap});
|
||||
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ToolbarButton(onTap: onTap, child: const Icon(Icons.more_vert));
|
||||
}
|
||||
}
|
||||
|
||||
class AddTabButton extends HookConsumerWidget {
|
||||
const AddTabButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabMenuController = useMenuController();
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: AddTabButtonView(
|
||||
onPressed: () async {
|
||||
final settings = ref.read(generalSettingsWithDefaultsProvider);
|
||||
|
||||
await SearchRoute(
|
||||
tabType:
|
||||
ref.read(selectedTabTypeProvider) ??
|
||||
settings.effectiveDefaultCreateTabType,
|
||||
).push(context);
|
||||
|
||||
if (context.mounted) {
|
||||
const BrowserRoute().go(context);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
if (tabMenuController.isOpen) {
|
||||
tabMenuController.close();
|
||||
} else {
|
||||
tabMenuController.open();
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AddTabButtonView extends StatelessWidget {
|
||||
const AddTabButtonView({super.key, this.onPressed, this.onLongPress});
|
||||
|
||||
final VoidCallback? onPressed;
|
||||
final VoidCallback? onLongPress;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return IconButton(
|
||||
onPressed: onPressed,
|
||||
icon: const Icon(MdiIcons.tabPlus),
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsCountButtonView extends StatelessWidget {
|
||||
const TabsCountButtonView({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
required this.onTap,
|
||||
this.onLongPress,
|
||||
this.buttonBuilder,
|
||||
});
|
||||
|
||||
final bool isActive;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback? onLongPress;
|
||||
final Widget Function(
|
||||
bool isActive,
|
||||
VoidCallback onTap,
|
||||
VoidCallback? onLongPress,
|
||||
)?
|
||||
buttonBuilder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return (buttonBuilder != null)
|
||||
? buttonBuilder!(isActive, onTap, onLongPress)
|
||||
: TabsActionButton(
|
||||
isActive: isActive,
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class TabsCountButton extends HookConsumerWidget {
|
||||
const TabsCountButton({
|
||||
super.key,
|
||||
required this.selectedTabId,
|
||||
required this.displayedSheet,
|
||||
required this.showLongPressMenu,
|
||||
});
|
||||
|
||||
final String? selectedTabId;
|
||||
final Sheet? displayedSheet;
|
||||
final bool showLongPressMenu;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabMenuController = useMenuController();
|
||||
|
||||
return TabCreationMenu(
|
||||
controller: tabMenuController,
|
||||
child: TabsCountButtonView(
|
||||
isActive: displayedSheet is ViewTabsSheet,
|
||||
onTap: () async {
|
||||
final tabViewBottomSheet = ref
|
||||
.read(generalSettingsWithDefaultsProvider)
|
||||
.tabViewBottomSheet;
|
||||
|
||||
if (tabViewBottomSheet) {
|
||||
if (displayedSheet case ViewTabsSheet()) {
|
||||
ref.read(bottomSheetControllerProvider.notifier).requestDismiss();
|
||||
} else {
|
||||
ref
|
||||
.read(bottomSheetControllerProvider.notifier)
|
||||
.show(ViewTabsSheet());
|
||||
}
|
||||
} else {
|
||||
await const TabViewRoute().push(context);
|
||||
}
|
||||
},
|
||||
onLongPress: showLongPressMenu
|
||||
? () {
|
||||
if (tabMenuController.isOpen) {
|
||||
tabMenuController.close();
|
||||
} else {
|
||||
tabMenuController.open();
|
||||
}
|
||||
}
|
||||
: null,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -45,7 +45,7 @@ final class StartupPreferenceEnforcementServiceProvider
|
||||
}
|
||||
|
||||
String _$startupPreferenceEnforcementServiceHash() =>
|
||||
r'107cdcf7550b7977b53a3cf7419861cdcac0fe3a';
|
||||
r'5cfe5aec33a9e9e077704ff519c10b0398c5085d';
|
||||
|
||||
abstract class _$StartupPreferenceEnforcementService extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
Reference in New Issue
Block a user