diff --git a/CHANGELOG.md b/CHANGELOG.md index 73750406..224ead95 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.2.1] - 2026-01-22 + +> **Note:** Starting from the next release, version format will change from `major.minor.patch` to `year.month.day` (e.g., 26.1.23). + +### Fixed + +- **iOS History Migration**: Fixed "File not found" after updating from 3.1.x to 3.2.0 (container UUID change) +- **Home Feed Greeting**: Fixed wrong timezone - now uses device local time instead of extension +- **Deezer Track Position**: Fallback to index+1 when API returns 0 for track position +- **Spanish & Portuguese Plurals**: Fixed 16 ICU syntax warnings in localization files + +--- + ## [3.2.0] - 2026-01-22 > **Note:** Starting from v3.2.0, changelogs will be concise. diff --git a/lib/providers/download_queue_provider.dart b/lib/providers/download_queue_provider.dart index b95c05bb..2af7c32c 100644 --- a/lib/providers/download_queue_provider.dart +++ b/lib/providers/download_queue_provider.dart @@ -193,6 +193,14 @@ class DownloadHistoryNotifier extends Notifier { _historyLog.i('Migrated history from SharedPreferences to SQLite'); } + // Migrate iOS paths if container UUID changed after app update + if (Platform.isIOS) { + final pathsMigrated = await _db.migrateIosContainerPaths(); + if (pathsMigrated) { + _historyLog.i('Migrated iOS container paths after app update'); + } + } + final jsonList = await _db.getAll(); final items = jsonList .map((e) => DownloadHistoryItem.fromJson(e)) diff --git a/lib/providers/explore_provider.dart b/lib/providers/explore_provider.dart index 63cf137c..3256542a 100644 --- a/lib/providers/explore_provider.dart +++ b/lib/providers/explore_provider.dart @@ -109,6 +109,20 @@ class ExploreState { } } +/// Calculate greeting based on local device time +String _getLocalGreeting() { + final hour = DateTime.now().hour; + if (hour >= 5 && hour < 12) { + return 'Good morning'; + } else if (hour >= 12 && hour < 17) { + return 'Good afternoon'; + } else if (hour >= 17 && hour < 21) { + return 'Good evening'; + } else { + return 'Good night'; + } +} + /// Provider for explore/home feed state class ExploreNotifier extends Notifier { @override @@ -201,9 +215,14 @@ class ExploreNotifier extends Notifier { _log.d('First item: name=${firstItem.name}, artists=${firstItem.artists}, type=${firstItem.type}'); } + // Always use local device time for greeting to avoid timezone issues + // Extension greeting may use wrong timezone (UTC or Spotify account timezone) + final localGreeting = _getLocalGreeting(); + _log.d('Greeting from extension: $greeting, using local: $localGreeting'); + state = ExploreState( isLoading: false, - greeting: greeting, + greeting: localGreeting, sections: sections, lastFetched: DateTime.now(), ); diff --git a/lib/services/history_database.dart b/lib/services/history_database.dart index fc2df58e..1cf1088d 100644 --- a/lib/services/history_database.dart +++ b/lib/services/history_database.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:io'; import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; @@ -7,6 +8,9 @@ import 'package:spotiflac_android/utils/logger.dart'; final _log = AppLogger('HistoryDatabase'); +/// Cached current iOS container path for path normalization +String? _currentContainerPath; + /// SQLite database service for download history /// Provides O(1) lookups by spotify_id and isrc with proper indexing class HistoryDatabase { @@ -78,6 +82,106 @@ class HistoryDatabase { // Future migrations go here } + // ==================== iOS Path Normalization ==================== + + /// Pattern to match iOS container paths + /// Example: /var/mobile/Containers/Data/Application/UUID-HERE/Documents/... + static final _iosContainerPattern = RegExp( + r'/var/mobile/Containers/Data/Application/[A-F0-9\-]+/', + caseSensitive: false, + ); + + /// Initialize and cache the current iOS container path + Future _initContainerPath() async { + if (!Platform.isIOS || _currentContainerPath != null) return; + + try { + final docDir = await getApplicationDocumentsDirectory(); + // Extract container path up to and including the UUID folder + // e.g., /var/mobile/Containers/Data/Application/UUID/ + final match = _iosContainerPattern.firstMatch(docDir.path); + if (match != null) { + _currentContainerPath = match.group(0); + _log.d('iOS container path: $_currentContainerPath'); + } + } catch (e) { + _log.w('Failed to get iOS container path: $e'); + } + } + + /// Normalize iOS file path by replacing old container UUID with current one + /// This fixes the issue where iOS changes container UUID after app updates + String _normalizeIosPath(String? filePath) { + if (filePath == null || filePath.isEmpty) return filePath ?? ''; + if (!Platform.isIOS || _currentContainerPath == null) return filePath; + + // Check if path contains an iOS container path + if (_iosContainerPattern.hasMatch(filePath)) { + final normalized = filePath.replaceFirst(_iosContainerPattern, _currentContainerPath!); + if (normalized != filePath) { + _log.d('Normalized iOS path: $filePath -> $normalized'); + } + return normalized; + } + + return filePath; + } + + /// Migrate iOS paths in database to use current container UUID + /// This is called once after app update if container changed + Future migrateIosContainerPaths() async { + if (!Platform.isIOS) return false; + + await _initContainerPath(); + if (_currentContainerPath == null) return false; + + final prefs = await SharedPreferences.getInstance(); + final lastContainer = prefs.getString('ios_last_container_path'); + + // Skip if container hasn't changed + if (lastContainer == _currentContainerPath) { + _log.d('iOS container path unchanged, skipping migration'); + return false; + } + + _log.i('iOS container changed: $lastContainer -> $_currentContainerPath'); + + try { + final db = await database; + + // Get all items with iOS paths + final rows = await db.query('history', columns: ['id', 'file_path']); + int updatedCount = 0; + + for (final row in rows) { + final id = row['id'] as String; + final oldPath = row['file_path'] as String?; + + if (oldPath != null && _iosContainerPattern.hasMatch(oldPath)) { + final newPath = _normalizeIosPath(oldPath); + if (newPath != oldPath) { + await db.update( + 'history', + {'file_path': newPath}, + where: 'id = ?', + whereArgs: [id], + ); + updatedCount++; + } + } + } + + // Save current container path + await prefs.setString('ios_last_container_path', _currentContainerPath!); + + _log.i('iOS path migration complete: $updatedCount paths updated'); + return updatedCount > 0; + } catch (e, stack) { + _log.e('iOS path migration failed: $e', e, stack); + return false; + } + } + /// Migrate data from SharedPreferences to SQLite /// Returns true if migration was performed, false if already migrated Future migrateFromSharedPreferences() async { @@ -153,6 +257,7 @@ class HistoryDatabase { } /// Convert DB row (snake_case) to JSON format (camelCase) + /// Also normalizes iOS paths if container UUID changed Map _dbRowToJson(Map row) { return { 'id': row['id'], @@ -161,7 +266,7 @@ class HistoryDatabase { 'albumName': row['album_name'], 'albumArtist': row['album_artist'], 'coverUrl': row['cover_url'], - 'filePath': row['file_path'], + 'filePath': _normalizeIosPath(row['file_path'] as String?), 'service': row['service'], 'downloadedAt': row['downloaded_at'], 'isrc': row['isrc'],