mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-04 05:38:12 +02:00
fix: align cross-service sharing and fallback routing
This commit is contained in:
@@ -96,7 +96,7 @@ func findCollectionForExtension(
|
||||
result.DisplayName = provider.extension.ID
|
||||
}
|
||||
|
||||
searchResult, err := provider.SearchTracks(query, 10)
|
||||
searchResult, err := searchCollectionCandidates(provider, itemType, query)
|
||||
if err != nil {
|
||||
result.Error = err.Error()
|
||||
return result
|
||||
@@ -130,14 +130,36 @@ func findCollectionForExtension(
|
||||
result.Found = true
|
||||
result.URL = url
|
||||
if itemType == "artist" {
|
||||
result.ItemName = best.Artists
|
||||
result.ItemName = collectionArtistName(*best)
|
||||
} else {
|
||||
result.ItemName = best.AlbumName
|
||||
result.ItemName = collectionAlbumName(*best)
|
||||
result.ItemArtists = best.Artists
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func searchCollectionCandidates(provider *extensionProviderWrapper, itemType string, query string) (*ExtSearchResult, error) {
|
||||
filter := ""
|
||||
switch itemType {
|
||||
case "album":
|
||||
filter = "albums"
|
||||
case "artist":
|
||||
filter = "artists"
|
||||
}
|
||||
|
||||
if filter != "" {
|
||||
tracks, err := provider.CustomSearch(query, map[string]interface{}{
|
||||
"filter": filter,
|
||||
"limit": 10,
|
||||
})
|
||||
if err == nil && len(tracks) > 0 {
|
||||
return &ExtSearchResult{Tracks: tracks, Total: len(tracks)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
return provider.SearchTracks(query, 10)
|
||||
}
|
||||
|
||||
func bestAlbumTrack(tracks []ExtTrackMetadata, albumName string, artists string) *ExtTrackMetadata {
|
||||
targetAlbum := normalizeLooseTitle(albumName)
|
||||
targetArtists := normalizeLooseArtistName(artists)
|
||||
@@ -146,10 +168,13 @@ func bestAlbumTrack(tracks []ExtTrackMetadata, albumName string, artists string)
|
||||
|
||||
for i := range tracks {
|
||||
track := tracks[i]
|
||||
album := normalizeLooseTitle(track.AlbumName)
|
||||
album := normalizeLooseTitle(collectionAlbumName(track))
|
||||
trackArtists := normalizeLooseArtistName(track.Artists + " " + track.AlbumArtist)
|
||||
|
||||
score := 0
|
||||
if isCollectionItemType(track, "album") {
|
||||
score += 25
|
||||
}
|
||||
if album == targetAlbum {
|
||||
score += 100
|
||||
} else if album != "" && targetAlbum != "" && (strings.Contains(album, targetAlbum) || strings.Contains(targetAlbum, album)) {
|
||||
@@ -176,8 +201,11 @@ func bestArtistTrack(tracks []ExtTrackMetadata, artistName string) *ExtTrackMeta
|
||||
bestIndex := -1
|
||||
|
||||
for i := range tracks {
|
||||
artist := normalizeLooseArtistName(tracks[i].Artists)
|
||||
artist := normalizeLooseArtistName(collectionArtistName(tracks[i]))
|
||||
score := 0
|
||||
if isCollectionItemType(tracks[i], "artist") {
|
||||
score += 25
|
||||
}
|
||||
if artist == targetArtist {
|
||||
score += 100
|
||||
} else if artist != "" && targetArtist != "" && (strings.Contains(artist, targetArtist) || strings.Contains(targetArtist, artist)) {
|
||||
@@ -201,30 +229,65 @@ func resolveCollectionShareURL(ext *loadedExtension, itemType string, track *Ext
|
||||
}
|
||||
|
||||
if itemType == "album" {
|
||||
if isCollectionItemType(*track, "album") {
|
||||
if url := normalizeShareURL(track.ExternalURL); url != "" {
|
||||
return url
|
||||
}
|
||||
}
|
||||
if url := normalizeShareURL(track.AlbumURL); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := urlFromExternalLinks(track.ExternalLinks, "album"); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := templateShareURL(ext, "album", firstNonEmptyString(track.AlbumID, track.AlbumURL)); url != "" {
|
||||
if url := templateShareURL(ext, "album", firstNonEmptyString(track.AlbumID, collectionID(*track, "album"), track.AlbumURL)); url != "" {
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
if isCollectionItemType(*track, "artist") {
|
||||
if url := normalizeShareURL(track.ExternalURL); url != "" {
|
||||
return url
|
||||
}
|
||||
}
|
||||
if url := normalizeShareURL(track.ArtistURL); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := urlFromExternalLinks(track.ExternalLinks, "artist"); url != "" {
|
||||
return url
|
||||
}
|
||||
if url := templateShareURL(ext, "artist", track.ArtistID); url != "" {
|
||||
if url := templateShareURL(ext, "artist", firstNonEmptyString(track.ArtistID, collectionID(*track, "artist"))); url != "" {
|
||||
return url
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func collectionAlbumName(track ExtTrackMetadata) string {
|
||||
if isCollectionItemType(track, "album") {
|
||||
return track.Name
|
||||
}
|
||||
return track.AlbumName
|
||||
}
|
||||
|
||||
func collectionArtistName(track ExtTrackMetadata) string {
|
||||
if isCollectionItemType(track, "artist") {
|
||||
return track.Name
|
||||
}
|
||||
return track.Artists
|
||||
}
|
||||
|
||||
func collectionID(track ExtTrackMetadata, itemType string) string {
|
||||
if isCollectionItemType(track, itemType) {
|
||||
return track.ID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isCollectionItemType(track ExtTrackMetadata, itemType string) bool {
|
||||
return strings.EqualFold(strings.TrimSpace(track.ItemType), itemType)
|
||||
}
|
||||
|
||||
func normalizeShareURL(value string) string {
|
||||
trimmed := strings.TrimSpace(value)
|
||||
if strings.HasPrefix(trimmed, "http://") || strings.HasPrefix(trimmed, "https://") {
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package gobackend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestCrossExtensionShareUsesAlbumCollectionItems(t *testing.T) {
|
||||
ext := &loadedExtension{
|
||||
Manifest: &ExtensionManifest{
|
||||
Capabilities: map[string]interface{}{
|
||||
"shareUrlTemplates": map[string]interface{}{
|
||||
"album": "https://music.apple.com/us/album/{id}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tracks := []ExtTrackMetadata{
|
||||
{
|
||||
ID: "1440783617",
|
||||
Name: "Nevermind",
|
||||
Artists: "Nirvana",
|
||||
ItemType: "album",
|
||||
},
|
||||
}
|
||||
|
||||
best := bestAlbumTrack(tracks, "Nevermind", "Nirvana")
|
||||
if best == nil {
|
||||
t.Fatal("expected album collection item to match")
|
||||
}
|
||||
if url := resolveCollectionShareURL(ext, "album", best); url != "https://music.apple.com/us/album/1440783617" {
|
||||
t.Fatalf("album share URL = %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
|
||||
ext := &loadedExtension{
|
||||
Manifest: &ExtensionManifest{
|
||||
Capabilities: map[string]interface{}{
|
||||
"shareUrlTemplates": map[string]interface{}{
|
||||
"artist": "https://music.youtube.com/browse/{id}",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
tracks := []ExtTrackMetadata{
|
||||
{
|
||||
ID: "UCrPe3hLA51968GwxHSZ1llw",
|
||||
Name: "Nirvana",
|
||||
ItemType: "artist",
|
||||
},
|
||||
}
|
||||
|
||||
best := bestArtistTrack(tracks, "Nirvana")
|
||||
if best == nil {
|
||||
t.Fatal("expected artist collection item to match")
|
||||
}
|
||||
if url := resolveCollectionShareURL(ext, "artist", best); url != "https://music.youtube.com/browse/UCrPe3hLA51968GwxHSZ1llw" {
|
||||
t.Fatalf("artist share URL = %q", url)
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
extensionHealthDefaultTimeout = 4 * time.Second
|
||||
extensionHealthMaxBodyBytes = 64 * 1024
|
||||
extensionHealthDefaultCache = 60 * time.Second
|
||||
)
|
||||
|
||||
type ExtensionHealthResult struct {
|
||||
@@ -38,6 +40,16 @@ type ExtensionHealthCheckResult struct {
|
||||
CheckedAt string `json:"checked_at"`
|
||||
}
|
||||
|
||||
type cachedExtensionHealthResult struct {
|
||||
result ExtensionHealthResult
|
||||
expiresAt time.Time
|
||||
}
|
||||
|
||||
var (
|
||||
extensionHealthCacheMu sync.Mutex
|
||||
extensionHealthCache = map[string]cachedExtensionHealthResult{}
|
||||
)
|
||||
|
||||
func CheckExtensionHealthJSON(extensionID string) (string, error) {
|
||||
manager := getExtensionManager()
|
||||
ext, err := manager.GetExtension(extensionID)
|
||||
@@ -53,6 +65,38 @@ func CheckExtensionHealthJSON(extensionID string) (string, error) {
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func CheckExtensionHealthCached(ext *loadedExtension) ExtensionHealthResult {
|
||||
if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
|
||||
return CheckExtensionHealth(ext)
|
||||
}
|
||||
|
||||
cacheKey := strings.TrimSpace(ext.ID)
|
||||
if cacheKey == "" {
|
||||
return CheckExtensionHealth(ext)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
extensionHealthCacheMu.Lock()
|
||||
cached, ok := extensionHealthCache[cacheKey]
|
||||
if ok && now.Before(cached.expiresAt) {
|
||||
extensionHealthCacheMu.Unlock()
|
||||
return cached.result
|
||||
}
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
result := CheckExtensionHealth(ext)
|
||||
ttl := extensionHealthCacheTTL(ext.Manifest.ServiceHealth)
|
||||
|
||||
extensionHealthCacheMu.Lock()
|
||||
extensionHealthCache[cacheKey] = cachedExtensionHealthResult{
|
||||
result: result,
|
||||
expiresAt: now.Add(ttl),
|
||||
}
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func CheckExtensionHealth(ext *loadedExtension) ExtensionHealthResult {
|
||||
now := time.Now().UTC().Format(time.RFC3339)
|
||||
result := ExtensionHealthResult{
|
||||
@@ -98,6 +142,20 @@ func CheckExtensionHealth(ext *loadedExtension) ExtensionHealthResult {
|
||||
return result
|
||||
}
|
||||
|
||||
func extensionHealthCacheTTL(checks []ExtensionHealthCheck) time.Duration {
|
||||
ttl := extensionHealthDefaultCache
|
||||
for _, check := range checks {
|
||||
if check.CacheTTLSeconds <= 0 {
|
||||
continue
|
||||
}
|
||||
checkTTL := time.Duration(check.CacheTTLSeconds) * time.Second
|
||||
if checkTTL < ttl {
|
||||
ttl = checkTTL
|
||||
}
|
||||
}
|
||||
return ttl
|
||||
}
|
||||
|
||||
func runExtensionHealthCheck(manifest *ExtensionManifest, check ExtensionHealthCheck) ExtensionHealthCheckResult {
|
||||
method := strings.ToUpper(strings.TrimSpace(check.Method))
|
||||
if method == "" {
|
||||
|
||||
@@ -382,6 +382,64 @@ func shouldStopProviderFallback(availability *ExtAvailabilityResult) bool {
|
||||
return availability != nil && availability.SkipFallback
|
||||
}
|
||||
|
||||
func fallbackRuntimeHealthStatus(ext *loadedExtension) string {
|
||||
if ext == nil || ext.Manifest == nil || len(ext.Manifest.ServiceHealth) == 0 {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
status := strings.ToLower(strings.TrimSpace(CheckExtensionHealthCached(ext).Status))
|
||||
switch status {
|
||||
case "online", "degraded", "offline":
|
||||
return status
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func prioritizeFallbackProvidersByHealth(priority []string, extManager *extensionManager, sourceProvider string) []string {
|
||||
if len(priority) == 0 || extManager == nil {
|
||||
return priority
|
||||
}
|
||||
|
||||
online := make([]string, 0, len(priority))
|
||||
degraded := make([]string, 0, len(priority))
|
||||
unknown := make([]string, 0, len(priority))
|
||||
|
||||
for _, rawProviderID := range priority {
|
||||
providerID := strings.TrimSpace(rawProviderID)
|
||||
if providerID == "" {
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(providerID, sourceProvider) || !isExtensionFallbackAllowed(providerID) {
|
||||
unknown = append(unknown, providerID)
|
||||
continue
|
||||
}
|
||||
|
||||
ext, err := extManager.GetExtension(providerID)
|
||||
if err != nil || ext == nil || !ext.Enabled || ext.Error != "" || ext.Manifest == nil || !ext.Manifest.IsDownloadProvider() {
|
||||
unknown = append(unknown, providerID)
|
||||
continue
|
||||
}
|
||||
|
||||
switch fallbackRuntimeHealthStatus(ext) {
|
||||
case "online":
|
||||
online = append(online, providerID)
|
||||
case "degraded":
|
||||
degraded = append(degraded, providerID)
|
||||
case "offline":
|
||||
GoLog("[DownloadWithExtensionFallback] Skipping extension provider %s (service health offline)\n", providerID)
|
||||
default:
|
||||
unknown = append(unknown, providerID)
|
||||
}
|
||||
}
|
||||
|
||||
result := make([]string, 0, len(online)+len(degraded)+len(unknown))
|
||||
result = append(result, online...)
|
||||
result = append(result, degraded...)
|
||||
result = append(result, unknown...)
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveExtensionAvailabilityReason(availability *ExtAvailabilityResult, err error) string {
|
||||
if availability != nil {
|
||||
if reason := strings.TrimSpace(availability.Reason); reason != "" {
|
||||
@@ -1800,7 +1858,9 @@ func isRetiredBuiltInDownloadProvider(providerID string) bool {
|
||||
}
|
||||
switch normalized {
|
||||
case "deezer", "qobuz", "tidal":
|
||||
return true
|
||||
return !hasEnabledExtensionProvider(normalized, func(manifest *ExtensionManifest) bool {
|
||||
return manifest.IsDownloadProvider()
|
||||
})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
@@ -1813,12 +1873,36 @@ func isRetiredBuiltInMetadataProvider(providerID string) bool {
|
||||
}
|
||||
switch normalized {
|
||||
case "deezer", "spotify", "qobuz", "tidal":
|
||||
return true
|
||||
return !hasEnabledExtensionProvider(normalized, func(manifest *ExtensionManifest) bool {
|
||||
return manifest.IsMetadataProvider()
|
||||
})
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func hasEnabledExtensionProvider(providerID string, matches func(*ExtensionManifest) bool) bool {
|
||||
if providerID == "" || matches == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
manager := getExtensionManager()
|
||||
manager.mu.RLock()
|
||||
defer manager.mu.RUnlock()
|
||||
|
||||
for id, ext := range manager.extensions {
|
||||
if !strings.EqualFold(strings.TrimSpace(id), providerID) {
|
||||
continue
|
||||
}
|
||||
if ext == nil || !ext.Enabled || ext.Error != "" || ext.Manifest == nil {
|
||||
return false
|
||||
}
|
||||
return matches(ext.Manifest)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func SetExtensionFallbackProviderIDs(providerIDs []string) {
|
||||
extensionFallbackProviderIDsMu.Lock()
|
||||
defer extensionFallbackProviderIDsMu.Unlock()
|
||||
@@ -2388,6 +2472,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
|
||||
}
|
||||
}
|
||||
|
||||
priority = prioritizeFallbackProvidersByHealth(priority, extManager, req.Source)
|
||||
|
||||
for _, providerID := range priority {
|
||||
if isDownloadCancelled(req.ItemID) {
|
||||
return nil, ErrDownloadCancelled
|
||||
|
||||
@@ -93,6 +93,125 @@ func TestSetProviderPriorityRemovesRetiredDeezerDownloader(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetProviderPriorityKeepsExtensionNamedLikeRetiredDownloader(t *testing.T) {
|
||||
original := GetProviderPriority()
|
||||
defer SetProviderPriority(original)
|
||||
|
||||
manager := getExtensionManager()
|
||||
ext := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
ext.ID = "deezer"
|
||||
ext.Manifest.Name = "deezer"
|
||||
|
||||
manager.mu.Lock()
|
||||
previous, hadPrevious := manager.extensions[ext.ID]
|
||||
manager.extensions[ext.ID] = ext
|
||||
manager.mu.Unlock()
|
||||
defer func() {
|
||||
manager.mu.Lock()
|
||||
if hadPrevious {
|
||||
manager.extensions[ext.ID] = previous
|
||||
} else {
|
||||
delete(manager.extensions, ext.ID)
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
}()
|
||||
|
||||
SetProviderPriority([]string{"deezer", "custom-ext"})
|
||||
|
||||
got := GetProviderPriority()
|
||||
want := []string{"deezer", "custom-ext"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected priority length: got %v want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("unexpected priority at %d: got %v want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrioritizeFallbackProvidersByHealthPrefersOnlineAndSkipsOffline(t *testing.T) {
|
||||
manager := getExtensionManager()
|
||||
amazon := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
amazon.ID = "amazon"
|
||||
amazon.Manifest.Name = "amazon"
|
||||
amazon.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
ID: "main",
|
||||
URL: "://bad",
|
||||
Required: true,
|
||||
}}
|
||||
|
||||
plain := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
plain.ID = "plain"
|
||||
plain.Manifest.Name = "plain"
|
||||
|
||||
deezer := newTestLoadedExtension(t, ExtensionTypeDownloadProvider)
|
||||
deezer.ID = "deezer"
|
||||
deezer.Manifest.Name = "deezer"
|
||||
deezer.Manifest.ServiceHealth = []ExtensionHealthCheck{{
|
||||
ID: "main",
|
||||
URL: "https://example.test/health",
|
||||
}}
|
||||
|
||||
manager.mu.Lock()
|
||||
previousAmazon, hadAmazon := manager.extensions[amazon.ID]
|
||||
previousPlain, hadPlain := manager.extensions[plain.ID]
|
||||
previousDeezer, hadDeezer := manager.extensions[deezer.ID]
|
||||
manager.extensions[amazon.ID] = amazon
|
||||
manager.extensions[plain.ID] = plain
|
||||
manager.extensions[deezer.ID] = deezer
|
||||
manager.mu.Unlock()
|
||||
defer func() {
|
||||
manager.mu.Lock()
|
||||
if hadAmazon {
|
||||
manager.extensions[amazon.ID] = previousAmazon
|
||||
} else {
|
||||
delete(manager.extensions, amazon.ID)
|
||||
}
|
||||
if hadPlain {
|
||||
manager.extensions[plain.ID] = previousPlain
|
||||
} else {
|
||||
delete(manager.extensions, plain.ID)
|
||||
}
|
||||
if hadDeezer {
|
||||
manager.extensions[deezer.ID] = previousDeezer
|
||||
} else {
|
||||
delete(manager.extensions, deezer.ID)
|
||||
}
|
||||
manager.mu.Unlock()
|
||||
|
||||
extensionHealthCacheMu.Lock()
|
||||
delete(extensionHealthCache, deezer.ID)
|
||||
extensionHealthCacheMu.Unlock()
|
||||
}()
|
||||
|
||||
extensionHealthCacheMu.Lock()
|
||||
extensionHealthCache[deezer.ID] = cachedExtensionHealthResult{
|
||||
result: ExtensionHealthResult{
|
||||
ExtensionID: deezer.ID,
|
||||
Status: "online",
|
||||
CheckedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
expiresAt: time.Now().Add(time.Minute),
|
||||
}
|
||||
extensionHealthCacheMu.Unlock()
|
||||
|
||||
got := prioritizeFallbackProvidersByHealth(
|
||||
[]string{"amazon", "plain", "deezer"},
|
||||
manager,
|
||||
"",
|
||||
)
|
||||
want := []string{"deezer", "plain"}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("unexpected provider order length: got %v want %v", got, want)
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("unexpected provider order at %d: got %v want %v", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeDownloadDecryptionInfoPromotesLegacyKey(t *testing.T) {
|
||||
normalized := normalizeDownloadDecryptionInfo(nil, " 001122 ")
|
||||
if normalized == nil {
|
||||
|
||||
@@ -567,20 +567,24 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
children: [
|
||||
_buildLoveAllButton(),
|
||||
const SizedBox(width: 12),
|
||||
_buildShareButton(context, tracks, artistName),
|
||||
const SizedBox(width: 12),
|
||||
FilledButton.icon(
|
||||
onPressed: () => _downloadAll(context),
|
||||
icon: Icon(Icons.download, size: 18),
|
||||
label: Text(
|
||||
context.l10n.downloadAllCount(tracks.length),
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
minimumSize: const Size(0, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
Flexible(
|
||||
child: FilledButton.icon(
|
||||
onPressed: () => _downloadAll(context),
|
||||
icon: Icon(Icons.download, size: 18),
|
||||
label: Text(
|
||||
context.l10n.downloadAllCount(
|
||||
tracks.length,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
minimumSize: const Size(0, 48),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -611,6 +615,23 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
tooltip: context.l10n.openInOtherServices,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.open_in_new_rounded, color: Colors.white),
|
||||
),
|
||||
onPressed: () => _showShareSheet(context, tracks, artistName),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -849,7 +870,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareButton(
|
||||
void _showShareSheet(
|
||||
BuildContext context,
|
||||
List<Track> tracks,
|
||||
String? artistName,
|
||||
@@ -861,30 +882,12 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen> {
|
||||
tracks.firstOrNull?.artistName ??
|
||||
'';
|
||||
|
||||
return Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Colors.white.withValues(alpha: 0.15),
|
||||
border: Border.all(
|
||||
color: Colors.white.withValues(alpha: 0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => CrossExtensionShareSheet.show(
|
||||
context,
|
||||
name: widget.albumName,
|
||||
artists: resolvedArtists,
|
||||
type: 'album',
|
||||
sourceExtensionId: sourceExtensionId,
|
||||
),
|
||||
icon: const Icon(Icons.open_in_new_rounded, size: 22),
|
||||
color: Colors.white,
|
||||
tooltip: context.l10n.openInOtherServices,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
CrossExtensionShareSheet.show(
|
||||
context,
|
||||
name: widget.albumName,
|
||||
artists: resolvedArtists,
|
||||
type: 'album',
|
||||
sourceExtensionId: sourceExtensionId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1334,6 +1334,33 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: IconButton(
|
||||
tooltip: context.l10n.openInOtherServices,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.4),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(Icons.open_in_new_rounded, color: Colors.white),
|
||||
),
|
||||
onPressed: () => _showShareSheet(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _showShareSheet(BuildContext context) {
|
||||
CrossExtensionShareSheet.show(
|
||||
context,
|
||||
name: widget.artistName,
|
||||
artists: '',
|
||||
type: 'artist',
|
||||
sourceExtensionId: _directMetadataProviderId() ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1422,31 +1449,10 @@ class _ArtistScreenState extends ConsumerState<ArtistScreen> {
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Container(
|
||||
width: 52,
|
||||
height: 52,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: () => CrossExtensionShareSheet.show(
|
||||
context,
|
||||
name: widget.artistName,
|
||||
artists: '',
|
||||
type: 'artist',
|
||||
sourceExtensionId: _directMetadataProviderId() ?? '',
|
||||
),
|
||||
icon: const Icon(Icons.open_in_new_rounded, size: 24),
|
||||
color: Colors.black87,
|
||||
tooltip: context.l10n.openInOtherServices,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user