mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-20 07:04:49 +02:00
feat: cross-script transliteration matching for Tidal/Qobuz and skip-downloaded option for CSV import
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package gobackend
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestQobuzTitlesMatchCrossScript(t *testing.T) {
|
||||
t.Run("rejects unrelated cross-script titles", func(t *testing.T) {
|
||||
if qobuzTitlesMatch("パンツ脱げるもん!", "Warrior of the Darkness") {
|
||||
t.Fatalf("expected unrelated cross-script titles to not match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accepts transliterated japanese title", func(t *testing.T) {
|
||||
if !qobuzTitlesMatch("パンツ脱げるもん!", "Pantsu Nugeru Mon") {
|
||||
t.Fatalf("expected transliterated japanese title to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestQobuzArtistsMatchCrossScript(t *testing.T) {
|
||||
t.Run("rejects unrelated cross-script artists", func(t *testing.T) {
|
||||
if qobuzArtistsMatch("TakeponG", "陳奕迅") {
|
||||
t.Fatalf("expected unrelated cross-script artists to not match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accepts transliterated japanese artist", func(t *testing.T) {
|
||||
if !qobuzArtistsMatch("たけぽんぐ", "takepong") {
|
||||
t.Fatalf("expected transliterated japanese artist to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTidalTitlesMatchCrossScript(t *testing.T) {
|
||||
t.Run("rejects unrelated cross-script titles", func(t *testing.T) {
|
||||
if titlesMatch("パンツ脱げるもん!", "Warrior of the Darkness") {
|
||||
t.Fatalf("expected unrelated cross-script titles to not match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accepts transliterated japanese title", func(t *testing.T) {
|
||||
if !titlesMatch("パンツ脱げるもん!", "Pantsu Nugeru Mon") {
|
||||
t.Fatalf("expected transliterated japanese title to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestTidalArtistsMatchCrossScript(t *testing.T) {
|
||||
t.Run("rejects unrelated cross-script artists", func(t *testing.T) {
|
||||
if artistsMatch("TakeponG", "陳奕迅") {
|
||||
t.Fatalf("expected unrelated cross-script artists to not match")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("accepts transliterated japanese artist", func(t *testing.T) {
|
||||
if !artistsMatch("たけぽんぐ", "takepong") {
|
||||
t.Fatalf("expected transliterated japanese artist to match")
|
||||
}
|
||||
})
|
||||
}
|
||||
+168
-10
@@ -81,13 +81,160 @@ func qobuzArtistsMatch(expectedArtist, foundArtist string) bool {
|
||||
expectedLatin := qobuzIsLatinScript(expectedArtist)
|
||||
foundLatin := qobuzIsLatinScript(foundArtist)
|
||||
if expectedLatin != foundLatin {
|
||||
GoLog("[Qobuz] Artist names in different scripts, assuming match: '%s' vs '%s'\n", expectedArtist, foundArtist)
|
||||
return true
|
||||
if qobuzCrossScriptEquivalent(expectedArtist, foundArtist) {
|
||||
GoLog("[Qobuz] Artist names in different scripts but transliteration matched: '%s' vs '%s'\n", expectedArtist, foundArtist)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func qobuzNormalizeScriptAware(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
normalized = CleanToASCII(JapaneseToRomaji(normalized))
|
||||
normalized = strings.Join(strings.Fields(normalized), " ")
|
||||
return strings.TrimSpace(normalized)
|
||||
}
|
||||
|
||||
func qobuzCrossScriptEquivalent(expected, found string) bool {
|
||||
normExpected := qobuzNormalizeScriptAware(expected)
|
||||
normFound := qobuzNormalizeScriptAware(found)
|
||||
|
||||
if normExpected == "" || normFound == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if normExpected == normFound {
|
||||
return true
|
||||
}
|
||||
|
||||
compactExpected := strings.ReplaceAll(normExpected, " ", "")
|
||||
compactFound := strings.ReplaceAll(normFound, " ", "")
|
||||
if len(compactExpected) >= 6 && len(compactFound) >= 6 {
|
||||
if compactExpected == compactFound ||
|
||||
strings.Contains(compactExpected, compactFound) ||
|
||||
strings.Contains(compactFound, compactExpected) {
|
||||
return true
|
||||
}
|
||||
|
||||
shorterLen := len(compactExpected)
|
||||
if len(compactFound) < shorterLen {
|
||||
shorterLen = len(compactFound)
|
||||
}
|
||||
|
||||
maxDistance := 1
|
||||
if shorterLen >= 10 {
|
||||
maxDistance = 2
|
||||
}
|
||||
if shorterLen >= 16 {
|
||||
maxDistance = 3
|
||||
}
|
||||
|
||||
if qobuzEditDistanceWithin(compactExpected, compactFound, maxDistance) {
|
||||
if qobuzCommonPrefixLen(compactExpected, compactFound) >= 4 ||
|
||||
qobuzCommonSuffixLen(compactExpected, compactFound) >= 4 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func qobuzCommonPrefixLen(a, b string) int {
|
||||
max := len(a)
|
||||
if len(b) < max {
|
||||
max = len(b)
|
||||
}
|
||||
count := 0
|
||||
for i := 0; i < max; i++ {
|
||||
if a[i] != b[i] {
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func qobuzCommonSuffixLen(a, b string) int {
|
||||
i := len(a) - 1
|
||||
j := len(b) - 1
|
||||
count := 0
|
||||
for i >= 0 && j >= 0 {
|
||||
if a[i] != b[j] {
|
||||
break
|
||||
}
|
||||
count++
|
||||
i--
|
||||
j--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func qobuzEditDistanceWithin(a, b string, maxDistance int) bool {
|
||||
if maxDistance < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
lenA := len(a)
|
||||
lenB := len(b)
|
||||
diff := lenA - lenB
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > maxDistance {
|
||||
return false
|
||||
}
|
||||
|
||||
prev := make([]int, lenB+1)
|
||||
for j := 0; j <= lenB; j++ {
|
||||
prev[j] = j
|
||||
}
|
||||
|
||||
for i := 1; i <= lenA; i++ {
|
||||
curr := make([]int, lenB+1)
|
||||
curr[0] = i
|
||||
minInRow := curr[0]
|
||||
|
||||
for j := 1; j <= lenB; j++ {
|
||||
cost := 0
|
||||
if a[i-1] != b[j-1] {
|
||||
cost = 1
|
||||
}
|
||||
|
||||
insertCost := curr[j-1] + 1
|
||||
deleteCost := prev[j] + 1
|
||||
replaceCost := prev[j-1] + cost
|
||||
|
||||
best := insertCost
|
||||
if deleteCost < best {
|
||||
best = deleteCost
|
||||
}
|
||||
if replaceCost < best {
|
||||
best = replaceCost
|
||||
}
|
||||
|
||||
curr[j] = best
|
||||
if best < minInRow {
|
||||
minInRow = best
|
||||
}
|
||||
}
|
||||
|
||||
if minInRow > maxDistance {
|
||||
return false
|
||||
}
|
||||
|
||||
prev = curr
|
||||
}
|
||||
|
||||
return prev[lenB] <= maxDistance
|
||||
}
|
||||
|
||||
func qobuzSplitArtists(artists string) []string {
|
||||
normalized := artists
|
||||
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
||||
@@ -177,8 +324,10 @@ func qobuzTitlesMatch(expectedTitle, foundTitle string) bool {
|
||||
expectedLatin := qobuzIsLatinScript(expectedTitle)
|
||||
foundLatin := qobuzIsLatinScript(foundTitle)
|
||||
if expectedLatin != foundLatin {
|
||||
GoLog("[Qobuz] Titles in different scripts, assuming match: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||
return true
|
||||
if qobuzCrossScriptEquivalent(expectedTitle, foundTitle) {
|
||||
GoLog("[Qobuz] Titles in different scripts but transliteration matched: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -702,8 +851,11 @@ func (q *QobuzDownloader) SearchTrackByMetadataWithDuration(trackName, artistNam
|
||||
GoLog("[Qobuz] Title matches: %d out of %d results\n", len(titleMatches), len(allTracks))
|
||||
|
||||
tracksToCheck := titleMatches
|
||||
if len(titleMatches) == 0 {
|
||||
GoLog("[Qobuz] WARNING: No title matches for '%s', checking all %d results\n", trackName, len(allTracks))
|
||||
if strings.TrimSpace(trackName) != "" && len(titleMatches) == 0 {
|
||||
return nil, fmt.Errorf("no tracks found with matching title (expected '%s')", trackName)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(trackName) == "" {
|
||||
for i := range allTracks {
|
||||
tracksToCheck = append(tracksToCheck, &allTracks[i])
|
||||
}
|
||||
@@ -1152,10 +1304,16 @@ func downloadFromQobuz(req DownloadRequest) (QobuzDownloadResult, error) {
|
||||
if track == nil {
|
||||
GoLog("[Qobuz] Trying metadata search: '%s' by '%s'\n", req.TrackName, req.ArtistName)
|
||||
track, err = downloader.SearchTrackByMetadataWithDuration(req.TrackName, req.ArtistName, expectedDurationSec)
|
||||
if track != nil && !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) {
|
||||
GoLog("[Qobuz] Artist mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
||||
req.ArtistName, track.Performer.Name)
|
||||
track = nil
|
||||
if track != nil {
|
||||
if !qobuzTitlesMatch(req.TrackName, track.Title) {
|
||||
GoLog("[Qobuz] Title mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
||||
req.TrackName, track.Title)
|
||||
track = nil
|
||||
} else if !qobuzArtistsMatch(req.ArtistName, track.Performer.Name) {
|
||||
GoLog("[Qobuz] Artist mismatch from metadata search: expected '%s', got '%s'. Rejecting.\n",
|
||||
req.ArtistName, track.Performer.Name)
|
||||
track = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+153
-4
@@ -1196,13 +1196,160 @@ func artistsMatch(spotifyArtist, tidalArtist string) bool {
|
||||
spotifyLatin := isLatinScript(spotifyArtist)
|
||||
tidalLatin := isLatinScript(tidalArtist)
|
||||
if spotifyLatin != tidalLatin {
|
||||
GoLog("[Tidal] Artist names in different scripts, assuming match: '%s' vs '%s'\n", spotifyArtist, tidalArtist)
|
||||
return true
|
||||
if crossScriptEquivalent(spotifyArtist, tidalArtist) {
|
||||
GoLog("[Tidal] Artist names in different scripts but transliteration matched: '%s' vs '%s'\n", spotifyArtist, tidalArtist)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func normalizeScriptAware(value string) string {
|
||||
normalized := strings.ToLower(strings.TrimSpace(value))
|
||||
normalized = CleanToASCII(JapaneseToRomaji(normalized))
|
||||
normalized = strings.Join(strings.Fields(normalized), " ")
|
||||
return strings.TrimSpace(normalized)
|
||||
}
|
||||
|
||||
func crossScriptEquivalent(expected, found string) bool {
|
||||
normExpected := normalizeScriptAware(expected)
|
||||
normFound := normalizeScriptAware(found)
|
||||
|
||||
if normExpected == "" || normFound == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if normExpected == normFound {
|
||||
return true
|
||||
}
|
||||
|
||||
compactExpected := strings.ReplaceAll(normExpected, " ", "")
|
||||
compactFound := strings.ReplaceAll(normFound, " ", "")
|
||||
if len(compactExpected) >= 6 && len(compactFound) >= 6 {
|
||||
if compactExpected == compactFound ||
|
||||
strings.Contains(compactExpected, compactFound) ||
|
||||
strings.Contains(compactFound, compactExpected) {
|
||||
return true
|
||||
}
|
||||
|
||||
shorterLen := len(compactExpected)
|
||||
if len(compactFound) < shorterLen {
|
||||
shorterLen = len(compactFound)
|
||||
}
|
||||
|
||||
maxDistance := 1
|
||||
if shorterLen >= 10 {
|
||||
maxDistance = 2
|
||||
}
|
||||
if shorterLen >= 16 {
|
||||
maxDistance = 3
|
||||
}
|
||||
|
||||
if editDistanceWithin(compactExpected, compactFound, maxDistance) {
|
||||
if commonPrefixLen(compactExpected, compactFound) >= 4 ||
|
||||
commonSuffixLen(compactExpected, compactFound) >= 4 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func commonPrefixLen(a, b string) int {
|
||||
max := len(a)
|
||||
if len(b) < max {
|
||||
max = len(b)
|
||||
}
|
||||
count := 0
|
||||
for i := 0; i < max; i++ {
|
||||
if a[i] != b[i] {
|
||||
break
|
||||
}
|
||||
count++
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func commonSuffixLen(a, b string) int {
|
||||
i := len(a) - 1
|
||||
j := len(b) - 1
|
||||
count := 0
|
||||
for i >= 0 && j >= 0 {
|
||||
if a[i] != b[j] {
|
||||
break
|
||||
}
|
||||
count++
|
||||
i--
|
||||
j--
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
func editDistanceWithin(a, b string, maxDistance int) bool {
|
||||
if maxDistance < 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
|
||||
lenA := len(a)
|
||||
lenB := len(b)
|
||||
diff := lenA - lenB
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff > maxDistance {
|
||||
return false
|
||||
}
|
||||
|
||||
prev := make([]int, lenB+1)
|
||||
for j := 0; j <= lenB; j++ {
|
||||
prev[j] = j
|
||||
}
|
||||
|
||||
for i := 1; i <= lenA; i++ {
|
||||
curr := make([]int, lenB+1)
|
||||
curr[0] = i
|
||||
minInRow := curr[0]
|
||||
|
||||
for j := 1; j <= lenB; j++ {
|
||||
cost := 0
|
||||
if a[i-1] != b[j-1] {
|
||||
cost = 1
|
||||
}
|
||||
|
||||
insertCost := curr[j-1] + 1
|
||||
deleteCost := prev[j] + 1
|
||||
replaceCost := prev[j-1] + cost
|
||||
|
||||
best := insertCost
|
||||
if deleteCost < best {
|
||||
best = deleteCost
|
||||
}
|
||||
if replaceCost < best {
|
||||
best = replaceCost
|
||||
}
|
||||
|
||||
curr[j] = best
|
||||
if best < minInRow {
|
||||
minInRow = best
|
||||
}
|
||||
}
|
||||
|
||||
if minInRow > maxDistance {
|
||||
return false
|
||||
}
|
||||
|
||||
prev = curr
|
||||
}
|
||||
|
||||
return prev[lenB] <= maxDistance
|
||||
}
|
||||
|
||||
func splitArtists(artists string) []string {
|
||||
normalized := artists
|
||||
normalized = strings.ReplaceAll(normalized, " feat. ", "|")
|
||||
@@ -1292,8 +1439,10 @@ func titlesMatch(expectedTitle, foundTitle string) bool {
|
||||
expectedLatin := isLatinScript(expectedTitle)
|
||||
foundLatin := isLatinScript(foundTitle)
|
||||
if expectedLatin != foundLatin {
|
||||
GoLog("[Tidal] Titles in different scripts, assuming match: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||
return true
|
||||
if crossScriptEquivalent(expectedTitle, foundTitle) {
|
||||
GoLog("[Tidal] Titles in different scripts but transliteration matched: '%s' vs '%s'\n", expectedTitle, foundTitle)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
+124
-37
@@ -43,6 +43,16 @@ class _RecentAccessView {
|
||||
});
|
||||
}
|
||||
|
||||
class _CsvImportOptions {
|
||||
final bool confirmed;
|
||||
final bool skipDownloaded;
|
||||
|
||||
const _CsvImportOptions({
|
||||
required this.confirmed,
|
||||
required this.skipDownloaded,
|
||||
});
|
||||
}
|
||||
|
||||
class _HomeTabState extends ConsumerState<HomeTab>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||
final _urlController = TextEditingController();
|
||||
@@ -475,19 +485,116 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
// ignore: use_build_context_synchronously
|
||||
final l10n = context.l10n;
|
||||
|
||||
final options = await showDialog<_CsvImportOptions>(
|
||||
context: this.context,
|
||||
builder: (dialogCtx) {
|
||||
var skipDownloaded = true;
|
||||
return StatefulBuilder(
|
||||
builder: (dialogCtx, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.dialogImportPlaylistTitle),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(l10n.dialogImportPlaylistMessage(tracks.length)),
|
||||
const SizedBox(height: 12),
|
||||
CheckboxListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Skip already downloaded songs'),
|
||||
value: skipDownloaded,
|
||||
onChanged: (value) {
|
||||
setDialogState(() {
|
||||
skipDownloaded = value ?? true;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
dialogCtx,
|
||||
const _CsvImportOptions(
|
||||
confirmed: false,
|
||||
skipDownloaded: true,
|
||||
),
|
||||
),
|
||||
child: Text(l10n.dialogCancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(
|
||||
dialogCtx,
|
||||
_CsvImportOptions(
|
||||
confirmed: true,
|
||||
skipDownloaded: skipDownloaded,
|
||||
),
|
||||
),
|
||||
child: Text(l10n.dialogImport),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (options == null || !options.confirmed) return;
|
||||
|
||||
var tracksToQueue = tracks;
|
||||
var skippedDownloadedCount = 0;
|
||||
|
||||
if (options.skipDownloaded) {
|
||||
final historyState = ref.read(downloadHistoryProvider);
|
||||
tracksToQueue = [];
|
||||
for (final track in tracks) {
|
||||
final isDownloaded =
|
||||
historyState.isDownloaded(track.id) ||
|
||||
(track.isrc != null &&
|
||||
historyState.getByIsrc(track.isrc!) != null);
|
||||
if (isDownloaded) {
|
||||
skippedDownloadedCount++;
|
||||
continue;
|
||||
}
|
||||
tracksToQueue.add(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (tracksToQueue.isEmpty) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.discographySkippedDownloaded(0, skippedDownloadedCount),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
final queueSnackbarMessage = skippedDownloadedCount > 0
|
||||
? l10n.discographySkippedDownloaded(
|
||||
tracksToQueue.length,
|
||||
skippedDownloadedCount,
|
||||
)
|
||||
: l10n.snackbarAddedTracksToQueue(tracksToQueue.length);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (settings.askQualityBeforeDownload) {
|
||||
DownloadServicePicker.show(
|
||||
this.context,
|
||||
trackName: l10n.csvImportTracks(tracks.length),
|
||||
trackName: l10n.csvImportTracks(tracksToQueue.length),
|
||||
artistName: l10n.dialogImportPlaylistTitle,
|
||||
onSelect: (quality, service) {
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addMultipleToQueue(tracks, service, qualityOverride: quality);
|
||||
ref.read(downloadQueueProvider.notifier).addMultipleToQueue(
|
||||
tracksToQueue,
|
||||
service,
|
||||
qualityOverride: quality,
|
||||
);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.snackbarAddedTracksToQueue(tracks.length)),
|
||||
content: Text(queueSnackbarMessage),
|
||||
action: SnackBarAction(
|
||||
label: l10n.snackbarViewQueue,
|
||||
onPressed: () {},
|
||||
@@ -498,39 +605,19 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: this.context,
|
||||
builder: (dialogCtx) => AlertDialog(
|
||||
title: Text(l10n.dialogImportPlaylistTitle),
|
||||
content: Text(l10n.dialogImportPlaylistMessage(tracks.length)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx, false),
|
||||
child: Text(l10n.dialogCancel),
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addMultipleToQueue(tracksToQueue, settings.defaultService);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(queueSnackbarMessage),
|
||||
action: SnackBarAction(
|
||||
label: l10n.snackbarViewQueue,
|
||||
onPressed: () {},
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(dialogCtx, true),
|
||||
child: Text(l10n.dialogImport),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed == true) {
|
||||
ref
|
||||
.read(downloadQueueProvider.notifier)
|
||||
.addMultipleToQueue(tracks, settings.defaultService);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(this.context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.snackbarAddedTracksToQueue(tracks.length)),
|
||||
action: SnackBarAction(
|
||||
label: l10n.snackbarViewQueue,
|
||||
onPressed: () {},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user