mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-06-04 05:38:12 +02:00
feat: polish search empty state and share caching
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -2,6 +2,7 @@ package gobackend
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -16,6 +17,16 @@ type CrossExtensionShareResult struct {
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
var crossExtensionShareResultCache = struct {
|
||||
sync.RWMutex
|
||||
entries map[string]string
|
||||
order []string
|
||||
}{
|
||||
entries: make(map[string]string),
|
||||
}
|
||||
|
||||
const crossExtensionShareResultCacheLimit = 128
|
||||
|
||||
func FindCollectionAcrossExtensionsJSON(requestJSON string) (string, error) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
@@ -49,6 +60,10 @@ func FindCollectionAcrossExtensionsJSON(requestJSON string) (string, error) {
|
||||
}
|
||||
work = append(work, provider)
|
||||
}
|
||||
cacheKey := crossExtensionShareCacheKey(req.Name, req.Artists, req.Type, req.SourceExtensionID, work)
|
||||
if cached := getCrossExtensionShareCache(cacheKey); cached != "" {
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
query := req.Name
|
||||
if req.Artists != "" {
|
||||
@@ -76,7 +91,85 @@ func FindCollectionAcrossExtensionsJSON(requestJSON string) (string, error) {
|
||||
if err != nil {
|
||||
return "[]", err
|
||||
}
|
||||
return string(data), nil
|
||||
response := string(data)
|
||||
if crossExtensionShareResultsCacheable(results) {
|
||||
setCrossExtensionShareCache(cacheKey, response)
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func crossExtensionShareCacheKey(name string, artists string, itemType string, sourceExtensionID string, providers []*extensionProviderWrapper) string {
|
||||
providerKeys := make([]string, 0, len(providers))
|
||||
for _, provider := range providers {
|
||||
if provider == nil || provider.extension == nil {
|
||||
continue
|
||||
}
|
||||
ext := provider.extension
|
||||
displayName := ""
|
||||
if ext.Manifest != nil {
|
||||
displayName = ext.Manifest.DisplayName
|
||||
}
|
||||
providerKeys = append(providerKeys, strings.Join([]string{
|
||||
strings.TrimSpace(ext.ID),
|
||||
strings.TrimSpace(displayName),
|
||||
strings.TrimSpace(ext.SourceDir),
|
||||
}, "\x1f"))
|
||||
}
|
||||
sort.Strings(providerKeys)
|
||||
|
||||
return strings.Join([]string{
|
||||
normalizeLooseTitle(itemType),
|
||||
normalizeLooseTitle(name),
|
||||
normalizeLooseArtistName(artists),
|
||||
strings.TrimSpace(sourceExtensionID),
|
||||
strings.Join(providerKeys, "\x1e"),
|
||||
}, "\x1d")
|
||||
}
|
||||
|
||||
func getCrossExtensionShareCache(key string) string {
|
||||
if key == "" {
|
||||
return ""
|
||||
}
|
||||
crossExtensionShareResultCache.RLock()
|
||||
defer crossExtensionShareResultCache.RUnlock()
|
||||
return crossExtensionShareResultCache.entries[key]
|
||||
}
|
||||
|
||||
func setCrossExtensionShareCache(key string, value string) {
|
||||
if key == "" || value == "" {
|
||||
return
|
||||
}
|
||||
crossExtensionShareResultCache.Lock()
|
||||
defer crossExtensionShareResultCache.Unlock()
|
||||
|
||||
if _, exists := crossExtensionShareResultCache.entries[key]; !exists {
|
||||
crossExtensionShareResultCache.order = append(crossExtensionShareResultCache.order, key)
|
||||
}
|
||||
crossExtensionShareResultCache.entries[key] = value
|
||||
|
||||
for len(crossExtensionShareResultCache.order) > crossExtensionShareResultCacheLimit {
|
||||
oldest := crossExtensionShareResultCache.order[0]
|
||||
crossExtensionShareResultCache.order = crossExtensionShareResultCache.order[1:]
|
||||
delete(crossExtensionShareResultCache.entries, oldest)
|
||||
}
|
||||
}
|
||||
|
||||
func crossExtensionShareResultsCacheable(results []CrossExtensionShareResult) bool {
|
||||
for _, result := range results {
|
||||
if result.Found {
|
||||
continue
|
||||
}
|
||||
errText := strings.ToLower(strings.TrimSpace(result.Error))
|
||||
if errText == "" ||
|
||||
errText == "no results" ||
|
||||
errText == "unsupported collection type" ||
|
||||
strings.HasSuffix(errText, " not found") ||
|
||||
strings.Contains(errText, "found without shareable link") {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func findCollectionForExtension(
|
||||
|
||||
@@ -56,3 +56,45 @@ func TestCrossExtensionShareUsesArtistCollectionItems(t *testing.T) {
|
||||
t.Fatalf("artist share URL = %q", url)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossExtensionShareCacheKeyIsProviderOrderStable(t *testing.T) {
|
||||
apple := &extensionProviderWrapper{
|
||||
extension: &loadedExtension{
|
||||
ID: "apple",
|
||||
SourceDir: "/extensions/apple",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Apple Music"},
|
||||
},
|
||||
}
|
||||
qobuz := &extensionProviderWrapper{
|
||||
extension: &loadedExtension{
|
||||
ID: "qobuz",
|
||||
SourceDir: "/extensions/qobuz",
|
||||
Manifest: &ExtensionManifest{DisplayName: "Qobuz"},
|
||||
},
|
||||
}
|
||||
|
||||
first := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{apple, qobuz})
|
||||
second := crossExtensionShareCacheKey("Nevermind", "Nirvana", "album", "spotify", []*extensionProviderWrapper{qobuz, apple})
|
||||
if first != second {
|
||||
t.Fatalf("cache key should not depend on provider order:\n%s\n%s", first, second)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossExtensionShareCacheableSkipsTransientErrors(t *testing.T) {
|
||||
cacheable := []CrossExtensionShareResult{
|
||||
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
|
||||
{ExtensionID: "qobuz", Error: "album not found"},
|
||||
{ExtensionID: "tidal", Error: "no results"},
|
||||
}
|
||||
if !crossExtensionShareResultsCacheable(cacheable) {
|
||||
t.Fatal("expected found and deterministic not-found results to be cacheable")
|
||||
}
|
||||
|
||||
transient := []CrossExtensionShareResult{
|
||||
{ExtensionID: "apple", Found: true, URL: "https://music.apple.com/us/album/1"},
|
||||
{ExtensionID: "qobuz", Error: "request failed: timeout"},
|
||||
}
|
||||
if crossExtensionShareResultsCacheable(transient) {
|
||||
t.Fatal("expected transient extension errors to skip cache")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1404,6 +1404,12 @@ abstract class AppLocalizations {
|
||||
/// **'No tracks found'**
|
||||
String get errorNoTracksFound;
|
||||
|
||||
/// Subtitle shown under the empty search result state on the home screen
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try another keyword'**
|
||||
String get searchEmptyResultSubtitle;
|
||||
|
||||
/// Error title - URL not handled by any extension or service
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -7040,7 +7046,7 @@ abstract class AppLocalizations {
|
||||
/// Title and tooltip for finding the current collection in other services
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open in other services'**
|
||||
/// **'Open in Other Services'**
|
||||
String get openInOtherServices;
|
||||
|
||||
/// Empty state when no extensions can be searched for cross-service links
|
||||
@@ -7058,7 +7064,7 @@ abstract class AppLocalizations {
|
||||
/// Tooltip for copying a cross-service link
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Copy link'**
|
||||
/// **'Copy Link'**
|
||||
String get shareSheetCopyLink;
|
||||
|
||||
/// Snackbar after copying a cross-service link
|
||||
@@ -7066,12 +7072,6 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'{service} link copied'**
|
||||
String shareSheetLinkCopied(Object service);
|
||||
|
||||
/// Tooltip for opening a cross-service link inside the app
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Open'**
|
||||
String get shareSheetOpen;
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -754,6 +754,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'Keine Titel gefunden';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link wurde nicht erkannt';
|
||||
|
||||
@@ -4256,7 +4259,7 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4265,13 +4268,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4227,7 +4230,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4236,13 +4239,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4221,7 +4224,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4230,15 +4233,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
/// The translations for Spanish Castilian, as used in Spain (`es_ES`).
|
||||
|
||||
@@ -745,6 +745,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4225,7 +4228,7 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4234,13 +4237,10 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4222,7 +4225,7 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4231,13 +4234,10 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -745,6 +745,9 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'Tidak ada lagu ditemukan';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Coba kata kunci lain';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Tautan tidak dikenali';
|
||||
|
||||
@@ -4213,7 +4216,7 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Matikan dan hapus';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Buka di layanan lain';
|
||||
String get openInOtherServices => 'Buka di Layanan Lain';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'Tidak ada layanan lain yang kompatibel';
|
||||
@@ -4222,13 +4225,10 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Tidak ditemukan';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Salin tautan';
|
||||
String get shareSheetCopyLink => 'Salin Tautan';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return 'Tautan $service disalin';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Buka';
|
||||
}
|
||||
|
||||
@@ -737,6 +737,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'トラックがありません';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4209,7 +4212,7 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4218,13 +4221,10 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -724,6 +724,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => '트랙을 찾을 수 없습니다';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4202,7 +4205,7 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4211,13 +4214,10 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4222,7 +4225,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4231,13 +4234,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4221,7 +4224,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4230,15 +4233,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
/// The translations for Portuguese, as used in Portugal (`pt_PT`).
|
||||
|
||||
@@ -755,6 +755,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'Треки не найдены';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Ссылка не распознана';
|
||||
|
||||
@@ -4281,7 +4284,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4290,13 +4293,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -750,6 +750,9 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'Parça bulunamadı';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Bağlantı tanınamadı';
|
||||
|
||||
@@ -4248,7 +4251,7 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4257,13 +4260,10 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -755,6 +755,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'Треків не знайдено';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Посилання не розпізнано';
|
||||
|
||||
@@ -4281,7 +4284,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4290,13 +4293,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
@@ -742,6 +742,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get errorNoTracksFound => 'No tracks found';
|
||||
|
||||
@override
|
||||
String get searchEmptyResultSubtitle => 'Try another keyword';
|
||||
|
||||
@override
|
||||
String get errorUrlNotRecognized => 'Link not recognized';
|
||||
|
||||
@@ -4221,7 +4224,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get dialogDisableAndClear => 'Turn off and clear';
|
||||
|
||||
@override
|
||||
String get openInOtherServices => 'Open in other services';
|
||||
String get openInOtherServices => 'Open in Other Services';
|
||||
|
||||
@override
|
||||
String get shareSheetNoExtensions => 'No other compatible services';
|
||||
@@ -4230,15 +4233,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get shareSheetNotFound => 'Not found';
|
||||
|
||||
@override
|
||||
String get shareSheetCopyLink => 'Copy link';
|
||||
String get shareSheetCopyLink => 'Copy Link';
|
||||
|
||||
@override
|
||||
String shareSheetLinkCopied(Object service) {
|
||||
return '$service link copied';
|
||||
}
|
||||
|
||||
@override
|
||||
String get shareSheetOpen => 'Open';
|
||||
}
|
||||
|
||||
/// The translations for Chinese, as used in China (`zh_CN`).
|
||||
|
||||
@@ -961,6 +961,10 @@
|
||||
"@errorNoTracksFound": {
|
||||
"description": "Error - search returned no results"
|
||||
},
|
||||
"searchEmptyResultSubtitle": "Try another keyword",
|
||||
"@searchEmptyResultSubtitle": {
|
||||
"description": "Subtitle shown under the empty search result state on the home screen"
|
||||
},
|
||||
"errorUrlNotRecognized": "Link not recognized",
|
||||
"@errorUrlNotRecognized": {
|
||||
"description": "Error title - URL not handled by any extension or service"
|
||||
@@ -5528,7 +5532,7 @@
|
||||
"@dialogDisableAndClear": {
|
||||
"description": "Confirmation action to disable download history and clear existing entries"
|
||||
},
|
||||
"openInOtherServices": "Open in other services",
|
||||
"openInOtherServices": "Open in Other Services",
|
||||
"@openInOtherServices": {
|
||||
"description": "Title and tooltip for finding the current collection in other services"
|
||||
},
|
||||
@@ -5540,7 +5544,7 @@
|
||||
"@shareSheetNotFound": {
|
||||
"description": "Cross-service share sheet row subtitle when a service has no match"
|
||||
},
|
||||
"shareSheetCopyLink": "Copy link",
|
||||
"shareSheetCopyLink": "Copy Link",
|
||||
"@shareSheetCopyLink": {
|
||||
"description": "Tooltip for copying a cross-service link"
|
||||
},
|
||||
@@ -5550,9 +5554,5 @@
|
||||
"placeholders": {
|
||||
"service": {}
|
||||
}
|
||||
},
|
||||
"shareSheetOpen": "Open",
|
||||
"@shareSheetOpen": {
|
||||
"description": "Tooltip for opening a cross-service link inside the app"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -905,6 +905,10 @@
|
||||
"@errorNoTracksFound": {
|
||||
"description": "Error - search returned no results"
|
||||
},
|
||||
"searchEmptyResultSubtitle": "Coba kata kunci lain",
|
||||
"@searchEmptyResultSubtitle": {
|
||||
"description": "Subtitle shown under the empty search result state on the home screen"
|
||||
},
|
||||
"errorUrlNotRecognized": "Tautan tidak dikenali",
|
||||
"@errorUrlNotRecognized": {
|
||||
"description": "Error title - URL not handled by any extension or service"
|
||||
@@ -4656,7 +4660,7 @@
|
||||
"@dialogDisableAndClear": {
|
||||
"description": "Confirmation action to disable download history and clear existing entries"
|
||||
},
|
||||
"openInOtherServices": "Buka di layanan lain",
|
||||
"openInOtherServices": "Buka di Layanan Lain",
|
||||
"@openInOtherServices": {
|
||||
"description": "Title and tooltip for finding the current collection in other services"
|
||||
},
|
||||
@@ -4668,7 +4672,7 @@
|
||||
"@shareSheetNotFound": {
|
||||
"description": "Cross-service share sheet row subtitle when a service has no match"
|
||||
},
|
||||
"shareSheetCopyLink": "Salin tautan",
|
||||
"shareSheetCopyLink": "Salin Tautan",
|
||||
"@shareSheetCopyLink": {
|
||||
"description": "Tooltip for copying a cross-service link"
|
||||
},
|
||||
@@ -4678,9 +4682,5 @@
|
||||
"placeholders": {
|
||||
"service": {}
|
||||
}
|
||||
},
|
||||
"shareSheetOpen": "Buka",
|
||||
"@shareSheetOpen": {
|
||||
"description": "Tooltip for opening a cross-service link inside the app"
|
||||
}
|
||||
}
|
||||
|
||||
+116
-10
@@ -46,6 +46,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final _urlController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String? _lastSearchQuery;
|
||||
String? _activeSearchInput;
|
||||
bool _isResettingSearchSurface = false;
|
||||
late final ProviderSubscription<TrackState> _trackStateSub;
|
||||
late final ProviderSubscription<bool> _extensionInitSub;
|
||||
late final ProviderSubscription<bool> _homeFeedExtSub;
|
||||
@@ -557,9 +559,18 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
if (text.isEmpty) {
|
||||
_liveSearchDebounce?.cancel();
|
||||
_activeSearchInput = null;
|
||||
_lastSearchQuery = null;
|
||||
if (!_isResettingSearchSurface) {
|
||||
_resetSearchSurface(clearText: false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (_activeSearchInput != null && _activeSearchInput != text) {
|
||||
_activeSearchInput = null;
|
||||
}
|
||||
|
||||
if (_isLiveSearchEnabled() && text.length >= _minLiveSearchChars) {
|
||||
if (text.startsWith('http') || text.startsWith('spotify:')) return;
|
||||
|
||||
@@ -638,10 +649,17 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
|
||||
final searchKey =
|
||||
'${searchProvider ?? 'default'}:$query:${selectedFilter ?? 'all'}';
|
||||
if (_lastSearchQuery == searchKey) return;
|
||||
if (_lastSearchQuery == searchKey) {
|
||||
_activeSearchInput = query;
|
||||
ref.read(trackProvider.notifier).setSearchText(query.trim().isNotEmpty);
|
||||
if (mounted) setState(() {});
|
||||
return;
|
||||
}
|
||||
_lastSearchQuery = searchKey;
|
||||
_activeSearchInput = query;
|
||||
_searchSortOption = _SearchSortOption.defaultOrder;
|
||||
_invalidateSearchSortCaches();
|
||||
ref.read(trackProvider.notifier).setSearchText(query.trim().isNotEmpty);
|
||||
|
||||
final isExtensionEnabled =
|
||||
searchProvider != null &&
|
||||
@@ -686,12 +704,26 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
|
||||
Future<void> _clearAndRefresh() async {
|
||||
_liveSearchDebounce?.cancel();
|
||||
_pendingLiveSearchQuery = null;
|
||||
_urlController.clear();
|
||||
_searchFocusNode.unfocus();
|
||||
_lastSearchQuery = null;
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
_resetSearchSurface();
|
||||
}
|
||||
|
||||
void _resetSearchSurface({bool clearText = true}) {
|
||||
if (_isResettingSearchSurface) return;
|
||||
_isResettingSearchSurface = true;
|
||||
try {
|
||||
_liveSearchDebounce?.cancel();
|
||||
_pendingLiveSearchQuery = null;
|
||||
_lastSearchQuery = null;
|
||||
_activeSearchInput = null;
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
if (clearText && _urlController.text.isNotEmpty) {
|
||||
_urlController.clear();
|
||||
}
|
||||
ref.read(trackProvider.notifier).clear();
|
||||
if (mounted) setState(() {});
|
||||
} finally {
|
||||
_isResettingSearchSurface = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _fetchMetadata() async {
|
||||
@@ -1114,6 +1146,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
);
|
||||
final isLoading = ref.watch(trackProvider.select((s) => s.isLoading));
|
||||
final searchError = ref.watch(trackProvider.select((s) => s.error));
|
||||
final hasSearchedBefore = ref.watch(
|
||||
settingsProvider.select((s) => s.hasSearchedBefore),
|
||||
);
|
||||
@@ -1153,6 +1186,18 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final isSearchFocused = _searchFocusNode.hasFocus;
|
||||
final hasShortSearchInput =
|
||||
hasSearchInput && searchText.length < _minLiveSearchChars;
|
||||
final hasSearchError = hasSearchInput && searchError != null;
|
||||
final hasActiveSearchSurface =
|
||||
hasSearchInput &&
|
||||
(_activeSearchInput == searchText ||
|
||||
hasActualResults ||
|
||||
isLoading ||
|
||||
hasSearchError);
|
||||
final showEmptySearchResult =
|
||||
hasActiveSearchSurface &&
|
||||
!hasActualResults &&
|
||||
!isLoading &&
|
||||
searchError == null;
|
||||
final isShowingRecentAccess = ref.watch(
|
||||
trackProvider.select((s) => s.isShowingRecentAccess),
|
||||
);
|
||||
@@ -1166,7 +1211,11 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final recentModeRequested = isShowingRecentAccess || isSearchFocused;
|
||||
final showRecentAccess =
|
||||
recentModeRequested &&
|
||||
(!hasSearchInput || hasShortSearchInput || !hasActualResults) &&
|
||||
(!hasSearchInput ||
|
||||
hasShortSearchInput ||
|
||||
(!hasActualResults &&
|
||||
!hasSearchError &&
|
||||
!hasActiveSearchSurface)) &&
|
||||
!isLoading;
|
||||
final isSearchProviderLoading =
|
||||
!extensionReadiness.isInitialized && extensionReadiness.error == null;
|
||||
@@ -1180,6 +1229,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
final showExplore =
|
||||
!hasActualResults &&
|
||||
!isLoading &&
|
||||
!hasActiveSearchSurface &&
|
||||
!showRecentAccess &&
|
||||
!homeFeedDisabled &&
|
||||
(hasHomeFeedExtension || hasExploreContent) &&
|
||||
@@ -1299,7 +1349,9 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
),
|
||||
),
|
||||
|
||||
if (hasActualResults && !showRecentAccess)
|
||||
if (hasActiveSearchSurface &&
|
||||
!showRecentAccess &&
|
||||
!showEmptySearchResult)
|
||||
Consumer(
|
||||
builder: (context, ref, _) {
|
||||
final currentSearchProvider = ref.watch(
|
||||
@@ -1466,7 +1518,8 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
(searchAlbums != null && searchAlbums.isNotEmpty) ||
|
||||
(searchPlaylists != null && searchPlaylists.isNotEmpty) ||
|
||||
isLoading ||
|
||||
error != null;
|
||||
error != null ||
|
||||
hasActiveSearchSurface;
|
||||
|
||||
return SliverMainAxisGroup(
|
||||
slivers: _buildSearchResults(
|
||||
@@ -1478,6 +1531,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
error: error,
|
||||
colorScheme: colorScheme,
|
||||
hasResults: hasResults,
|
||||
showEmptySearchResult: showEmptySearchResult,
|
||||
searchExtensionId: searchExtensionId,
|
||||
showLocalLibraryIndicator: showLocalLibraryIndicator,
|
||||
thumbnailSizesByExtensionId: thumbnailSizesByExtensionId,
|
||||
@@ -2611,6 +2665,47 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptySearchResultWidget(ColorScheme colorScheme) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 340),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 86,
|
||||
height: 86,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.manage_search,
|
||||
size: 46,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.errorNoTracksFound,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.searchEmptyResultSubtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _sortOptionLabel(_SearchSortOption option) {
|
||||
switch (option) {
|
||||
case _SearchSortOption.defaultOrder:
|
||||
@@ -2888,6 +2983,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
required String? error,
|
||||
required ColorScheme colorScheme,
|
||||
required bool hasResults,
|
||||
required bool showEmptySearchResult,
|
||||
required String? searchExtensionId,
|
||||
required bool showLocalLibraryIndicator,
|
||||
required Map<String, (double, double)> thumbnailSizesByExtensionId,
|
||||
@@ -2934,6 +3030,16 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
child: LinearProgressIndicator(),
|
||||
),
|
||||
),
|
||||
if (showEmptySearchResult && !hasActualData)
|
||||
SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 24, 24, 96),
|
||||
child: _buildEmptySearchResultWidget(colorScheme),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
bool sortButtonShown = false;
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/services/cross_extension_share_service.dart';
|
||||
import 'package:spotiflac_android/services/share_intent_service.dart';
|
||||
|
||||
class CrossExtensionShareSheet extends StatefulWidget {
|
||||
class CrossExtensionShareSheet extends ConsumerStatefulWidget {
|
||||
final String name;
|
||||
final String artists;
|
||||
final String type;
|
||||
@@ -44,11 +48,12 @@ class CrossExtensionShareSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
State<CrossExtensionShareSheet> createState() =>
|
||||
ConsumerState<CrossExtensionShareSheet> createState() =>
|
||||
_CrossExtensionShareSheetState();
|
||||
}
|
||||
|
||||
class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
|
||||
class _CrossExtensionShareSheetState
|
||||
extends ConsumerState<CrossExtensionShareSheet> {
|
||||
late final Future<List<CrossExtensionShareResult>> _future;
|
||||
|
||||
@override
|
||||
@@ -71,6 +76,18 @@ class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
|
||||
});
|
||||
}
|
||||
|
||||
String? _iconPathFor(String extensionId) {
|
||||
if (extensionId.isEmpty) return null;
|
||||
final extensions = ref.read(extensionProvider).extensions;
|
||||
for (final ext in extensions) {
|
||||
if (ext.id == extensionId) {
|
||||
final path = ext.iconPath;
|
||||
return (path != null && path.isNotEmpty) ? path : null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
@@ -86,25 +103,23 @@ class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 12, bottom: 8),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(24, 8, 24, 4),
|
||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 4),
|
||||
child: Text(
|
||||
context.l10n.openInOtherServices,
|
||||
style: textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
style: textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -147,14 +162,16 @@ class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 16),
|
||||
padding: const EdgeInsets.only(bottom: 16, top: 4),
|
||||
itemBuilder: (context, index) {
|
||||
return _CrossExtensionShareTile(result: results[index]);
|
||||
final result = results[index];
|
||||
return _CrossExtensionShareTile(
|
||||
result: result,
|
||||
iconPath: _iconPathFor(result.extensionId),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, indent: 72),
|
||||
itemCount: results.length,
|
||||
);
|
||||
},
|
||||
@@ -169,33 +186,28 @@ class _CrossExtensionShareSheetState extends State<CrossExtensionShareSheet> {
|
||||
|
||||
class _CrossExtensionShareTile extends StatelessWidget {
|
||||
final CrossExtensionShareResult result;
|
||||
final String? iconPath;
|
||||
|
||||
const _CrossExtensionShareTile({required this.result});
|
||||
const _CrossExtensionShareTile({required this.result, this.iconPath});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final url = result.found ? result.url : null;
|
||||
final hasUrl = url != null && url.isNotEmpty;
|
||||
final url = result.url;
|
||||
final hasUrl = result.found && url != null && url.isNotEmpty;
|
||||
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
final tile = ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 4),
|
||||
leading: Container(
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: hasUrl
|
||||
? colorScheme.primaryContainer
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
hasUrl ? Icons.link_rounded : Icons.link_off_rounded,
|
||||
color: hasUrl
|
||||
? colorScheme.onPrimaryContainer
|
||||
: colorScheme.onSurfaceVariant,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: _buildIcon(colorScheme),
|
||||
),
|
||||
title: Text(
|
||||
result.displayName,
|
||||
@@ -210,39 +222,53 @@ class _CrossExtensionShareTile extends StatelessWidget {
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: hasUrl ? colorScheme.primary : colorScheme.onSurfaceVariant,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
trailing: hasUrl
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.shareSheetCopyLink,
|
||||
icon: const Icon(Icons.copy_rounded, size: 20),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.shareSheetLinkCopied(result.displayName),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.shareSheetOpen,
|
||||
icon: const Icon(Icons.open_in_new_rounded, size: 20),
|
||||
color: colorScheme.primary,
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
ShareIntentService().injectUrl(url);
|
||||
},
|
||||
),
|
||||
],
|
||||
? IconButton(
|
||||
tooltip: context.l10n.shareSheetCopyLink,
|
||||
icon: const Icon(Icons.copy_rounded, size: 20),
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
context.l10n.shareSheetLinkCopied(result.displayName),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: null,
|
||||
onTap: hasUrl
|
||||
? () {
|
||||
Navigator.pop(context);
|
||||
ShareIntentService().injectUrl(url);
|
||||
}
|
||||
: null,
|
||||
);
|
||||
|
||||
if (hasUrl) return tile;
|
||||
return Opacity(opacity: 0.5, child: tile);
|
||||
}
|
||||
|
||||
Widget _buildIcon(ColorScheme colorScheme) {
|
||||
final fallbackIcon = Icon(
|
||||
Icons.extension_rounded,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
);
|
||||
|
||||
final path = iconPath;
|
||||
if (path == null) return fallbackIcon;
|
||||
|
||||
return Image.file(
|
||||
File(path),
|
||||
width: 44,
|
||||
height: 44,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (_, _, _) => fallbackIcon,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+8
-8
@@ -705,10 +705,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: meta
|
||||
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.17.0"
|
||||
version: "1.18.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1246,26 +1246,26 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test
|
||||
sha256: "280d6d890011ca966ad08df7e8a4ddfab0fb3aa49f96ed6de56e3521347a9ae7"
|
||||
sha256: "8d9ceddbab833f180fbefed08afa76d7c03513dfdba87ffcec2718b02bbcbf20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.30.0"
|
||||
version: "1.31.0"
|
||||
test_api:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_api
|
||||
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.10"
|
||||
version: "0.7.11"
|
||||
test_core:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: test_core
|
||||
sha256: "0381bd1585d1a924763c308100f2138205252fb90c9d4eeaf28489ee65ccde51"
|
||||
sha256: "1991d4cfe85d5043241acac92962c3977c8d2f2add1ee73130c7b286417d1d34"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.16"
|
||||
version: "0.6.17"
|
||||
timezone:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
Reference in New Issue
Block a user