From 0330fd4d451f04fb798a5d6bb47d522463636ec6 Mon Sep 17 00:00:00 2001 From: mthwJsmith Date: Fri, 27 Feb 2026 02:28:55 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20live=20search=20toggle=20?= =?UTF-8?q?=E2=80=94=20update=20results=20as=20you=20type?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Live Search" setting in Display Settings (default: ON). When enabled, typing in the search box updates the full results view in real time (debounced 300ms) instead of showing the autocomplete dropdown overlay. The overlay behavior is preserved when the setting is turned off. --- lib/l10n/app_en.arb | 3 ++ lib/screens/search_screen.dart | 14 +++++- lib/screens/settings_display_tab.dart | 51 ++++++++++++++++++++ lib/services/player_ui_settings_service.dart | 13 +++++ 4 files changed, 80 insertions(+), 1 deletion(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0c75028..24480cb 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -693,6 +693,9 @@ "tryDifferentSearch": "Try a different search", "noSuggestions": "No suggestions", "browseCategories": "Browse Categories", + "liveSearchSection": "Search", + "liveSearch": "Live Search", + "liveSearchSubtitle": "Update results as you type instead of showing a dropdown", "categoryMadeForYou": "Made For You", "categoryNewReleases": "New Releases", "categoryTopRated": "Top Rated", diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 35b686b..5718114 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -17,6 +17,7 @@ import 'top_rated_screen.dart'; import 'favorites_screen.dart'; import 'radio_screen.dart'; import '../l10n/app_localizations.dart'; +import '../services/player_ui_settings_service.dart'; class SearchScreen extends StatefulWidget { const SearchScreen({super.key}); @@ -35,6 +36,13 @@ class _SearchScreenState extends State { bool _showSuggestions = false; String _query = ''; Timer? _debounceTimer; + bool _liveSearch = true; + + @override + void initState() { + super.initState(); + _liveSearch = PlayerUiSettingsService().getLiveSearch(); + } @override void dispose() { @@ -93,7 +101,11 @@ class _SearchScreenState extends State { } _debounceTimer = Timer(const Duration(milliseconds: 300), () { - _loadAutocomplete(value); + if (_liveSearch) { + _search(value); + } else { + _loadAutocomplete(value); + } }); } diff --git a/lib/screens/settings_display_tab.dart b/lib/screens/settings_display_tab.dart index 1ba41dc..f006c19 100644 --- a/lib/screens/settings_display_tab.dart +++ b/lib/screens/settings_display_tab.dart @@ -26,6 +26,7 @@ class _SettingsDisplayTabState extends State { String _artworkShape = 'rounded'; String _artworkShadow = 'soft'; String _artworkShadowColor = 'black'; + bool _liveSearch = true; bool get _isDesktop { if (kIsWeb) return false; @@ -50,6 +51,7 @@ class _SettingsDisplayTabState extends State { _artworkShape = _playerUiSettings.getArtworkShape(); _artworkShadow = _playerUiSettings.getArtworkShadow(); _artworkShadowColor = _playerUiSettings.getArtworkShadowColor(); + _liveSearch = _playerUiSettings.getLiveSearch(); }); } @@ -77,6 +79,13 @@ class _SettingsDisplayTabState extends State { ], ), const SizedBox(height: 24), + _buildSection( + title: AppLocalizations.of(context)!.liveSearchSection.toUpperCase(), + children: [ + _buildLiveSearchToggle(), + ], + ), + const SizedBox(height: 24), _buildSection( title: AppLocalizations.of( context, @@ -231,6 +240,48 @@ class _SettingsDisplayTabState extends State { ); } + Widget _buildLiveSearchToggle() { + return ListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + leading: Container( + width: 32, + height: 32, + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFFFF9500), Color(0xFFFFCC00)], + ), + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + CupertinoIcons.search, + color: Colors.white, + size: 18, + ), + ), + title: Text( + AppLocalizations.of(context)!.liveSearch, + style: const TextStyle(fontSize: 16), + ), + subtitle: Text( + AppLocalizations.of(context)!.liveSearchSubtitle, + style: TextStyle( + fontSize: 13, + color: _isDark + ? AppTheme.darkSecondaryText + : AppTheme.lightSecondaryText, + ), + ), + trailing: CupertinoSwitch( + value: _liveSearch, + activeTrackColor: AppTheme.appleMusicRed, + onChanged: (value) async { + setState(() => _liveSearch = value); + await _playerUiSettings.setLiveSearch(value); + }, + ), + ); + } + // ── Artwork Style Editor ────────────────────────────────────────────────── double _artworkPreviewRadius() { diff --git a/lib/services/player_ui_settings_service.dart b/lib/services/player_ui_settings_service.dart index 4dfa745..16abd6c 100644 --- a/lib/services/player_ui_settings_service.dart +++ b/lib/services/player_ui_settings_service.dart @@ -8,6 +8,7 @@ class PlayerUiSettingsService { static const String _keyArtworkShape = 'artwork_shape'; static const String _keyArtworkShadow = 'artwork_shadow'; static const String _keyArtworkShadowColor = 'artwork_shadow_color'; + static const String _keyLiveSearch = 'search_live_search'; static final PlayerUiSettingsService _instance = PlayerUiSettingsService._internal(); @@ -17,6 +18,7 @@ class PlayerUiSettingsService { SharedPreferences? _prefs; final ValueNotifier showStarRatingsNotifier = ValueNotifier(false); + final ValueNotifier liveSearchNotifier = ValueNotifier(true); final ValueNotifier albumArtCornerRadiusNotifier = ValueNotifier(8.0); /// 'rounded' | 'circle' | 'square' @@ -37,6 +39,7 @@ class PlayerUiSettingsService { artworkShapeNotifier.value = getArtworkShape(); artworkShadowNotifier.value = getArtworkShadow(); artworkShadowColorNotifier.value = getArtworkShadowColor(); + liveSearchNotifier.value = getLiveSearch(); } Future setShowVolumeSlider(bool show) async { @@ -97,4 +100,14 @@ class PlayerUiSettingsService { String getArtworkShadowColor() { return _prefs?.getString(_keyArtworkShadowColor) ?? 'black'; } + + Future setLiveSearch(bool enabled) async { + await initialize(); + await _prefs!.setBool(_keyLiveSearch, enabled); + liveSearchNotifier.value = enabled; + } + + bool getLiveSearch() { + return _prefs?.getBool(_keyLiveSearch) ?? true; + } } From 606ea904b4c63e05f7b71f1171a5c6f271645b2d Mon Sep 17 00:00:00 2001 From: mthwJsmith Date: Fri, 27 Feb 2026 03:10:10 +0000 Subject: [PATCH 2/2] fix: subscribe to liveSearchNotifier for reactive setting updates The setting was only read once in initState(). Now listens to the ValueNotifier so toggling Live Search in Settings takes effect immediately without leaving the search screen. --- lib/screens/search_screen.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/screens/search_screen.dart b/lib/screens/search_screen.dart index 5718114..a049895 100644 --- a/lib/screens/search_screen.dart +++ b/lib/screens/search_screen.dart @@ -42,10 +42,18 @@ class _SearchScreenState extends State { void initState() { super.initState(); _liveSearch = PlayerUiSettingsService().getLiveSearch(); + PlayerUiSettingsService().liveSearchNotifier.addListener(_onLiveSearchChanged); + } + + void _onLiveSearchChanged() { + setState(() { + _liveSearch = PlayerUiSettingsService().liveSearchNotifier.value; + }); } @override void dispose() { + PlayerUiSettingsService().liveSearchNotifier.removeListener(_onLiveSearchChanged); _searchController.dispose(); _focusNode.dispose(); _debounceTimer?.cancel();