From 15acf181d1c9429fabca665a869f94a9325cfc1c Mon Sep 17 00:00:00 2001 From: zarzet Date: Tue, 13 Jan 2026 23:48:02 +0700 Subject: [PATCH] fix: back gesture freeze on Android 13+ and add album folder structure setting - Add PopScope with canPop:true to all settings pages for predictive back gesture support - Change settings navigation to use PageRouteBuilder instead of MaterialPageRoute - Add album folder structure setting (artist_album vs album_only) - Fix extension search result parsing to handle both array and object formats - Update CHANGELOG Fixes back gesture freeze issue on OnePlus and other Android 13+ devices with gesture navigation --- CHANGELOG.md | 36 ++++++++++- go_backend/cover.go | 19 +----- go_backend/extension_providers.go | 13 +++- lib/models/settings.dart | 7 ++- lib/models/settings.g.dart | 2 + lib/providers/download_queue_provider.dart | 49 +++++++++++++-- lib/providers/settings_provider.dart | 11 +++- lib/screens/home_tab.dart | 12 +++- lib/screens/settings/about_page.dart | 13 ++-- .../settings/appearance_settings_page.dart | 15 +++-- .../settings/download_settings_page.dart | 61 +++++++++++++++++-- .../settings/extension_detail_page.dart | 13 ++-- lib/screens/settings/extensions_page.dart | 9 ++- lib/screens/settings/log_screen.dart | 17 +++--- .../settings/options_settings_page.dart | 15 +++-- lib/screens/settings/settings_tab.dart | 23 ++++++- lib/screens/store_tab.dart | 4 ++ 17 files changed, 254 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fb47087..49930c6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,46 @@ ## [3.0.0-beta.2] - 2026-01-13 +### Added + +- **Album Folder Structure Setting**: Option to remove artist folder from album path + - New setting in Download Settings when "Separate Singles Folder" is enabled + - `Artist / Album` (default): `Albums/Artist Name/Album Name/` + - `Album Only`: `Albums/Album Name/` + - Requested by user who prefers flat album organization + ### Fixed +- **Back Gesture Freeze on OnePlus/Android 13+**: Fixed app freeze when using back gesture in settings + - Added `PopScope` with `canPop: true` to all settings pages + - Changed navigation to use `PageRouteBuilder` with proper slide transition + - Fixes predictive back gesture conflict on devices with gesture navigation + - Affected pages: Download, Appearance, Options, Extensions, About, Logs, Extension Detail + +- **Extension Search Result Parsing**: Fixed "cannot unmarshal array into Go value" error + - Go backend now handles both array and object formats from extensions + - Extensions returning `[{track}, {track}]` now work correctly + - Extensions returning `{tracks: [...], total: N}` still work as before + - **Max Resolution Cover Download**: Fixed cover not upgrading to max resolution on mobile - Added missing `spotifySize300` constant (300x300 size code) - Mobile now correctly upgrades 300x300 → 640x640 → max resolution (~2000x2000) - - Matches PC version behavior when "Download max resolution song cover" is enabled + - Added `_upgradeToMaxQualityCover()` helper in Flutter for M4A conversion path + - Go backend `cover.go` now directly replaces URL without HEAD verification + +- **Extension Search Provider Reset**: Fixed search provider not resetting to default when disabled + - `copyWith` in `AppSettings` couldn't set `searchProvider` to `null` + - Added `clearSearchProvider` boolean parameter to properly clear the value + - Settings menu now correctly switches back to default provider + +- **Extension Disabled Search Fallback**: Fixed error when extension is disabled but still called + - `_performSearch` now checks if extension is still enabled before calling custom search + - Automatically falls back to Deezer/Spotify search if extension was disabled + - Clears `searchProvider` setting if extension no longer available + +- **Store Tab Unmount Crash**: Fixed "Using ref when widget is unmounted" error + - Added `mounted` check after async operation in `_initialize()` + - Prevents crash when navigating away from Store tab during initialization - **EXISTS: Prefix in File Path**: Fixed "File not found" error in metadata screen after download - Duplicate detection was adding `EXISTS:` prefix to file paths diff --git a/go_backend/cover.go b/go_backend/cover.go index d8e6bcf3..af43d754 100644 --- a/go_backend/cover.go +++ b/go_backend/cover.go @@ -91,7 +91,8 @@ func downloadCoverToMemory(coverURL string, maxQuality bool) ([]byte, error) { } // upgradeToMaxQuality upgrades Spotify cover URL to maximum quality -// Uses same logic as PC version - replaces 640x640 size code with max resolution +// Same logic as PC version - directly replaces 640x640 size code with max resolution +// No HEAD verification needed - Spotify CDN always serves max resolution if available func upgradeToMaxQuality(coverURL string) string { // Spotify image URLs can be upgraded by changing the size parameter // Format: https://i.scdn.co/image/ab67616d0000b273... @@ -99,21 +100,7 @@ func upgradeToMaxQuality(coverURL string) string { // ab67616d000082c1 = Max resolution (~2000x2000) if strings.Contains(coverURL, spotifySize640) { - // Try max resolution first - maxURL := strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1) - - // Verify max resolution URL is available - client := NewHTTPClientWithTimeout(DefaultTimeout) - req, err := http.NewRequest("HEAD", maxURL, nil) - if err == nil { - resp, err := DoRequestWithUserAgent(client, req) - if err == nil { - resp.Body.Close() - if resp.StatusCode == http.StatusOK { - return maxURL - } - } - } + return strings.Replace(coverURL, spotifySize640, spotifySizeMax, 1) } return coverURL diff --git a/go_backend/extension_providers.go b/go_backend/extension_providers.go index 336fc1d8..578bc2bb 100644 --- a/go_backend/extension_providers.go +++ b/go_backend/extension_providers.go @@ -162,8 +162,19 @@ func (p *ExtensionProviderWrapper) SearchTracks(query string, limit int) (*ExtSe } var searchResult ExtSearchResult + + // Try to parse as ExtSearchResult object first if err := json.Unmarshal(jsonBytes, &searchResult); err != nil { - return nil, fmt.Errorf("failed to parse search result: %w", err) + // If that fails, try parsing as array of tracks directly + var tracks []ExtTrackMetadata + if arrErr := json.Unmarshal(jsonBytes, &tracks); arrErr != nil { + return nil, fmt.Errorf("failed to parse search result: %w (also tried array: %v)", err, arrErr) + } + // Wrap array in ExtSearchResult + searchResult = ExtSearchResult{ + Tracks: tracks, + Total: len(tracks), + } } // Set provider ID on all tracks diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 5c6f2b4f..5462a4a3 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -28,6 +28,7 @@ class AppSettings { final bool useExtensionProviders; // Use extension providers for downloads when available final String? searchProvider; // null/empty = default (Deezer/Spotify), otherwise extension ID final bool separateSingles; // Separate singles/EPs into their own folder + final String albumFolderStructure; // artist_album or album_only final bool showExtensionStore; // Show Extension Store tab in navigation const AppSettings({ @@ -55,6 +56,7 @@ class AppSettings { this.useExtensionProviders = true, // Default: use extensions when available this.searchProvider, // Default: null (use Deezer/Spotify) this.separateSingles = false, // Default: disabled + this.albumFolderStructure = 'artist_album', // Default: Albums/Artist/Album this.showExtensionStore = true, // Default: show store }); @@ -82,7 +84,9 @@ class AppSettings { bool? enableLogging, bool? useExtensionProviders, String? searchProvider, + bool clearSearchProvider = false, // Set to true to clear searchProvider to null bool? separateSingles, + String? albumFolderStructure, bool? showExtensionStore, }) { return AppSettings( @@ -108,8 +112,9 @@ class AppSettings { metadataSource: metadataSource ?? this.metadataSource, enableLogging: enableLogging ?? this.enableLogging, useExtensionProviders: useExtensionProviders ?? this.useExtensionProviders, - searchProvider: searchProvider ?? this.searchProvider, + searchProvider: clearSearchProvider ? null : (searchProvider ?? this.searchProvider), separateSingles: separateSingles ?? this.separateSingles, + albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure, showExtensionStore: showExtensionStore ?? this.showExtensionStore, ); } diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index bdbb8745..06cd85b7 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -32,6 +32,7 @@ AppSettings _$AppSettingsFromJson(Map json) => AppSettings( useExtensionProviders: json['useExtensionProviders'] as bool? ?? true, searchProvider: json['searchProvider'] as String?, separateSingles: json['separateSingles'] as bool? ?? false, + albumFolderStructure: json['albumFolderStructure'] as String? ?? 'artist_album', showExtensionStore: json['showExtensionStore'] as bool? ?? true, ); @@ -61,5 +62,6 @@ Map _$AppSettingsToJson(AppSettings instance) => 'useExtensionProviders': instance.useExtensionProviders, 'searchProvider': instance.searchProvider, 'separateSingles': instance.separateSingles, + 'albumFolderStructure': instance.albumFolderStructure, 'showExtensionStore': instance.showExtensionStore, }; diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index 52d67db0..ccbab4a8 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -669,7 +669,7 @@ class DownloadQueueNotifier extends Notifier { } /// Build output directory based on folder organization setting and separateSingles - Future _buildOutputDir(Track track, String folderOrganization, {bool separateSingles = false}) async { + Future _buildOutputDir(Track track, String folderOrganization, {bool separateSingles = false, String albumFolderStructure = 'artist_album'}) async { String baseDir = state.outputDir; // If separateSingles is enabled, use Albums/Singles structure @@ -686,10 +686,19 @@ class DownloadQueueNotifier extends Notifier { } return singlesPath; } else { - // Albums go to Albums/Artist/Album structure - final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + // Albums folder structure based on setting final albumName = _sanitizeFolderName(track.albumName); - final albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; + String albumPath; + + if (albumFolderStructure == 'album_only') { + // Albums/Album structure (no artist folder) + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$albumName'; + } else { + // Albums/Artist/Album structure (default) + final artistName = _sanitizeFolderName(track.albumArtist ?? track.artistName); + albumPath = '$baseDir${Platform.pathSeparator}Albums${Platform.pathSeparator}$artistName${Platform.pathSeparator}$albumName'; + } + final dir = Directory(albumPath); if (!await dir.exists()) { await dir.create(recursive: true); @@ -1001,13 +1010,42 @@ class DownloadQueueNotifier extends Notifier { } } + /// Upgrade Spotify cover URL to max quality (~2000x2000) + /// Same logic as Go backend cover.go + String _upgradeToMaxQualityCover(String coverUrl) { + const spotifySize300 = 'ab67616d00001e02'; // 300x300 (small) + const spotifySize640 = 'ab67616d0000b273'; // 640x640 (medium) + const spotifySizeMax = 'ab67616d000082c1'; // Max resolution (~2000x2000) + + // First upgrade small (300) to medium (640) + var result = coverUrl; + if (result.contains(spotifySize300)) { + result = result.replaceFirst(spotifySize300, spotifySize640); + } + + // Then upgrade medium (640) to max + if (result.contains(spotifySize640)) { + result = result.replaceFirst(spotifySize640, spotifySizeMax); + } + + return result; + } + /// Embed metadata and cover to a FLAC file after M4A conversion Future _embedMetadataAndCover(String flacPath, Track track) async { + final settings = ref.read(settingsProvider); + // Download cover first String? coverPath; - final coverUrl = track.coverUrl; + var coverUrl = track.coverUrl; if (coverUrl != null && coverUrl.isNotEmpty) { try { + // Upgrade cover URL to max quality if setting is enabled + if (settings.maxQualityCover) { + coverUrl = _upgradeToMaxQualityCover(coverUrl); + _log.d('Cover URL upgraded to max quality: $coverUrl'); + } + final tempDir = await getTemporaryDirectory(); final uniqueId = '${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(10000)}'; @@ -1446,6 +1484,7 @@ class DownloadQueueNotifier extends Notifier { trackToDownload, settings.folderOrganization, separateSingles: settings.separateSingles, + albumFolderStructure: settings.albumFolderStructure, ); // Use quality override if set, otherwise use default from settings diff --git a/lib/providers/settings_provider.dart b/lib/providers/settings_provider.dart index f56da477..ba34cc72 100644 --- a/lib/providers/settings_provider.dart +++ b/lib/providers/settings_provider.dart @@ -196,7 +196,11 @@ class SettingsNotifier extends Notifier { } void setSearchProvider(String? provider) { - state = state.copyWith(searchProvider: provider); + if (provider == null || provider.isEmpty) { + state = state.copyWith(clearSearchProvider: true); + } else { + state = state.copyWith(searchProvider: provider); + } _saveSettings(); } @@ -217,6 +221,11 @@ class SettingsNotifier extends Notifier { _saveSettings(); } + void setAlbumFolderStructure(String structure) { + state = state.copyWith(albumFolderStructure: structure); + _saveSettings(); + } + void setShowExtensionStore(bool enabled) { state = state.copyWith(showExtensionStore: enabled); _saveSettings(); diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index 81adbf54..846d583e 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -81,6 +81,7 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient Future _performSearch(String query) async { final settings = ref.read(settingsProvider); + final extState = ref.read(extensionProvider); final searchProvider = settings.searchProvider; // Skip if same query already searched with same provider @@ -88,11 +89,20 @@ class _HomeTabState extends ConsumerState with AutomaticKeepAliveClient if (_lastSearchQuery == searchKey) return; _lastSearchQuery = searchKey; - if (searchProvider != null && searchProvider.isNotEmpty) { + // Check if extension search provider is set AND still enabled + final isExtensionEnabled = searchProvider != null && + searchProvider.isNotEmpty && + extState.extensions.any((e) => e.id == searchProvider && e.enabled); + + if (isExtensionEnabled) { // Use custom search from extension await ref.read(trackProvider.notifier).customSearch(searchProvider, query); } else { // Use default search (Deezer/Spotify) + // Also clear searchProvider if it was set but extension is disabled + if (searchProvider != null && searchProvider.isNotEmpty && !isExtensionEnabled) { + ref.read(settingsProvider.notifier).setSearchProvider(null); + } await ref.read(trackProvider.notifier).search(query, metadataSource: settings.metadataSource); } ref.read(settingsProvider.notifier).setHasSearchedBefore(); diff --git a/lib/screens/settings/about_page.dart b/lib/screens/settings/about_page.dart index c1bbc2aa..a17857f3 100644 --- a/lib/screens/settings/about_page.dart +++ b/lib/screens/settings/about_page.dart @@ -12,11 +12,13 @@ class AboutPage extends StatelessWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - return Scaffold( - body: CustomScrollView( - slivers: [ - // Collapsing App Bar with back button - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ + // Collapsing App Bar with back button + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -218,6 +220,7 @@ class AboutPage extends StatelessWidget { const SliverToBoxAdapter(child: SizedBox(height: 16)), ], ), + ), ); } diff --git a/lib/screens/settings/appearance_settings_page.dart b/lib/screens/settings/appearance_settings_page.dart index 88c50de0..63e3d048 100644 --- a/lib/screens/settings/appearance_settings_page.dart +++ b/lib/screens/settings/appearance_settings_page.dart @@ -14,11 +14,13 @@ class AppearanceSettingsPage extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - return Scaffold( - body: CustomScrollView( - slivers: [ - // Collapsing App Bar with back button - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ + // Collapsing App Bar with back button + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -129,7 +131,8 @@ class AppearanceSettingsPage extends ConsumerWidget { ), ], ), - ); + ), + ); } } diff --git a/lib/screens/settings/download_settings_page.dart b/lib/screens/settings/download_settings_page.dart index 0fec3ed3..dc0e927a 100644 --- a/lib/screens/settings/download_settings_page.dart +++ b/lib/screens/settings/download_settings_page.dart @@ -22,11 +22,13 @@ class DownloadSettingsPage extends ConsumerWidget { // Check if current service is built-in (supports quality options) final isBuiltInService = _builtInServices.contains(settings.defaultService); - return Scaffold( - body: CustomScrollView( - slivers: [ - // Collapsing App Bar with back button - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ + // Collapsing App Bar with back button + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -194,6 +196,19 @@ class DownloadSettingsPage extends ConsumerWidget { .read(settingsProvider.notifier) .setSeparateSingles(value), ), + if (settings.separateSingles) + SettingsItem( + icon: Icons.folder_outlined, + title: 'Album Folder Structure', + subtitle: settings.albumFolderStructure == 'album_only' + ? 'Albums/Album Name/' + : 'Albums/Artist/Album Name/', + onTap: () => _showAlbumFolderStructurePicker( + context, + ref, + settings.albumFolderStructure, + ), + ), if (!settings.separateSingles) SettingsItem( icon: Icons.create_new_folder_outlined, @@ -215,7 +230,41 @@ class DownloadSettingsPage extends ConsumerWidget { const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), - ); + ), + ); + } + + void _showAlbumFolderStructurePicker(BuildContext context, WidgetRef ref, String current) { + showModalBottomSheet( + context: context, + builder: (context) => SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ListTile( + leading: const Icon(Icons.folder_outlined), + title: const Text('Artist / Album'), + subtitle: const Text('Albums/Artist Name/Album Name/'), + trailing: current == 'artist_album' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setAlbumFolderStructure('artist_album'); + Navigator.pop(context); + }, + ), + ListTile( + leading: const Icon(Icons.album_outlined), + title: const Text('Album Only'), + subtitle: const Text('Albums/Album Name/'), + trailing: current == 'album_only' ? const Icon(Icons.check) : null, + onTap: () { + ref.read(settingsProvider.notifier).setAlbumFolderStructure('album_only'); + Navigator.pop(context); + }, + ), + ], + ), + ), + ); } void _showFormatEditor(BuildContext context, WidgetRef ref, String current) { diff --git a/lib/screens/settings/extension_detail_page.dart b/lib/screens/settings/extension_detail_page.dart index 50635540..90a12922 100644 --- a/lib/screens/settings/extension_detail_page.dart +++ b/lib/screens/settings/extension_detail_page.dart @@ -56,11 +56,13 @@ class _ExtensionDetailPageState extends ConsumerState { final topPadding = MediaQuery.of(context).padding.top; final hasError = extension.status == 'error'; - return Scaffold( - body: CustomScrollView( - slivers: [ - // App Bar - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ + // App Bar + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -348,6 +350,7 @@ class _ExtensionDetailPageState extends ConsumerState { const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), + ), ); } diff --git a/lib/screens/settings/extensions_page.dart b/lib/screens/settings/extensions_page.dart index 6f2a6a71..b4a86143 100644 --- a/lib/screens/settings/extensions_page.dart +++ b/lib/screens/settings/extensions_page.dart @@ -45,9 +45,11 @@ class _ExtensionsPageState extends ConsumerState { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - return Scaffold( - body: CustomScrollView( - slivers: [ + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ // App Bar SliverAppBar( expandedHeight: 120 + topPadding, @@ -248,6 +250,7 @@ class _ExtensionsPageState extends ConsumerState { ), ], ), + ), ); } diff --git a/lib/screens/settings/log_screen.dart b/lib/screens/settings/log_screen.dart index 5e33ec46..4c4ebeb7 100644 --- a/lib/screens/settings/log_screen.dart +++ b/lib/screens/settings/log_screen.dart @@ -124,12 +124,14 @@ class _LogScreenState extends State { final topPadding = MediaQuery.of(context).padding.top; final logs = _filteredLogs; - return Scaffold( - body: CustomScrollView( - controller: _scrollController, - slivers: [ - // Collapsing App Bar with back button - same as other settings pages - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + controller: _scrollController, + slivers: [ + // Collapsing App Bar with back button - same as other settings pages + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -378,7 +380,8 @@ class _LogScreenState extends State { const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), - ); + ), + ); } } diff --git a/lib/screens/settings/options_settings_page.dart b/lib/screens/settings/options_settings_page.dart index f95c5da3..3752656b 100644 --- a/lib/screens/settings/options_settings_page.dart +++ b/lib/screens/settings/options_settings_page.dart @@ -17,11 +17,13 @@ class OptionsSettingsPage extends ConsumerWidget { final colorScheme = Theme.of(context).colorScheme; final topPadding = MediaQuery.of(context).padding.top; - return Scaffold( - body: CustomScrollView( - slivers: [ - // Collapsing App Bar with back button - SliverAppBar( + return PopScope( + canPop: true, // Always allow back gesture + child: Scaffold( + body: CustomScrollView( + slivers: [ + // Collapsing App Bar with back button + SliverAppBar( expandedHeight: 120 + topPadding, collapsedHeight: kToolbarHeight, floating: false, @@ -271,7 +273,8 @@ class OptionsSettingsPage extends ConsumerWidget { const SliverToBoxAdapter(child: SizedBox(height: 32)), ], ), - ); + ), + ); } void _showClearHistoryDialog( diff --git a/lib/screens/settings/settings_tab.dart b/lib/screens/settings/settings_tab.dart index 1861c3d2..a589b730 100644 --- a/lib/screens/settings/settings_tab.dart +++ b/lib/screens/settings/settings_tab.dart @@ -116,6 +116,27 @@ class SettingsTab extends ConsumerWidget { } void _navigateTo(BuildContext context, Widget page) { - Navigator.of(context).push(MaterialPageRoute(builder: (_) => page)); + Navigator.of(context).push( + // Use PageRouteBuilder for better predictive back gesture support + // MaterialPageRoute can cause freeze on some devices with gesture navigation + PageRouteBuilder( + pageBuilder: (context, animation, secondaryAnimation) => page, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + // Use slide transition similar to MaterialPageRoute + const begin = Offset(1.0, 0.0); + const end = Offset.zero; + const curve = Curves.easeInOut; + var tween = Tween(begin: begin, end: end).chain( + CurveTween(curve: curve), + ); + return SlideTransition( + position: animation.drive(tween), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 300), + reverseTransitionDuration: const Duration(milliseconds: 250), + ), + ); } } diff --git a/lib/screens/store_tab.dart b/lib/screens/store_tab.dart index 000da84a..8935f3d8 100644 --- a/lib/screens/store_tab.dart +++ b/lib/screens/store_tab.dart @@ -26,6 +26,10 @@ class _StoreTabState extends ConsumerState { _isInitialized = true; final cacheDir = await getApplicationCacheDirectory(); + + // Check if widget is still mounted after async operation + if (!mounted) return; + await ref.read(storeProvider.notifier).initialize(cacheDir.path); }