added more contextual menu buttons
This commit is contained in:
@@ -96,6 +96,16 @@ class TabSession extends _$TabSession {
|
|||||||
return _sessionService.translateRestore();
|
return _sessionService.translateRestore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> pageUp() {
|
||||||
|
// Android KeyEvent.KEYCODE_PAGE_UP = 92
|
||||||
|
return _sessionService.dispatchKeyEvent(keyCode: 92);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> pageDown() {
|
||||||
|
// Android KeyEvent.KEYCODE_PAGE_DOWN = 93
|
||||||
|
return _sessionService.dispatchKeyEvent(keyCode: 93);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void build({required String? tabId}) {
|
void build({required String? tabId}) {
|
||||||
_sessionService = (tabId != null)
|
_sessionService = (tabId != null)
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/*
|
||||||
|
* 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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fontSizeStep = 0.1;
|
||||||
|
const fontSizeMin = 0.5;
|
||||||
|
const fontSizeMax = 3.0;
|
||||||
|
const fontSizeDefault = 1.0;
|
||||||
+14
@@ -26,4 +26,18 @@ enum ToolbarButtonId {
|
|||||||
addTab,
|
addTab,
|
||||||
tabsCount,
|
tabsCount,
|
||||||
navigationMenu,
|
navigationMenu,
|
||||||
|
reload,
|
||||||
|
readerMode,
|
||||||
|
desktop,
|
||||||
|
translation,
|
||||||
|
findInPage,
|
||||||
|
closeTab,
|
||||||
|
inputUrl,
|
||||||
|
duplicateTab,
|
||||||
|
increaseFont,
|
||||||
|
decreaseFont,
|
||||||
|
moveToBackground,
|
||||||
|
pageUp,
|
||||||
|
pageDown,
|
||||||
|
font,
|
||||||
}
|
}
|
||||||
|
|||||||
+90
@@ -71,6 +71,82 @@ const navigationMenuToolbarButtonSpec = ToolbarButtonSpec(
|
|||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reloadToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.reload,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const readerModeToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.readerMode,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const desktopToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.desktop,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const translationToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.translation,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const findInPageToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.findInPage,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const closeTabToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.closeTab,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const inputUrlToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.inputUrl,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const duplicateTabToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.duplicateTab,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const increaseFontToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.increaseFont,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const decreaseFontToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.decreaseFont,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const moveToBackgroundToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.moveToBackground,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageUpToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.pageUp,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageDownToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.pageDown,
|
||||||
|
defaultVisible: false,
|
||||||
|
canBeFallbackTarget: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
const fontToolbarButtonSpec = ToolbarButtonSpec(
|
||||||
|
id: ToolbarButtonId.font,
|
||||||
|
defaultVisible: false,
|
||||||
|
);
|
||||||
|
|
||||||
const toolbarButtonSpecs = [
|
const toolbarButtonSpecs = [
|
||||||
backToolbarButtonSpec,
|
backToolbarButtonSpec,
|
||||||
forwardToolbarButtonSpec,
|
forwardToolbarButtonSpec,
|
||||||
@@ -79,6 +155,20 @@ const toolbarButtonSpecs = [
|
|||||||
addTabToolbarButtonSpec,
|
addTabToolbarButtonSpec,
|
||||||
tabsCountToolbarButtonSpec,
|
tabsCountToolbarButtonSpec,
|
||||||
navigationMenuToolbarButtonSpec,
|
navigationMenuToolbarButtonSpec,
|
||||||
|
reloadToolbarButtonSpec,
|
||||||
|
readerModeToolbarButtonSpec,
|
||||||
|
desktopToolbarButtonSpec,
|
||||||
|
translationToolbarButtonSpec,
|
||||||
|
findInPageToolbarButtonSpec,
|
||||||
|
closeTabToolbarButtonSpec,
|
||||||
|
inputUrlToolbarButtonSpec,
|
||||||
|
duplicateTabToolbarButtonSpec,
|
||||||
|
increaseFontToolbarButtonSpec,
|
||||||
|
decreaseFontToolbarButtonSpec,
|
||||||
|
moveToBackgroundToolbarButtonSpec,
|
||||||
|
pageUpToolbarButtonSpec,
|
||||||
|
pageDownToolbarButtonSpec,
|
||||||
|
fontToolbarButtonSpec,
|
||||||
];
|
];
|
||||||
|
|
||||||
final Map<String, ToolbarButtonSpec> toolbarButtonSpecsById = {
|
final Map<String, ToolbarButtonSpec> toolbarButtonSpecsById = {
|
||||||
|
|||||||
+452
@@ -22,11 +22,29 @@ 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:weblibre/core/routing/routes.dart';
|
import 'package:weblibre/core/routing/routes.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/entities/states/readerable.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/domain/providers.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/repositories/tab.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/domain/entities/font_size_constants.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/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/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/features/contextual_toolbar/presentation/widgets/contextual_bar_buttons.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/font_size_bottom_sheet.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/navigation_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';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/tabs_action_button.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/translation_bottom_sheet.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/tabs/data/entities/tab_mode.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||||
|
import 'package:weblibre/utils/move_to_background.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class ToolbarButtonDefinition {
|
class ToolbarButtonDefinition {
|
||||||
final ToolbarButtonSpec spec;
|
final ToolbarButtonSpec spec;
|
||||||
@@ -155,8 +173,442 @@ final List<ToolbarButtonDefinition> toolbarButtonRegistry = [
|
|||||||
? NavigationMenuButtonView(onTap: () {})
|
? NavigationMenuButtonView(onTap: () {})
|
||||||
: NavigationMenuButton(selectedTabId: scope.selectedTabId),
|
: NavigationMenuButton(selectedTabId: scope.selectedTabId),
|
||||||
),
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: reloadToolbarButtonSpec,
|
||||||
|
label: 'Reload',
|
||||||
|
icon: Icons.refresh,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () async {
|
||||||
|
final tabId = scope.selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
|
.reload();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.refresh),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: readerModeToolbarButtonSpec,
|
||||||
|
label: 'Reader Mode',
|
||||||
|
icon: MdiIcons.bookOpenOutline,
|
||||||
|
isPrimaryAvailable: (scope, ref) {
|
||||||
|
final readerableState =
|
||||||
|
scope.tabState?.readerableState ?? ReaderableState.$default();
|
||||||
|
final enableReadability = ref.read(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(value) => value.enableReadability,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final enforceReadability = ref.read(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(value) => value.enforceReadability,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return (readerableState.readerable &&
|
||||||
|
(enableReadability || readerableState.active)) ||
|
||||||
|
(enforceReadability && enableReadability);
|
||||||
|
},
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(MdiIcons.bookOpenOutline),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _ReaderModeToolbarButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: desktopToolbarButtonSpec,
|
||||||
|
label: 'Desktop Site',
|
||||||
|
icon: Icons.desktop_windows,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.desktop_windows_outlined),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _DesktopModeToolbarButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: translationToolbarButtonSpec,
|
||||||
|
label: 'Translate',
|
||||||
|
icon: Icons.translate,
|
||||||
|
isPrimaryAvailable: (scope, ref) {
|
||||||
|
final engineState = ref.read(translationEngineStateProvider);
|
||||||
|
final readerActive = scope.tabState?.readerableState.active ?? false;
|
||||||
|
return !readerActive && engineState?.isEngineSupported == true;
|
||||||
|
},
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return IconButton(onPressed: () {}, icon: const Icon(Icons.translate));
|
||||||
|
}
|
||||||
|
return _TranslateToolbarButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: findInPageToolbarButtonSpec,
|
||||||
|
label: 'Find in Page',
|
||||||
|
icon: Icons.search,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () {
|
||||||
|
final tabId = scope.selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
ref.read(findInPageControllerProvider(tabId).notifier).show();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: closeTabToolbarButtonSpec,
|
||||||
|
label: 'Close Tab',
|
||||||
|
icon: MdiIcons.tabMinus,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () => _closeTab(context, ref, scope.selectedTabId),
|
||||||
|
icon: const Icon(MdiIcons.tabMinus),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: inputUrlToolbarButtonSpec,
|
||||||
|
label: 'Address Bar',
|
||||||
|
icon: Icons.edit,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () async {
|
||||||
|
final tabState = scope.tabState;
|
||||||
|
if (tabState != null) {
|
||||||
|
final searchText = tabState.url.scheme == 'about'
|
||||||
|
? SearchRoute.emptySearchText
|
||||||
|
: tabState.url.toString();
|
||||||
|
await SearchRoute(
|
||||||
|
tabId: tabState.id,
|
||||||
|
searchText: searchText.isEmpty
|
||||||
|
? SearchRoute.emptySearchText
|
||||||
|
: searchText,
|
||||||
|
tabType: tabState.tabMode.toTabType(),
|
||||||
|
).push(context);
|
||||||
|
} else {
|
||||||
|
await const SearchRoute(
|
||||||
|
tabType: TabType.regular,
|
||||||
|
).push(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: duplicateTabToolbarButtonSpec,
|
||||||
|
label: 'Duplicate Tab',
|
||||||
|
icon: MdiIcons.tabPlus,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return scope.isPreview
|
||||||
|
? CloneTabButtonView(onPressed: () {}, onLongPress: () {})
|
||||||
|
: CloneTabButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: increaseFontToolbarButtonSpec,
|
||||||
|
label: 'Increase Font',
|
||||||
|
icon: MdiIcons.formatFontSizeIncrease,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () => _adjustFontSize(context, ref, increase: true),
|
||||||
|
icon: const Icon(MdiIcons.formatFontSizeIncrease),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: decreaseFontToolbarButtonSpec,
|
||||||
|
label: 'Decrease Font',
|
||||||
|
icon: MdiIcons.formatFontSizeDecrease,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () => _adjustFontSize(context, ref, increase: false),
|
||||||
|
icon: const Icon(MdiIcons.formatFontSizeDecrease),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: moveToBackgroundToolbarButtonSpec,
|
||||||
|
label: 'Background',
|
||||||
|
icon: MdiIcons.arrowCollapseDown,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview ? () {} : moveToBackground,
|
||||||
|
icon: const Icon(MdiIcons.arrowCollapseDown),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: pageUpToolbarButtonSpec,
|
||||||
|
label: 'Page Up',
|
||||||
|
icon: MdiIcons.chevronDoubleUp,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () async {
|
||||||
|
final tabId = scope.selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
|
.pageUp();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.chevronDoubleUp),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: pageDownToolbarButtonSpec,
|
||||||
|
label: 'Page Down',
|
||||||
|
icon: MdiIcons.chevronDoubleDown,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: scope.isPreview
|
||||||
|
? () {}
|
||||||
|
: () async {
|
||||||
|
final tabId = scope.selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
await ref
|
||||||
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
|
.pageDown();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(MdiIcons.chevronDoubleDown),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ToolbarButtonDefinition(
|
||||||
|
spec: fontToolbarButtonSpec,
|
||||||
|
label: 'Text Size',
|
||||||
|
icon: MdiIcons.formatSize,
|
||||||
|
builder: (scope, context, ref) {
|
||||||
|
if (scope.isPreview) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(MdiIcons.formatSize),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _FontToolbarButton(selectedTabId: scope.selectedTabId);
|
||||||
|
},
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
final Map<String, ToolbarButtonDefinition> toolbarButtonRegistryById = {
|
final Map<String, ToolbarButtonDefinition> toolbarButtonRegistryById = {
|
||||||
for (final def in toolbarButtonRegistry) def.spec.id.name: def,
|
for (final def in toolbarButtonRegistry) def.spec.id.name: def,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Future<void> _closeTab(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String? selectedTabId,
|
||||||
|
) async {
|
||||||
|
if (selectedTabId == null) return;
|
||||||
|
|
||||||
|
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||||
|
if (tabState != null && tabState.tabMode is IsolatedTabMode) {
|
||||||
|
final allStates = ref.read(tabStatesProvider);
|
||||||
|
final groupCount = allStates.values
|
||||||
|
.where((s) => s.isolationContextId == tabState.isolationContextId)
|
||||||
|
.length;
|
||||||
|
if (groupCount <= 1 && context.mounted) {
|
||||||
|
final confirmed = await ui_helper.confirmIsolatedTabClose(context);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await ref.read(tabRepositoryProvider.notifier).closeTab(selectedTabId);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabUndoClose(
|
||||||
|
context,
|
||||||
|
ref.read(tabRepositoryProvider.notifier).undoClose,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _adjustFontSize(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref, {
|
||||||
|
required bool increase,
|
||||||
|
}) async {
|
||||||
|
final settings = ref.read(engineSettingsWithDefaultsProvider);
|
||||||
|
|
||||||
|
if (settings.automaticFontSizeAdjustment) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text(
|
||||||
|
'Disable automatic font size in settings to adjust manually',
|
||||||
|
),
|
||||||
|
duration: Duration(seconds: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final current = settings.fontSizeFactor;
|
||||||
|
final newValue = increase
|
||||||
|
? (current + fontSizeStep).clamp(fontSizeMin, fontSizeMax)
|
||||||
|
: (current - fontSizeStep).clamp(fontSizeMin, fontSizeMax);
|
||||||
|
final rounded = (newValue * 10).round() / 10;
|
||||||
|
|
||||||
|
if (rounded == current) return;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(saveEngineSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) => currentSettings.copyWith.fontSizeFactor(rounded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReaderModeToolbarButton extends ConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const _ReaderModeToolbarButton({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final readerableState = ref.watch(
|
||||||
|
tabStateProvider(
|
||||||
|
selectedTabId,
|
||||||
|
).select((state) => state?.readerableState ?? ReaderableState.$default()),
|
||||||
|
);
|
||||||
|
final isReaderLoading = ref.watch(
|
||||||
|
readerableScreenControllerProvider.select((state) => state.isLoading),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
onPressed: isReaderLoading
|
||||||
|
? null
|
||||||
|
: () async {
|
||||||
|
await ref
|
||||||
|
.read(readerableScreenControllerProvider.notifier)
|
||||||
|
.toggleReaderView(!readerableState.active);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
readerableState.active ? MdiIcons.bookOpen : MdiIcons.bookOpenOutline,
|
||||||
|
color: readerableState.active
|
||||||
|
? Theme.of(context).colorScheme.primary
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _DesktopModeToolbarButton extends ConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const _DesktopModeToolbarButton({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (selectedTabId == null) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(Icons.desktop_windows),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final desktopEnabled = ref.watch(desktopModeProvider(selectedTabId!));
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(desktopModeProvider(selectedTabId!).notifier).toggle();
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
desktopEnabled ? Icons.desktop_windows : Icons.desktop_windows_outlined,
|
||||||
|
color: desktopEnabled ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TranslateToolbarButton extends ConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const _TranslateToolbarButton({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isTranslated = ref.watch(
|
||||||
|
tabStateProvider(
|
||||||
|
selectedTabId,
|
||||||
|
).select((s) => s?.translationState.isTranslated ?? false),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final tabId = selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
if (isTranslated) {
|
||||||
|
await ref
|
||||||
|
.read(tabSessionProvider(tabId: tabId).notifier)
|
||||||
|
.translateRestore();
|
||||||
|
} else {
|
||||||
|
await showTranslationBottomSheet(context, selectedTabId: tabId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongPress: () async {
|
||||||
|
final tabId = selectedTabId;
|
||||||
|
if (tabId != null) {
|
||||||
|
await showTranslationBottomSheet(context, selectedTabId: tabId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
isTranslated ? MdiIcons.translateOff : Icons.translate,
|
||||||
|
color: isTranslated ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FontToolbarButton extends ConsumerWidget {
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
const _FontToolbarButton({required this.selectedTabId});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isCustom = ref.watch(
|
||||||
|
engineSettingsWithDefaultsProvider.select(
|
||||||
|
(s) => !s.automaticFontSizeAdjustment && s.fontSizeFactor != 1.0,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return IconButton(
|
||||||
|
onPressed: () => showFontSizeBottomSheet(context),
|
||||||
|
icon: Icon(
|
||||||
|
MdiIcons.formatSize,
|
||||||
|
color: isCustom ? Theme.of(context).colorScheme.primary : null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+231
@@ -20,17 +20,24 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.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/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/entities/tab_container_selection.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/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/widgets/browser_menu_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/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_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/tabs_action_button.dart';
|
||||||
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
import 'package:weblibre/features/geckoview/features/browser/presentation/widgets/toolbar_button.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/data/entities/tab_mode.dart';
|
||||||
|
import 'package:weblibre/features/geckoview/features/tabs/domain/repositories/tab.dart'
|
||||||
|
as tab_data;
|
||||||
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/menu_controller.dart';
|
import 'package:weblibre/presentation/hooks/menu_controller.dart';
|
||||||
|
import 'package:weblibre/utils/ui_helper.dart' as ui_helper;
|
||||||
|
|
||||||
class ShareMenuButton extends StatelessWidget {
|
class ShareMenuButton extends StatelessWidget {
|
||||||
final String? selectedTabId;
|
final String? selectedTabId;
|
||||||
@@ -138,6 +145,112 @@ class AddTabButtonView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CloneTabButton extends HookConsumerWidget {
|
||||||
|
const CloneTabButton({super.key, required this.selectedTabId});
|
||||||
|
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final tabMenuController = useMenuController();
|
||||||
|
|
||||||
|
return CloneTabMenu(
|
||||||
|
controller: tabMenuController,
|
||||||
|
selectedTabId: selectedTabId,
|
||||||
|
child: CloneTabButtonView(
|
||||||
|
onPressed: selectedTabId == null
|
||||||
|
? null
|
||||||
|
: () => _cloneCurrentTabMode(context, ref, selectedTabId!),
|
||||||
|
onLongPress: selectedTabId == null
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
if (tabMenuController.isOpen) {
|
||||||
|
tabMenuController.close();
|
||||||
|
} else {
|
||||||
|
tabMenuController.open();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CloneTabButtonView extends StatelessWidget {
|
||||||
|
const CloneTabButtonView({super.key, this.onPressed, this.onLongPress});
|
||||||
|
|
||||||
|
final VoidCallback? onPressed;
|
||||||
|
final VoidCallback? onLongPress;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
onLongPress: onLongPress,
|
||||||
|
icon: const Icon(MdiIcons.contentDuplicate),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CloneTabMenu extends HookConsumerWidget {
|
||||||
|
const CloneTabMenu({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.controller,
|
||||||
|
required this.selectedTabId,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Widget child;
|
||||||
|
final MenuController controller;
|
||||||
|
final String? selectedTabId;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final showIsolatedTabUi = ref.watch(
|
||||||
|
generalSettingsWithDefaultsProvider.select(
|
||||||
|
(value) => value.showIsolatedTabUi,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return MenuAnchor(
|
||||||
|
controller: controller,
|
||||||
|
builder: (context, controller, child) {
|
||||||
|
return child!;
|
||||||
|
},
|
||||||
|
menuChildren: [
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: const Icon(MdiIcons.tab),
|
||||||
|
onPressed: selectedTabId == null
|
||||||
|
? null
|
||||||
|
: () => _cloneTabAsRegular(context, ref, selectedTabId!),
|
||||||
|
child: const Text('Clone as Regular'),
|
||||||
|
),
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.dominoMask,
|
||||||
|
color: AppColors.of(context).privateTabPurple,
|
||||||
|
),
|
||||||
|
onPressed: selectedTabId == null
|
||||||
|
? null
|
||||||
|
: () => _cloneTabAsPrivate(context, ref, selectedTabId!),
|
||||||
|
child: const Text('Clone as Private'),
|
||||||
|
),
|
||||||
|
if (showIsolatedTabUi)
|
||||||
|
MenuItemButton(
|
||||||
|
leadingIcon: Icon(
|
||||||
|
MdiIcons.snowflake,
|
||||||
|
color: AppColors.of(context).isolatedTabTeal,
|
||||||
|
),
|
||||||
|
onPressed: selectedTabId == null
|
||||||
|
? null
|
||||||
|
: () => _cloneTabAsIsolated(context, ref, selectedTabId!),
|
||||||
|
child: const Text('Clone as Isolated'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabsCountButtonView extends StatelessWidget {
|
class TabsCountButtonView extends StatelessWidget {
|
||||||
const TabsCountButtonView({
|
const TabsCountButtonView({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -169,6 +282,124 @@ class TabsCountButtonView extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _cloneCurrentTabMode(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String selectedTabId,
|
||||||
|
) async {
|
||||||
|
final containerData = await ref
|
||||||
|
.read(tab_data.tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerData(selectedTabId);
|
||||||
|
|
||||||
|
final tabId = await ref
|
||||||
|
.read(tabRepositoryProvider.notifier)
|
||||||
|
.duplicateTab(
|
||||||
|
selectTabId: selectedTabId,
|
||||||
|
containerData: containerData,
|
||||||
|
selectTab: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
ui_helper.showTabSwitchMessage(
|
||||||
|
context,
|
||||||
|
onSwitch: () => repo.selectTab(tabId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cloneTabAsRegular(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String selectedTabId,
|
||||||
|
) {
|
||||||
|
return _cloneTabAsMode(context, ref, selectedTabId, mode: TabMode.regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cloneTabAsPrivate(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String selectedTabId,
|
||||||
|
) {
|
||||||
|
return _cloneTabAsMode(context, ref, selectedTabId, mode: TabMode.private);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cloneTabAsIsolated(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String selectedTabId,
|
||||||
|
) {
|
||||||
|
return _cloneTabAsMode(
|
||||||
|
context,
|
||||||
|
ref,
|
||||||
|
selectedTabId,
|
||||||
|
mode: TabMode.newIsolated(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _cloneTabAsMode(
|
||||||
|
BuildContext context,
|
||||||
|
WidgetRef ref,
|
||||||
|
String selectedTabId, {
|
||||||
|
required TabMode mode,
|
||||||
|
}) async {
|
||||||
|
final tabState = ref.read(tabStateProvider(selectedTabId));
|
||||||
|
if (tabState == null) return;
|
||||||
|
|
||||||
|
final containerData = await ref
|
||||||
|
.read(tab_data.tabDataRepositoryProvider.notifier)
|
||||||
|
.getTabContainerData(selectedTabId);
|
||||||
|
final repo = ref.read(tabRepositoryProvider.notifier);
|
||||||
|
|
||||||
|
final tabId = switch (mode) {
|
||||||
|
RegularTabMode() =>
|
||||||
|
tabState.tabMode is RegularTabMode
|
||||||
|
? await repo.duplicateTab(
|
||||||
|
selectTabId: selectedTabId,
|
||||||
|
containerData: containerData,
|
||||||
|
selectTab: false,
|
||||||
|
)
|
||||||
|
: await repo.addTab(
|
||||||
|
tabMode: TabMode.regular,
|
||||||
|
url: tabState.url,
|
||||||
|
containerSelection: containerData == null
|
||||||
|
? const TabContainerSelection.unassigned()
|
||||||
|
: TabContainerSelection.specific(containerData),
|
||||||
|
selectTab: false,
|
||||||
|
),
|
||||||
|
PrivateTabMode() =>
|
||||||
|
tabState.tabMode is PrivateTabMode
|
||||||
|
? await repo.duplicateTab(
|
||||||
|
selectTabId: selectedTabId,
|
||||||
|
containerData: containerData,
|
||||||
|
selectTab: false,
|
||||||
|
)
|
||||||
|
: await repo.addTab(
|
||||||
|
tabMode: TabMode.private,
|
||||||
|
url: tabState.url,
|
||||||
|
containerSelection: containerData == null
|
||||||
|
? const TabContainerSelection.unassigned()
|
||||||
|
: TabContainerSelection.specific(containerData),
|
||||||
|
selectTab: false,
|
||||||
|
),
|
||||||
|
IsolatedTabMode() => await repo.addTab(
|
||||||
|
tabMode: TabMode.newIsolated(),
|
||||||
|
url: tabState.url,
|
||||||
|
containerSelection: containerData == null
|
||||||
|
? const TabContainerSelection.unassigned()
|
||||||
|
: TabContainerSelection.specific(containerData),
|
||||||
|
selectTab: false,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (context.mounted) {
|
||||||
|
ui_helper.showTabSwitchMessage(
|
||||||
|
context,
|
||||||
|
onSwitch: () => repo.selectTab(tabId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class TabsCountButton extends HookConsumerWidget {
|
class TabsCountButton extends HookConsumerWidget {
|
||||||
const TabsCountButton({
|
const TabsCountButton({
|
||||||
super.key,
|
super.key,
|
||||||
|
|||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
/*
|
||||||
|
* 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/features/geckoview/features/browser/domain/entities/font_size_constants.dart';
|
||||||
|
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/data/models/engine_settings.dart';
|
||||||
|
import 'package:weblibre/features/user/domain/repositories/engine_settings.dart';
|
||||||
|
|
||||||
|
Future<void> showFontSizeBottomSheet(BuildContext context) {
|
||||||
|
return showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => const FontSizeBottomSheet(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FontSizeBottomSheet extends ConsumerWidget {
|
||||||
|
const FontSizeBottomSheet({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final settings = ref.watch(engineSettingsWithDefaultsProvider);
|
||||||
|
final factor = settings.fontSizeFactor;
|
||||||
|
final isAutomatic = settings.automaticFontSizeAdjustment;
|
||||||
|
final canDecrease = !isAutomatic && factor > fontSizeMin;
|
||||||
|
final canIncrease = !isAutomatic && factor < fontSizeMax;
|
||||||
|
final isDefault = factor == fontSizeDefault;
|
||||||
|
|
||||||
|
return SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(24, 20, 24, 24),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
MdiIcons.formatSize,
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Text Size',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
if (isAutomatic)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Card(
|
||||||
|
color: Theme.of(context)
|
||||||
|
.colorScheme
|
||||||
|
.surfaceContainerHighest,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.info_outline,
|
||||||
|
size: 20,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'Automatic font size is enabled. '
|
||||||
|
'Disable in Settings to adjust manually.',
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
IconButton.filled(
|
||||||
|
onPressed: canDecrease
|
||||||
|
? () => _adjustFontSize(ref, increase: false)
|
||||||
|
: null,
|
||||||
|
icon: const Icon(MdiIcons.formatFontSizeDecrease),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
SizedBox(
|
||||||
|
width: 72,
|
||||||
|
child: Text(
|
||||||
|
'${(factor * 100).round()}%',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 24),
|
||||||
|
IconButton.filled(
|
||||||
|
onPressed: canIncrease
|
||||||
|
? () => _adjustFontSize(ref, increase: true)
|
||||||
|
: null,
|
||||||
|
icon: const Icon(MdiIcons.formatFontSizeIncrease),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (!isDefault && !isAutomatic) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => _resetFontSize(ref),
|
||||||
|
child: const Text('Reset to 100%'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _adjustFontSize(WidgetRef ref, {required bool increase}) async {
|
||||||
|
final current = ref.read(engineSettingsWithDefaultsProvider).fontSizeFactor;
|
||||||
|
final newValue = increase
|
||||||
|
? (current + fontSizeStep).clamp(fontSizeMin, fontSizeMax)
|
||||||
|
: (current - fontSizeStep).clamp(fontSizeMin, fontSizeMax);
|
||||||
|
final rounded = (newValue * 10).round() / 10;
|
||||||
|
|
||||||
|
if (rounded == current) return;
|
||||||
|
|
||||||
|
await ref
|
||||||
|
.read(saveEngineSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) => currentSettings.copyWith.fontSizeFactor(rounded),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _resetFontSize(WidgetRef ref) async {
|
||||||
|
await ref
|
||||||
|
.read(saveEngineSettingsControllerProvider.notifier)
|
||||||
|
.save(
|
||||||
|
(currentSettings) =>
|
||||||
|
currentSettings.copyWith.fontSizeFactor(fontSizeDefault),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+22
@@ -245,6 +245,28 @@ class GeckoSessionApiImpl : GeckoSessionApi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(keyCode: Long) {
|
||||||
|
try {
|
||||||
|
logger.debug("$TAG: Dispatching key event with keyCode: $keyCode")
|
||||||
|
val engineView = components.activeEngineView
|
||||||
|
?: throw IllegalStateException("No active engine view")
|
||||||
|
val view = engineView.asView()
|
||||||
|
|
||||||
|
val downEvent = android.view.KeyEvent(
|
||||||
|
android.view.KeyEvent.ACTION_DOWN, keyCode.toInt()
|
||||||
|
)
|
||||||
|
view.dispatchKeyEvent(downEvent)
|
||||||
|
|
||||||
|
val upEvent = android.view.KeyEvent(
|
||||||
|
android.view.KeyEvent.ACTION_UP, keyCode.toInt()
|
||||||
|
)
|
||||||
|
view.dispatchKeyEvent(upEvent)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logger.error("$TAG: Failed to dispatch key event", e)
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit) {
|
override fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit) {
|
||||||
try {
|
try {
|
||||||
val tab = components.core.store.state.selectedTab
|
val tab = components.core.store.state.selectedTab
|
||||||
|
|||||||
+19
@@ -5401,6 +5401,7 @@ interface GeckoSessionApi {
|
|||||||
fun purgeHistory()
|
fun purgeHistory()
|
||||||
fun updateLastAccess(tabId: String?, lastAccess: Long?)
|
fun updateLastAccess(tabId: String?, lastAccess: Long?)
|
||||||
fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit)
|
fun requestScreenshot(sendBack: Boolean, callback: (Result<ByteArray?>) -> Unit)
|
||||||
|
fun dispatchKeyEvent(keyCode: Long)
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/** The codec used by GeckoSessionApi. */
|
/** The codec used by GeckoSessionApi. */
|
||||||
@@ -5732,6 +5733,24 @@ interface GeckoSessionApi {
|
|||||||
channel.setMessageHandler(null)
|
channel.setMessageHandler(null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
run {
|
||||||
|
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.dispatchKeyEvent$separatedMessageChannelSuffix", codec)
|
||||||
|
if (api != null) {
|
||||||
|
channel.setMessageHandler { message, reply ->
|
||||||
|
val args = message as List<Any?>
|
||||||
|
val keyCodeArg = args[0] as Long
|
||||||
|
val wrapped: List<Any?> = try {
|
||||||
|
api.dispatchKeyEvent(keyCodeArg)
|
||||||
|
listOf(null)
|
||||||
|
} catch (exception: Throwable) {
|
||||||
|
GeckoPigeonUtils.wrapError(exception)
|
||||||
|
}
|
||||||
|
reply.reply(wrapped)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
channel.setMessageHandler(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,6 +117,10 @@ class GeckoSessionService {
|
|||||||
return _api.requestScreenshot(sendBack);
|
return _api.requestScreenshot(sendBack);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> dispatchKeyEvent({required int keyCode}) {
|
||||||
|
return _api.dispatchKeyEvent(keyCode: keyCode);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> updateLastAccess({
|
Future<void> updateLastAccess({
|
||||||
String? tabId, //If null = current tab
|
String? tabId, //If null = current tab
|
||||||
DateTime? lastAccess, //If null datetime.now
|
DateTime? lastAccess, //If null datetime.now
|
||||||
|
|||||||
@@ -6673,6 +6673,28 @@ class GeckoSessionApi {
|
|||||||
return (pigeonVar_replyList[0] as Uint8List?);
|
return (pigeonVar_replyList[0] as Uint8List?);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> dispatchKeyEvent({required int keyCode}) async {
|
||||||
|
final pigeonVar_channelName = 'dev.flutter.pigeon.flutter_mozilla_components.GeckoSessionApi.dispatchKeyEvent$pigeonVar_messageChannelSuffix';
|
||||||
|
final pigeonVar_channel = BasicMessageChannel<Object?>(
|
||||||
|
pigeonVar_channelName,
|
||||||
|
pigeonChannelCodec,
|
||||||
|
binaryMessenger: pigeonVar_binaryMessenger,
|
||||||
|
);
|
||||||
|
final Future<Object?> pigeonVar_sendFuture = pigeonVar_channel.send(<Object?>[keyCode]);
|
||||||
|
final pigeonVar_replyList = await pigeonVar_sendFuture as List<Object?>?;
|
||||||
|
if (pigeonVar_replyList == null) {
|
||||||
|
throw _createConnectionError(pigeonVar_channelName);
|
||||||
|
} else if (pigeonVar_replyList.length > 1) {
|
||||||
|
throw PlatformException(
|
||||||
|
code: pigeonVar_replyList[0]! as String,
|
||||||
|
message: pigeonVar_replyList[1] as String?,
|
||||||
|
details: pigeonVar_replyList[2],
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeckoTabsApi {
|
class GeckoTabsApi {
|
||||||
|
|||||||
@@ -1378,6 +1378,8 @@ abstract class GeckoSessionApi {
|
|||||||
|
|
||||||
@async
|
@async
|
||||||
Uint8List? requestScreenshot(bool sendBack);
|
Uint8List? requestScreenshot(bool sendBack);
|
||||||
|
|
||||||
|
void dispatchKeyEvent({required int keyCode});
|
||||||
}
|
}
|
||||||
|
|
||||||
@HostApi()
|
@HostApi()
|
||||||
|
|||||||
Reference in New Issue
Block a user