mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-20 15:15:33 +02:00
refactor: route spotify URLs through extensions
This commit is contained in:
@@ -1941,13 +1941,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"parseSpotifyUrl" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.parseSpotifyURL(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkAvailability" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
@@ -2711,13 +2704,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getSpotifyMetadataWithFallback" -> {
|
||||
val url = call.argument<String>("url") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getSpotifyMetadataWithDeezerFallback(url)
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkAvailabilityFromDeezerID" -> {
|
||||
val deezerTrackId = call.argument<String>("deezer_track_id") ?: ""
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -13,25 +13,6 @@ import (
|
||||
"github.com/dop251/goja"
|
||||
)
|
||||
|
||||
func ParseSpotifyURL(url string) (string, error) {
|
||||
parsed, err := parseSpotifyURI(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
result := map[string]string{
|
||||
"type": parsed.Type,
|
||||
"id": parsed.ID,
|
||||
}
|
||||
|
||||
jsonBytes, err := json.Marshal(result)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
func CheckAvailability(spotifyID, isrc string) (string, error) {
|
||||
client := NewSongLinkClient()
|
||||
availability, err := client.CheckTrackAvailability(spotifyID, isrc)
|
||||
@@ -1526,72 +1507,6 @@ func ConvertSpotifyToDeezer(resourceType, spotifyID string) (string, error) {
|
||||
return "", fmt.Errorf("Spotify to Deezer conversion only supported for tracks and albums. Please search by name for %s", resourceType)
|
||||
}
|
||||
|
||||
func GetSpotifyMetadataWithDeezerFallback(spotifyURL string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
spotFetchData, apiErr := GetSpotifyDataWithAPI(ctx, spotifyURL, DefaultSpotFetchAPIBaseURL)
|
||||
if apiErr == nil {
|
||||
GoLog("[Fallback] Spotify metadata fetched via SpotFetch API\n")
|
||||
jsonBytes, err := json.Marshal(spotFetchData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
GoLog("[Fallback] SpotFetch API fallback failed: %v\n", apiErr)
|
||||
|
||||
parsed, parseErr := parseSpotifyURI(spotifyURL)
|
||||
if parseErr != nil {
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v) and URL parsing failed: %w", apiErr, parseErr)
|
||||
}
|
||||
|
||||
GoLog("[Fallback] Trying Deezer conversion fallback for %s...\n", parsed.Type)
|
||||
|
||||
if parsed.Type == "track" || parsed.Type == "album" {
|
||||
return ConvertSpotifyToDeezer(parsed.Type, parsed.ID)
|
||||
}
|
||||
|
||||
if parsed.Type == "artist" {
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v). Artist pages now require SpotFetch or a metadata extension such as spotify-web", apiErr)
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("SpotFetch fallback failed (%v), and Deezer conversion is unavailable for playlists", apiErr)
|
||||
}
|
||||
|
||||
func shouldTrySpotFetchFallback(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if errors.Is(err, ErrNoSpotifyCredentials) {
|
||||
return true
|
||||
}
|
||||
|
||||
errStr := strings.ToLower(err.Error())
|
||||
indicators := []string{
|
||||
"429",
|
||||
"rate",
|
||||
"limit",
|
||||
"403",
|
||||
"forbidden",
|
||||
"401",
|
||||
"unauthorized",
|
||||
"timeout",
|
||||
"connection",
|
||||
"spotify error",
|
||||
"access token",
|
||||
"client token",
|
||||
"eof",
|
||||
}
|
||||
|
||||
for _, indicator := range indicators {
|
||||
if strings.Contains(errStr, indicator) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func CheckAvailabilityFromDeezerID(deezerTrackID string) (string, error) {
|
||||
client := NewSongLinkClient()
|
||||
availability, err := client.CheckAvailabilityFromDeezer(deezerTrackID)
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package gobackend
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const DefaultSpotFetchAPIBaseURL = "https://sp.afkarxyz.qzz.io/api"
|
||||
|
||||
// GetSpotifyDataWithAPI fetches Spotify metadata through SpotFetch-compatible API.
|
||||
// This is used as a fallback when direct Spotify API access is blocked/limited.
|
||||
func GetSpotifyDataWithAPI(ctx context.Context, spotifyURL, apiBaseURL string) (interface{}, error) {
|
||||
parsed, err := parseSpotifyURI(spotifyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid Spotify URL: %w", err)
|
||||
}
|
||||
|
||||
base := strings.TrimSpace(apiBaseURL)
|
||||
if base == "" {
|
||||
base = DefaultSpotFetchAPIBaseURL
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("%s/%s/%s", strings.TrimSuffix(base, "/"), parsed.Type, parsed.ID)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", endpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create SpotFetch API request: %w", err)
|
||||
}
|
||||
req.Header.Set("User-Agent", getRandomUserAgent())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := NewHTTPClientWithTimeout(30 * time.Second)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("SpotFetch API request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("SpotFetch API error: HTTP %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read SpotFetch API response: %w", err)
|
||||
}
|
||||
|
||||
switch parsed.Type {
|
||||
case "track":
|
||||
var trackResp TrackResponse
|
||||
if err := json.Unmarshal(bodyBytes, &trackResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode track response: %w", err)
|
||||
}
|
||||
return trackResp, nil
|
||||
case "album":
|
||||
var albumResp AlbumResponsePayload
|
||||
if err := json.Unmarshal(bodyBytes, &albumResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode album response: %w", err)
|
||||
}
|
||||
return &albumResp, nil
|
||||
case "playlist":
|
||||
var playlistResp PlaylistResponsePayload
|
||||
if err := json.Unmarshal(bodyBytes, &playlistResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode playlist response: %w", err)
|
||||
}
|
||||
return playlistResp, nil
|
||||
case "artist":
|
||||
var artistResp ArtistResponsePayload
|
||||
if err := json.Unmarshal(bodyBytes, &artistResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode artist response: %w", err)
|
||||
}
|
||||
return &artistResp, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported Spotify type: %s", parsed.Type)
|
||||
}
|
||||
}
|
||||
@@ -153,13 +153,6 @@ import Gobackend // Import Go framework
|
||||
var error: NSError?
|
||||
|
||||
switch call.method {
|
||||
case "parseSpotifyUrl":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let url = args["url"] as! String
|
||||
let response = GobackendParseSpotifyURL(url, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "checkAvailability":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let spotifyId = args["spotify_id"] as! String
|
||||
@@ -469,13 +462,6 @@ import Gobackend // Import Go framework
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "getSpotifyMetadataWithFallback":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let url = args["url"] as! String
|
||||
let response = GobackendGetSpotifyMetadataWithDeezerFallback(url, &error)
|
||||
if let error = error { throw error }
|
||||
return response
|
||||
|
||||
case "checkAvailabilityFromDeezerID":
|
||||
let args = call.arguments as! [String: Any]
|
||||
let deezerTrackId = args["deezer_track_id"] as! String
|
||||
|
||||
@@ -151,7 +151,7 @@ abstract class AppLocalizations {
|
||||
/// Bottom navigation - Extension store tab
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Store'**
|
||||
/// **'Repo'**
|
||||
String get navStore;
|
||||
|
||||
/// Home screen title
|
||||
@@ -163,7 +163,7 @@ abstract class AppLocalizations {
|
||||
/// Subtitle shown below search box
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Paste a Spotify link or search by name'**
|
||||
/// **'Paste a supported URL or search by name'**
|
||||
String get homeSubtitle;
|
||||
|
||||
/// Info text about supported URL types
|
||||
@@ -427,13 +427,13 @@ abstract class AppLocalizations {
|
||||
/// Show/hide store tab
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Extension Store'**
|
||||
/// **'Extension Repo'**
|
||||
String get optionsExtensionStore;
|
||||
|
||||
/// Subtitle for extension store toggle
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Show Store tab in navigation'**
|
||||
/// **'Show Repo tab in navigation'**
|
||||
String get optionsExtensionStoreSubtitle;
|
||||
|
||||
/// Auto update check toggle
|
||||
@@ -565,7 +565,7 @@ abstract class AppLocalizations {
|
||||
/// Store screen title
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Extension Store'**
|
||||
/// **'Extension Repo'**
|
||||
String get storeTitle;
|
||||
|
||||
/// Store search placeholder
|
||||
@@ -2365,7 +2365,7 @@ abstract class AppLocalizations {
|
||||
/// Error heading when the store cannot be loaded
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Failed to load store'**
|
||||
/// **'Failed to load repository'**
|
||||
String get storeLoadError;
|
||||
|
||||
/// Message when store has no extensions
|
||||
@@ -3613,7 +3613,7 @@ abstract class AppLocalizations {
|
||||
/// Tutorial extensions tip 1
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Browse the Store tab to discover useful extensions'**
|
||||
/// **'Browse the Repo tab to discover useful extensions'**
|
||||
String get tutorialExtensionsTip1;
|
||||
|
||||
/// Tutorial extensions tip 2
|
||||
|
||||
@@ -1281,7 +1281,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -21,13 +21,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get navSettings => 'Settings';
|
||||
|
||||
@override
|
||||
String get navStore => 'Store';
|
||||
String get navStore => 'Repo';
|
||||
|
||||
@override
|
||||
String get homeTitle => 'Home';
|
||||
|
||||
@override
|
||||
String get homeSubtitle => 'Paste a Spotify link or search by name';
|
||||
String get homeSubtitle => 'Paste a supported URL or search by name';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Supports: Track, Album, Playlist, Artist URLs';
|
||||
@@ -170,10 +170,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'Parallel downloads may trigger rate limiting';
|
||||
|
||||
@override
|
||||
String get optionsExtensionStore => 'Extension Store';
|
||||
String get optionsExtensionStore => 'Extension Repo';
|
||||
|
||||
@override
|
||||
String get optionsExtensionStoreSubtitle => 'Show Store tab in navigation';
|
||||
String get optionsExtensionStoreSubtitle => 'Show Repo tab in navigation';
|
||||
|
||||
@override
|
||||
String get optionsCheckUpdates => 'Check for Updates';
|
||||
@@ -250,7 +250,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get extensionsUninstall => 'Uninstall';
|
||||
|
||||
@override
|
||||
String get storeTitle => 'Extension Store';
|
||||
String get storeTitle => 'Extension Repo';
|
||||
|
||||
@override
|
||||
String get storeSearch => 'Search extensions...';
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
@@ -1997,7 +1997,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
'Browse the Repo tab to discover useful extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
@@ -1997,7 +1997,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
'Browse the Repo tab to discover useful extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
|
||||
@@ -1263,7 +1263,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -21,13 +21,14 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get navSettings => 'Pengaturan';
|
||||
|
||||
@override
|
||||
String get navStore => 'Toko';
|
||||
String get navStore => 'Repo';
|
||||
|
||||
@override
|
||||
String get homeTitle => 'Beranda';
|
||||
|
||||
@override
|
||||
String get homeSubtitle => 'Tempel link Spotify atau cari berdasarkan nama';
|
||||
String get homeSubtitle =>
|
||||
'Tempel URL yang didukung atau cari berdasarkan nama';
|
||||
|
||||
@override
|
||||
String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis';
|
||||
@@ -173,10 +174,10 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
'Unduhan paralel dapat memicu pembatasan rate';
|
||||
|
||||
@override
|
||||
String get optionsExtensionStore => 'Toko Ekstensi';
|
||||
String get optionsExtensionStore => 'Repo Ekstensi';
|
||||
|
||||
@override
|
||||
String get optionsExtensionStoreSubtitle => 'Tampilkan tab Toko di navigasi';
|
||||
String get optionsExtensionStoreSubtitle => 'Tampilkan tab Repo di navigasi';
|
||||
|
||||
@override
|
||||
String get optionsCheckUpdates => 'Periksa Pembaruan';
|
||||
@@ -252,7 +253,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get extensionsUninstall => 'Copot';
|
||||
|
||||
@override
|
||||
String get storeTitle => 'Toko Ekstensi';
|
||||
String get storeTitle => 'Repo Ekstensi';
|
||||
|
||||
@override
|
||||
String get storeSearch => 'Cari ekstensi...';
|
||||
@@ -1267,7 +1268,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Gagal memuat repo';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
@@ -2006,7 +2007,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
'Buka tab Repo untuk menemukan ekstensi yang berguna';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
|
||||
@@ -1255,7 +1255,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1241,7 +1241,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
@@ -1997,7 +1997,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
'Browse the Repo tab to discover useful extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
|
||||
@@ -1282,7 +1282,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1267,7 +1267,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
|
||||
@@ -1261,7 +1261,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get storeNewRepoUrlLabel => 'New Repository URL';
|
||||
|
||||
@override
|
||||
String get storeLoadError => 'Failed to load store';
|
||||
String get storeLoadError => 'Failed to load repository';
|
||||
|
||||
@override
|
||||
String get storeEmptyNoExtensions => 'No extensions available';
|
||||
@@ -1997,7 +1997,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip1 =>
|
||||
'Browse the Store tab to discover useful extensions';
|
||||
'Browse the Repo tab to discover useful extensions';
|
||||
|
||||
@override
|
||||
String get tutorialExtensionsTip2 =>
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"@navSettings": {
|
||||
"description": "Bottom navigation - Settings tab"
|
||||
},
|
||||
"navStore": "Store",
|
||||
"navStore": "Repo",
|
||||
"@navStore": {
|
||||
"description": "Bottom navigation - Extension store tab"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homeTitle": {
|
||||
"description": "Home screen title"
|
||||
},
|
||||
"homeSubtitle": "Paste a Spotify link or search by name",
|
||||
"homeSubtitle": "Paste a supported URL or search by name",
|
||||
"@homeSubtitle": {
|
||||
"description": "Subtitle shown below search box"
|
||||
},
|
||||
@@ -211,11 +211,11 @@
|
||||
"@optionsConcurrentWarning": {
|
||||
"description": "Warning about rate limits"
|
||||
},
|
||||
"optionsExtensionStore": "Extension Store",
|
||||
"optionsExtensionStore": "Extension Repo",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
},
|
||||
"optionsExtensionStoreSubtitle": "Show Store tab in navigation",
|
||||
"optionsExtensionStoreSubtitle": "Show Repo tab in navigation",
|
||||
"@optionsExtensionStoreSubtitle": {
|
||||
"description": "Subtitle for extension store toggle"
|
||||
},
|
||||
@@ -318,7 +318,7 @@
|
||||
"@extensionsUninstall": {
|
||||
"description": "Uninstall extension button"
|
||||
},
|
||||
"storeTitle": "Extension Store",
|
||||
"storeTitle": "Extension Repo",
|
||||
"@storeTitle": {
|
||||
"description": "Store screen title"
|
||||
},
|
||||
@@ -1654,7 +1654,7 @@
|
||||
"@storeNewRepoUrlLabel": {
|
||||
"description": "Label for the new repository URL field inside the dialog"
|
||||
},
|
||||
"storeLoadError": "Failed to load store",
|
||||
"storeLoadError": "Failed to load repository",
|
||||
"@storeLoadError": {
|
||||
"description": "Error heading when the store cannot be loaded"
|
||||
},
|
||||
@@ -2611,7 +2611,7 @@
|
||||
"@tutorialExtensionsDesc": {
|
||||
"description": "Tutorial extensions page description"
|
||||
},
|
||||
"tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions",
|
||||
"tutorialExtensionsTip1": "Browse the Repo tab to discover useful extensions",
|
||||
"@tutorialExtensionsTip1": {
|
||||
"description": "Tutorial extensions tip 1"
|
||||
},
|
||||
|
||||
+10
-6
@@ -17,7 +17,7 @@
|
||||
"@navSettings": {
|
||||
"description": "Bottom navigation - Settings tab"
|
||||
},
|
||||
"navStore": "Toko",
|
||||
"navStore": "Repo",
|
||||
"@navStore": {
|
||||
"description": "Bottom navigation - Extension store tab"
|
||||
},
|
||||
@@ -25,7 +25,7 @@
|
||||
"@homeTitle": {
|
||||
"description": "Home screen title"
|
||||
},
|
||||
"homeSubtitle": "Tempel link Spotify atau cari berdasarkan nama",
|
||||
"homeSubtitle": "Tempel URL yang didukung atau cari berdasarkan nama",
|
||||
"@homeSubtitle": {
|
||||
"description": "Subtitle shown below search box"
|
||||
},
|
||||
@@ -211,11 +211,11 @@
|
||||
"@optionsConcurrentWarning": {
|
||||
"description": "Warning about rate limits"
|
||||
},
|
||||
"optionsExtensionStore": "Toko Ekstensi",
|
||||
"optionsExtensionStore": "Repo Ekstensi",
|
||||
"@optionsExtensionStore": {
|
||||
"description": "Show/hide store tab"
|
||||
},
|
||||
"optionsExtensionStoreSubtitle": "Tampilkan tab Toko di navigasi",
|
||||
"optionsExtensionStoreSubtitle": "Tampilkan tab Repo di navigasi",
|
||||
"@optionsExtensionStoreSubtitle": {
|
||||
"description": "Subtitle for extension store toggle"
|
||||
},
|
||||
@@ -318,10 +318,14 @@
|
||||
"@extensionsUninstall": {
|
||||
"description": "Uninstall extension button"
|
||||
},
|
||||
"storeTitle": "Toko Ekstensi",
|
||||
"storeTitle": "Repo Ekstensi",
|
||||
"@storeTitle": {
|
||||
"description": "Store screen title"
|
||||
},
|
||||
"storeLoadError": "Gagal memuat repo",
|
||||
"@storeLoadError": {
|
||||
"description": "Error heading when the store cannot be loaded"
|
||||
},
|
||||
"storeSearch": "Cari ekstensi...",
|
||||
"@storeSearch": {
|
||||
"description": "Store search placeholder"
|
||||
@@ -2459,7 +2463,7 @@
|
||||
"@tutorialExtensionsDesc": {
|
||||
"description": "Tutorial extensions page description"
|
||||
},
|
||||
"tutorialExtensionsTip1": "Browse the Store tab to discover useful extensions",
|
||||
"tutorialExtensionsTip1": "Buka tab Repo untuk menemukan ekstensi yang berguna",
|
||||
"@tutorialExtensionsTip1": {
|
||||
"description": "Tutorial extensions tip 1"
|
||||
},
|
||||
|
||||
@@ -538,90 +538,11 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
return;
|
||||
}
|
||||
|
||||
final isSpotifyUrl =
|
||||
url.contains('open.spotify.com') ||
|
||||
url.contains('spotify.link') ||
|
||||
url.startsWith('spotify:');
|
||||
if (!isSpotifyUrl) {
|
||||
state = TrackState(
|
||||
isLoading: false,
|
||||
error: 'url_not_recognized',
|
||||
hasSearchText: state.hasSearchText,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final parsed = await PlatformBridge.parseSpotifyUrl(url);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
final type = parsed['type'] as String;
|
||||
|
||||
Map<String, dynamic> metadata;
|
||||
|
||||
try {
|
||||
metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
|
||||
if (type == 'track') {
|
||||
final trackData = metadata['track'] as Map<String, dynamic>;
|
||||
final track = _parseTrack(trackData);
|
||||
state = TrackState(
|
||||
tracks: [track],
|
||||
isLoading: false,
|
||||
coverUrl: track.coverUrl,
|
||||
);
|
||||
} else if (type == 'album') {
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
albumId: parsed['id'] as String?,
|
||||
albumName: albumInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(albumInfo['images']?.toString()),
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'playlist') {
|
||||
final playlistInfo = metadata['playlist_info'] as Map<String, dynamic>;
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
final owner = playlistInfo['owner'] as Map<String, dynamic>?;
|
||||
final playlistName =
|
||||
(playlistInfo['name'] ?? owner?['name']) as String?;
|
||||
final coverUrl = normalizeRemoteHttpUrl(
|
||||
(playlistInfo['images'] ?? owner?['images'])?.toString(),
|
||||
);
|
||||
state = TrackState(
|
||||
tracks: tracks,
|
||||
isLoading: false,
|
||||
playlistName: playlistName,
|
||||
coverUrl: coverUrl,
|
||||
);
|
||||
_preWarmCacheForTracks(tracks);
|
||||
} else if (type == 'artist') {
|
||||
final artistInfo = metadata['artist_info'] as Map<String, dynamic>;
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
final albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
state = TrackState(
|
||||
tracks: [],
|
||||
isLoading: false,
|
||||
artistId: artistInfo['id'] as String?,
|
||||
artistName: artistInfo['name'] as String?,
|
||||
coverUrl: normalizeRemoteHttpUrl(artistInfo['images']?.toString()),
|
||||
artistAlbums: albums,
|
||||
);
|
||||
}
|
||||
state = TrackState(
|
||||
isLoading: false,
|
||||
error: 'url_not_recognized',
|
||||
hasSearchText: state.hasSearchText,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
state = TrackState(
|
||||
|
||||
@@ -174,42 +174,107 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
Future<void> _fetchTracks() async {
|
||||
setState(() => _isLoading = true);
|
||||
try {
|
||||
Map<String, dynamic> metadata;
|
||||
|
||||
if (widget.albumId.startsWith('deezer:')) {
|
||||
final deezerAlbumId = widget.albumId.replaceFirst('deezer:', '');
|
||||
metadata = await PlatformBridge.getDeezerMetadata(
|
||||
final metadata = await PlatformBridge.getDeezerMetadata(
|
||||
'album',
|
||||
deezerAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else if (widget.albumId.startsWith('qobuz:')) {
|
||||
final qobuzAlbumId = widget.albumId.replaceFirst('qobuz:', '');
|
||||
metadata = await PlatformBridge.getQobuzMetadata('album', qobuzAlbumId);
|
||||
final metadata = await PlatformBridge.getQobuzMetadata(
|
||||
'album',
|
||||
qobuzAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else if (widget.albumId.startsWith('tidal:')) {
|
||||
final tidalAlbumId = widget.albumId.replaceFirst('tidal:', '');
|
||||
metadata = await PlatformBridge.getTidalMetadata('album', tidalAlbumId);
|
||||
final metadata = await PlatformBridge.getTidalMetadata(
|
||||
'album',
|
||||
tidalAlbumId,
|
||||
);
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
} else {
|
||||
final url = 'https://open.spotify.com/album/${widget.albumId}';
|
||||
metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
|
||||
}
|
||||
final result = await PlatformBridge.handleURLWithExtension(url);
|
||||
if (result == null || result['tracks'] == null) {
|
||||
throw StateError('Failed to load album metadata from extension');
|
||||
}
|
||||
|
||||
final trackList = metadata['track_list'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
final trackList = result['tracks'] as List<dynamic>;
|
||||
final tracks = trackList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
final albumInfo = metadata['album_info'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
final albumInfo = result['album'] as Map<String, dynamic>?;
|
||||
final artistId = (albumInfo?['artist_id'] ?? albumInfo?['artistId'])
|
||||
?.toString();
|
||||
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
_AlbumCache.set(widget.albumId, tracks);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_isLoading = false;
|
||||
});
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_tracks = tracks;
|
||||
_artistId = artistId;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
|
||||
@@ -343,13 +343,7 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
headerImage = artistData['header_image'] as String?;
|
||||
listeners = artistData['listeners'] as int?;
|
||||
} else {
|
||||
final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(
|
||||
url,
|
||||
);
|
||||
final albumsList = metadata['albums'] as List<dynamic>;
|
||||
albums = albumsList
|
||||
.map((a) => _parseArtistAlbum(a as Map<String, dynamic>))
|
||||
.toList();
|
||||
throw StateError('Failed to load artist metadata from extension');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,15 +1099,6 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>, album: album))
|
||||
.toList();
|
||||
}
|
||||
|
||||
// Fallback to direct Spotify metadata
|
||||
final metadata = await PlatformBridge.getSpotifyMetadataWithFallback(url);
|
||||
if (metadata['tracks'] != null) {
|
||||
final tracksList = metadata['tracks'] as List<dynamic>;
|
||||
return tracksList
|
||||
.map((t) => _parseTrack(t as Map<String, dynamic>, album: album))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -2313,7 +2313,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
error.contains('429') ||
|
||||
error.toLowerCase().contains('rate limit') ||
|
||||
error.toLowerCase().contains('too many requests');
|
||||
|
||||
final isUrlNotRecognized = error == 'url_not_recognized';
|
||||
|
||||
if (isRateLimit) {
|
||||
@@ -3087,7 +3086,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final extState = ref.read(extensionProvider);
|
||||
|
||||
if (!extState.isInitialized) {
|
||||
return 'Paste Spotify URL or search...';
|
||||
return 'Paste supported URL or search...';
|
||||
}
|
||||
|
||||
if (searchProvider != null && searchProvider.isNotEmpty) {
|
||||
@@ -3108,7 +3107,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
return 'Search with ${ext.displayName}...';
|
||||
}
|
||||
}
|
||||
return 'Paste Spotify URL or search...';
|
||||
return 'Paste supported URL or search...';
|
||||
}
|
||||
|
||||
Widget _buildSearchFilterBar(
|
||||
|
||||
+21
-21
@@ -12,7 +12,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/store_provider.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
import 'package:spotiflac_android/screens/home_tab.dart';
|
||||
import 'package:spotiflac_android/screens/store_tab.dart';
|
||||
import 'package:spotiflac_android/screens/repo_tab.dart';
|
||||
import 'package:spotiflac_android/screens/queue_tab.dart';
|
||||
import 'package:spotiflac_android/screens/settings/settings_tab.dart';
|
||||
import 'package:spotiflac_android/services/platform_bridge.dart';
|
||||
@@ -44,8 +44,8 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
ShellNavigationService.homeTabNavigatorKey;
|
||||
final GlobalKey<NavigatorState> _libraryTabNavigatorKey =
|
||||
ShellNavigationService.libraryTabNavigatorKey;
|
||||
final GlobalKey<NavigatorState> _storeTabNavigatorKey =
|
||||
ShellNavigationService.storeTabNavigatorKey;
|
||||
final GlobalKey<NavigatorState> _repoTabNavigatorKey =
|
||||
ShellNavigationService.repoTabNavigatorKey;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -58,7 +58,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
);
|
||||
ShellNavigationService.syncState(
|
||||
currentTabIndex: _currentIndex,
|
||||
showStoreTab: false,
|
||||
showRepoTab: false,
|
||||
);
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_checkForUpdates();
|
||||
@@ -268,7 +268,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
);
|
||||
ShellNavigationService.syncState(
|
||||
currentTabIndex: _currentIndex,
|
||||
showStoreTab: showStore,
|
||||
showRepoTab: showStore,
|
||||
);
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
// Jump directly when skipping intermediate tabs to avoid
|
||||
@@ -295,7 +295,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
);
|
||||
ShellNavigationService.syncState(
|
||||
currentTabIndex: _currentIndex,
|
||||
showStoreTab: showStore,
|
||||
showRepoTab: showStore,
|
||||
);
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
}
|
||||
@@ -414,7 +414,7 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
NavigatorState? _navigatorForTab(int index, bool showStore) {
|
||||
if (index == 0) return _homeTabNavigatorKey.currentState;
|
||||
if (index == 1) return _libraryTabNavigatorKey.currentState;
|
||||
if (showStore && index == 2) return _storeTabNavigatorKey.currentState;
|
||||
if (showStore && index == 2) return _repoTabNavigatorKey.currentState;
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -428,9 +428,9 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
);
|
||||
ShellNavigationService.syncState(
|
||||
currentTabIndex: _currentIndex,
|
||||
showStoreTab: showStore,
|
||||
showRepoTab: showStore,
|
||||
);
|
||||
final storeUpdatesCount = ref.watch(
|
||||
final repoUpdatesCount = ref.watch(
|
||||
storeProvider.select((s) => s.updatesAvailableCount),
|
||||
);
|
||||
|
||||
@@ -447,9 +447,9 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
),
|
||||
if (showStore)
|
||||
_TabNavigator(
|
||||
key: const ValueKey('tab-store'),
|
||||
navigatorKey: _storeTabNavigatorKey,
|
||||
child: const StoreTab(),
|
||||
key: const ValueKey('tab-repo'),
|
||||
navigatorKey: _repoTabNavigatorKey,
|
||||
child: const RepoTab(),
|
||||
),
|
||||
const SettingsTab(),
|
||||
];
|
||||
@@ -485,20 +485,20 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
if (showStore)
|
||||
NavigationDestination(
|
||||
icon: AnimatedBadge(
|
||||
count: storeUpdatesCount,
|
||||
count: repoUpdatesCount,
|
||||
child: Badge(
|
||||
isLabelVisible: storeUpdatesCount > 0,
|
||||
label: Text('$storeUpdatesCount'),
|
||||
child: const Icon(Icons.store_outlined),
|
||||
isLabelVisible: repoUpdatesCount > 0,
|
||||
label: Text('$repoUpdatesCount'),
|
||||
child: const Icon(Icons.extension_outlined),
|
||||
),
|
||||
),
|
||||
selectedIcon: SwingIcon(
|
||||
selectedIcon: BouncingIcon(
|
||||
child: AnimatedBadge(
|
||||
count: storeUpdatesCount,
|
||||
count: repoUpdatesCount,
|
||||
child: Badge(
|
||||
isLabelVisible: storeUpdatesCount > 0,
|
||||
label: Text('$storeUpdatesCount'),
|
||||
child: const Icon(Icons.store),
|
||||
isLabelVisible: repoUpdatesCount > 0,
|
||||
label: Text('$repoUpdatesCount'),
|
||||
child: const Icon(Icons.extension),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,14 +8,14 @@ import 'package:spotiflac_android/widgets/animation_utils.dart';
|
||||
import 'package:spotiflac_android/screens/store/extension_details_screen.dart';
|
||||
import 'package:spotiflac_android/utils/app_bar_layout.dart';
|
||||
|
||||
class StoreTab extends ConsumerStatefulWidget {
|
||||
const StoreTab({super.key});
|
||||
class RepoTab extends ConsumerStatefulWidget {
|
||||
const RepoTab({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<StoreTab> createState() => _StoreTabState();
|
||||
ConsumerState<RepoTab> createState() => _RepoTabState();
|
||||
}
|
||||
|
||||
class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
class _RepoTabState extends ConsumerState<RepoTab> {
|
||||
final _searchController = TextEditingController();
|
||||
final _repoUrlController = TextEditingController();
|
||||
bool _isInitialized = false;
|
||||
@@ -323,7 +323,7 @@ class _StoreTabState extends ConsumerState<StoreTab> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.store_outlined,
|
||||
Icons.extension_outlined,
|
||||
size: 72,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -158,7 +158,7 @@ class OptionsSettingsPage extends ConsumerWidget {
|
||||
child: SettingsGroup(
|
||||
children: [
|
||||
SettingsSwitchItem(
|
||||
icon: Icons.store,
|
||||
icon: Icons.extension,
|
||||
title: context.l10n.optionsExtensionStore,
|
||||
subtitle: context.l10n.optionsExtensionStoreSubtitle,
|
||||
value: settings.showExtensionStore,
|
||||
|
||||
@@ -185,7 +185,7 @@ class _TutorialScreenState extends ConsumerState<TutorialScreen> {
|
||||
title: l10n.tutorialExtensionsTitle,
|
||||
description: l10n.tutorialExtensionsDesc,
|
||||
content: _buildFeatureList(context, [
|
||||
(Icons.storefront_rounded, l10n.tutorialExtensionsTip1),
|
||||
(Icons.extension_rounded, l10n.tutorialExtensionsTip1),
|
||||
(
|
||||
Icons.add_circle_outline_rounded,
|
||||
l10n.tutorialExtensionsTip2,
|
||||
|
||||
@@ -20,12 +20,6 @@ class PlatformBridge {
|
||||
static bool get supportsExtensionSystem =>
|
||||
Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static Future<Map<String, dynamic>> parseSpotifyUrl(String url) async {
|
||||
_log.d('parseSpotifyUrl: $url');
|
||||
final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> checkAvailability(
|
||||
String spotifyId,
|
||||
String isrc,
|
||||
@@ -654,16 +648,6 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getSpotifyMetadataWithFallback(
|
||||
String url,
|
||||
) async {
|
||||
final result = await _channel.invokeMethod(
|
||||
'getSpotifyMetadataWithFallback',
|
||||
{'url': url},
|
||||
);
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<List<Map<String, dynamic>>> getGoLogs() async {
|
||||
final result = await _channel.invokeMethod('getLogs');
|
||||
final logs = jsonDecode(result as String) as List<dynamic>;
|
||||
|
||||
@@ -5,25 +5,25 @@ class ShellNavigationService {
|
||||
GlobalKey<NavigatorState>();
|
||||
static final GlobalKey<NavigatorState> libraryTabNavigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
static final GlobalKey<NavigatorState> storeTabNavigatorKey =
|
||||
static final GlobalKey<NavigatorState> repoTabNavigatorKey =
|
||||
GlobalKey<NavigatorState>();
|
||||
|
||||
static int _currentTabIndex = 0;
|
||||
static bool _showStoreTab = false;
|
||||
static bool _showRepoTab = false;
|
||||
|
||||
static void syncState({
|
||||
required int currentTabIndex,
|
||||
required bool showStoreTab,
|
||||
required bool showRepoTab,
|
||||
}) {
|
||||
_currentTabIndex = currentTabIndex;
|
||||
_showStoreTab = showStoreTab;
|
||||
_showRepoTab = showRepoTab;
|
||||
}
|
||||
|
||||
static NavigatorState? activeTabNavigator() {
|
||||
if (_currentTabIndex == 0) return homeTabNavigatorKey.currentState;
|
||||
if (_currentTabIndex == 1) return libraryTabNavigatorKey.currentState;
|
||||
if (_showStoreTab && _currentTabIndex == 2) {
|
||||
return storeTabNavigatorKey.currentState;
|
||||
if (_showRepoTab && _currentTabIndex == 2) {
|
||||
return repoTabNavigatorKey.currentState;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user