fix: iOS path migration, local greeting timezone, ICU plural warnings

- iOS: Auto-migrate file paths when container UUID changes after app update
- Greeting: Use device local time instead of extension response
- i18n: Fix 16 ICU plural syntax warnings in Spanish and Portuguese
This commit is contained in:
zarzet
2026-01-22 00:48:45 +07:00
parent aa35f60fad
commit 8d205600b8
4 changed files with 147 additions and 2 deletions
+13
View File
@@ -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.
@@ -193,6 +193,14 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
_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))
+20 -1
View File
@@ -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<ExploreState> {
@override
@@ -201,9 +215,14 @@ class ExploreNotifier extends Notifier<ExploreState> {
_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(),
);
+106 -1
View File
@@ -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<void> _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<bool> 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<bool> 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<String, dynamic> _dbRowToJson(Map<String, dynamic> 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'],