feat(settings): polish search and navigation layout

This commit is contained in:
zarzet
2026-07-29 19:27:30 +07:00
parent af28bbd492
commit cc93c5020d
28 changed files with 147 additions and 119 deletions
+1 -1
View File
@@ -7885,7 +7885,7 @@ abstract class AppLocalizations {
/// Settings group covering look and feel
///
/// In en, this message translates to:
/// **'Interface'**
/// **'Extensions & appearance'**
String get settingsGroupInterface;
/// Settings group covering the library, metadata and lyrics
+1 -1
View File
@@ -4856,7 +4856,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Oberfläche';
String get settingsGroupInterface => 'Erweiterungen & Darstellung';
@override
String get settingsGroupContent => 'Inhalte & Metadaten';
+1 -1
View File
@@ -4811,7 +4811,7 @@ class AppLocalizationsEn extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Interface';
String get settingsGroupInterface => 'Extensions & appearance';
@override
String get settingsGroupContent => 'Content & metadata';
+2 -2
View File
@@ -4805,7 +4805,7 @@ class AppLocalizationsEs extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Interfaz';
String get settingsGroupInterface => 'Extensiones y apariencia';
@override
String get settingsGroupContent => 'Contenido y metadatos';
@@ -9531,7 +9531,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
}
@override
String get settingsGroupInterface => 'Interfaz';
String get settingsGroupInterface => 'Extensiones y apariencia';
@override
String get settingsGroupContent => 'Contenido y metadatos';
+1 -1
View File
@@ -4927,7 +4927,7 @@ class AppLocalizationsFr extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Interface';
String get settingsGroupInterface => 'Extensions et apparence';
@override
String get settingsGroupContent => 'Contenu et métadonnées';
+1 -1
View File
@@ -4809,7 +4809,7 @@ class AppLocalizationsId extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Antarmuka';
String get settingsGroupInterface => 'Ekstensi & tampilan';
@override
String get settingsGroupContent => 'Konten & metadata';
+1 -1
View File
@@ -4800,7 +4800,7 @@ class AppLocalizationsJa extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'インターフェース';
String get settingsGroupInterface => '拡張機能と外観';
@override
String get settingsGroupContent => 'コンテンツとメタデータ';
+1 -1
View File
@@ -4681,7 +4681,7 @@ class AppLocalizationsKo extends AppLocalizations {
}
@override
String get settingsGroupInterface => '인터페이스';
String get settingsGroupInterface => '확장 기능 및 외관';
@override
String get settingsGroupContent => '콘텐츠 및 메타데이터';
+2 -2
View File
@@ -4805,7 +4805,7 @@ class AppLocalizationsPt extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Interface';
String get settingsGroupInterface => 'Extensões e aparência';
@override
String get settingsGroupContent => 'Conteúdo e metadados';
@@ -9496,7 +9496,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
}
@override
String get settingsGroupInterface => 'Interface';
String get settingsGroupInterface => 'Extensões e aparência';
@override
String get settingsGroupContent => 'Conteúdo e metadados';
+1 -1
View File
@@ -4842,7 +4842,7 @@ class AppLocalizationsRu extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Интерфейс';
String get settingsGroupInterface => 'Расширения и внешний вид';
@override
String get settingsGroupContent => 'Контент и метаданные';
+1 -1
View File
@@ -4841,7 +4841,7 @@ class AppLocalizationsTr extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Arayüz';
String get settingsGroupInterface => 'Uzantılar ve görünüm';
@override
String get settingsGroupContent => 'İçerik ve meta veriler';
+1 -1
View File
@@ -4859,7 +4859,7 @@ class AppLocalizationsUk extends AppLocalizations {
}
@override
String get settingsGroupInterface => 'Інтерфейс';
String get settingsGroupInterface => 'Розширення та вигляд';
@override
String get settingsGroupContent => 'Вміст і метадані';
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Einstellungen durchsuchen",
"settingsSearchNoResults": "Keine Einstellungen für \"{query}\" gefunden",
"settingsGroupInterface": "Oberfläche",
"settingsGroupInterface": "Erweiterungen & Darstellung",
"settingsGroupContent": "Inhalte & Metadaten",
"settingsGroupDownloads": "Downloads & Dateien",
"settingsGroupSystem": "System",
+1 -1
View File
@@ -6236,7 +6236,7 @@
}
}
},
"settingsGroupInterface": "Interface",
"settingsGroupInterface": "Extensions & appearance",
"@settingsGroupInterface": {
"description": "Settings group covering look and feel"
},
+1 -1
View File
@@ -4282,7 +4282,7 @@
},
"settingsSearchHint": "Buscar en los ajustes",
"settingsSearchNoResults": "Ningún ajuste coincide con \"{query}\"",
"settingsGroupInterface": "Interfaz",
"settingsGroupInterface": "Extensiones y apariencia",
"settingsGroupContent": "Contenido y metadatos",
"settingsGroupDownloads": "Descargas y archivos",
"settingsGroupSystem": "Sistema",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Buscar en los ajustes",
"settingsSearchNoResults": "Ningún ajuste coincide con \"{query}\"",
"settingsGroupInterface": "Interfaz",
"settingsGroupInterface": "Extensiones y apariencia",
"settingsGroupContent": "Contenido y metadatos",
"settingsGroupDownloads": "Descargas y archivos",
"settingsGroupSystem": "Sistema",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Reprise après vérification...",
"settingsSearchHint": "Rechercher dans les paramètres",
"settingsSearchNoResults": "Aucun paramètre ne correspond à « {query} »",
"settingsGroupInterface": "Interface",
"settingsGroupInterface": "Extensions et apparence",
"settingsGroupContent": "Contenu et métadonnées",
"settingsGroupDownloads": "Téléchargements et fichiers",
"settingsGroupSystem": "Système",
+1 -1
View File
@@ -6062,7 +6062,7 @@
},
"settingsSearchHint": "Cari pengaturan",
"settingsSearchNoResults": "Tidak ada pengaturan yang cocok dengan \"{query}\"",
"settingsGroupInterface": "Antarmuka",
"settingsGroupInterface": "Ekstensi & tampilan",
"settingsGroupContent": "Konten & metadata",
"settingsGroupDownloads": "Unduhan & file",
"settingsGroupSystem": "Sistem",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "設定を検索",
"settingsSearchNoResults": "「{query}」に一致する設定はありません",
"settingsGroupInterface": "インターフェース",
"settingsGroupInterface": "拡張機能と外観",
"settingsGroupContent": "コンテンツとメタデータ",
"settingsGroupDownloads": "ダウンロードとファイル",
"settingsGroupSystem": "システム",
+1 -1
View File
@@ -6021,7 +6021,7 @@
},
"settingsSearchHint": "설정 검색",
"settingsSearchNoResults": "\"{query}\"와(과) 일치하는 설정이 없습니다",
"settingsGroupInterface": "인터페이스",
"settingsGroupInterface": "확장 기능 및 외관",
"settingsGroupContent": "콘텐츠 및 메타데이터",
"settingsGroupDownloads": "다운로드 및 파일",
"settingsGroupSystem": "시스템",
+1 -1
View File
@@ -4282,7 +4282,7 @@
},
"settingsSearchHint": "Pesquisar configurações",
"settingsSearchNoResults": "Nenhuma configuração corresponde a \"{query}\"",
"settingsGroupInterface": "Interface",
"settingsGroupInterface": "Extensões e aparência",
"settingsGroupContent": "Conteúdo e metadados",
"settingsGroupDownloads": "Downloads e arquivos",
"settingsGroupSystem": "Sistema",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Pesquisar definições",
"settingsSearchNoResults": "Nenhuma definição corresponde a \"{query}\"",
"settingsGroupInterface": "Interface",
"settingsGroupInterface": "Extensões e aparência",
"settingsGroupContent": "Conteúdo e metadados",
"settingsGroupDownloads": "Transferências e ficheiros",
"settingsGroupSystem": "Sistema",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Поиск в настройках",
"settingsSearchNoResults": "Нет настроек, соответствующих запросу «{query}»",
"settingsGroupInterface": "Интерфейс",
"settingsGroupInterface": "Расширения и внешний вид",
"settingsGroupContent": "Контент и метаданные",
"settingsGroupDownloads": "Загрузки и файлы",
"settingsGroupSystem": "Система",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Ayarlarda ara",
"settingsSearchNoResults": "\"{query}\" ile eşleşen ayar bulunamadı",
"settingsGroupInterface": "Arayüz",
"settingsGroupInterface": "Uzantılar ve görünüm",
"settingsGroupContent": "İçerik ve meta veriler",
"settingsGroupDownloads": "İndirmeler ve dosyalar",
"settingsGroupSystem": "Sistem",
+1 -1
View File
@@ -6021,7 +6021,7 @@
"queueResumingAfterVerification": "Resuming after verification...",
"settingsSearchHint": "Пошук у налаштуваннях",
"settingsSearchNoResults": "Немає налаштувань, що відповідають запиту «{query}»",
"settingsGroupInterface": "Інтерфейс",
"settingsGroupInterface": "Розширення та вигляд",
"settingsGroupContent": "Вміст і метадані",
"settingsGroupDownloads": "Завантаження та файли",
"settingsGroupSystem": "Система",
+9 -54
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/widgets/extension_row.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/widgets/app_search_field.dart';
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
@@ -107,60 +108,14 @@ class _RepoTabState extends ConsumerState<RepoTab> {
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _searchController,
builder: (context, value, _) {
return TextField(
controller: _searchController,
decoration: InputDecoration(
hintText: context.l10n.storeSearch,
prefixIcon: const Icon(Icons.search),
suffixIcon: value.text.isNotEmpty
? IconButton(
tooltip: context.l10n.dialogClear,
icon: const Icon(Icons.clear),
onPressed: () {
_searchController.clear();
ref
.read(repoProvider.notifier)
.setSearchQuery('');
},
)
: null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: colorScheme.outlineVariant,
),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: colorScheme.outlineVariant,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(28),
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
filled: true,
fillColor: settingsGroupColor(context),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
onChanged: (value) {
ref.read(repoProvider.notifier).setSearchQuery(value);
},
onTapOutside: (_) {
FocusScope.of(context).unfocus();
},
);
},
child: AppSearchField(
controller: _searchController,
hintText: context.l10n.storeSearch,
clearTooltip: context.l10n.dialogClear,
onChanged: (value) =>
ref.read(repoProvider.notifier).setSearchQuery(value),
onClear: () =>
ref.read(repoProvider.notifier).setSearchQuery(''),
),
),
),
+33 -38
View File
@@ -19,6 +19,7 @@ import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/widgets/animation_utils.dart';
import 'package:spotiflac_android/widgets/app_search_field.dart';
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
@@ -48,11 +49,12 @@ class _Destination {
}
}
/// A labelled group of destinations.
/// A settings group. The optional label lets priority shortcuts sit directly
/// below search without a misleading section heading.
class _Group {
const _Group({required this.label, required this.destinations});
const _Group({this.label, required this.destinations});
final String label;
final String? label;
final List<_Destination> destinations;
}
@@ -82,9 +84,27 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
List<_Group> _groups(BuildContext context) {
final l10n = context.l10n;
return [
_Group(
destinations: [
_Destination(
icon: Icons.favorite_outline,
title: l10n.settingsDonate,
subtitle: l10n.settingsDonateSubtitle,
keywords: const ['support', 'ko-fi', 'sponsor'],
pageBuilder: () => const DonatePage(),
),
],
),
_Group(
label: l10n.settingsGroupInterface,
destinations: [
_Destination(
icon: Icons.extension_outlined,
title: l10n.settingsExtensions,
subtitle: l10n.settingsExtensionsSubtitle,
keywords: const ['plugin', 'provider', 'priority', 'store'],
pageBuilder: () => const ExtensionsPage(),
),
_Destination(
icon: Icons.palette_outlined,
title: l10n.settingsAppearance,
@@ -168,13 +188,6 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
],
pageBuilder: () => const FilesSettingsPage(),
),
_Destination(
icon: Icons.extension_outlined,
title: l10n.settingsExtensions,
subtitle: l10n.settingsExtensionsSubtitle,
keywords: const ['plugin', 'provider', 'priority', 'store'],
pageBuilder: () => const ExtensionsPage(),
),
],
),
_Group(
@@ -213,13 +226,6 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
_Group(
label: l10n.settingsGroupHelp,
destinations: [
_Destination(
icon: Icons.favorite_outline,
title: l10n.settingsDonate,
subtitle: l10n.settingsDonateSubtitle,
keywords: const ['support', 'ko-fi', 'sponsor'],
pageBuilder: () => const DonatePage(),
),
_Destination(
icon: Icons.info_outline,
title: l10n.settingsAbout,
@@ -266,12 +272,13 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
if (query.isEmpty) {
body = [
for (final group in groups) ...[
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.only(left: wideInset, right: wideInset),
child: SettingsSectionHeader(title: group.label),
if (group.label != null)
SliverToBoxAdapter(
child: Padding(
padding: EdgeInsets.only(left: wideInset, right: wideInset),
child: SettingsSectionHeader(title: group.label!),
),
),
),
SliverToBoxAdapter(
child: SettingsGroup(
margin: margin,
@@ -333,24 +340,12 @@ class _SettingsTabState extends ConsumerState<SettingsTab> {
tokens.gapLg + wideInset,
tokens.gapSm,
),
child: TextField(
child: AppSearchField(
controller: _searchController,
textInputAction: TextInputAction.search,
hintText: context.l10n.settingsSearchHint,
clearTooltip: context.l10n.dialogClear,
onChanged: (value) => setState(() => _query = value),
decoration: InputDecoration(
hintText: context.l10n.settingsSearchHint,
prefixIcon: const Icon(Icons.search),
suffixIcon: query.isEmpty
? null
: IconButton(
tooltip: context.l10n.dialogClear,
icon: const Icon(Icons.close),
onPressed: () {
_searchController.clear();
setState(() => _query = '');
},
),
),
onClear: () => setState(() => _query = ''),
),
),
),
+78
View File
@@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/theme/app_tokens.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
/// The standard search field used by top-level app surfaces.
///
/// Keeping the decoration here prevents Settings, Store, and future search
/// surfaces from drifting into slightly different shapes and colors.
class AppSearchField extends StatelessWidget {
const AppSearchField({
super.key,
required this.controller,
required this.hintText,
required this.clearTooltip,
required this.onChanged,
required this.onClear,
this.focusNode,
this.autofocus = false,
});
final TextEditingController controller;
final String hintText;
final String clearTooltip;
final ValueChanged<String> onChanged;
final VoidCallback onClear;
final FocusNode? focusNode;
final bool autofocus;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final radius = BorderRadius.circular(context.tokens.radiusSheet);
return ValueListenableBuilder<TextEditingValue>(
valueListenable: controller,
builder: (context, value, _) => TextField(
controller: controller,
focusNode: focusNode,
autofocus: autofocus,
textInputAction: TextInputAction.search,
decoration: InputDecoration(
hintText: hintText,
prefixIcon: const Icon(Icons.search),
suffixIcon: value.text.isEmpty
? null
: IconButton(
tooltip: clearTooltip,
icon: const Icon(Icons.clear),
onPressed: () {
controller.clear();
onClear();
},
),
border: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: radius,
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
filled: true,
fillColor: settingsGroupColor(context),
contentPadding: const EdgeInsets.symmetric(
horizontal: 20,
vertical: 16,
),
),
onChanged: onChanged,
onTapOutside: (_) => FocusScope.of(context).unfocus(),
),
);
}
}