mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-13 12:34:59 +02:00
fix provider fallbacks and public branding
This commit is contained in:
@@ -18,7 +18,7 @@
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
android:label="SpotiFLAC"
|
||||
android:label="SpotiFLAC Mobile"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="false"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "SpotiFLAC Source",
|
||||
"name": "SpotiFLAC Mobile Source",
|
||||
"identifier": "com.zarzet.spotiflac.source",
|
||||
"subtitle": "FLAC Downloader for iOS",
|
||||
"apps": [
|
||||
{
|
||||
"name": "SpotiFLAC",
|
||||
"name": "SpotiFLAC Mobile",
|
||||
"bundleIdentifier": "com.zarzet.spotiflac",
|
||||
"developerName": "zarzet",
|
||||
"version": "4.3.1",
|
||||
"versionDate": "2026-04-14",
|
||||
"downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v4.3.1/SpotiFLAC-v4.3.1-ios-unsigned.ipa",
|
||||
"localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.",
|
||||
"localizedDescription": "SpotiFLAC Mobile is written in Flutter. Download tracks in true FLAC from Tidal, Qobuz, & Amazon Music.",
|
||||
"iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png",
|
||||
"size": 34773644
|
||||
}
|
||||
|
||||
@@ -17,6 +17,24 @@ type HTTPResponse struct {
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
|
||||
const maxExtensionHTTPResponseBytes = 16 << 20
|
||||
|
||||
func readExtensionHTTPResponseBody(resp *http.Response) ([]byte, error) {
|
||||
body, err := io.ReadAll(
|
||||
io.LimitReader(resp.Body, maxExtensionHTTPResponseBytes+1),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(body) > maxExtensionHTTPResponseBytes {
|
||||
return nil, fmt.Errorf(
|
||||
"response body exceeds %d byte limit; use file.download for large media",
|
||||
maxExtensionHTTPResponseBytes,
|
||||
)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (r *extensionRuntime) validateDomain(urlStr string) error {
|
||||
parsed, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
@@ -99,7 +117,7 @@ func (r *extensionRuntime) httpGet(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -197,7 +215,7 @@ func (r *extensionRuntime) httpPost(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -307,7 +325,7 @@ func (r *extensionRuntime) httpRequest(call goja.FunctionCall) goja.Value {
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
@@ -433,7 +451,7 @@ func (r *extensionRuntime) httpMethodShortcut(method string, call goja.FunctionC
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
body, err := readExtensionHTTPResponseBody(resp)
|
||||
if err != nil {
|
||||
return r.vm.ToValue(map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<string>zh-Hant</string>
|
||||
</array>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>SpotiFLAC</string>
|
||||
<string>SpotiFLAC Mobile</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -30,7 +30,7 @@
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SpotiFLAC</string>
|
||||
<string>SpotiFLAC Mobile</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
@@ -80,7 +80,7 @@
|
||||
|
||||
<!-- Photo Library (for cover art if needed) -->
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>SpotiFLAC needs access to save album artwork</string>
|
||||
<string>SpotiFLAC Mobile needs access to save album artwork</string>
|
||||
|
||||
<!-- URL Schemes for deep linking -->
|
||||
<key>CFBundleURLTypes</key>
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/screens/main_shell.dart';
|
||||
import 'package:spotiflac_android/screens/setup_screen.dart';
|
||||
import 'package:spotiflac_android/screens/tutorial_screen.dart';
|
||||
@@ -105,7 +106,7 @@ class SpotiFLACApp extends ConsumerWidget {
|
||||
return DynamicColorWrapper(
|
||||
builder: (lightTheme, darkTheme, themeMode) {
|
||||
return MaterialApp.router(
|
||||
title: 'SpotiFLAC',
|
||||
title: AppInfo.appName,
|
||||
debugShowCheckedModeBanner: false,
|
||||
theme: lightTheme,
|
||||
darkTheme: darkTheme,
|
||||
|
||||
@@ -5912,7 +5912,7 @@ abstract class AppLocalizations {
|
||||
/// Notification title while downloading an app update
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Downloading SpotiFLAC v{version}'**
|
||||
/// **'Downloading SpotiFLAC Mobile v{version}'**
|
||||
String notifDownloadingUpdate(String version);
|
||||
|
||||
/// Notification body showing update download progress
|
||||
@@ -5930,7 +5930,7 @@ abstract class AppLocalizations {
|
||||
/// Notification body when app update is ready to install
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'SpotiFLAC v{version} downloaded. Tap to install.'**
|
||||
/// **'SpotiFLAC Mobile v{version} downloaded. Tap to install.'**
|
||||
String notifUpdateReadyBody(String version);
|
||||
|
||||
/// Notification title when app update download fails
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
AppLocalizationsDe([String locale = 'de']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Startseite';
|
||||
@@ -3508,7 +3508,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3521,7 +3521,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
AppLocalizationsEn([String locale = 'en']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3473,7 +3473,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3486,7 +3486,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
AppLocalizationsEs([String locale = 'es']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3473,7 +3473,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3486,7 +3486,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3788,7 +3788,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
AppLocalizationsEsEs() : super('es_ES');
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Inicio';
|
||||
@@ -7211,7 +7211,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -7224,7 +7224,7 @@ class AppLocalizationsEsEs extends AppLocalizationsEs {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
AppLocalizationsFr([String locale = 'fr']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Accueil';
|
||||
@@ -3477,7 +3477,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3490,7 +3490,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
AppLocalizationsHi([String locale = 'hi']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFlac';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'होम';
|
||||
@@ -3474,7 +3474,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3487,7 +3487,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
AppLocalizationsId([String locale = 'id']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Beranda';
|
||||
@@ -3483,7 +3483,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3496,7 +3496,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
AppLocalizationsJa([String locale = 'ja']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'ホーム';
|
||||
@@ -3461,7 +3461,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3474,7 +3474,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
AppLocalizationsKo([String locale = 'ko']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3454,7 +3454,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3467,7 +3467,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
AppLocalizationsNl([String locale = 'nl']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3474,7 +3474,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3487,7 +3487,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
AppLocalizationsPt([String locale = 'pt']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3473,7 +3473,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3486,7 +3486,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3788,7 +3788,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
AppLocalizationsPtPt() : super('pt_PT');
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Início';
|
||||
@@ -7204,7 +7204,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -7217,7 +7217,7 @@ class AppLocalizationsPtPt extends AppLocalizationsPt {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
AppLocalizationsRu([String locale = 'ru']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Главная';
|
||||
@@ -3533,7 +3533,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3546,7 +3546,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
AppLocalizationsTr([String locale = 'tr']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Ana sayfa';
|
||||
@@ -3500,7 +3500,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3513,7 +3513,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
AppLocalizationsUk([String locale = 'uk']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Головна';
|
||||
@@ -3533,7 +3533,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Завантаження SpotiFLAC v$version';
|
||||
return 'Завантаження SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3546,7 +3546,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version завантажений. Натисніть щоб установити.';
|
||||
return 'SpotiFLAC Mobile v$version завантажений. Натисніть щоб установити.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -9,7 +9,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
AppLocalizationsZh([String locale = 'zh']) : super(locale);
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -3473,7 +3473,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3486,7 +3486,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -3788,7 +3788,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh {
|
||||
AppLocalizationsZhCn() : super('zh_CN');
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => '主页';
|
||||
@@ -7170,7 +7170,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -7183,7 +7183,7 @@ class AppLocalizationsZhCn extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -7266,7 +7266,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
AppLocalizationsZhTw() : super('zh_TW');
|
||||
|
||||
@override
|
||||
String get appName => 'SpotiFLAC';
|
||||
String get appName => 'SpotiFLAC Mobile';
|
||||
|
||||
@override
|
||||
String get navHome => 'Home';
|
||||
@@ -10661,7 +10661,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String notifDownloadingUpdate(String version) {
|
||||
return 'Downloading SpotiFLAC v$version';
|
||||
return 'Downloading SpotiFLAC Mobile v$version';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -10674,7 +10674,7 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
|
||||
|
||||
@override
|
||||
String notifUpdateReadyBody(String version) {
|
||||
return 'SpotiFLAC v$version downloaded. Tap to install.';
|
||||
return 'SpotiFLAC Mobile v$version downloaded. Tap to install.';
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "de",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "en",
|
||||
"@@last_modified": "2026-04-28",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4564,7 +4564,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4592,7 +4592,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "es",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "es_ES",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "fr",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "hi",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFlac",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "id",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4481,7 +4481,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4509,7 +4509,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "ja",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "ko",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "nl",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "pt",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "pt_PT",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "ru",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "tr",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4477,7 +4477,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4505,7 +4505,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "uk",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Завантаження SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Завантаження SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} завантажений. Натисніть щоб установити.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} завантажений. Натисніть щоб установити.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "zh",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "zh_CN",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"@@locale": "zh_TW",
|
||||
"@@last_modified": "2026-01-16",
|
||||
"appName": "SpotiFLAC",
|
||||
"appName": "SpotiFLAC Mobile",
|
||||
"@appName": {
|
||||
"description": "App name - DO NOT TRANSLATE"
|
||||
},
|
||||
@@ -4473,7 +4473,7 @@
|
||||
"@notifLibraryScanStopped": {
|
||||
"description": "Notification body when library scan is cancelled"
|
||||
},
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC v{version}",
|
||||
"notifDownloadingUpdate": "Downloading SpotiFLAC Mobile v{version}",
|
||||
"@notifDownloadingUpdate": {
|
||||
"description": "Notification title while downloading an app update",
|
||||
"placeholders": {
|
||||
@@ -4501,7 +4501,7 @@
|
||||
"@notifUpdateReady": {
|
||||
"description": "Notification title when app update download is complete"
|
||||
},
|
||||
"notifUpdateReadyBody": "SpotiFLAC v{version} downloaded. Tap to install.",
|
||||
"notifUpdateReadyBody": "SpotiFLAC Mobile v{version} downloaded. Tap to install.",
|
||||
"@notifUpdateReadyBody": {
|
||||
"description": "Notification body when app update is ready to install",
|
||||
"placeholders": {
|
||||
|
||||
@@ -110,6 +110,7 @@ class ExploreState {
|
||||
final bool isLoading;
|
||||
final String? error;
|
||||
final String? greeting;
|
||||
final String? providerId;
|
||||
final List<ExploreSection> sections;
|
||||
final DateTime? lastFetched;
|
||||
|
||||
@@ -117,6 +118,7 @@ class ExploreState {
|
||||
this.isLoading = false,
|
||||
this.error,
|
||||
this.greeting,
|
||||
this.providerId,
|
||||
this.sections = const [],
|
||||
this.lastFetched,
|
||||
});
|
||||
@@ -127,6 +129,8 @@ class ExploreState {
|
||||
bool? isLoading,
|
||||
String? error,
|
||||
String? greeting,
|
||||
String? providerId,
|
||||
bool clearProviderId = false,
|
||||
List<ExploreSection>? sections,
|
||||
DateTime? lastFetched,
|
||||
}) {
|
||||
@@ -134,6 +138,7 @@ class ExploreState {
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
error: error,
|
||||
greeting: greeting ?? this.greeting,
|
||||
providerId: clearProviderId ? null : (providerId ?? this.providerId),
|
||||
sections: sections ?? this.sections,
|
||||
lastFetched: lastFetched ?? this.lastFetched,
|
||||
);
|
||||
@@ -189,14 +194,54 @@ List<Map<String, Object?>> _normalizeExploreSectionsPayload(
|
||||
return sections;
|
||||
}
|
||||
|
||||
List<Map<String, Object?>> _decodeExploreCacheSections(String rawCache) {
|
||||
final decoded = jsonDecode(rawCache);
|
||||
if (decoded is! Map) return const [];
|
||||
return _normalizeExploreSectionsPayload(decoded['sections']);
|
||||
List<Map<String, Object?>> _withDefaultExploreProviderId(
|
||||
List<Map<String, Object?>> normalizedSections,
|
||||
String providerId,
|
||||
) {
|
||||
final normalizedProviderId = providerId.trim();
|
||||
if (normalizedProviderId.isEmpty) return normalizedSections;
|
||||
|
||||
return normalizedSections
|
||||
.map((section) {
|
||||
final rawItems = section['items'];
|
||||
if (rawItems is! List) return section;
|
||||
|
||||
return <String, Object?>{
|
||||
...section,
|
||||
'items': rawItems
|
||||
.map((rawItem) {
|
||||
if (rawItem is! Map) return rawItem;
|
||||
final item = Map<String, Object?>.from(rawItem);
|
||||
final itemProviderId =
|
||||
item['provider_id']?.toString().trim() ?? '';
|
||||
if (itemProviderId.isEmpty) {
|
||||
item['provider_id'] = normalizedProviderId;
|
||||
}
|
||||
return item;
|
||||
})
|
||||
.toList(growable: false),
|
||||
};
|
||||
})
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
String _encodeExploreCacheSections(List<Map<String, Object?>> sections) {
|
||||
return jsonEncode({'sections': sections});
|
||||
Map<String, Object?> _decodeExploreCache(String rawCache) {
|
||||
final decoded = jsonDecode(rawCache);
|
||||
if (decoded is! Map) {
|
||||
return const {'provider_id': null, 'sections': <Map<String, Object?>>[]};
|
||||
}
|
||||
|
||||
final providerId = decoded['provider_id']?.toString().trim();
|
||||
var sections = _normalizeExploreSectionsPayload(decoded['sections']);
|
||||
if (providerId != null && providerId.isNotEmpty) {
|
||||
sections = _withDefaultExploreProviderId(sections, providerId);
|
||||
}
|
||||
|
||||
return {'provider_id': providerId, 'sections': sections};
|
||||
}
|
||||
|
||||
String _encodeExploreCache(Map<String, Object?> cachePayload) {
|
||||
return jsonEncode(cachePayload);
|
||||
}
|
||||
|
||||
List<ExploreSection> _buildExploreSectionsFromNormalizedPayload(
|
||||
@@ -234,10 +279,24 @@ class ExploreNotifier extends Notifier<ExploreState> {
|
||||
final cachedTs = prefs.getInt(_cacheTsKey);
|
||||
if (cached == null || cached.isEmpty) return;
|
||||
|
||||
final normalizedSections = await compute(
|
||||
_decodeExploreCacheSections,
|
||||
cached,
|
||||
);
|
||||
final cachePayload = await compute(_decodeExploreCache, cached);
|
||||
final providerId = cachePayload['provider_id']?.toString().trim();
|
||||
final rawSections = cachePayload['sections'];
|
||||
var normalizedSections = rawSections is List
|
||||
? rawSections
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((section) => Map<String, Object?>.from(section))
|
||||
.toList(growable: false)
|
||||
: const <Map<String, Object?>>[];
|
||||
final resolvedProviderId = providerId?.isNotEmpty == true
|
||||
? providerId
|
||||
: _resolveHomeFeedExtension()?.id;
|
||||
if (resolvedProviderId != null && resolvedProviderId.isNotEmpty) {
|
||||
normalizedSections = _withDefaultExploreProviderId(
|
||||
normalizedSections,
|
||||
resolvedProviderId,
|
||||
);
|
||||
}
|
||||
final sections = _buildExploreSectionsFromNormalizedPayload(
|
||||
normalizedSections,
|
||||
);
|
||||
@@ -251,23 +310,51 @@ class ExploreNotifier extends Notifier<ExploreState> {
|
||||
_log.i('Restored ${sections.length} cached explore sections');
|
||||
state = ExploreState(
|
||||
greeting: _getLocalGreeting(),
|
||||
providerId: resolvedProviderId,
|
||||
sections: sections,
|
||||
lastFetched: lastFetched,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.w('Failed to restore explore cache: $e');
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_cacheKey);
|
||||
await prefs.remove(_cacheTsKey);
|
||||
_log.d('Removed invalid explore cache');
|
||||
} catch (clearError) {
|
||||
_log.w('Failed to remove invalid explore cache: $clearError');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Extension? _resolveHomeFeedExtension() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final preferredId = settings.homeFeedProvider;
|
||||
final enabledHomeFeedExtensions = ref
|
||||
.read(extensionProvider)
|
||||
.extensions
|
||||
.where((extension) => extension.enabled && extension.hasHomeFeed)
|
||||
.toList(growable: false);
|
||||
|
||||
if (preferredId != null && preferredId.isNotEmpty) {
|
||||
return enabledHomeFeedExtensions
|
||||
.where((extension) => extension.id == preferredId)
|
||||
.firstOrNull;
|
||||
}
|
||||
|
||||
return enabledHomeFeedExtensions.firstOrNull;
|
||||
}
|
||||
|
||||
Future<void> _saveToCache(
|
||||
List<Map<String, Object?>> normalizedSections,
|
||||
String providerId,
|
||||
) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final encoded = await compute(
|
||||
_encodeExploreCacheSections,
|
||||
normalizedSections,
|
||||
);
|
||||
final encoded = await compute(_encodeExploreCache, {
|
||||
'provider_id': providerId,
|
||||
'sections': normalizedSections,
|
||||
});
|
||||
await prefs.setString(_cacheKey, encoded);
|
||||
await prefs.setInt(_cacheTsKey, DateTime.now().millisecondsSinceEpoch);
|
||||
_log.d('Saved ${normalizedSections.length} explore sections to cache');
|
||||
@@ -313,24 +400,7 @@ class ExploreNotifier extends Notifier<ExploreState> {
|
||||
'Extensions count: ${extState.extensions.length}, preferred home feed: $preferredId',
|
||||
);
|
||||
|
||||
Extension? targetExt;
|
||||
for (final extension in extState.extensions) {
|
||||
if (!extension.enabled || !extension.hasHomeFeed) {
|
||||
continue;
|
||||
}
|
||||
if (preferredId != null &&
|
||||
preferredId.isNotEmpty &&
|
||||
extension.id == preferredId) {
|
||||
targetExt = extension;
|
||||
break;
|
||||
}
|
||||
if (targetExt == null || extension.id == 'spotify-web') {
|
||||
targetExt = extension;
|
||||
if (preferredId == null && extension.id == 'spotify-web') {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
final targetExt = _resolveHomeFeedExtension();
|
||||
|
||||
if (targetExt == null) {
|
||||
_log.w('No extension with homeFeed capability found');
|
||||
@@ -367,10 +437,14 @@ class ExploreNotifier extends Notifier<ExploreState> {
|
||||
|
||||
final greeting = result['greeting'] as String?;
|
||||
final sectionsData = result['sections'] as List<dynamic>? ?? [];
|
||||
final normalizedSections = await compute(
|
||||
final normalizedSectionsWithoutProvider = await compute(
|
||||
_normalizeExploreSectionsPayload,
|
||||
sectionsData,
|
||||
);
|
||||
final normalizedSections = _withDefaultExploreProviderId(
|
||||
normalizedSectionsWithoutProvider,
|
||||
targetExt.id,
|
||||
);
|
||||
if (requestId != _homeFeedRequestId) return;
|
||||
final sections = _buildExploreSectionsFromNormalizedPayload(
|
||||
normalizedSections,
|
||||
@@ -391,11 +465,12 @@ class ExploreNotifier extends Notifier<ExploreState> {
|
||||
state = ExploreState(
|
||||
isLoading: false,
|
||||
greeting: localGreeting,
|
||||
providerId: targetExt.id,
|
||||
sections: sections,
|
||||
lastFetched: DateTime.now(),
|
||||
);
|
||||
|
||||
_saveToCache(normalizedSections);
|
||||
_saveToCache(normalizedSections, targetExt.id);
|
||||
} catch (e, stack) {
|
||||
_log.e('Error fetching home feed: $e', e, stack);
|
||||
if (requestId != _homeFeedRequestId) return;
|
||||
|
||||
@@ -24,6 +24,30 @@ bool _stringListEquals(List<String> a, List<String> b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
List<String>? _tryDecodeStringListPreference(String rawJson, String key) {
|
||||
try {
|
||||
final decoded = jsonDecode(rawJson);
|
||||
if (decoded is! List) {
|
||||
throw const FormatException('expected a JSON list');
|
||||
}
|
||||
|
||||
final values = <String>[];
|
||||
for (final item in decoded) {
|
||||
if (item is! String) {
|
||||
throw const FormatException('expected string entries');
|
||||
}
|
||||
final trimmed = item.trim();
|
||||
if (trimmed.isNotEmpty) {
|
||||
values.add(trimmed);
|
||||
}
|
||||
}
|
||||
return values;
|
||||
} catch (e) {
|
||||
_log.w('Ignoring invalid $key preference: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class BuiltInProviderSpec {
|
||||
final String id;
|
||||
final String displayName;
|
||||
@@ -1630,15 +1654,27 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
List<String> priority;
|
||||
if (savedJson != null) {
|
||||
final saved = jsonDecode(savedJson) as List<dynamic>;
|
||||
priority = saved.map((e) => e as String).toList();
|
||||
priority = _sanitizeDownloadProviderPriority(priority);
|
||||
_log.d('Loaded provider priority from prefs: $priority');
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(priority));
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
final saved = _tryDecodeStringListPreference(
|
||||
savedJson,
|
||||
_providerPriorityKey,
|
||||
);
|
||||
if (saved != null) {
|
||||
priority = _sanitizeDownloadProviderPriority(saved);
|
||||
_log.d('Loaded provider priority from prefs: $priority');
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(priority));
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
} else {
|
||||
await prefs.remove(_providerPriorityKey);
|
||||
priority = await PlatformBridge.getProviderPriority();
|
||||
priority = _sanitizeDownloadProviderPriority(priority);
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(priority));
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
_log.d('Recovered provider priority from defaults: $priority');
|
||||
}
|
||||
} else {
|
||||
priority = await PlatformBridge.getProviderPriority();
|
||||
priority = _sanitizeDownloadProviderPriority(priority);
|
||||
await prefs.setString(_providerPriorityKey, jsonEncode(priority));
|
||||
await PlatformBridge.setProviderPriority(priority);
|
||||
_log.d('Using default provider priority: $priority');
|
||||
}
|
||||
@@ -1691,18 +1727,34 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
List<String> priority;
|
||||
if (savedJson != null) {
|
||||
final saved = jsonDecode(savedJson) as List<dynamic>;
|
||||
priority = _sanitizeMetadataProviderPriority(
|
||||
_replaceRetiredBuiltInMetadataProviders(
|
||||
saved.map((e) => e as String).toList(),
|
||||
),
|
||||
);
|
||||
_log.d('Loaded metadata provider priority from prefs: $priority');
|
||||
await prefs.setString(
|
||||
final saved = _tryDecodeStringListPreference(
|
||||
savedJson,
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(priority),
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
if (saved != null) {
|
||||
priority = _sanitizeMetadataProviderPriority(
|
||||
_replaceRetiredBuiltInMetadataProviders(saved),
|
||||
);
|
||||
_log.d('Loaded metadata provider priority from prefs: $priority');
|
||||
await prefs.setString(
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(priority),
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
} else {
|
||||
await prefs.remove(_metadataProviderPriorityKey);
|
||||
final backendPriority =
|
||||
await PlatformBridge.getMetadataProviderPriority();
|
||||
priority = _sanitizeMetadataProviderPriority(backendPriority);
|
||||
await prefs.setString(
|
||||
_metadataProviderPriorityKey,
|
||||
jsonEncode(priority),
|
||||
);
|
||||
await PlatformBridge.setMetadataProviderPriority(priority);
|
||||
_log.d(
|
||||
'Recovered metadata provider priority from defaults: $priority',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
final backendPriority =
|
||||
await PlatformBridge.getMetadataProviderPriority();
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:spotiflac_android/utils/file_access.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
const _settingsKey = 'app_settings';
|
||||
const _settingsCorruptBackupKey = 'app_settings_corrupt_backup';
|
||||
const _migrationVersionKey = 'settings_migration_version';
|
||||
const _currentMigrationVersion = 11;
|
||||
const _spotifyClientSecretKey = 'spotify_client_secret';
|
||||
@@ -41,40 +42,56 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
||||
|
||||
Future<void> _loadSettings() async {
|
||||
final prefs = await _prefs;
|
||||
final json = prefs.getString(_settingsKey);
|
||||
if (json != null) {
|
||||
final loaded = AppSettings.fromJson(
|
||||
Map<String, dynamic>.from(jsonDecode(json) as Map),
|
||||
);
|
||||
final sanitizedDownloadFallbackExtensionIds =
|
||||
_sanitizeDownloadFallbackExtensionIds(
|
||||
loaded.downloadFallbackExtensionIds,
|
||||
);
|
||||
final sanitizedDefaultSearchTab = _normalizeDefaultSearchTab(
|
||||
loaded.defaultSearchTab,
|
||||
);
|
||||
final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.defaultService,
|
||||
);
|
||||
final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.searchProvider,
|
||||
);
|
||||
state = loaded.copyWith(
|
||||
useExtensionProviders: true,
|
||||
downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds,
|
||||
clearDownloadFallbackExtensionIds:
|
||||
loaded.downloadFallbackExtensionIds != null &&
|
||||
sanitizedDownloadFallbackExtensionIds == null,
|
||||
defaultSearchTab: sanitizedDefaultSearchTab,
|
||||
defaultService: sanitizedDefaultService ?? '',
|
||||
searchProvider: sanitizedSearchProvider,
|
||||
clearSearchProvider:
|
||||
loaded.searchProvider != null && sanitizedSearchProvider == null,
|
||||
);
|
||||
final rawSettings = prefs.getString(_settingsKey);
|
||||
if (rawSettings != null) {
|
||||
AppSettings? loaded;
|
||||
try {
|
||||
final decoded = jsonDecode(rawSettings);
|
||||
if (decoded is! Map) {
|
||||
throw const FormatException('settings root must be a JSON object');
|
||||
}
|
||||
loaded = AppSettings.fromJson(Map<String, dynamic>.from(decoded));
|
||||
} catch (e, stack) {
|
||||
_log.e('Failed to load settings, resetting to defaults: $e', e, stack);
|
||||
try {
|
||||
await prefs.setString(_settingsCorruptBackupKey, rawSettings);
|
||||
await prefs.remove(_settingsKey);
|
||||
} catch (backupError) {
|
||||
_log.w('Failed to backup corrupt settings: $backupError');
|
||||
}
|
||||
}
|
||||
|
||||
await _runMigrations(prefs);
|
||||
await _normalizeIosDownloadDirectoryIfNeeded();
|
||||
await _normalizeSongLinkRegionIfNeeded();
|
||||
if (loaded != null) {
|
||||
final sanitizedDownloadFallbackExtensionIds =
|
||||
_sanitizeDownloadFallbackExtensionIds(
|
||||
loaded.downloadFallbackExtensionIds,
|
||||
);
|
||||
final sanitizedDefaultSearchTab = _normalizeDefaultSearchTab(
|
||||
loaded.defaultSearchTab,
|
||||
);
|
||||
final sanitizedDefaultService = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.defaultService,
|
||||
);
|
||||
final sanitizedSearchProvider = _sanitizeRetiredBuiltInProviderId(
|
||||
loaded.searchProvider,
|
||||
);
|
||||
state = loaded.copyWith(
|
||||
useExtensionProviders: true,
|
||||
downloadFallbackExtensionIds: sanitizedDownloadFallbackExtensionIds,
|
||||
clearDownloadFallbackExtensionIds:
|
||||
loaded.downloadFallbackExtensionIds != null &&
|
||||
sanitizedDownloadFallbackExtensionIds == null,
|
||||
defaultSearchTab: sanitizedDefaultSearchTab,
|
||||
defaultService: sanitizedDefaultService ?? '',
|
||||
searchProvider: sanitizedSearchProvider,
|
||||
clearSearchProvider:
|
||||
loaded.searchProvider != null && sanitizedSearchProvider == null,
|
||||
);
|
||||
|
||||
await _runMigrations(prefs);
|
||||
await _normalizeIosDownloadDirectoryIfNeeded();
|
||||
await _normalizeSongLinkRegionIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
await _cleanupRetiredSpotifySettings();
|
||||
|
||||
@@ -449,7 +449,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
_preWarmCacheForTracks(tracks, service: providerId);
|
||||
return;
|
||||
case 'playlist':
|
||||
final playlistInfo = metadata['playlist_info'] as Map<String, dynamic>;
|
||||
@@ -469,7 +469,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
playlistName: playlistName,
|
||||
coverUrl: coverUrl,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
_preWarmCacheForTracks(tracks, service: providerId);
|
||||
return;
|
||||
case 'artist':
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
@@ -1054,7 +1054,7 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _preWarmCacheForTracks(List<Track> tracks) {
|
||||
void _preWarmCacheForTracks(List<Track> tracks, {String? service}) {
|
||||
if (tracks.isEmpty) return;
|
||||
final cacheRequests = <Map<String, String>>[];
|
||||
for (final track in tracks) {
|
||||
@@ -1062,12 +1062,16 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
if (isrc == null || isrc.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final effectiveService =
|
||||
(track.source?.trim().isNotEmpty == true ? track.source : service)
|
||||
?.trim();
|
||||
cacheRequests.add({
|
||||
'isrc': isrc,
|
||||
'track_name': track.name,
|
||||
'artist_name': track.artistName,
|
||||
'spotify_id': track.id,
|
||||
'service': 'tidal',
|
||||
if (effectiveService != null && effectiveService.isNotEmpty)
|
||||
'service': effectiveService,
|
||||
});
|
||||
if (cacheRequests.length >= _maxPreWarmTracksPerRequest) {
|
||||
break;
|
||||
|
||||
@@ -1896,14 +1896,38 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
}
|
||||
|
||||
String? _providerIdForExploreItem(ExploreItem item) {
|
||||
final itemProviderId = item.providerId?.trim();
|
||||
if (itemProviderId != null && itemProviderId.isNotEmpty) {
|
||||
return itemProviderId;
|
||||
}
|
||||
|
||||
final feedProviderId = ref.read(exploreProvider).providerId?.trim();
|
||||
if (feedProviderId != null && feedProviderId.isNotEmpty) {
|
||||
return feedProviderId;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
void _showMissingExploreProviderMessage() {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.extensionsNoHomeFeedExtensions)),
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToExploreItem(ExploreItem item) async {
|
||||
final extensionId = item.providerId ?? 'spotify-web';
|
||||
final extensionId = _providerIdForExploreItem(item);
|
||||
|
||||
switch (item.type) {
|
||||
case 'track':
|
||||
_showTrackBottomSheet(item);
|
||||
return;
|
||||
case 'album':
|
||||
if (extensionId == null) {
|
||||
_showMissingExploreProviderMessage();
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -1917,6 +1941,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
);
|
||||
return;
|
||||
case 'playlist':
|
||||
if (extensionId == null) {
|
||||
_showMissingExploreProviderMessage();
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -1930,6 +1958,10 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
);
|
||||
return;
|
||||
case 'artist':
|
||||
if (extensionId == null) {
|
||||
_showMissingExploreProviderMessage();
|
||||
return;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
@@ -2064,7 +2096,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
isrc: null,
|
||||
releaseDate: item.releaseDate,
|
||||
coverUrl: item.coverUrl,
|
||||
source: item.providerId ?? 'spotify-web',
|
||||
source: _providerIdForExploreItem(item),
|
||||
);
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
@@ -2105,11 +2137,17 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
Future<void> _navigateToTrackAlbum(ExploreItem item) async {
|
||||
if (item.albumId != null && item.albumId!.isNotEmpty) {
|
||||
final extensionId = _providerIdForExploreItem(item);
|
||||
if (extensionId == null) {
|
||||
_showMissingExploreProviderMessage();
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => ExtensionAlbumScreen(
|
||||
extensionId: item.providerId ?? 'spotify-web',
|
||||
extensionId: extensionId,
|
||||
albumId: item.albumId!,
|
||||
albumName: item.albumName ?? 'Album',
|
||||
coverUrl: item.coverUrl,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/l10n/app_localizations.dart';
|
||||
|
||||
class NotificationService {
|
||||
@@ -547,7 +548,7 @@ class NotificationService {
|
||||
id: updateDownloadId,
|
||||
title:
|
||||
_l10n?.notifDownloadingUpdate(version) ??
|
||||
'Downloading SpotiFLAC v$version',
|
||||
'Downloading ${AppInfo.appName} v$version',
|
||||
body:
|
||||
_l10n?.notifUpdateProgress(receivedMB, totalMB, percentage) ??
|
||||
'$receivedMB / $totalMB MB • $percentage%',
|
||||
@@ -585,7 +586,7 @@ class NotificationService {
|
||||
title: _l10n?.notifUpdateReady ?? 'Update Ready',
|
||||
body:
|
||||
_l10n?.notifUpdateReadyBody(version) ??
|
||||
'SpotiFLAC v$version downloaded. Tap to install.',
|
||||
'${AppInfo.appName} v$version downloaded. Tap to install.',
|
||||
details: details,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/screens/artist_screen.dart';
|
||||
import 'package:spotiflac_android/screens/album_screen.dart';
|
||||
import 'package:spotiflac_android/screens/home_tab.dart'
|
||||
@@ -12,20 +14,151 @@ import 'package:spotiflac_android/utils/artist_utils.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('ClickableMetadata');
|
||||
const _deezerExtensionId = 'deezer';
|
||||
|
||||
Future<List<Map<String, dynamic>>> _searchDeezerExtension(
|
||||
class _MetadataSearchResult {
|
||||
final String providerId;
|
||||
final List<Map<String, dynamic>> items;
|
||||
|
||||
const _MetadataSearchResult({required this.providerId, required this.items});
|
||||
}
|
||||
|
||||
Future<_MetadataSearchResult?> _searchMetadataProviders(
|
||||
BuildContext context,
|
||||
String query, {
|
||||
required String filter,
|
||||
int limit = 5,
|
||||
}) {
|
||||
String? sourceProviderId,
|
||||
}) async {
|
||||
final providerIds = _metadataSearchProviderCandidates(
|
||||
context,
|
||||
sourceProviderId: sourceProviderId,
|
||||
);
|
||||
|
||||
for (final providerId in providerIds) {
|
||||
try {
|
||||
final items = await _searchMetadataProvider(
|
||||
providerId,
|
||||
query,
|
||||
filter: filter,
|
||||
limit: limit,
|
||||
);
|
||||
if (items.isNotEmpty) {
|
||||
return _MetadataSearchResult(providerId: providerId, items: items);
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w(
|
||||
'Metadata lookup failed for provider "$providerId", filter=$filter: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> _searchMetadataProvider(
|
||||
String providerId,
|
||||
String query, {
|
||||
required String filter,
|
||||
required int limit,
|
||||
}) async {
|
||||
if (isBuiltInSearchProvider(providerId)) {
|
||||
final result = await PlatformBridge.searchProviderAll(
|
||||
providerId,
|
||||
query,
|
||||
trackLimit: 0,
|
||||
artistLimit: filter == 'artist' ? limit : 0,
|
||||
filter: filter,
|
||||
);
|
||||
return _extractSearchItems(result, filter);
|
||||
}
|
||||
|
||||
return PlatformBridge.customSearchWithExtension(
|
||||
_deezerExtensionId,
|
||||
providerId,
|
||||
query,
|
||||
options: {'filter': filter, 'limit': limit},
|
||||
);
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> _extractSearchItems(
|
||||
Map<String, dynamic> result,
|
||||
String filter,
|
||||
) {
|
||||
final key = switch (filter) {
|
||||
'artist' => 'artists',
|
||||
'album' => 'albums',
|
||||
_ => '${filter}s',
|
||||
};
|
||||
final items = result[key];
|
||||
if (items is! List) return const [];
|
||||
|
||||
return items
|
||||
.whereType<Map<Object?, Object?>>()
|
||||
.map((item) => Map<String, dynamic>.from(item))
|
||||
.toList(growable: false);
|
||||
}
|
||||
|
||||
List<String> _metadataSearchProviderCandidates(
|
||||
BuildContext context, {
|
||||
String? sourceProviderId,
|
||||
}) {
|
||||
final container = ProviderScope.containerOf(context, listen: false);
|
||||
final extensionState = container.read(extensionProvider);
|
||||
final settings = container.read(settingsProvider);
|
||||
final extensionNotifier = container.read(extensionProvider.notifier);
|
||||
final candidates = <String>[];
|
||||
|
||||
void addProvider(String? providerId) {
|
||||
final normalized = providerId?.trim();
|
||||
if (normalized == null ||
|
||||
normalized.isEmpty ||
|
||||
candidates.contains(normalized) ||
|
||||
!_canSearchMetadataProvider(normalized, extensionState)) {
|
||||
return;
|
||||
}
|
||||
candidates.add(normalized);
|
||||
}
|
||||
|
||||
addProvider(sourceProviderId);
|
||||
addProvider(settings.searchProvider);
|
||||
|
||||
for (final providerId in extensionState.metadataProviderPriority) {
|
||||
addProvider(providerId);
|
||||
}
|
||||
for (final providerId in extensionNotifier.getAllMetadataProviders()) {
|
||||
addProvider(providerId);
|
||||
}
|
||||
|
||||
final searchExtensions = extensionState.extensions
|
||||
.where((ext) => ext.enabled && ext.hasCustomSearch)
|
||||
.toList(growable: false);
|
||||
for (final extension in searchExtensions.where(
|
||||
(ext) => ext.searchBehavior?.primary == true,
|
||||
)) {
|
||||
addProvider(extension.id);
|
||||
}
|
||||
for (final extension in searchExtensions.where(
|
||||
(ext) => ext.searchBehavior?.primary != true,
|
||||
)) {
|
||||
addProvider(extension.id);
|
||||
}
|
||||
|
||||
for (final providerId in builtInSearchProviderIds) {
|
||||
addProvider(providerId);
|
||||
}
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
bool _canSearchMetadataProvider(
|
||||
String providerId,
|
||||
ExtensionState extensionState,
|
||||
) {
|
||||
if (isBuiltInSearchProvider(providerId)) return true;
|
||||
return extensionState.extensions.any(
|
||||
(ext) => ext.enabled && ext.hasCustomSearch && ext.id == providerId,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> navigateToArtist(
|
||||
BuildContext context, {
|
||||
required String artistName,
|
||||
@@ -54,14 +187,17 @@ Future<void> navigateToArtist(
|
||||
|
||||
_showLoadingSnackBar(context, context.l10n.clickableLookingUpArtist);
|
||||
try {
|
||||
final artistList = await _searchDeezerExtension(
|
||||
final searchResult = await _searchMetadataProviders(
|
||||
context,
|
||||
artistName,
|
||||
filter: 'artist',
|
||||
limit: 3,
|
||||
sourceProviderId: extensionId,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
final artistList = searchResult?.items ?? const <Map<String, dynamic>>[];
|
||||
if (artistList.isEmpty) {
|
||||
_showUnavailable(context, context.l10n.trackArtist);
|
||||
return;
|
||||
@@ -81,6 +217,10 @@ Future<void> navigateToArtist(
|
||||
final resolvedId = bestMatch['id'] as String? ?? '';
|
||||
final resolvedName = bestMatch['name'] as String? ?? artistName;
|
||||
final resolvedImage = bestMatch['images'] as String?;
|
||||
final resolvedProviderId = _resolveResultProviderId(
|
||||
bestMatch,
|
||||
searchResult?.providerId,
|
||||
);
|
||||
|
||||
if (resolvedId.isEmpty) {
|
||||
_showUnavailable(context, context.l10n.trackArtist);
|
||||
@@ -93,7 +233,7 @@ Future<void> navigateToArtist(
|
||||
artistId: resolvedId,
|
||||
artistName: resolvedName,
|
||||
coverUrl: resolvedImage ?? coverUrl,
|
||||
extensionId: _deezerExtensionId,
|
||||
extensionId: resolvedProviderId,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.e('Failed to look up artist "$artistName": $e', e);
|
||||
@@ -113,10 +253,7 @@ Future<void> navigateToAlbum(
|
||||
}) async {
|
||||
if (albumName.isEmpty) return;
|
||||
|
||||
if (albumId != null &&
|
||||
albumId.isNotEmpty &&
|
||||
albumId != 'unknown' &&
|
||||
albumId != 'deezer:unknown') {
|
||||
if (albumId != null && albumId.isNotEmpty && !_isUnknownResourceId(albumId)) {
|
||||
_pushAlbumScreen(
|
||||
context,
|
||||
albumId: albumId,
|
||||
@@ -127,25 +264,23 @@ Future<void> navigateToAlbum(
|
||||
return;
|
||||
}
|
||||
|
||||
if (extensionId != null) {
|
||||
_showUnavailable(context, 'Album');
|
||||
return;
|
||||
}
|
||||
|
||||
_showLoadingSnackBar(context, 'Looking up album...');
|
||||
try {
|
||||
final query = artistName != null && artistName.isNotEmpty
|
||||
? '$albumName $artistName'
|
||||
: albumName;
|
||||
|
||||
final albumList = await _searchDeezerExtension(
|
||||
final searchResult = await _searchMetadataProviders(
|
||||
context,
|
||||
query,
|
||||
filter: 'album',
|
||||
limit: 5,
|
||||
sourceProviderId: extensionId,
|
||||
);
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
|
||||
final albumList = searchResult?.items ?? const <Map<String, dynamic>>[];
|
||||
if (albumList.isEmpty) {
|
||||
_showUnavailable(context, 'Album');
|
||||
return;
|
||||
@@ -165,6 +300,10 @@ Future<void> navigateToAlbum(
|
||||
final resolvedId = bestMatch['id'] as String? ?? '';
|
||||
final resolvedName = bestMatch['name'] as String? ?? albumName;
|
||||
final resolvedImage = bestMatch['images'] as String?;
|
||||
final resolvedProviderId = _resolveResultProviderId(
|
||||
bestMatch,
|
||||
searchResult?.providerId,
|
||||
);
|
||||
|
||||
if (resolvedId.isEmpty) {
|
||||
_showUnavailable(context, 'Album');
|
||||
@@ -177,7 +316,7 @@ Future<void> navigateToAlbum(
|
||||
albumId: resolvedId,
|
||||
albumName: resolvedName,
|
||||
coverUrl: resolvedImage ?? coverUrl,
|
||||
extensionId: _deezerExtensionId,
|
||||
extensionId: resolvedProviderId,
|
||||
);
|
||||
} catch (e) {
|
||||
_log.e('Failed to look up album "$albumName": $e', e);
|
||||
@@ -194,11 +333,15 @@ void _pushArtistScreen(
|
||||
String? coverUrl,
|
||||
String? extensionId,
|
||||
}) {
|
||||
final isExtension =
|
||||
extensionId != null && !isBuiltInMetadataProvider(extensionId);
|
||||
final resolvedProviderId = extensionId;
|
||||
|
||||
_pushViaPreferredNavigator(
|
||||
context,
|
||||
(context) => extensionId != null
|
||||
(context) => isExtension && resolvedProviderId != null
|
||||
? ExtensionArtistScreen(
|
||||
extensionId: extensionId,
|
||||
extensionId: resolvedProviderId,
|
||||
artistId: artistId,
|
||||
artistName: artistName,
|
||||
coverUrl: coverUrl,
|
||||
@@ -207,6 +350,7 @@ void _pushArtistScreen(
|
||||
artistId: artistId,
|
||||
artistName: artistName,
|
||||
coverUrl: coverUrl,
|
||||
extensionId: resolvedProviderId,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -235,6 +379,7 @@ void _pushAlbumScreen(
|
||||
albumId: albumId,
|
||||
albumName: albumName,
|
||||
coverUrl: coverUrl,
|
||||
extensionId: resolvedExtensionId,
|
||||
tracks: const [],
|
||||
),
|
||||
);
|
||||
@@ -289,9 +434,7 @@ void _showLoadingSnackBar(BuildContext context, String message) {
|
||||
}
|
||||
|
||||
void _showUnavailable(BuildContext context, String type) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.clickableInformationUnavailable(type))),
|
||||
);
|
||||
}
|
||||
@@ -504,21 +647,49 @@ List<String> _parseArtistIds(String? rawArtistIds) {
|
||||
|
||||
String? _normalizeArtistId(String? artistId) {
|
||||
final id = artistId?.trim();
|
||||
if (id == null || id.isEmpty || id == 'unknown' || id == 'deezer:unknown') {
|
||||
if (id == null || _isUnknownResourceId(id)) {
|
||||
return null;
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
bool _isUnknownResourceId(String id) {
|
||||
final normalized = id.trim().toLowerCase();
|
||||
return normalized.isEmpty ||
|
||||
normalized == 'unknown' ||
|
||||
normalized.endsWith(':unknown');
|
||||
}
|
||||
|
||||
String? _resolveResultProviderId(
|
||||
Map<String, dynamic> result,
|
||||
String? fallbackProviderId,
|
||||
) {
|
||||
final providerId = result['provider_id']?.toString().trim();
|
||||
if (providerId != null && providerId.isNotEmpty) return providerId;
|
||||
final source = result['source']?.toString().trim();
|
||||
if (source != null && source.isNotEmpty) return source;
|
||||
final fallback = fallbackProviderId?.trim();
|
||||
return fallback != null && fallback.isNotEmpty ? fallback : null;
|
||||
}
|
||||
|
||||
bool _canNavigateArtistDirectly({
|
||||
required String artistId,
|
||||
required String? extensionId,
|
||||
}) {
|
||||
if (extensionId != null) return true;
|
||||
if (artistId.startsWith('deezer:')) return true;
|
||||
final providerPrefix = _resourceProviderPrefix(artistId);
|
||||
if (providerPrefix != null && isBuiltInMetadataProvider(providerPrefix)) {
|
||||
return true;
|
||||
}
|
||||
return _spotifyArtistIdPattern.hasMatch(artistId);
|
||||
}
|
||||
|
||||
String? _resourceProviderPrefix(String resourceId) {
|
||||
final colonIndex = resourceId.indexOf(':');
|
||||
if (colonIndex <= 0) return null;
|
||||
return resourceId.substring(0, colonIndex).trim();
|
||||
}
|
||||
|
||||
final RegExp _spotifyArtistIdPattern = RegExp(r'^[A-Za-z0-9]{22}$');
|
||||
|
||||
class ClickableAlbumName extends StatelessWidget {
|
||||
|
||||
Reference in New Issue
Block a user