feat: add recent access history, artist screen redesign, and extension improvements

Recent Access History:
- Quick access to recently visited artists, albums, playlists, and tracks
- Tap search bar to show recent access list
- Stays visible after keyboard dismiss, exit with back button
- Persists across app restarts (SharedPreferences)
- X button to remove items, Clear All button for all

Artist Screen Redesign:
- Full-width header image with gradient overlay
- Monthly listeners display with compact notation
- Popular section with top 5 tracks and download status
- Extension artists skip Spotify/Deezer fetch (no rate limit errors)

Go Backend:
- GetArtistWithExtensionJSON now returns top_tracks, header_image, listeners

Bug Fixes:
- Search bar unfocus when tapping outside
- Keyboard not appearing on Settings navigation return
- Recent access artist navigation uses correct screen for extensions
- Extension artist screen correctly parses and forwards top tracks

Localization:
- Added recentPlaylistInfo, errorGeneric strings
- Multi-language support via Crowdin

Extensions:
- YT Music: v1.5.0 (top_tracks in getArtist)
- Spotify Web: v1.6.0
This commit is contained in:
zarzet
2026-01-17 04:29:39 +07:00
parent 9eac6e6e56
commit fc8cfb05d0
29 changed files with 2014 additions and 240 deletions
+24 -4
View File
@@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/providers/download_queue_provider.dart';
import 'package:spotiflac_android/providers/settings_provider.dart';
import 'package:spotiflac_android/providers/store_provider.dart';
import 'package:spotiflac_android/providers/track_provider.dart';
import 'package:spotiflac_android/screens/home_tab.dart';
import 'package:spotiflac_android/screens/store_tab.dart';
@@ -124,7 +125,8 @@ class _MainShellState extends ConsumerState<MainShell> {
if (_currentIndex != index) {
setState(() => _currentIndex = index);
// Unfocus any text field when switching tabs to prevent keyboard from appearing
FocusScope.of(context).unfocus();
// Use primaryFocus for more aggressive unfocus that works with keep-alive widgets
FocusManager.instance.primaryFocus?.unfocus();
}
}
@@ -135,7 +137,15 @@ class _MainShellState extends ConsumerState<MainShell> {
// Check if keyboard is visible - if so, just dismiss keyboard, don't clear search
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
if (isKeyboardVisible) {
FocusScope.of(context).unfocus();
FocusManager.instance.primaryFocus?.unfocus();
return;
}
// If on Home tab and showing recent access mode, exit it
if (_currentIndex == 0 && trackState.isShowingRecentAccess) {
ref.read(trackProvider.notifier).setShowingRecentAccess(false);
// Also unfocus search bar when exiting recent access mode
FocusManager.instance.primaryFocus?.unfocus();
return;
}
@@ -177,6 +187,7 @@ class _MainShellState extends ConsumerState<MainShell> {
final queueState = ref.watch(downloadQueueProvider.select((s) => s.queuedCount));
final trackState = ref.watch(trackProvider);
final showStore = ref.watch(settingsProvider.select((s) => s.showExtensionStore));
final storeUpdatesCount = ref.watch(storeProvider.select((s) => s.updatesAvailableCount));
// Check if keyboard is visible (bottom inset > 0 means keyboard is showing)
final isKeyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0;
@@ -188,6 +199,7 @@ class _MainShellState extends ConsumerState<MainShell> {
!trackState.hasSearchText &&
!trackState.hasContent &&
!trackState.isLoading &&
!trackState.isShowingRecentAccess &&
!isKeyboardVisible;
// Build tabs and destinations based on settings
@@ -224,8 +236,16 @@ class _MainShellState extends ConsumerState<MainShell> {
),
if (showStore)
NavigationDestination(
icon: const Icon(Icons.store_outlined),
selectedIcon: const Icon(Icons.store),
icon: Badge(
isLabelVisible: storeUpdatesCount > 0,
label: Text('$storeUpdatesCount'),
child: const Icon(Icons.store_outlined),
),
selectedIcon: Badge(
isLabelVisible: storeUpdatesCount > 0,
label: Text('$storeUpdatesCount'),
child: const Icon(Icons.store),
),
label: l10n.navStore,
),
NavigationDestination(