advanced desktop mode
This commit is contained in:
@@ -21,7 +21,6 @@ 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';
|
||||
|
||||
@@ -103,7 +102,7 @@ class GestureSettings with FastEquatable {
|
||||
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`).
|
||||
/// equals or is a subdomain of any entry (see `hostMatchesRule`).
|
||||
final List<String> excludedSites;
|
||||
|
||||
/// Canonical gesture key → action. Keys follow the grammar documented on
|
||||
@@ -177,34 +176,3 @@ class GestureSettings with FastEquatable {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ import 'package:weblibre/features/user/domain/repositories/engine_settings.dart'
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/features/web_search/domain/controllers/sandbox_capture_controller.dart';
|
||||
import 'package:weblibre/utils/exit_app.dart';
|
||||
import 'package:weblibre/utils/host_rules.dart';
|
||||
import 'package:weblibre/utils/move_to_background.dart';
|
||||
|
||||
part 'gesture_control.g.dart';
|
||||
@@ -311,7 +312,7 @@ bool gestureSiteExcluded(Ref ref) {
|
||||
final url = ref.watch(tabStateProvider(tabId).select((state) => state?.url));
|
||||
if (url == null) return false;
|
||||
|
||||
return isGestureSiteExcluded(url, excludedSites);
|
||||
return hostMatchesRule(url, excludedSites);
|
||||
}
|
||||
|
||||
/// The effective native recognizer configuration: the user's settings with the
|
||||
|
||||
@@ -143,7 +143,7 @@ final class GestureSiteExcludedProvider
|
||||
}
|
||||
|
||||
String _$gestureSiteExcludedHash() =>
|
||||
r'15dd371a176303a7c52c37df560c93bbb03c5039';
|
||||
r'c7c9f8128a7262b858c954b7217d6a00f1d062e4';
|
||||
|
||||
/// The effective native recognizer configuration: the user's settings with the
|
||||
/// recognizer disabled while the current site is excluded.
|
||||
|
||||
+16
-33
@@ -21,8 +21,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/gestures/data/models/gesture_settings.dart';
|
||||
import 'package:weblibre/features/gestures/domain/repositories/gesture_settings.dart';
|
||||
import 'package:weblibre/features/gestures/presentation/widgets/string_list_editor.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/settings_detail.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/string_list_settings_screen.dart';
|
||||
import 'package:weblibre/utils/host_rules.dart';
|
||||
|
||||
/// Manages the list of sites on which gestures are disabled.
|
||||
class GestureExcludedSitesScreen extends HookConsumerWidget {
|
||||
@@ -34,38 +34,21 @@ class GestureExcludedSitesScreen extends HookConsumerWidget {
|
||||
gestureSettingsWithDefaultsProvider.select((s) => s.excludedSites),
|
||||
);
|
||||
|
||||
return SettingsCustomScrollScaffold(
|
||||
return StringListSettingsScreen(
|
||||
title: 'Excluded sites',
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
'Gestures are disabled on these sites. Subdomains are included '
|
||||
'(e.g. "example.com" also covers "m.example.com").',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: StringListEditor(
|
||||
values: excludedSites,
|
||||
hintText: 'example.com',
|
||||
itemIcon: Icons.public_off,
|
||||
emptyLabel: 'No sites excluded.',
|
||||
normalize: normalizeGestureSiteHost,
|
||||
onChanged: (next) async {
|
||||
await ref
|
||||
.read(gestureSettingsRepositoryProvider.notifier)
|
||||
.updateSettings(
|
||||
(current) => current.copyWith.excludedSites(next),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
description:
|
||||
'Gestures are disabled on these sites. Subdomains are included '
|
||||
'(e.g. "example.com" also covers "m.example.com").',
|
||||
values: excludedSites,
|
||||
hintText: 'example.com',
|
||||
itemIcon: Icons.public_off,
|
||||
emptyLabel: 'No sites excluded.',
|
||||
normalize: normalizeRuleHost,
|
||||
onChanged: (next) async {
|
||||
await ref
|
||||
.read(gestureSettingsRepositoryProvider.notifier)
|
||||
.updateSettings((current) => current.copyWith.excludedSites(next));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
/*
|
||||
* 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';
|
||||
|
||||
/// Reusable editor for a list of unique string entries: a labelled input field
|
||||
/// with an add button, followed by the current entries each with a delete
|
||||
/// action.
|
||||
///
|
||||
/// Entries are passed through [normalize] before being added; returning null
|
||||
/// rejects the input (e.g. an unparseable value), and duplicates are ignored.
|
||||
class StringListEditor extends HookWidget {
|
||||
final List<String> values;
|
||||
final ValueChanged<List<String>> onChanged;
|
||||
|
||||
/// Hint shown in the input field.
|
||||
final String hintText;
|
||||
|
||||
/// Leading icon for each entry row.
|
||||
final IconData itemIcon;
|
||||
|
||||
/// Message shown when the list is empty.
|
||||
final String emptyLabel;
|
||||
|
||||
/// Canonicalises raw input before adding. Returns null to reject the value.
|
||||
final String? Function(String input) normalize;
|
||||
|
||||
const StringListEditor({
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
required this.hintText,
|
||||
required this.normalize,
|
||||
this.itemIcon = Icons.link,
|
||||
this.emptyLabel = 'Nothing added yet.',
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final controller = useTextEditingController();
|
||||
// Rebuild the add button's enabled state as the field changes.
|
||||
useListenable(controller);
|
||||
|
||||
void add() {
|
||||
final normalized = normalize(controller.text);
|
||||
if (normalized == null) return;
|
||||
if (!values.contains(normalized)) {
|
||||
onChanged([...values, normalized]);
|
||||
}
|
||||
controller.clear();
|
||||
}
|
||||
|
||||
void remove(String value) {
|
||||
onChanged(values.where((v) => v != value).toList());
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.url,
|
||||
autocorrect: false,
|
||||
textInputAction: TextInputAction.done,
|
||||
onSubmitted: (_) => add(),
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
border: const OutlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
IconButton.filled(
|
||||
icon: const Icon(Icons.add),
|
||||
tooltip: 'Add',
|
||||
onPressed: controller.text.trim().isEmpty ? null : add,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (values.isEmpty)
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
emptyLabel,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
)
|
||||
else
|
||||
for (final value in values)
|
||||
ListTile(
|
||||
leading: Icon(itemIcon),
|
||||
title: Text(value),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
tooltip: 'Remove',
|
||||
onPressed: () => remove(value),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user