gesture feature initial
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/// Actions that can be bound to a touch gesture.
|
||||
///
|
||||
/// Each value carries a human-readable [title]/[description] for the settings
|
||||
/// UI and an [icon] mirroring the action's representation elsewhere in the app
|
||||
/// (contextual toolbar, browser menu sheet). The dispatcher resolves each value
|
||||
/// against the currently selected tab.
|
||||
enum GestureAction {
|
||||
// Navigation
|
||||
back(
|
||||
'Back',
|
||||
'Go back in history',
|
||||
Icons.arrow_back,
|
||||
GestureActionCategory.navigation,
|
||||
),
|
||||
forward(
|
||||
'Forward',
|
||||
'Go forward in history',
|
||||
Icons.arrow_forward,
|
||||
GestureActionCategory.navigation,
|
||||
),
|
||||
reload(
|
||||
'Reload',
|
||||
'Reload the current page',
|
||||
Icons.refresh,
|
||||
GestureActionCategory.navigation,
|
||||
),
|
||||
|
||||
// Scrolling
|
||||
scrollTop(
|
||||
'Scroll to Top',
|
||||
'Jump to the top of the page',
|
||||
Icons.vertical_align_top,
|
||||
GestureActionCategory.scrolling,
|
||||
),
|
||||
scrollBottom(
|
||||
'Scroll to Bottom',
|
||||
'Jump to the bottom of the page',
|
||||
Icons.vertical_align_bottom,
|
||||
GestureActionCategory.scrolling,
|
||||
),
|
||||
pageUp(
|
||||
'Page Up',
|
||||
'Scroll up by one screen',
|
||||
MdiIcons.chevronDoubleUp,
|
||||
GestureActionCategory.scrolling,
|
||||
),
|
||||
pageDown(
|
||||
'Page Down',
|
||||
'Scroll down by one screen',
|
||||
MdiIcons.chevronDoubleDown,
|
||||
GestureActionCategory.scrolling,
|
||||
),
|
||||
|
||||
// Tabs
|
||||
newTab(
|
||||
'New Tab',
|
||||
'Open a new tab',
|
||||
MdiIcons.tabPlus,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
closeTab(
|
||||
'Close Tab',
|
||||
'Close the current tab',
|
||||
MdiIcons.tabMinus,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
duplicateTab(
|
||||
'Duplicate Tab',
|
||||
'Open a copy of the current tab',
|
||||
MdiIcons.contentDuplicate,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
nextTab(
|
||||
'Next Tab',
|
||||
'Switch to the next tab',
|
||||
Icons.skip_next,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
previousTab(
|
||||
'Previous Tab',
|
||||
'Switch to the previous tab',
|
||||
Icons.skip_previous,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
lastUsedTab(
|
||||
'Last Used Tab',
|
||||
'Switch to the previously used tab',
|
||||
Icons.swap_horiz,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
togglePinTab(
|
||||
'Pin / Unpin Tab',
|
||||
'Toggle the pinned state of the current tab',
|
||||
MdiIcons.pin,
|
||||
GestureActionCategory.tabs,
|
||||
),
|
||||
|
||||
// Page tools
|
||||
toggleReaderMode(
|
||||
'Reader Mode',
|
||||
'Toggle reader mode for the current page',
|
||||
MdiIcons.bookOpenOutline,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
toggleDesktopMode(
|
||||
'Desktop Site',
|
||||
'Toggle desktop site for the current page',
|
||||
Icons.desktop_windows,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
findInPage(
|
||||
'Find in Page',
|
||||
'Open find in page',
|
||||
Icons.find_in_page,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
increaseFontSize(
|
||||
'Increase Font',
|
||||
'Increase the page font size',
|
||||
MdiIcons.formatFontSizeIncrease,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
decreaseFontSize(
|
||||
'Decrease Font',
|
||||
'Decrease the page font size',
|
||||
MdiIcons.formatFontSizeDecrease,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
toggleBookmark(
|
||||
'Bookmark',
|
||||
'Bookmark or unbookmark the current page',
|
||||
Icons.bookmark_border,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
translatePage(
|
||||
'Translate',
|
||||
'Open the page translation sheet',
|
||||
Icons.translate,
|
||||
GestureActionCategory.page,
|
||||
),
|
||||
|
||||
// Open
|
||||
showHistory(
|
||||
'History',
|
||||
'Open browsing history',
|
||||
Icons.history,
|
||||
GestureActionCategory.open,
|
||||
),
|
||||
showBookmarks(
|
||||
'Bookmarks',
|
||||
'Open bookmarks',
|
||||
MdiIcons.bookmarkMultiple,
|
||||
GestureActionCategory.open,
|
||||
),
|
||||
|
||||
// App
|
||||
moveToBackground(
|
||||
'Minimize',
|
||||
'Send WebLibre to the background',
|
||||
MdiIcons.arrowCollapseDown,
|
||||
GestureActionCategory.app,
|
||||
),
|
||||
quitBrowser(
|
||||
'Quit',
|
||||
'Close all tabs and quit WebLibre',
|
||||
MdiIcons.power,
|
||||
GestureActionCategory.app,
|
||||
);
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final IconData icon;
|
||||
|
||||
/// Grouping used to organise actions in the bindings list and picker.
|
||||
final GestureActionCategory category;
|
||||
|
||||
const GestureAction(this.title, this.description, this.icon, this.category);
|
||||
}
|
||||
|
||||
/// High-level grouping of [GestureAction]s for the settings UI.
|
||||
enum GestureActionCategory {
|
||||
navigation('Navigation'),
|
||||
scrolling('Scrolling'),
|
||||
tabs('Tabs'),
|
||||
page('Page'),
|
||||
open('Open'),
|
||||
app('App');
|
||||
|
||||
final String label;
|
||||
|
||||
const GestureActionCategory(this.label);
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
/*
|
||||
* 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:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:weblibre/features/gestures/data/models/gesture_action.dart';
|
||||
import 'package:weblibre/utils/uri_input_parser.dart';
|
||||
|
||||
part 'gesture_settings.g.dart';
|
||||
|
||||
const defaultGestureStrokeSize = 50;
|
||||
const minGestureStrokeSize = 20;
|
||||
const maxGestureStrokeSize = 100;
|
||||
|
||||
const defaultGestureTimeoutMs = 1500;
|
||||
const minGestureTimeoutMs = 500;
|
||||
const maxGestureTimeoutMs = 3000;
|
||||
|
||||
const defaultGestureMaxFingers = 1;
|
||||
|
||||
const defaultGestureIntervalMs = 0;
|
||||
const minGestureIntervalMs = 0;
|
||||
const maxGestureIntervalMs = 2000;
|
||||
|
||||
/// Minimum number of strokes drawn before the live overlay starts suggesting
|
||||
/// the other possible completions (mirrors the reference add-on's
|
||||
/// `toastMinStroke`).
|
||||
const defaultGestureMinSuggestionStroke = 2;
|
||||
const minGestureMinSuggestionStroke = 1;
|
||||
const maxGestureMinSuggestionStroke = 5;
|
||||
|
||||
/// Default gesture-to-action bindings, aligned with the reference add-on's
|
||||
/// defaults for the actions WebLibre currently supports.
|
||||
const defaultGestureBindings = <String, GestureAction>{
|
||||
'D-L': GestureAction.forward,
|
||||
'D-R': GestureAction.back,
|
||||
'R-D': GestureAction.scrollTop,
|
||||
'R-U': GestureAction.scrollBottom,
|
||||
'D-R-U': GestureAction.reload,
|
||||
'L-D-R': GestureAction.closeTab,
|
||||
};
|
||||
|
||||
@CopyWith()
|
||||
@JsonSerializable(includeIfNull: true, constructor: 'withDefaults')
|
||||
class GestureSettings with FastEquatable {
|
||||
/// Master switch. When false, gesture recognition is fully disabled and the
|
||||
/// quick toggles ([active]) have no effect.
|
||||
final bool enabled;
|
||||
|
||||
/// Runtime toggle exposed via the quick toggles (menu sheet tile, contextual
|
||||
/// toolbar button). Lets the user suspend gestures without touching the
|
||||
/// master switch. The recognizer runs only when [enabled] && [active].
|
||||
final bool active;
|
||||
|
||||
/// Base stroke length in logical pixels (scaled to the screen by native).
|
||||
final int strokeSize;
|
||||
|
||||
/// Milliseconds of inactivity after which an in-progress gesture is dropped.
|
||||
final int timeoutMs;
|
||||
|
||||
/// Maximum simultaneous pointers a gesture may use.
|
||||
final int maxFingers;
|
||||
|
||||
/// Cooldown in milliseconds after a gesture fires, during which further
|
||||
/// gestures are ignored. 0 disables the cooldown.
|
||||
final int intervalMs;
|
||||
|
||||
/// Whether to show the live feedback overlay while a stroke is being drawn
|
||||
/// (the in-progress arrows plus the matching/possible actions).
|
||||
final bool showFeedback;
|
||||
|
||||
/// Within the live overlay, also suggest the other possible completions once
|
||||
/// at least [minSuggestionStroke] strokes have been drawn.
|
||||
final bool suggestNext;
|
||||
|
||||
/// Minimum strokes drawn before [suggestNext] kicks in.
|
||||
final int minSuggestionStroke;
|
||||
|
||||
/// Hosts on which gestures are disabled. A page is excluded when its host
|
||||
/// equals or is a subdomain of any entry (see `isGestureSiteExcluded`).
|
||||
final List<String> excludedSites;
|
||||
|
||||
/// Canonical gesture key → action. Keys follow the grammar documented on
|
||||
/// [GestureStroke].
|
||||
final Map<String, GestureAction> bindings;
|
||||
|
||||
GestureSettings({
|
||||
required this.enabled,
|
||||
required this.active,
|
||||
required this.strokeSize,
|
||||
required this.timeoutMs,
|
||||
required this.maxFingers,
|
||||
required this.intervalMs,
|
||||
required this.showFeedback,
|
||||
required this.suggestNext,
|
||||
required this.minSuggestionStroke,
|
||||
required this.excludedSites,
|
||||
required this.bindings,
|
||||
});
|
||||
|
||||
GestureSettings.withDefaults({
|
||||
bool? enabled,
|
||||
bool? active,
|
||||
int? strokeSize,
|
||||
int? timeoutMs,
|
||||
int? maxFingers,
|
||||
int? intervalMs,
|
||||
bool? showFeedback,
|
||||
bool? suggestNext,
|
||||
int? minSuggestionStroke,
|
||||
List<String>? excludedSites,
|
||||
Map<String, GestureAction>? bindings,
|
||||
}) : enabled = enabled ?? false,
|
||||
active = active ?? true,
|
||||
strokeSize = strokeSize ?? defaultGestureStrokeSize,
|
||||
timeoutMs = timeoutMs ?? defaultGestureTimeoutMs,
|
||||
maxFingers = maxFingers ?? defaultGestureMaxFingers,
|
||||
intervalMs = intervalMs ?? defaultGestureIntervalMs,
|
||||
showFeedback = showFeedback ?? true,
|
||||
suggestNext = suggestNext ?? true,
|
||||
minSuggestionStroke =
|
||||
minSuggestionStroke ?? defaultGestureMinSuggestionStroke,
|
||||
excludedSites = excludedSites ?? const [],
|
||||
bindings = bindings ?? defaultGestureBindings;
|
||||
|
||||
/// Whether the recognizer should actually run.
|
||||
bool get effectiveEnabled => enabled && active;
|
||||
|
||||
factory GestureSettings.fromJson(Map<String, dynamic> json) =>
|
||||
_$GestureSettingsFromJson(json);
|
||||
|
||||
Map<String, dynamic> toJson() => _$GestureSettingsToJson(this);
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [
|
||||
enabled,
|
||||
active,
|
||||
strokeSize,
|
||||
timeoutMs,
|
||||
maxFingers,
|
||||
intervalMs,
|
||||
showFeedback,
|
||||
suggestNext,
|
||||
minSuggestionStroke,
|
||||
excludedSites,
|
||||
bindings,
|
||||
];
|
||||
}
|
||||
|
||||
/// Normalises a user-entered site into a bare lowercase host, e.g.
|
||||
/// `https://News.example.com/foo` → `news.example.com`. Accepts either a full
|
||||
/// URL or a bare host, and validates the result with the same rules the address
|
||||
/// bar uses ([isValidHostCandidate]). Returns null for input without a valid
|
||||
/// host.
|
||||
String? normalizeGestureSiteHost(String input) {
|
||||
final trimmed = input.trim().toLowerCase();
|
||||
if (trimmed.isEmpty) return null;
|
||||
|
||||
final candidate = trimmed.contains('://') ? trimmed : 'https://$trimmed';
|
||||
final host = Uri.tryParse(candidate)?.host;
|
||||
if (host == null || host.isEmpty) return null;
|
||||
|
||||
return isValidHostCandidate(host) ? host : null;
|
||||
}
|
||||
|
||||
/// Whether [url] is covered by any entry in [excludedSites]. An entry matches
|
||||
/// the URL's host exactly or as a parent domain (so `example.com` also covers
|
||||
/// `m.example.com`).
|
||||
bool isGestureSiteExcluded(Uri url, List<String> excludedSites) {
|
||||
final host = url.host.toLowerCase();
|
||||
if (host.isEmpty) return false;
|
||||
|
||||
for (final entry in excludedSites) {
|
||||
final pattern = entry.toLowerCase();
|
||||
if (pattern.isEmpty) continue;
|
||||
if (host == pattern || host.endsWith('.$pattern')) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'gesture_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$GestureSettingsCWProxy {
|
||||
GestureSettings enabled(bool enabled);
|
||||
|
||||
GestureSettings active(bool active);
|
||||
|
||||
GestureSettings strokeSize(int strokeSize);
|
||||
|
||||
GestureSettings timeoutMs(int timeoutMs);
|
||||
|
||||
GestureSettings maxFingers(int maxFingers);
|
||||
|
||||
GestureSettings intervalMs(int intervalMs);
|
||||
|
||||
GestureSettings showFeedback(bool showFeedback);
|
||||
|
||||
GestureSettings suggestNext(bool suggestNext);
|
||||
|
||||
GestureSettings minSuggestionStroke(int minSuggestionStroke);
|
||||
|
||||
GestureSettings excludedSites(List<String> excludedSites);
|
||||
|
||||
GestureSettings bindings(Map<String, GestureAction> bindings);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GestureSettings(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// GestureSettings(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
GestureSettings call({
|
||||
bool enabled,
|
||||
bool active,
|
||||
int strokeSize,
|
||||
int timeoutMs,
|
||||
int maxFingers,
|
||||
int intervalMs,
|
||||
bool showFeedback,
|
||||
bool suggestNext,
|
||||
int minSuggestionStroke,
|
||||
List<String> excludedSites,
|
||||
Map<String, GestureAction> bindings,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfGestureSettings.copyWith(...)` or call `instanceOfGestureSettings.copyWith.fieldName(value)` for a single field.
|
||||
class _$GestureSettingsCWProxyImpl implements _$GestureSettingsCWProxy {
|
||||
const _$GestureSettingsCWProxyImpl(this._value);
|
||||
|
||||
final GestureSettings _value;
|
||||
|
||||
@override
|
||||
GestureSettings enabled(bool enabled) => call(enabled: enabled);
|
||||
|
||||
@override
|
||||
GestureSettings active(bool active) => call(active: active);
|
||||
|
||||
@override
|
||||
GestureSettings strokeSize(int strokeSize) => call(strokeSize: strokeSize);
|
||||
|
||||
@override
|
||||
GestureSettings timeoutMs(int timeoutMs) => call(timeoutMs: timeoutMs);
|
||||
|
||||
@override
|
||||
GestureSettings maxFingers(int maxFingers) => call(maxFingers: maxFingers);
|
||||
|
||||
@override
|
||||
GestureSettings intervalMs(int intervalMs) => call(intervalMs: intervalMs);
|
||||
|
||||
@override
|
||||
GestureSettings showFeedback(bool showFeedback) =>
|
||||
call(showFeedback: showFeedback);
|
||||
|
||||
@override
|
||||
GestureSettings suggestNext(bool suggestNext) =>
|
||||
call(suggestNext: suggestNext);
|
||||
|
||||
@override
|
||||
GestureSettings minSuggestionStroke(int minSuggestionStroke) =>
|
||||
call(minSuggestionStroke: minSuggestionStroke);
|
||||
|
||||
@override
|
||||
GestureSettings excludedSites(List<String> excludedSites) =>
|
||||
call(excludedSites: excludedSites);
|
||||
|
||||
@override
|
||||
GestureSettings bindings(Map<String, GestureAction> bindings) =>
|
||||
call(bindings: bindings);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GestureSettings(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// GestureSettings(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
GestureSettings call({
|
||||
Object? enabled = const $CopyWithPlaceholder(),
|
||||
Object? active = const $CopyWithPlaceholder(),
|
||||
Object? strokeSize = const $CopyWithPlaceholder(),
|
||||
Object? timeoutMs = const $CopyWithPlaceholder(),
|
||||
Object? maxFingers = const $CopyWithPlaceholder(),
|
||||
Object? intervalMs = const $CopyWithPlaceholder(),
|
||||
Object? showFeedback = const $CopyWithPlaceholder(),
|
||||
Object? suggestNext = const $CopyWithPlaceholder(),
|
||||
Object? minSuggestionStroke = const $CopyWithPlaceholder(),
|
||||
Object? excludedSites = const $CopyWithPlaceholder(),
|
||||
Object? bindings = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return GestureSettings(
|
||||
enabled: enabled == const $CopyWithPlaceholder() || enabled == null
|
||||
? _value.enabled
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: enabled as bool,
|
||||
active: active == const $CopyWithPlaceholder() || active == null
|
||||
? _value.active
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: active as bool,
|
||||
strokeSize:
|
||||
strokeSize == const $CopyWithPlaceholder() || strokeSize == null
|
||||
? _value.strokeSize
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: strokeSize as int,
|
||||
timeoutMs: timeoutMs == const $CopyWithPlaceholder() || timeoutMs == null
|
||||
? _value.timeoutMs
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: timeoutMs as int,
|
||||
maxFingers:
|
||||
maxFingers == const $CopyWithPlaceholder() || maxFingers == null
|
||||
? _value.maxFingers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: maxFingers as int,
|
||||
intervalMs:
|
||||
intervalMs == const $CopyWithPlaceholder() || intervalMs == null
|
||||
? _value.intervalMs
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: intervalMs as int,
|
||||
showFeedback:
|
||||
showFeedback == const $CopyWithPlaceholder() || showFeedback == null
|
||||
? _value.showFeedback
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: showFeedback as bool,
|
||||
suggestNext:
|
||||
suggestNext == const $CopyWithPlaceholder() || suggestNext == null
|
||||
? _value.suggestNext
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: suggestNext as bool,
|
||||
minSuggestionStroke:
|
||||
minSuggestionStroke == const $CopyWithPlaceholder() ||
|
||||
minSuggestionStroke == null
|
||||
? _value.minSuggestionStroke
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: minSuggestionStroke as int,
|
||||
excludedSites:
|
||||
excludedSites == const $CopyWithPlaceholder() || excludedSites == null
|
||||
? _value.excludedSites
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: excludedSites as List<String>,
|
||||
bindings: bindings == const $CopyWithPlaceholder() || bindings == null
|
||||
? _value.bindings
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: bindings as Map<String, GestureAction>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $GestureSettingsCopyWith on GestureSettings {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfGestureSettings.copyWith(...)` or `instanceOfGestureSettings.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$GestureSettingsCWProxy get copyWith => _$GestureSettingsCWProxyImpl(this);
|
||||
}
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
GestureSettings _$GestureSettingsFromJson(Map<String, dynamic> json) =>
|
||||
GestureSettings.withDefaults(
|
||||
enabled: json['enabled'] as bool?,
|
||||
active: json['active'] as bool?,
|
||||
strokeSize: (json['strokeSize'] as num?)?.toInt(),
|
||||
timeoutMs: (json['timeoutMs'] as num?)?.toInt(),
|
||||
maxFingers: (json['maxFingers'] as num?)?.toInt(),
|
||||
intervalMs: (json['intervalMs'] as num?)?.toInt(),
|
||||
showFeedback: json['showFeedback'] as bool?,
|
||||
suggestNext: json['suggestNext'] as bool?,
|
||||
minSuggestionStroke: (json['minSuggestionStroke'] as num?)?.toInt(),
|
||||
excludedSites: (json['excludedSites'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
bindings: (json['bindings'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, $enumDecode(_$GestureActionEnumMap, e)),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$GestureSettingsToJson(GestureSettings instance) =>
|
||||
<String, dynamic>{
|
||||
'enabled': instance.enabled,
|
||||
'active': instance.active,
|
||||
'strokeSize': instance.strokeSize,
|
||||
'timeoutMs': instance.timeoutMs,
|
||||
'maxFingers': instance.maxFingers,
|
||||
'intervalMs': instance.intervalMs,
|
||||
'showFeedback': instance.showFeedback,
|
||||
'suggestNext': instance.suggestNext,
|
||||
'minSuggestionStroke': instance.minSuggestionStroke,
|
||||
'excludedSites': instance.excludedSites,
|
||||
'bindings': instance.bindings.map(
|
||||
(k, e) => MapEntry(k, _$GestureActionEnumMap[e]!),
|
||||
),
|
||||
};
|
||||
|
||||
const _$GestureActionEnumMap = {
|
||||
GestureAction.back: 'back',
|
||||
GestureAction.forward: 'forward',
|
||||
GestureAction.reload: 'reload',
|
||||
GestureAction.scrollTop: 'scrollTop',
|
||||
GestureAction.scrollBottom: 'scrollBottom',
|
||||
GestureAction.pageUp: 'pageUp',
|
||||
GestureAction.pageDown: 'pageDown',
|
||||
GestureAction.newTab: 'newTab',
|
||||
GestureAction.closeTab: 'closeTab',
|
||||
GestureAction.duplicateTab: 'duplicateTab',
|
||||
GestureAction.nextTab: 'nextTab',
|
||||
GestureAction.previousTab: 'previousTab',
|
||||
GestureAction.lastUsedTab: 'lastUsedTab',
|
||||
GestureAction.togglePinTab: 'togglePinTab',
|
||||
GestureAction.toggleReaderMode: 'toggleReaderMode',
|
||||
GestureAction.toggleDesktopMode: 'toggleDesktopMode',
|
||||
GestureAction.findInPage: 'findInPage',
|
||||
GestureAction.increaseFontSize: 'increaseFontSize',
|
||||
GestureAction.decreaseFontSize: 'decreaseFontSize',
|
||||
GestureAction.toggleBookmark: 'toggleBookmark',
|
||||
GestureAction.translatePage: 'translatePage',
|
||||
GestureAction.showHistory: 'showHistory',
|
||||
GestureAction.showBookmarks: 'showBookmarks',
|
||||
GestureAction.moveToBackground: 'moveToBackground',
|
||||
GestureAction.quitBrowser: 'quitBrowser',
|
||||
};
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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:copy_with_extension/copy_with_extension.dart';
|
||||
import 'package:fast_equatable/fast_equatable.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_material_design_icons/flutter_material_design_icons.dart';
|
||||
|
||||
part 'gesture_stroke.g.dart';
|
||||
|
||||
/// Where a gesture must begin. Mirrors the reference add-on's start-position
|
||||
/// tokens (the trailing colon is part of the canonical key prefix).
|
||||
enum GestureStartPosition {
|
||||
anywhere('', 'Anywhere', MdiIcons.borderNone),
|
||||
leftEdge('L:', 'Left edge', MdiIcons.borderLeft),
|
||||
rightEdge('R:', 'Right edge', MdiIcons.borderRight),
|
||||
topEdge('T:', 'Top edge', MdiIcons.borderTop),
|
||||
bottomEdge('B:', 'Bottom edge', MdiIcons.borderBottom),
|
||||
leftHalf('W:', 'Left half', MdiIcons.borderLeftVariant),
|
||||
rightHalf('E:', 'Right half', MdiIcons.borderRightVariant);
|
||||
|
||||
/// Canonical key prefix, e.g. `R:` (empty for [anywhere]).
|
||||
final String prefix;
|
||||
final String label;
|
||||
final IconData icon;
|
||||
|
||||
const GestureStartPosition(this.prefix, this.label, this.icon);
|
||||
|
||||
static GestureStartPosition fromPrefixLetter(String letter) {
|
||||
return GestureStartPosition.values.firstWhere(
|
||||
(position) => position.prefix == '$letter:',
|
||||
orElse: () => GestureStartPosition.anywhere,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A single dominant swipe direction within a gesture.
|
||||
enum GestureArrow {
|
||||
up('U', '↑'),
|
||||
down('D', '↓'),
|
||||
left('L', '←'),
|
||||
right('R', '→');
|
||||
|
||||
/// Canonical key token, e.g. `D`.
|
||||
final String token;
|
||||
|
||||
/// Compact glyph for rendering a stroke sequence.
|
||||
final String symbol;
|
||||
|
||||
const GestureArrow(this.token, this.symbol);
|
||||
|
||||
static GestureArrow fromToken(String token) {
|
||||
return GestureArrow.values.firstWhere((arrow) => arrow.token == token);
|
||||
}
|
||||
}
|
||||
|
||||
/// A configurable gesture: an ordered sequence of swipe directions, optionally
|
||||
/// constrained by where the touch begins and how many fingers are used.
|
||||
///
|
||||
/// The canonical [key] is the on-the-wire identifier shared with the native
|
||||
/// recognizer: `<start-prefix><finger-prefix><arrows joined by '-'>`, e.g.
|
||||
/// `R:2:D-L`. The finger prefix is omitted for a single finger and the start
|
||||
/// prefix is omitted for [GestureStartPosition.anywhere].
|
||||
@CopyWith()
|
||||
class GestureStroke with FastEquatable {
|
||||
final GestureStartPosition startPosition;
|
||||
final int fingers;
|
||||
final List<GestureArrow> arrows;
|
||||
|
||||
GestureStroke({
|
||||
this.startPosition = GestureStartPosition.anywhere,
|
||||
this.fingers = 1,
|
||||
this.arrows = const [],
|
||||
});
|
||||
|
||||
/// The canonical gesture key (see class docs).
|
||||
String get key {
|
||||
final fingerPrefix = fingers >= 2 ? '$fingers:' : '';
|
||||
final arrowPart = arrows.map((arrow) => arrow.token).join('-');
|
||||
return '${startPosition.prefix}$fingerPrefix$arrowPart';
|
||||
}
|
||||
|
||||
/// Parses a canonical [key] back into a stroke.
|
||||
///
|
||||
/// The arrow sequence is always the final colon-separated segment; preceding
|
||||
/// segments are either a single start-position letter or a finger count.
|
||||
factory GestureStroke.fromKey(String key) {
|
||||
final segments = key.split(':');
|
||||
final arrowPart = segments.removeLast();
|
||||
|
||||
var startPosition = GestureStartPosition.anywhere;
|
||||
var fingers = 1;
|
||||
for (final segment in segments) {
|
||||
final asFingers = int.tryParse(segment);
|
||||
if (asFingers != null) {
|
||||
fingers = asFingers;
|
||||
} else if (segment.isNotEmpty) {
|
||||
startPosition = GestureStartPosition.fromPrefixLetter(segment);
|
||||
}
|
||||
}
|
||||
|
||||
final arrows = arrowPart.isEmpty
|
||||
? <GestureArrow>[]
|
||||
: arrowPart.split('-').map(GestureArrow.fromToken).toList();
|
||||
|
||||
return GestureStroke(
|
||||
startPosition: startPosition,
|
||||
fingers: fingers,
|
||||
arrows: arrows,
|
||||
);
|
||||
}
|
||||
|
||||
/// Whether this stroke is complete enough to be bound to an action.
|
||||
bool get isValid => arrows.isNotEmpty;
|
||||
|
||||
/// Human-readable rendering, e.g. `Right edge · ✌ · ↓→`.
|
||||
String get displayLabel {
|
||||
final parts = <String>[
|
||||
if (startPosition != GestureStartPosition.anywhere) startPosition.label,
|
||||
if (fingers >= 2) '$fingers fingers',
|
||||
arrows.map((arrow) => arrow.symbol).join(),
|
||||
];
|
||||
return parts.join(' · ');
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get hashParameters => [startPosition, fingers, arrows];
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'gesture_stroke.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// CopyWithGenerator
|
||||
// **************************************************************************
|
||||
|
||||
abstract class _$GestureStrokeCWProxy {
|
||||
GestureStroke startPosition(GestureStartPosition startPosition);
|
||||
|
||||
GestureStroke fingers(int fingers);
|
||||
|
||||
GestureStroke arrows(List<GestureArrow> arrows);
|
||||
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GestureStroke(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// GestureStroke(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
GestureStroke call({
|
||||
GestureStartPosition startPosition,
|
||||
int fingers,
|
||||
List<GestureArrow> arrows,
|
||||
});
|
||||
}
|
||||
|
||||
/// Callable proxy for `copyWith` functionality.
|
||||
/// Use as `instanceOfGestureStroke.copyWith(...)` or call `instanceOfGestureStroke.copyWith.fieldName(value)` for a single field.
|
||||
class _$GestureStrokeCWProxyImpl implements _$GestureStrokeCWProxy {
|
||||
const _$GestureStrokeCWProxyImpl(this._value);
|
||||
|
||||
final GestureStroke _value;
|
||||
|
||||
@override
|
||||
GestureStroke startPosition(GestureStartPosition startPosition) =>
|
||||
call(startPosition: startPosition);
|
||||
|
||||
@override
|
||||
GestureStroke fingers(int fingers) => call(fingers: fingers);
|
||||
|
||||
@override
|
||||
GestureStroke arrows(List<GestureArrow> arrows) => call(arrows: arrows);
|
||||
|
||||
@override
|
||||
/// Creates a new instance with the provided field values.
|
||||
/// Passing `null` to a nullable field nullifies it, while `null` for a non-nullable field is ignored. To update a single field use `GestureStroke(...).copyWith.fieldName(value)`.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// GestureStroke(...).copyWith(id: 12, name: "My name")
|
||||
/// ```
|
||||
GestureStroke call({
|
||||
Object? startPosition = const $CopyWithPlaceholder(),
|
||||
Object? fingers = const $CopyWithPlaceholder(),
|
||||
Object? arrows = const $CopyWithPlaceholder(),
|
||||
}) {
|
||||
return GestureStroke(
|
||||
startPosition:
|
||||
startPosition == const $CopyWithPlaceholder() || startPosition == null
|
||||
? _value.startPosition
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: startPosition as GestureStartPosition,
|
||||
fingers: fingers == const $CopyWithPlaceholder() || fingers == null
|
||||
? _value.fingers
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: fingers as int,
|
||||
arrows: arrows == const $CopyWithPlaceholder() || arrows == null
|
||||
? _value.arrows
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
: arrows as List<GestureArrow>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension $GestureStrokeCopyWith on GestureStroke {
|
||||
/// Returns a callable class used to build a new instance with modified fields.
|
||||
/// Example: `instanceOfGestureStroke.copyWith(...)` or `instanceOfGestureStroke.copyWith.fieldName(...)`.
|
||||
// ignore: library_private_types_in_public_api
|
||||
_$GestureStrokeCWProxy get copyWith => _$GestureStrokeCWProxyImpl(this);
|
||||
}
|
||||
Reference in New Issue
Block a user