/*
* 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 .
*/
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;
const defaultGestureStrokeIntervalMs = 0;
const minGestureStrokeIntervalMs = 0;
const maxGestureStrokeIntervalMs = 500;
/// 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 = {
'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;
/// Minimum milliseconds required between two consecutive direction changes
/// within a single gesture. A faster direction change aborts the in-progress
/// gesture, guarding against accidental fast scribbles. 0 disables the check.
final int minStrokeIntervalMs;
/// 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 excludedSites;
/// Canonical gesture key → action. Keys follow the grammar documented on
/// [GestureStroke].
final Map bindings;
GestureSettings({
required this.enabled,
required this.active,
required this.strokeSize,
required this.timeoutMs,
required this.maxFingers,
required this.intervalMs,
required this.minStrokeIntervalMs,
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,
int? minStrokeIntervalMs,
bool? showFeedback,
bool? suggestNext,
int? minSuggestionStroke,
List? excludedSites,
Map? bindings,
}) : enabled = enabled ?? false,
active = active ?? true,
strokeSize = strokeSize ?? defaultGestureStrokeSize,
timeoutMs = timeoutMs ?? defaultGestureTimeoutMs,
maxFingers = maxFingers ?? defaultGestureMaxFingers,
intervalMs = intervalMs ?? defaultGestureIntervalMs,
minStrokeIntervalMs =
minStrokeIntervalMs ?? defaultGestureStrokeIntervalMs,
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 json) =>
_$GestureSettingsFromJson(json);
Map toJson() => _$GestureSettingsToJson(this);
@override
List