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