advanced desktop mode
This commit is contained in:
@@ -105,12 +105,23 @@ const List<SettingsSectionDefinition> browsingSettingsSections = [
|
||||
keywords: ['app links', 'external apps'],
|
||||
child: _AppLinksModeSection(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSectionDefinition(
|
||||
title: 'Desktop Mode',
|
||||
entries: [
|
||||
SettingsEntryDefinition(
|
||||
title: 'Always Request Desktop Site',
|
||||
subtitle: 'Open new tabs in desktop mode by default',
|
||||
keywords: ['desktop mode', 'user agent', 'mobile site', 'tablet'],
|
||||
child: _GlobalDesktopModeTile(),
|
||||
),
|
||||
SettingsEntryDefinition(
|
||||
title: 'Desktop Mode Sites',
|
||||
subtitle: 'Sites that always load in desktop mode',
|
||||
keywords: ['desktop mode', 'per-site', 'user agent', 'exceptions'],
|
||||
child: _DesktopModeSitesTile(),
|
||||
),
|
||||
],
|
||||
),
|
||||
SettingsSectionDefinition(
|
||||
@@ -764,6 +775,23 @@ class _GlobalDesktopModeTile extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _DesktopModeSitesTile extends StatelessWidget {
|
||||
const _DesktopModeSitesTile();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListTile(
|
||||
leading: const Icon(Icons.desktop_windows),
|
||||
title: const Text('Desktop Mode Sites'),
|
||||
subtitle: const Text('Sites that always load in desktop mode'),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
await const DesktopModeSitesRoute().push(context);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PullToRefreshTile extends HookConsumerWidget {
|
||||
const _PullToRefreshTile();
|
||||
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* 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:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:weblibre/features/settings/presentation/controllers/save_settings.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/string_list_settings_screen.dart';
|
||||
import 'package:weblibre/features/user/data/models/general_settings.dart';
|
||||
import 'package:weblibre/features/user/domain/repositories/general_settings.dart';
|
||||
import 'package:weblibre/utils/host_rules.dart';
|
||||
|
||||
/// Manages the list of sites that always load in desktop mode.
|
||||
class DesktopModeSitesScreen extends HookConsumerWidget {
|
||||
const DesktopModeSitesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final desktopModeSites = ref.watch(
|
||||
generalSettingsWithDefaultsProvider.select((s) => s.desktopModeSites),
|
||||
);
|
||||
|
||||
return StringListSettingsScreen(
|
||||
title: 'Desktop mode sites',
|
||||
description:
|
||||
'These sites always load in desktop mode, overriding the default. '
|
||||
'Subdomains are included (e.g. "example.com" also covers '
|
||||
'"m.example.com").',
|
||||
values: desktopModeSites,
|
||||
hintText: 'example.com',
|
||||
itemIcon: Icons.desktop_windows,
|
||||
emptyLabel: 'No sites added.',
|
||||
normalize: normalizeRuleHost,
|
||||
onChanged: (next) async {
|
||||
await ref
|
||||
.read(saveGeneralSettingsControllerProvider.notifier)
|
||||
.save((current) => current.copyWith.desktopModeSites(next));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
/*
|
||||
* 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:weblibre/features/settings/presentation/widgets/settings_detail.dart';
|
||||
import 'package:weblibre/features/settings/presentation/widgets/string_list_editor.dart';
|
||||
|
||||
/// Settings sub-screen that manages a list of unique string entries: an
|
||||
/// optional description followed by a [StringListEditor]. Callers own the
|
||||
/// state — pass [values] and handle persistence in [onChanged] — keeping this
|
||||
/// screen provider-agnostic and reusable across features (e.g. gesture-excluded
|
||||
/// sites, per-site desktop mode).
|
||||
class StringListSettingsScreen extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
/// Optional explanatory text shown above the editor.
|
||||
final String? description;
|
||||
|
||||
final List<String> values;
|
||||
final ValueChanged<List<String>> onChanged;
|
||||
|
||||
/// Hint shown in the add field.
|
||||
final String hintText;
|
||||
|
||||
/// Canonicalises raw input before adding. Returns null to reject the value.
|
||||
final String? Function(String input) normalize;
|
||||
|
||||
/// Leading icon for each entry row.
|
||||
final IconData itemIcon;
|
||||
|
||||
/// Message shown when the list is empty.
|
||||
final String emptyLabel;
|
||||
|
||||
const StringListSettingsScreen({
|
||||
required this.title,
|
||||
required this.values,
|
||||
required this.onChanged,
|
||||
required this.hintText,
|
||||
required this.normalize,
|
||||
this.description,
|
||||
this.itemIcon = Icons.link,
|
||||
this.emptyLabel = 'Nothing added yet.',
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SettingsCustomScrollScaffold(
|
||||
title: title,
|
||||
slivers: [
|
||||
if (description != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: Text(
|
||||
description!,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: StringListEditor(
|
||||
values: values,
|
||||
hintText: hintText,
|
||||
itemIcon: itemIcon,
|
||||
emptyLabel: emptyLabel,
|
||||
normalize: normalize,
|
||||
onChanged: onChanged,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user