Compare commits

..

105 Commits

Author SHA1 Message Date
zarzet 6c3d92cee4 fix: map missing Crowdin locale variants 2026-04-18 23:39:38 +07:00
Zarz Eleutherius d87e0d7e01 Merge pull request #331 from spotiflacapp/merge-l10n-dev-into-main-safe
chore: import l10n updates into main and enable Ukrainian locale
2026-04-18 23:06:35 +07:00
zarzet 86b8709ea1 chore: enable Ukrainian locale on main 2026-04-18 23:02:00 +07:00
zarzet 702b917929 chore: import l10n updates from l10n_dev into main 2026-04-18 22:35:57 +07:00
github-actions[bot] 4b219ad18e chore: update AltStore source to v4.3.1 2026-04-14 14:21:29 +00:00
zarzet 57051bd649 fix: handle .mp4 as alias for .m4a throughout download pipeline 2026-04-14 21:12:14 +07:00
zarzet d6fca6ca55 feat: carry extension download metadata through host pipeline and avoid FLAC-only genre/label pre-embed on non-FLAC files 2026-04-14 21:12:14 +07:00
zarzet 153ec2d9e5 chore: bump version to 4.3.1+126 2026-04-14 21:12:14 +07:00
zarzet be90e85d94 fix: show filter button in all/singles modes when tracks are empty but filters are active 2026-04-14 21:12:14 +07:00
zarzet 4af089f56c feat: improve Tidal metadata (copyright, album artist), remove Qobuz metadata search fallback, fix DATE/YAR tag sync 2026-04-14 21:12:14 +07:00
zarzet 62519d2d1c feat: add preserveNativeOutputExtensions capability for extensions 2026-04-14 21:12:14 +07:00
zarzet 27c0880e87 feat: convert M4A to FLAC when extension doesn't prefer native M4A output
When an extension's preferred output isn't .m4a, downloaded M4A streams
are now automatically converted to FLAC via FFmpeg instead of being
preserved. This applies to both SAF and non-SAF download paths.
2026-04-14 21:12:14 +07:00
zarzet f312b74b30 fix: ensure non-null search provider fallback and update default labels to Tidal
- Add monochrome.tf and samidy.com Tidal API mirrors
- Guarantee resolvedProvider is never null by defaulting to 'tidal'
- Replace stale 'Deezer' default label with 'Tidal' (Deezer moved to extension)
- Show dynamic provider target in auto label for search dropdown
2026-04-14 21:12:14 +07:00
zarzet bd49e307ef fix: reset OutputExt on extension→extension fallback too 2026-04-14 21:12:14 +07:00
zarzet e904a836c1 fix: reset OutputExt on extension→built-in fallback 2026-04-14 21:12:14 +07:00
zarzet 763c9478f1 fix: normalize extension codec for built-in fallback, remove dead Tidal ISRC 2026-04-14 21:12:13 +07:00
zarzet 427bdf74dc chore: reduce Gradle memory, add extension network timeout, fix tr locale 2026-04-14 21:12:13 +07:00
zarzet 373a276c54 fix: respect user provider choice over source extension priority 2026-04-14 21:12:13 +07:00
github-actions[bot] dccadf1f87 chore: update AltStore source to v4.3.1 2026-04-14 13:58:11 +00:00
github-actions[bot] d9933fe038 chore: update AltStore source to v4.3.0 2026-04-13 16:39:57 +00:00
zarzet d47ac0934d chore: bump version to 4.3.0 and fix SAF document file race condition
- Bump app version to 4.3.0 (build 125)
- Extract createOrReuseDocumentFile() to handle SAF auto-rename races
  between findFile() and createFile(), preferring the exact-named sibling
  and discarding duplicate documents
2026-04-13 23:35:03 +07:00
zarzet dbba4d6630 feat: propagate download cancel to extension HTTP requests and fix SAF filename extension mismatch
- Bind cancel context to all extension HTTP calls (fetch, httpGet, httpPost,
  httpRequest, fileDownload, authExchangeCodeWithPKCE) so in-flight requests
  are aborted when user cancels a download
- Make initDownloadCancel idempotent: return existing context if entry already
  exists and preserve pre-cancelled state
- Force SAF output filename to match actual file extension when extension
  returns a different format than requested (e.g. FLAC requested but M4A produced)
- Map ALAC/AAC quality to .m4a instead of falling through to default .flac
2026-04-13 23:35:03 +07:00
zarzet 7405855e01 fix: handle extension oauth callback on ios 2026-04-13 23:35:03 +07:00
zarzet ed020c9303 feat: native M4A ReplayGain tag writing and SAF picker error handling 2026-04-13 23:35:03 +07:00
zarzet 378742e37a refactor: remove author field from extension manifest and UI 2026-04-13 23:35:03 +07:00
zarzet c79bee534e fix: align default search tab layout with primary provider selector using Row+Expanded 2026-04-13 23:35:03 +07:00
zarzet 1d6df75829 fix: preserve existing M4A metadata during embed and enable BuildConfig generation 2026-04-13 23:35:03 +07:00
zarzet b7f51b5f14 feat: expose extension utils, preserve M4A native container, and bump to v4.2.3+124 2026-04-13 23:35:03 +07:00
zarzet 1c8e9df727 feat: add artist search filter and normalize search filter handling 2026-04-13 23:35:03 +07:00
zarzet 01540fe3fc fix: improve ALAC M4A quality parsing 2026-04-13 23:35:03 +07:00
zarzet 071db2f109 refactor: move deezer search flow to extension 2026-04-13 23:35:02 +07:00
zarzet e097d3f605 fix: stabilize shared extension link handling 2026-04-13 23:35:02 +07:00
zarzet 277f783f62 feat: add default search tab preference 2026-04-13 23:35:02 +07:00
zarzet 7637aaf168 fix: fallback extra metadata genre 2026-04-13 23:35:02 +07:00
zarzet c4878470bf chore: thank Ldav Nico and Feuerstern on donate page 2026-04-13 23:35:02 +07:00
zarzet a3725e8c48 feat: add keep android open link 2026-04-13 23:35:02 +07:00
zarzet 917ba842f5 fix: align metadata sanitization and lyrics editing 2026-04-13 23:32:19 +07:00
zarzet dac17ead33 chore: bump app to v4.2.2 2026-04-13 23:32:19 +07:00
zarzet 6845ebe04c refactor: move deezer to extension 2026-04-13 23:32:18 +07:00
zarzet eff709480d fix: preserve flat singles output for extension releases 2026-04-13 23:32:18 +07:00
zarzet 67833424cc fix: align re-enrich matching with autofill metadata 2026-04-13 23:32:18 +07:00
zarzet 5c48e1b476 fix: persist downloaded metadata and refine metadata navigation 2026-04-13 23:32:18 +07:00
zarzet 5e17c9f238 feat: add configurable extension download fallback 2026-04-13 23:32:18 +07:00
zarzet 7d330fb2ec fix: preserve composer metadata across qobuz and history 2026-04-13 23:32:18 +07:00
zarzet cd6a4594fa chore: bump app version to v4.2.1 2026-04-13 23:32:17 +07:00
zarzet bcf727f4ec fix: remove stale audio service manifest entries causing crashes on some devices 2026-04-13 23:32:17 +07:00
zarzet 4c4553913f fix: harden gomobile extension bindings and m4a cover retention 2026-04-13 23:32:17 +07:00
zarzet f0013fac16 fix: preserve local convert format and library entries 2026-04-13 23:32:17 +07:00
zarzet ce4be0ba97 feat: enrich composer and track totals metadata 2026-04-13 23:32:17 +07:00
zarzet 4bac38ef2a fix: preserve embedded metadata details 2026-04-13 23:32:17 +07:00
zarzet 4b213f47d9 ci: pin iOS release builds to macOS 15 and Xcode 26.1.1 2026-04-13 23:32:16 +07:00
zarzet a1010f72f2 fix: patch device_info_plus iOS build for older Xcode SDKs 2026-04-13 23:32:16 +07:00
zarzet 21077a26d0 feat: add additional search/metadata API with separate rate limiting 2026-04-13 23:32:16 +07:00
zarzet b50eec5a47 perf: incremental download queue lookup updates, async cover cleanup, and native JSON decoding on iOS
- Embed DownloadQueueLookup into DownloadQueueState; add updatedForIndices() for O(changed) incremental updates during frequent progress ticks instead of full O(n) rebuild
- downloadQueueLookupProvider now reads pre-computed lookup from state directly
- Replace sync file deletion in DownloadedEmbeddedCoverResolver with unawaited async cleanup to avoid blocking the main thread
- Parse JSON payloads on iOS native side (parseJsonPayload) so event sinks and method channel responses return native objects, avoiding redundant Dart-side JSON decode
- Use .cast<String, dynamic>() instead of Map.from() in _decodeMapResult for zero-copy map handling
2026-04-13 23:32:16 +07:00
zarzet 38a8b715f8 perf: reduce UI jank via memoization, compute isolates, SQL-backed playlist picker, and viewport-aware image caching
- Move explore JSON decode/encode to compute() isolate to avoid blocking main thread
- Memoize search sort results (artists/albums/playlists/tracks) in HomeTab; invalidate on new query
- Extract _DownloadedOrRemoteCover StatefulWidget with proper embedded-cover lifecycle management
- Replace O(playlists x tracks) in-memory playlist picker check with SQL loadPlaylistPickerSummaries query
- Add FutureProvider.family (libraryPlaylistPickerSummariesProvider) invalidated on all playlist mutations
- Memoize _buildQueueHistoryStats, localPathMatchKeys, and localSingleItems in QueueTab
- Add coverCacheWidthForViewport util; apply memCacheWidth/cacheWidth based on real DPR across all album/playlist/track screens
- Convert sync file ops in TrackMetadataScreen to async; use mtime+size as validation token
- Fetch Deezer album nb_tracks in parallel via fetchAlbumTrackCounts
2026-04-13 23:32:16 +07:00
zarzet 2b47537bb5 fix: route Qobuz API calls through authenticated gateway to resolve 401 errors 2026-04-13 23:32:16 +07:00
zarzet a5cf241846 refactor: consolidate FLAC/MP3/Opus metadata embedding into unified _embedMetadataToFile 2026-04-13 23:32:16 +07:00
zarzet 53a4773480 feat: add skipLyrics manifest field for extensions to opt out of lyrics fetching 2026-04-13 23:32:16 +07:00
zarzet 89603af1f1 chore: remove redundant comments and update donor list 2026-04-13 23:32:15 +07:00
zarzet 2143084d3c fix: resolve missing track/disc numbers from search downloads and suppress FFmpeg log noise
- Tidal: use actual API track_number/disc_number when request values are 0
  (fixes search/popular downloads having no track position in metadata)
- Extension enrichment: copy TrackNumber/DiscNumber back from enriched results
- Extension fallback download: add request metadata fallback for non-source
  extensions (Album, AlbumArtist, ReleaseDate, ISRC, TrackNumber, DiscNumber)
- FFmpeg: add -v error -hide_banner to all commands (embed, convert, CUE split)
  to eliminate banner, build config, and full metadata/lyrics dump in logcat
- ebur128: add framelog=quiet to suppress per-frame loudness measurements
  while keeping the summary needed for ReplayGain parsing
- Track metadata screen: separate embedded lyrics check from online fetch,
  show file-only state with manual online fetch button
2026-04-13 23:32:15 +07:00
zarzet 0e265193b8 fix: improve extension runtime safety, HTTP response URL, SongLink parsing, and recommended service for extensions
- Add 'url' field (final URL after redirects) to all extension HTTP responses and fix fetch polyfill to return final URL instead of original request URL
- Fix RunWithTimeout race condition: increase force-timeout from 1s to 60s to prevent concurrent VM access crashes, add nil guards
- Use lockReadyVM() for thread-safe VM access in GetPlaylistWithExtensionJSON and InvokeAction
- Handle mixed JSON types (string, null, array) in SongLink resolve API SongUrls field
- Fix recommended download service not showing for extension-based searches in download picker
2026-04-13 23:32:15 +07:00
zarzet c7e9749ce4 fix: resolve label and copyright from file metadata on info screen
The info screen was not reading label/copyright from the actual file metadata, so these fields were always empty for local library items and download history items that lacked them in-memory. Now _resolveAudioMetadata() extracts and displays them without requiring a manual save first.
2026-04-13 23:32:15 +07:00
zarzet e21cffff0b fix: validate ISRC in track metadata screen to prevent ID leakage
Sanitize the isrc getter to only return valid ISRC codes (12-char format per ISO 3901). Invalid values such as Spotify/Deezer/Tidal IDs that may leak into the ISRC field are now silently discarded, preventing them from being displayed or embedded into file tags.
2026-04-13 23:32:15 +07:00
zarzet d9e20040be fix: correct track/disc defaults, forward extension metadata, and fix service ID display
- Default track/disc number to 0 (unknown) instead of 1, letting the
  backend use the service-provided value or skip the field entirely
- Add releaseDate to ExploreItem so explore downloads carry release info
- Pass discNumber and releaseDate from extension album/playlist tracks
- Fix isDeezer detection using service field instead of substring match
- Add _displayServiceTrackId() to properly strip prefixes for all services
2026-04-13 23:32:15 +07:00
zarzet 6689173525 chore: bump version to 4.2.0 (build 121) 2026-04-13 23:32:14 +07:00
zarzet f37e4704a6 feat: add ReplayGain scanning, APEv2 tag support, and fix metadata bugs
ReplayGain (track + album):
- Scan track loudness via FFmpeg ebur128 filter (-18 LUFS reference)
- Duration-weighted power-mean for album gain computation
- Support for FLAC (native Vorbis), MP3 (ID3v2 TXXX), Opus, M4A
- Album RG auto-finalizes when all album tracks complete
- Retryable gate: blocks finalization while failed/skipped items exist
- SAF support: lossy album RG writes via temp file + writeTempToSaf
- New embedReplayGain setting (off by default) with UI toggle

APEv2 tag support:
- Full APEv2 reader/writer with header+items+footer format
- Merge-based editing with override keys for explicit deletions
- Binary cover art embedding (Cover Art (Front) item)
- Library scanner support for .ape/.wv/.mpc files
- ReplayGain fields in APE read/write/edit pipeline

Bug fixes (26):
- setArtistComments wiping fields on empty string value
- APEv2 rewrite corrupting files with ID3v1 trailer
- APE edit replacing entire tag instead of merging
- ReplayGain lost on manual MP3/Opus/M4A metadata edit
- Editor metadata save losing custom tags (preserveMetadata)
- Album RG accumulator not cleaned on queue mutation
- Album gain using unweighted mean instead of power-mean
- writeAlbumReplayGainTags return value silently ignored
- SAF album RG writing to deleted temp path
- Cancelled tracks polluting album gain computation
- APE ReplayGain not wired end-to-end
- APE field deletion not working in merge
- APE cover edit was a no-op
- Album RG duplicate entries on retry
- APE apeKeysFromFields missing track/disc/lyrics mappings
- Album RG entries purged by removeItem before computation
- FFmpeg converters discarding empty metadata values
- _appendVorbisArtistEntries skipping empty value (null vs empty)
- Album RG write-back fails for SAF lossy files
- Album RG partial finalization on failed tracks
- FLAC ClearEmpty flag destroying tags on partial callers
- clearCompleted not retriggering album RG checks
- ReadFileMetadata MP3/Ogg missing label and copyright
- Cover embed on CUE split destroying split artist tags
- Album RG gain format inconsistent (missing + prefix)
- FLAC reader/editor missing tag aliases (ALBUMARTIST, LABEL, etc.)
- dart:math log shadowed by logger.dart export
2026-04-13 23:32:14 +07:00
zarzet 65dbd5c8e4 fix: remove deleted local library item from provider state after file deletion
When deleting a non-CUE local library track from the metadata screen,
only the file was removed but the library database entry and provider
state were left untouched, causing the track to persist in the library UI.
Now calls removeItem() on localLibraryProvider after deleteFile().
2026-04-13 23:32:14 +07:00
zarzet d034144e9c feat: add resolve API with SongLink fallback, fix multi-artist tags (#288), and cleanup
Resolve API (api.zarz.moe):
- Refactor songlink.go: Spotify URLs use resolve API, non-Spotify uses SongLink API
- Add SongLink fallback when resolve API fails for Spotify (two-layer resilience)
- Remove dead code: page parser, XOR-obfuscated keys, legacy helpers

Multi-artist tag fix (#288):
- Add RewriteSplitArtistTags() in Go to rewrite ARTIST/ALBUMARTIST as split Vorbis comments
- Wire method channel handler in Android (MainActivity.kt) and iOS (AppDelegate.swift)
- Add PlatformBridge.rewriteSplitArtistTags() in Dart
- Call native FLAC rewriter after FFmpeg embed when split_vorbis mode is active
- Extract deezerTrackArtistDisplay() helper to use Contributors in album/playlist tracks

Code cleanup:
- Remove unused imports, dead code, and redundant comments across Go and Dart
- Fix build: remove stale getQobuzDebugKey() reference in deezer_download.go
2026-04-13 23:32:14 +07:00
zarzet 7c4309955e feat: add separate filename format for singles and EPs (#271)
Add singleFilenameFormat setting so singles/EPs can use a different filename template than albums. The format editor is reused with custom title/description. Dart selects the correct format based on track.isSingle before passing to Go, so no backend changes needed. Also fix isSingle getter to include all EPs regardless of totalTracks. Closes #271
2026-04-13 23:32:13 +07:00
zarzet 63e90d13d4 fix: match system navigation bar color with app theme
Set systemNavigationBarColor to surfaceContainer (matching the in-app
NavigationBar) via AppBarTheme.systemOverlayStyle. Handles light, dark,
AMOLED and dynamic color schemes automatically.

Closes zarzet/SpotiFLAC-Mobile#284
2026-04-13 23:32:13 +07:00
zarzet bfb0cad603 feat: add field selection dialog for bulk re-enrich metadata
Add a bottom sheet dialog that lets users choose which metadata field
groups to update during bulk re-enrich (cover, lyrics, album/album
artist, track/disc number, date/ISRC, genre/label/copyright).

Backend (Go):
- Filter FLAC Metadata struct and FFmpeg metadata map by selected
  update_fields so non-selected groups preserve existing file values
- Guard Deezer extended metadata fetch with shouldUpdateField(extra)
- Title/Artist are never overwritten by re-enrich (search keys only)
- enrichedMeta response only includes selected field groups

Frontend (Dart):
- New re_enrich_field_dialog.dart bottom sheet with checkboxes
- FFmpegService embed methods gain preserveMetadata param that uses
  -map_metadata 0 instead of -1 to preserve non-selected tags
- Hide selection overlay/bar before showing dialog, restore on cancel
- Fix setState-after-dispose guard in cancel branches

Cleanup:
- Remove dead code in library_tracks_folder_screen.dart
- Fix use_build_context_synchronously in main_shell.dart
- Suppress false-positive use_null_aware_elements lints
- Update l10n label from 'Title, Artist, Album' to 'Album, Album Artist'
2026-04-13 23:32:13 +07:00
zarzet cc10a917dc fix: prefer local file for cover/lyrics save and update build dependencies
- Cover art: extract from downloaded file first, fall back to URL download
- Lyrics: check embedded lyrics/sidecar LRC before fetching online
- Add audioFilePath param to FetchAndSaveLyrics (Go, Kotlin, Swift, Dart)
- Handle SAF content:// URIs for lyrics extraction in Kotlin bridge
- Update Go 1.25.7 -> 1.25.8, Gradle 9.3.1 -> 9.4.1, Kotlin 2.2.21 -> 2.3.20
- Update NDK r27d -> r28b, Flutter FVM 3.41.4 -> 3.41.5
- Upgrade all Flutter and Go module dependencies to latest
2026-04-13 23:32:13 +07:00
zarzet 5e833c1f75 refactor: remove legacy API clients, Yoinkify fallback, and unused lyrics provider
- Delete dead metadata client and extract shared types to metadata_types.go
- Remove Yoinkify download fallback from Deezer, use MusicDL only
- Clean up retired settings fields and metadataSource
- Remove dead l10n keys for retired provider
- Add migration to strip retired provider from existing users' lyrics config
2026-04-13 23:32:13 +07:00
zarzet 8c576ac7e4 chore: bump version to 4.1.3 (build 120) 2026-04-13 23:32:13 +07:00
zarzet 92160537c0 fix: Samsung SAF library scan, Qobuz album cover, M4A metadata save and log improvements
- Fix M4A/ALAC scan silently failing on Samsung by adding proper fallback
  to scanFromFilename when ReadM4ATags fails (consistent with MP3/FLAC/Ogg)
- Propagate displayNameHint to all format scanners so fd numbers (214, 207)
  no longer appear as track names when /proc/self/fd/ paths are used
- Cache /proc/self/fd/ readability in Kotlin to skip failed attempts after
  first failure, reducing error log noise and improving scan speed on Samsung
- Fix Qobuz download returning wrong album cover when track exists on
  multiple albums by preferring req.CoverURL over API default
- Fix FFmpeg M4A metadata save failing with 'codec not currently supported
  in container' by forcing mp4 muxer instead of ipod when cover art present
- Clean up FLAC SAF temp file after metadata write-back (was leaking)
- Update LRC lyrics tag to credit Paxsenix API
- Remove log message truncation, defer to UI preview truncation instead
2026-04-13 23:32:13 +07:00
zarzet 120ecaa0e5 feat: add artist tag mode setting with split Vorbis support and improve library scan progress
- Add artist_tag_mode setting (joined / split_vorbis) for FLAC/Opus multi-artist tags
- Split 'Artist A, Artist B' into separate ARTIST= Vorbis comments when split mode is enabled
- Join repeated ARTIST/ALBUMARTIST Vorbis comments when reading metadata
- Propagate artistTagMode through download pipeline, re-enrich, and metadata editor
- Improve library scan progress: separate polling intervals, finalizing state, indeterminate progress
- Add initial progress snapshot on library scan stream connect
- Use req.ArtistName consistently for Qobuz downloads instead of track.Performer.Name
- Add l10n keys for artist tag mode, library files unit, and scan finalizing status
2026-04-13 23:32:12 +07:00
zarzet fd3a34303e feat: add stable cover cache keys, Qobuz album-search fallback, metadata filters and extended sort options
- Introduce coverCacheKey parameter through Go backend and Kotlin bridge for stable SAF cover caching
- Add MetadataFromFilename flag to skip filename-only metadata and retry via temp-file copy
- Add Qobuz album-search fallback between API search and store scraping
- Extract buildReEnrichFFmpegMetadata to skip empty metadata fields
- Add metadata completeness filter (complete, missing year/genre/album artist)
- Add sort modes: artist, album, release date, genre (asc/desc)
- Prune stale library cover cache files after full scan
- Skip empty values and zero track/disc numbers in FFmpeg metadata
- Add new l10n keys for metadata filter and sort options
2026-04-13 23:32:12 +07:00
zarzet d89b70e155 fix: use Tidal quality options as fallback instead of DEFAULT for extensions 2026-04-13 23:32:12 +07:00
zarzet e3b63c1d27 fix: normalize DEFAULT quality to prevent Tidal/Qobuz API failures 2026-04-13 23:32:12 +07:00
zarzet 96301c0dbf feat: replace batch operation snackbars with progress dialog
Add reusable BatchProgressDialog widget with circular/linear progress
indicators, cancel support, and track detail display. Uses ValueNotifier
pattern to communicate progress from caller to dialog across navigator
routes.
2026-04-13 23:32:12 +07:00
zarzet a2458c1292 refactor: extract and improve ReEnrich track selection with scoring-based matching 2026-04-13 23:32:11 +07:00
zarzet 1737e12dd2 fix: add attached_pic disposition to ALAC cover art embedding 2026-04-13 23:32:11 +07:00
zarzet b770d7d9ca i18n: extract hardcoded strings into l10n keys
Move hardcoded UI strings across multiple screens and the notification
service into ARB-backed l10n keys so they can be translated via Crowdin.
Adds 62 new keys covering sort labels, dialog copy, metadata error
snackbars, folder-picker errors, home-tab error states, extensions home
feed selector, and all notification titles/bodies. NotificationService
now caches an AppLocalizations instance (injected from MainShell via
didChangeDependencies) and falls back to English literals when no locale
is available.
2026-04-13 23:32:11 +07:00
zarzet b712b9f509 refactor: route spotify URLs through extensions 2026-04-13 23:32:11 +07:00
zarzet 51496cd34e chore: bump version to 4.1.2+119 2026-04-13 23:32:11 +07:00
zarzet 2b2c2bc90a feat: improve track matching 2026-04-13 23:32:11 +07:00
zarzet e2a489ec92 feat: add haptic feedback when swiping library tabs 2026-04-13 23:32:11 +07:00
zarzet 4f46dd947d feat: add play button to playlist/library track tiles
Show a play IconButton (matching local album style) next to the
more-options button when a track has a local file available.
Uses PlaybackController.playTrackList to resolve and open the file.
2026-04-13 23:32:11 +07:00
zarzet fbb8d30db0 fix: use START_NOT_STICKY for DownloadService to prevent auto-restart
Prevents Android from automatically recreating the download service
after it is killed, avoiding duplicate or orphaned download processes.
2026-04-13 23:32:10 +07:00
Zarz Eleutherius c0637006af Merge pull request #313 from AlexRabbit/main
Extension OAuth + store: flatten action JSON, open auth URLs, spotifl…
2026-04-13 23:30:56 +07:00
Alex 3fc371b8c4 Extension OAuth + store: flatten action JSON, open auth URLs, spotiflac:// callback
Third-party extensions (e.g. Spotify PKCE addons) need three things the current app does not fully provide:

Extension button results – The Go runtime returned { success, result: { message, open_auth_url, … } } while Flutter read message / open_auth_url only on the outer map, so OAuth buttons appeared to do nothing. InvokeAction now merges the extension’s return object onto the top-level JSON (arrays/non-objects still use result).

Flutter – extension_detail_page: unwrap nested result for compatibility, merge setting_updates into saved extension settings (for copyable OAuth URLs), and launchUrl when open_auth_url is set.

Mobile OAuth return – spotiflac://callback?code=…&state=<extension_id> was not handled on Android (manifest + MainActivity) or iOS (AppDelegate open URL + cold-start launchOptions). This wires SetExtensionAuthCodeByID + invokeExtensionAction(..., "completeSpotifyLogin") so PKCE extensions can finish login after the browser redirect.

Extension store HTTP – Add Cache-Control: no-cache on registry and extension package downloads to reduce stale CDN/proxy responses.

Testing: Install a metadata extension that uses PKCE; tap Connect; confirm browser opens, return via spotiflac://callback, and tokens complete without pasting the code manually.

extension InvokeAction JSON was nested under result while the Flutter settings UI only read the top level, so OAuth-related buttons never showed messages or opened the browser. This PR flattens that payload, merges optional setting_updates, launches open_auth_url, adds spotiflac://callback handling on Android and iOS, and sends no-cache on store HTTP fetches. Needed for extensions like SpoitiLists (Spotify Web API + PKCE).
2026-04-12 02:40:31 -06:00
Zarz Eleutherius ee5b3824e9 Merge pull request #297 from mikropsoft/patch-1
Update app_tr.arb
2026-04-09 16:29:33 +07:00
github-actions[bot] be6a856773 chore: update AltStore source to v4.2.2 2026-04-06 07:38:35 +00:00
github-actions[bot] e41c299d49 chore: update AltStore source to v4.2.1 2026-04-04 15:02:40 +00:00
Zarz Eleutherius 981786b4a2 Merge pull request #298 from Amonoman/main
Center images in layout
2026-04-04 19:06:24 +07:00
Amonoman eeb6f11808 Center the images 2026-04-04 13:27:33 +02:00
𝗛𝗼𝗹𝗶 8e361e14b4 Update app_tr.arb 2026-04-04 12:05:33 +03:00
github-actions[bot] d58d46eb1f chore: update AltStore source to v4.2.0 2026-04-04 09:04:47 +00:00
Zarz Eleutherius 562a17f7ae Merge pull request #295 from Amonoman/main
Update logo
2026-04-04 01:24:34 +07:00
Amonoman b035e66540 Update logo 2026-04-03 18:28:16 +02:00
github-actions[bot] 38792a753e chore: update AltStore source to v4.1.3 2026-03-30 11:43:03 +00:00
github-actions[bot] d5b34b4f15 chore: update AltStore source to v4.1.2 2026-03-29 11:31:37 +00:00
github-actions[bot] 2a45c8dcdb chore: update AltStore source to v4.1.1 2026-03-27 15:47:00 +00:00
zarzet e7a2166a4f Merge branch 'dev' 2026-03-27 22:34:15 +07:00
github-actions[bot] f54597e655 chore: update AltStore source to v4.1.0 2026-03-26 10:47:40 +00:00
55 changed files with 30684 additions and 1240 deletions
+1 -1
View File
@@ -168,7 +168,7 @@ Interested in contributing? Check out the [Contributing Guide](CONTRIBUTING.md)
|---|---|---|---|---|
| [hifi-api](https://github.com/binimum/hifi-api) | [music.binimum.org](https://music.binimum.org) | [qqdl.site](https://qqdl.site) | [squid.wtf](https://squid.wtf) | [spotisaver.net](https://spotisaver.net) |
| [dabmusic.xyz](https://dabmusic.xyz) | [AfkarXYZ](https://github.com/afkarxyz) | [LRCLib](https://lrclib.net) | [Paxsenix](https://lyrics.paxsenix.org) | [Cobalt](https://cobalt.tools) |
| [qwkuns.me](https://qwkuns.me) | [SpotubeDL](https://spotubedl.com) | [Song.link](https://song.link) | [IDHS](https://github.com/sjdonado/idonthavespotify) | |
| [qwkuns.me](https://qwkuns.me) | [SpotubeDL](https://spotubedl.com) | [Song.link](https://song.link) | [IDHS](https://github.com/sjdonado/idonthavespotify) | [Monochrome](https://monochrome.tf) |
---
+1 -1
View File
@@ -1,2 +1,2 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:ReservedCodeCacheSize=256m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
+4 -4
View File
@@ -7,12 +7,12 @@
"name": "SpotiFLAC",
"bundleIdentifier": "com.zarzet.spotiflac",
"developerName": "zarzet",
"version": "3.9.0",
"versionDate": "2026-03-25",
"downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v3.9.0/SpotiFLAC-v3.9.0-ios-unsigned.ipa",
"version": "4.3.1",
"versionDate": "2026-04-14",
"downloadURL": "https://github.com/zarzet/SpotiFLAC-Mobile/releases/download/v4.3.1/SpotiFLAC-v4.3.1-ios-unsigned.ipa",
"localizedDescription": "Mobile version of SpotiFLAC written in Flutter. Download Tracks in true FLAC from Tidal, Qobuz, & Amazon Music.",
"iconURL": "https://raw.githubusercontent.com/zarzet/SpotiFLAC-Mobile/main/assets/images/logo.png",
"size": 34477323
"size": 34773644
}
]
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

+5
View File
@@ -6,6 +6,7 @@ files:
# Short codes for single-variant languages
de: de
es: es
es-ES: es_ES
fr: fr
hi: hi
id: id
@@ -13,7 +14,11 @@ files:
ko: ko
nl: nl
pt: pt
pt-PT: pt_PT
ru: ru
tr: tr
uk: uk
zh: zh
# Full codes for Chinese variants
zh-CN: zh_CN
zh-TW: zh_TW
+66 -2
View File
@@ -118,9 +118,16 @@ type ExtDownloadResult struct {
AlbumArtist string `json:"album_artist,omitempty"`
TrackNumber int `json:"track_number,omitempty"`
DiscNumber int `json:"disc_number,omitempty"`
TotalTracks int `json:"total_tracks,omitempty"`
TotalDiscs int `json:"total_discs,omitempty"`
ReleaseDate string `json:"release_date,omitempty"`
CoverURL string `json:"cover_url,omitempty"`
ISRC string `json:"isrc,omitempty"`
Genre string `json:"genre,omitempty"`
Label string `json:"label,omitempty"`
Copyright string `json:"copyright,omitempty"`
Composer string `json:"composer,omitempty"`
LyricsLRC string `json:"lyrics_lrc,omitempty"`
DecryptionKey string `json:"decryption_key,omitempty"`
Decryption *DownloadDecryptionInfo `json:"decryption,omitempty"`
}
@@ -951,6 +958,19 @@ func isBuiltInDownloadProvider(providerID string) bool {
}
}
func normalizeQualityForBuiltIn(quality string) string {
switch strings.ToLower(strings.TrimSpace(quality)) {
case "alac", "hi_res_lossless", "lossless":
return "HI_RES_LOSSLESS"
case "atmos", "ac3", "dolby_atmos":
return "LOSSLESS"
case "aac", "aac-legacy":
return "LOSSLESS"
default:
return quality
}
}
func normalizeBuiltInMetadataTrack(track TrackMetadata, providerID string) ExtTrackMetadata {
deezerID := ""
tidalID := ""
@@ -1319,8 +1339,8 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if req.Source != "" &&
!isBuiltInProvider(strings.ToLower(req.Source)) &&
(!strictMode || selectedProvider == "" || strings.EqualFold(selectedProvider, req.Source)) {
GoLog("[DownloadWithExtensionFallback] Track source is extension '%s', trying it first\n", req.Source)
selectedProvider == req.Source {
GoLog("[DownloadWithExtensionFallback] Track source is extension '%s' matching selected provider, trying it first\n", req.Source)
ext, err := extManager.GetExtension(req.Source)
if err == nil && ext.Enabled && ext.Error == "" && ext.Manifest.IsDownloadProvider() {
@@ -1402,6 +1422,12 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if result.DiscNumber > 0 {
resp.DiscNumber = result.DiscNumber
}
if result.TotalTracks > 0 {
resp.TotalTracks = result.TotalTracks
}
if result.TotalDiscs > 0 {
resp.TotalDiscs = result.TotalDiscs
}
if result.ReleaseDate != "" {
resp.ReleaseDate = result.ReleaseDate
}
@@ -1411,8 +1437,29 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if result.ISRC != "" {
resp.ISRC = result.ISRC
}
if result.Genre != "" {
resp.Genre = result.Genre
}
if result.Label != "" {
resp.Label = result.Label
}
if result.Copyright != "" {
resp.Copyright = result.Copyright
}
if result.Composer != "" {
resp.Composer = result.Composer
}
if result.LyricsLRC != "" {
resp.LyricsLRC = result.LyricsLRC
}
}
if req.TrackName != "" && resp.Title == "" {
resp.Title = req.TrackName
}
if req.ArtistName != "" && resp.Artist == "" {
resp.Artist = req.ArtistName
}
if req.AlbumName != "" && resp.Album == "" {
resp.Album = req.AlbumName
}
@@ -1431,9 +1478,18 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
if req.DiscNumber > 0 && resp.DiscNumber == 0 {
resp.DiscNumber = req.DiscNumber
}
if req.TotalTracks > 0 && resp.TotalTracks == 0 {
resp.TotalTracks = req.TotalTracks
}
if req.TotalDiscs > 0 && resp.TotalDiscs == 0 {
resp.TotalDiscs = req.TotalDiscs
}
if req.CoverURL != "" && resp.CoverURL == "" {
resp.CoverURL = req.CoverURL
}
if req.Composer != "" && resp.Composer == "" {
resp.Composer = req.Composer
}
return resp, nil
}
@@ -1490,13 +1546,17 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
GoLog("[DownloadWithExtensionFallback] Trying provider: %s\n", providerID)
if isBuiltInDownloadProvider(providerIDNormalized) {
req.OutputExt = ""
if (req.Genre == "" || req.Label == "" || req.Copyright == "") &&
req.ISRC != "" {
GoLog("[DownloadWithExtensionFallback] Enriching extra metadata from ISRC: %s\n", req.ISRC)
enrichExtraMetadataByISRC("DownloadWithExtensionFallback", req.ISRC, &req.Genre, &req.Label, &req.Copyright)
}
origQuality := req.Quality
req.Quality = normalizeQualityForBuiltIn(req.Quality)
result, err := tryBuiltInProvider(providerIDNormalized, req)
req.Quality = origQuality
if err == nil && result.Success {
result.Service = providerIDNormalized
if req.Label != "" {
@@ -1547,6 +1607,7 @@ func DownloadWithExtensionFallback(req DownloadRequest) (*DownloadResponse, erro
continue
}
req.OutputExt = ""
outputPath := buildOutputPathForExtension(req, ext)
if req.ItemID != "" {
StartItemProgress(req.ItemID)
@@ -1859,6 +1920,9 @@ func canEmbedGenreLabel(filePath string) bool {
if path == "" || strings.HasPrefix(path, "content://") || strings.HasPrefix(path, "/proc/self/fd/") {
return false
}
if strings.ToLower(filepath.Ext(path)) != ".flac" {
return false
}
if !filepath.IsAbs(path) {
return false
}
+7
View File
@@ -185,6 +185,10 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) {
if err := os.WriteFile(tempFile, []byte("fLaC"), 0644); err != nil {
t.Fatalf("failed to create temp file: %v", err)
}
tempM4A := filepath.Join(t.TempDir(), "track.m4a")
if err := os.WriteFile(tempM4A, []byte("not-flac"), 0644); err != nil {
t.Fatalf("failed to create temp m4a file: %v", err)
}
if canEmbedGenreLabel("relative.flac") {
t.Fatal("expected relative path to be rejected")
@@ -195,6 +199,9 @@ func TestCanEmbedGenreLabelRequiresExistingAbsoluteLocalFile(t *testing.T) {
if canEmbedGenreLabel(filepath.Join(t.TempDir(), "missing.flac")) {
t.Fatal("expected missing file to be rejected")
}
if canEmbedGenreLabel(tempM4A) {
t.Fatalf("expected non-FLAC file %q to be rejected", tempM4A)
}
if !canEmbedGenreLabel(tempFile) {
t.Fatalf("expected existing absolute file %q to be accepted", tempFile)
}
+50 -1
View File
@@ -5,6 +5,7 @@ import (
"net"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
@@ -136,12 +137,60 @@ func newExtensionRuntime(ext *loadedExtension) *extensionRuntime {
storageFlushDelay: defaultStorageFlushDelay,
}
runtime.httpClient = newExtensionHTTPClient(ext, jar, 30*time.Second)
runtime.httpClient = newExtensionHTTPClient(ext, jar, extensionHTTPTimeout(ext, 30*time.Second))
runtime.downloadClient = newExtensionHTTPClient(ext, jar, DownloadTimeout)
return runtime
}
func extensionHTTPTimeout(ext *loadedExtension, fallback time.Duration) time.Duration {
if ext == nil || ext.Manifest == nil || ext.Manifest.Capabilities == nil {
return fallback
}
raw, ok := ext.Manifest.Capabilities["networkTimeoutSeconds"]
if !ok {
return fallback
}
seconds := parseExtensionTimeoutSeconds(raw)
if seconds <= 0 {
return fallback
}
if seconds < 5 {
seconds = 5
}
if seconds > 300 {
seconds = 300
}
return time.Duration(seconds) * time.Second
}
func parseExtensionTimeoutSeconds(raw interface{}) int {
switch v := raw.(type) {
case int:
return v
case int32:
return int(v)
case int64:
return int(v)
case float32:
return int(v)
case float64:
return int(v)
case string:
parsed, err := strconv.Atoi(strings.TrimSpace(v))
if err != nil {
return 0
}
return parsed
default:
return 0
}
}
func (r *extensionRuntime) setActiveDownloadItemID(itemID string) {
r.activeDownloadMu.Lock()
defer r.activeDownloadMu.Unlock()
+1 -10
View File
@@ -2655,17 +2655,8 @@ func resolveQobuzTrackForRequest(req DownloadRequest, downloader *QobuzDownloade
}
}
// Strategy 5: Metadata search with strict matching (duration tolerance: 10 seconds)
if track == nil {
GoLog("[%s] Trying metadata search: '%s' by '%s'\n", logPrefix, req.TrackName, req.ArtistName)
track, err = qobuzSearchTrackByMetadataWithDurationFunc(downloader, req.TrackName, req.ArtistName, expectedDurationSec)
if track != nil && !qobuzTrackMatchesRequest(req, track, logPrefix, "metadata search", false) {
track = nil
}
}
if track == nil {
errMsg := "could not find matching track on Qobuz (artist/duration mismatch)"
errMsg := "could not find matching track on Qobuz without identifier match"
if err != nil {
errMsg = err.Error()
}
+7 -9
View File
@@ -429,11 +429,9 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) {
t.Fatal("ISRC fallback should not run without an ISRC")
return nil, nil
}
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, trackName, artistName string, expectedDurationSec int) (*QobuzTrack, error) {
if trackName != "Taste Back" || artistName != "Harry Styles" || expectedDurationSec != 181 {
t.Fatalf("unexpected metadata fallback arguments: %q / %q / %d", trackName, artistName, expectedDurationSec)
}
return testQobuzTrack(444, "Taste Back", "Harry Styles", 181), nil
qobuzSearchTrackByMetadataWithDurationFunc = func(_ *QobuzDownloader, _, _ string, _ int) (*QobuzTrack, error) {
t.Fatal("metadata fallback should not run")
return nil, nil
}
songLinkCheckTrackAvailabilityFunc = func(_ *SongLinkClient, _, _ string) (*TrackAvailability, error) {
t.Fatal("SongLink should not run when Odesli QobuzID is provided")
@@ -448,11 +446,11 @@ func TestResolveQobuzTrackForRequestRejectsOdesliMismatch(t *testing.T) {
}
track, err := resolveQobuzTrackForRequest(req, &QobuzDownloader{}, "Test")
if err != nil {
t.Fatalf("expected no error, got %v", err)
if err == nil {
t.Fatalf("expected error, got track %+v", track)
}
if track == nil || track.ID != 444 || track.Title != "Taste Back" {
t.Fatalf("unexpected resolved track: %+v", track)
if track != nil {
t.Fatalf("expected nil track, got %+v", track)
}
}
+39 -15
View File
@@ -51,6 +51,7 @@ type TidalTrack struct {
ID int64 `json:"id"`
Title string `json:"title"`
ISRC string `json:"isrc"`
Copyright string `json:"copyright"`
AudioQuality string `json:"audioQuality"`
TrackNumber int `json:"trackNumber"`
VolumeNumber int `json:"volumeNumber"`
@@ -135,6 +136,7 @@ type tidalPublicAlbum struct {
Type string `json:"type"`
Cover string `json:"cover"`
ReleaseDate string `json:"releaseDate"`
Copyright string `json:"copyright"`
URL string `json:"url"`
NumberOfTracks int `json:"numberOfTracks"`
Explicit bool `json:"explicit"`
@@ -306,6 +308,29 @@ func tidalTrackArtistsDisplay(track *TidalTrack) string {
return strings.TrimSpace(track.Artist.Name)
}
func tidalTrackAlbumArtistDisplay(track *TidalTrack) string {
if track == nil {
return ""
}
if len(track.Artists) > 0 {
names := make([]string, 0, len(track.Artists))
for _, artist := range track.Artists {
if strings.ToUpper(strings.TrimSpace(artist.Type)) != "MAIN" {
continue
}
if trimmed := strings.TrimSpace(artist.Name); trimmed != "" {
names = append(names, trimmed)
}
}
if len(names) > 0 {
return strings.Join(names, ", ")
}
}
return strings.TrimSpace(track.Artist.Name)
}
func tidalAlbumArtistsDisplay(album *tidalPublicAlbum) string {
if album == nil {
return ""
@@ -354,7 +379,7 @@ func tidalTrackToTrackMetadata(track *TidalTrack) TrackMetadata {
Artists: tidalTrackArtistsDisplay(track),
Name: strings.TrimSpace(track.Title),
AlbumName: strings.TrimSpace(track.Album.Title),
AlbumArtist: strings.TrimSpace(track.Artist.Name),
AlbumArtist: tidalTrackAlbumArtistDisplay(track),
DurationMS: track.Duration * 1000,
Images: tidalImageURL(track.Album.Cover, "1280x1280"),
ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate),
@@ -377,7 +402,7 @@ func tidalTrackToAlbumTrackMetadata(track *TidalTrack) AlbumTrackMetadata {
Artists: tidalTrackArtistsDisplay(track),
Name: strings.TrimSpace(track.Title),
AlbumName: strings.TrimSpace(track.Album.Title),
AlbumArtist: strings.TrimSpace(track.Artist.Name),
AlbumArtist: tidalTrackAlbumArtistDisplay(track),
DurationMS: track.Duration * 1000,
Images: tidalImageURL(track.Album.Cover, "1280x1280"),
ReleaseDate: strings.TrimSpace(track.Album.ReleaseDate),
@@ -407,6 +432,7 @@ func tidalAlbumToAlbumInfo(album *tidalPublicAlbum) AlbumInfoMetadata {
Artists: tidalAlbumArtistsDisplay(album),
ArtistId: artistID,
Images: tidalImageURL(album.Cover, "1280x1280"),
Copyright: strings.TrimSpace(album.Copyright),
}
}
@@ -688,6 +714,10 @@ func findTidalArtistPageModule(page *tidalPublicArtistPage, moduleType string) *
func (t *TidalDownloader) GetAvailableAPIs() []string {
return []string{
"https://eu-central.monochrome.tf",
"https://us-west.monochrome.tf",
"https://api.monochrome.tf",
"https://monochrome-api.samidy.com",
"https://tidal-api.binimum.org",
"https://tidal.kinoplus.online",
"https://triton.squid.wtf",
@@ -1736,6 +1766,7 @@ type TidalDownloadResult struct {
TrackNumber int
DiscNumber int
ISRC string
Copyright string
LyricsLRC string // LRC content for embedding in converted files
}
@@ -2049,18 +2080,6 @@ func resolveTidalTrackForRequest(req DownloadRequest, downloader *TidalDownloade
}
}
if !gotTidalID && req.ISRC != "" {
GoLog("[%s] Trying direct Tidal ISRC search: %s\n", logPrefix, req.ISRC)
directTrack, directErr := downloader.SearchTrackByISRC(req.ISRC)
if directErr == nil && directTrack != nil && directTrack.ID > 0 {
trackID = directTrack.ID
gotTidalID = true
GoLog("[%s] Got Tidal ID %d from direct ISRC search\n", logPrefix, trackID)
} else if directErr != nil {
GoLog("[%s] Direct Tidal ISRC search failed: %v\n", logPrefix, directErr)
}
}
if !gotTidalID && req.ISRC != "" && req.TrackName != "" && req.ArtistName != "" {
GoLog("[%s] Trying Tidal public metadata search with ISRC\n", logPrefix)
searchTrack, searchErr := downloader.SearchTrackByMetadataWithISRC(
@@ -2356,6 +2375,10 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
if actualDiscNumber == 0 {
actualDiscNumber = track.VolumeNumber
}
copyright := strings.TrimSpace(req.Copyright)
if copyright == "" {
copyright = strings.TrimSpace(track.Copyright)
}
metadata := Metadata{
Title: req.TrackName,
@@ -2371,7 +2394,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
ISRC: track.ISRC,
Genre: req.Genre,
Label: req.Label,
Copyright: req.Copyright,
Copyright: copyright,
Composer: req.Composer,
}
@@ -2482,6 +2505,7 @@ func downloadFromTidal(req DownloadRequest) (TidalDownloadResult, error) {
TrackNumber: resultTrackNumber,
DiscNumber: resultDiscNumber,
ISRC: track.ISRC,
Copyright: copyright,
LyricsLRC: lyricsLRC,
}, nil
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+2 -2
View File
@@ -3,8 +3,8 @@ import 'package:flutter/foundation.dart';
/// App version and info constants
/// Update version here only - all other files will reference this
class AppInfo {
static const String version = '4.3.0';
static const String buildNumber = '125';
static const String version = '4.3.1';
static const String buildNumber = '126';
static const String fullVersion = '$version+$buildNumber';
/// Shows "Internal" in debug builds, actual version in release.
+5
View File
@@ -17,6 +17,7 @@ import 'app_localizations_nl.dart';
import 'app_localizations_pt.dart';
import 'app_localizations_ru.dart';
import 'app_localizations_tr.dart';
import 'app_localizations_uk.dart';
import 'app_localizations_zh.dart';
// ignore_for_file: type=lint
@@ -119,6 +120,7 @@ abstract class AppLocalizations {
Locale('pt', 'PT'),
Locale('ru'),
Locale('tr'),
Locale('uk'),
Locale('zh'),
Locale('zh', 'CN'),
Locale('zh', 'TW'),
@@ -5857,6 +5859,7 @@ class _AppLocalizationsDelegate
'pt',
'ru',
'tr',
'uk',
'zh',
].contains(locale.languageCode);
@@ -5921,6 +5924,8 @@ AppLocalizations lookupAppLocalizations(Locale locale) {
return AppLocalizationsRu();
case 'tr':
return AppLocalizationsTr();
case 'uk':
return AppLocalizationsUk();
case 'zh':
return AppLocalizationsZh();
}
+139 -136
View File
@@ -21,13 +21,13 @@ class AppLocalizationsDe extends AppLocalizations {
String get navSettings => 'Einstellungen';
@override
String get navStore => 'Store';
String get navStore => 'Repo';
@override
String get homeTitle => 'Startseite';
@override
String get homeSubtitle => 'Spotify-Link einfügen oder nach Namen suchen';
String get homeSubtitle => 'Unterstützte URL einfügen oder nach Namen suchen';
@override
String get homeSupports =>
@@ -184,21 +184,21 @@ class AppLocalizationsDe extends AppLocalizations {
'Disabled: no loudness normalization tags';
@override
String get optionsArtistTagMode => 'Artist Tag Mode';
String get optionsArtistTagMode => 'Künstler Tag-Modus';
@override
String get optionsArtistTagModeDescription =>
'Choose how multiple artists are written into embedded tags.';
'Wähle aus, wie mehrere Künstler in eingebetteten Tags geschrieben sind.';
@override
String get optionsArtistTagModeJoined => 'Single joined value';
String get optionsArtistTagModeJoined => 'Einzelne beigefügte Werte';
@override
String get optionsArtistTagModeJoinedSubtitle =>
'Write one ARTIST value like \"Artist A, Artist B\" for maximum player compatibility.';
'Einen Künstler wert wie \"Artist A, Artist B\" für maximale Spieler-Kompatibilität schreiben.';
@override
String get optionsArtistTagModeSplitVorbis => 'Split tags for FLAC/Opus';
String get optionsArtistTagModeSplitVorbis => 'Tags für FLAC/Opus aufteilen';
@override
String get optionsArtistTagModeSplitVorbisSubtitle =>
@@ -220,11 +220,11 @@ class AppLocalizationsDe extends AppLocalizations {
'Parallele Downloads können Ratenlimitierung auslösen';
@override
String get optionsExtensionStore => 'Erweiterungs-Store';
String get optionsExtensionStore => 'Erweiterungs-Repo';
@override
String get optionsExtensionStoreSubtitle =>
'Store-Tab in Navigation anzeigen';
'Repo-Tab in der Navigation anzeigen';
@override
String get optionsCheckUpdates => 'Nach Updates suchen';
@@ -303,7 +303,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get extensionsUninstall => 'Deinstallieren';
@override
String get storeTitle => 'Erweiterungs-Store';
String get storeTitle => 'Erweiterungs-Repo';
@override
String get storeSearch => 'Erweiterungen suchen...';
@@ -586,7 +586,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get dialogImport => 'Importieren';
@override
String get dialogDownload => 'Download';
String get dialogDownload => 'Herunterladen';
@override
String get dialogDiscard => 'Verwerfen';
@@ -819,37 +819,37 @@ class AppLocalizationsDe extends AppLocalizations {
String get searchAlbums => 'Alben';
@override
String get searchPlaylists => 'Playlisten';
String get searchPlaylists => 'Playlists';
@override
String get searchSortTitle => 'Sort Results';
String get searchSortTitle => 'Ergebnisse sortieren';
@override
String get searchSortDefault => 'Default';
String get searchSortDefault => 'Standard';
@override
String get searchSortTitleAZ => 'Title (A-Z)';
String get searchSortTitleAZ => 'Titel (A-Z)';
@override
String get searchSortTitleZA => 'Title (Z-A)';
String get searchSortTitleZA => 'Titel (Z-A)';
@override
String get searchSortArtistAZ => 'Artist (A-Z)';
String get searchSortArtistAZ => 'Künstler (A-Z)';
@override
String get searchSortArtistZA => 'Artist (Z-A)';
String get searchSortArtistZA => 'Künstler (Z-A)';
@override
String get searchSortDurationShort => 'Duration (Shortest)';
String get searchSortDurationShort => 'Dauer (kürzeste)';
@override
String get searchSortDurationLong => 'Duration (Longest)';
String get searchSortDurationLong => 'Dauer (längste)';
@override
String get searchSortDateOldest => 'Release Date (Oldest)';
String get searchSortDateOldest => 'Veröffentlichungsdatum (älteste)';
@override
String get searchSortDateNewest => 'Release Date (Newest)';
String get searchSortDateNewest => 'Veröffentlichungsdatum (Neueste)';
@override
String get tooltipPlay => 'Abspielen';
@@ -1315,36 +1315,36 @@ class AppLocalizationsDe extends AppLocalizations {
String get storeClearFilters => 'Filter entfernen';
@override
String get storeAddRepoTitle => 'Add Extension Repository';
String get storeAddRepoTitle => 'Erweiterungs-Repository hinzufügen';
@override
String get storeAddRepoDescription =>
'Enter a GitHub repository URL that contains a registry.json file to browse and install extensions.';
'Gib eine GitHub Repository-URL ein, die eine Registry.json Datei enthält, um Erweiterungen zu durchsuchen und zu installieren.';
@override
String get storeRepoUrlLabel => 'Repository URL';
String get storeRepoUrlLabel => 'Repository-URL';
@override
String get storeRepoUrlHint => 'https://github.com/user/repo';
@override
String get storeRepoUrlHelper =>
'e.g. https://github.com/user/extensions-repo';
'z.B. https://github.com/user/extensions-repo';
@override
String get storeAddRepoButton => 'Add Repository';
String get storeAddRepoButton => 'Repository hinzufügen';
@override
String get storeChangeRepoTooltip => 'Change repository';
String get storeChangeRepoTooltip => 'Repository ändern';
@override
String get storeRepoDialogTitle => 'Extension Repository';
String get storeRepoDialogTitle => 'Erweiterungs-Repository';
@override
String get storeRepoDialogCurrent => 'Current repository:';
String get storeRepoDialogCurrent => 'Aktuelles Repository:';
@override
String get storeNewRepoUrlLabel => 'New Repository URL';
String get storeNewRepoUrlLabel => 'Neue Repository-URL';
@override
String get storeLoadError => 'Failed to load repository';
@@ -1356,7 +1356,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Standard (Deezer/Spotify)';
String get extensionDefaultProvider => 'Standard (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Eingebaute Suche verwenden';
@@ -1517,36 +1517,36 @@ class AppLocalizationsDe extends AppLocalizations {
String get qualityHiResFlacMaxSubtitle => '24-Bit / bis 192kHz';
@override
String get downloadLossy320 => 'Lossy 320kbps';
String get downloadLossy320 => 'Verlustbehaftet 320kbps';
@override
String get downloadLossyFormat => 'Lossy Format';
String get downloadLossyFormat => 'Verlustbehaftetes Format';
@override
String get downloadLossy320Format => 'Lossy 320kbps Format';
@override
String get downloadLossy320FormatDesc =>
'Choose the output format for Tidal 320kbps lossy downloads. The original AAC stream will be converted to your selected format.';
'Wähle das Ausgabeformat für Tidal 320kbps verlustbehaftete Downloads. Der ursprüngliche AAC Stream wird in das ausgewählte Format konvertiert.';
@override
String get downloadLossyMp3 => 'MP3 320kbps';
@override
String get downloadLossyMp3Subtitle => 'Best compatibility, ~10MB per track';
String get downloadLossyMp3Subtitle =>
'Beste Kompatibilität, ~10MB pro Titel';
@override
String get downloadLossyOpus256 => 'Opus 256kbps';
@override
String get downloadLossyOpus256Subtitle =>
'Best quality Opus, ~8MB per track';
String get downloadLossyOpus256Subtitle => 'Beste Qualität, ~8MB pro Titel';
@override
String get downloadLossyOpus128 => 'Opus 128kbps';
@override
String get downloadLossyOpus128Subtitle => 'Smallest size, ~4MB per track';
String get downloadLossyOpus128Subtitle => 'Kleinste Größe, ~4MB pro Track';
@override
String get qualityNote =>
@@ -1856,23 +1856,23 @@ class AppLocalizationsDe extends AppLocalizations {
'Bei der Suche nach vorhandenen Titeln anzeigen';
@override
String get libraryAutoScan => 'Auto Scan';
String get libraryAutoScan => 'Auto-Scan';
@override
String get libraryAutoScanSubtitle =>
'Automatically scan your library for new files';
@override
String get libraryAutoScanOff => 'Off';
String get libraryAutoScanOff => 'Aus';
@override
String get libraryAutoScanOnOpen => 'Every app open';
String get libraryAutoScanOnOpen => 'Bei jeder App Öffnung';
@override
String get libraryAutoScanDaily => 'Daily';
String get libraryAutoScanDaily => 'Täglich';
@override
String get libraryAutoScanWeekly => 'Weekly';
String get libraryAutoScanWeekly => 'Wöchentlich';
@override
String get libraryActions => 'Aktionen';
@@ -1929,8 +1929,8 @@ class AppLocalizationsDe extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'files',
one: 'file',
other: '$count Datein',
one: '1 Datei',
);
return '$_temp0';
}
@@ -1947,7 +1947,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get libraryScanning => 'Scannen...';
@override
String get libraryScanFinalizing => 'Finalizing library...';
String get libraryScanFinalizing => 'Bibliothek wird aktualisiert...';
@override
String libraryScanProgress(String progress, int total) {
@@ -2018,22 +2018,23 @@ class AppLocalizationsDe extends AppLocalizations {
String get libraryFilterFormat => 'Format';
@override
String get libraryFilterMetadata => 'Metadata';
String get libraryFilterMetadata => 'Metadaten';
@override
String get libraryFilterMetadataComplete => 'Complete metadata';
String get libraryFilterMetadataComplete => 'Komplette Metadaten';
@override
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
String get libraryFilterMetadataMissingAny => 'Metadaten fehlen';
@override
String get libraryFilterMetadataMissingYear => 'Missing year';
String get libraryFilterMetadataMissingYear => 'Jahr fehlt';
@override
String get libraryFilterMetadataMissingGenre => 'Missing genre';
String get libraryFilterMetadataMissingGenre => 'Genre fehlt';
@override
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
String get libraryFilterMetadataMissingAlbumArtist =>
'Fehlender Album-Künstler';
@override
String get libraryFilterSort => 'Sortieren';
@@ -2065,7 +2066,7 @@ class AppLocalizationsDe extends AppLocalizations {
count,
locale: localeName,
other: 'vor $count Minuten',
one: 'vor $count Minute',
one: 'vor 1 Minute',
);
return '$_temp0';
}
@@ -2076,7 +2077,7 @@ class AppLocalizationsDe extends AppLocalizations {
count,
locale: localeName,
other: 'vor $count Stunden',
one: 'vor $count Stunde',
one: 'vor 1 Stunde',
);
return '$_temp0';
}
@@ -2142,7 +2143,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get tutorialExtensionsTip1 =>
'Im Store Tab findest du nützliche Erweiterungen';
'Browse the Repo tab to discover useful extensions';
@override
String get tutorialExtensionsTip2 =>
@@ -2396,11 +2397,11 @@ class AppLocalizationsDe extends AppLocalizations {
'FFmpeg Metadaten-Einbettung fehlgeschlagen';
@override
String get queueFlacAction => 'Queue FLAC';
String get queueFlacAction => 'Warteschlange FLAC';
@override
String queueFlacConfirmMessage(int count) {
return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected';
return 'Suche Online-Matches für ausgewählte Titel und Playlists für FLAC-Downloads.\n\nVorhandene Dateien werden weder geändert noch gelöscht.\n\nNur eindeutige Treffer werden automatisch zur Warteschlange hinzugefügt.\n\n$count ausgewählt';
}
@override
@@ -2426,7 +2427,8 @@ class AppLocalizationsDe extends AppLocalizations {
String get trackConvertFormat => 'Format konvertieren';
@override
String get trackConvertFormatSubtitle => 'In MP3 oder Opus konvertieren';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Audio konvertieren';
@@ -2454,7 +2456,7 @@ class AppLocalizationsDe extends AppLocalizations {
String sourceFormat,
String targetFormat,
) {
return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.';
return 'Konvertieren von $sourceFormat in $targetFormat? (kein Qualitätsverlust)\n\nDie Originaldatei wird nach der Konvertierung gelöscht.';
}
@override
@@ -2534,7 +2536,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get collectionLoved => 'Lieblingssongs';
@override
String get collectionPlaylists => 'Playlisten';
String get collectionPlaylists => 'Playlists';
@override
String get collectionPlaylist => 'Playlist';
@@ -2721,10 +2723,10 @@ class AppLocalizationsDe extends AppLocalizations {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'tracks',
one: 'track',
other: 'Titel',
one: 'Titel',
);
return 'Convert $count $_temp0 to $format? (Lossless — no quality loss)\n\nOriginal files will be deleted after conversion.';
return 'Konvertiere $count $_temp0 in $format? (kein Qualitätsverlust)\n\nOriginaldateien werden nach der Konvertierung gelöscht.';
}
@override
@@ -2751,24 +2753,24 @@ class AppLocalizationsDe extends AppLocalizations {
'Künstler-Ordner nur für Titel-Künstler';
@override
String get lyricsProvidersTitle => 'Lyrics Providers';
String get lyricsProvidersTitle => 'Lyrics-Anbieter';
@override
String get lyricsProvidersDescription =>
'Enable, disable and reorder lyrics sources. Providers are tried top-to-bottom until lyrics are found.';
'Lyrics aktivieren, deaktivieren und neu ordnen. Anbieter werden von oben nach unten ausprobiert, bis Lyrics gefunden werden.';
@override
String get lyricsProvidersInfoText =>
'Extension lyrics providers always run before built-in providers. At least one provider must remain enabled.';
'Erweiterungsanbieter werden immer vor eingebauten ausgeführt. Mindestens ein Anbieter muss aktiviert bleiben.';
@override
String lyricsProvidersEnabledSection(int count) {
return 'Enabled ($count)';
return '($count) aktiviert';
}
@override
String lyricsProvidersDisabledSection(int count) {
return 'Disabled ($count)';
return '($count) deaktiviert';
}
@override
@@ -2787,52 +2789,53 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get lyricsProviderNeteaseDesc =>
'NetEase Cloud Music (good for Asian songs)';
'NetEase Cloud Music (gut für asiatische Lieder)';
@override
String get lyricsProviderMusixmatchDesc =>
'Largest lyrics database (multi-language)';
'Größte Lyrics-Datenbank (mehrsprachig)';
@override
String get lyricsProviderAppleMusicDesc =>
'Word-by-word synced lyrics (via proxy)';
'Wort-für-Wort-synchronisierte Lyrics (via Proxy)';
@override
String get lyricsProviderQqMusicDesc =>
'QQ Music (good for Chinese songs, via proxy)';
'QQ Music (gut für chinesische Lieder, via Proxy)';
@override
String get lyricsProviderExtensionDesc => 'Extension provider';
String get lyricsProviderExtensionDesc => 'Erweiterungsanbieter';
@override
String get safMigrationTitle => 'Storage Update Required';
String get safMigrationTitle => 'Speicheraktualisierung erforderlich';
@override
String get safMigrationMessage1 =>
'SpotiFLAC now uses Android Storage Access Framework (SAF) for downloads. This fixes \"permission denied\" errors on Android 10+.';
'SpotiFLAC verwendet jetzt Android Storage Access Framework (SAF) beim Herunterladen. Dies behebt Fehler bei Android 10+.';
@override
String get safMigrationMessage2 =>
'Please select your download folder again to switch to the new storage system.';
'Bitte wähle dein Download-Ordner erneut aus, um zum neuen System zu wechseln.';
@override
String get safMigrationSuccess => 'Download folder updated to SAF mode';
@override
String get settingsDonate => 'Donate';
String get settingsDonate => 'Unterstützen';
@override
String get settingsDonateSubtitle => 'Support SpotiFLAC-Mobile development';
String get settingsDonateSubtitle =>
'Unterstütze die SpotiFLAC-Mobile Entwickler';
@override
String get tooltipLoveAll => 'Love All';
String get tooltipLoveAll => 'Alle lieben';
@override
String get tooltipAddToPlaylist => 'Add to Playlist';
String get tooltipAddToPlaylist => 'Zur Wiedergabeliste hinzufügen';
@override
String snackbarRemovedTracksFromLoved(int count) {
return 'Removed $count tracks from Loved';
return '$count Titel von geliebt entfernt';
}
@override
@@ -2841,7 +2844,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get dialogDownloadAllTitle => 'Download All';
String get dialogDownloadAllTitle => 'Alle Herunterladen';
@override
String dialogDownloadAllMessage(int count) {
@@ -2852,7 +2855,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get homeSkipAlreadyDownloaded => 'Skip already downloaded songs';
@override
String get homeGoToAlbum => 'Go to Album';
String get homeGoToAlbum => 'Zum Album gehen';
@override
String get homeAlbumInfoUnavailable => 'Album info not available';
@@ -2871,7 +2874,7 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String snackbarError(String error) {
return 'Error: $error';
return 'Fehler: $error';
}
@override
@@ -2891,7 +2894,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get storageModeAppFolderSubtitle => 'Use default Music/SpotiFLAC path';
@override
String get storageModeSaf => 'SAF folder';
String get storageModeSaf => 'SAF-Ordner';
@override
String get storageModeSafSubtitle =>
@@ -2902,7 +2905,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Customize how your files are named.';
@override
String get downloadFilenameInsertTag => 'Tap to insert tag:';
String get downloadFilenameInsertTag => 'Tippe, um Tag einzufügen:';
@override
String get downloadSeparateSinglesEnabled => 'Albums/ and Singles/ folders';
@@ -2930,10 +2933,10 @@ class AppLocalizationsDe extends AppLocalizations {
'By Playlist already places downloads inside a playlist folder.';
@override
String get downloadSongLinkRegion => 'SongLink Region';
String get downloadSongLinkRegion => 'SongLink-Region';
@override
String get downloadNetworkCompatibilityMode => 'Network compatibility mode';
String get downloadNetworkCompatibilityMode => 'Netzwerkkompatibilitätsmodus';
@override
String get downloadNetworkCompatibilityModeEnabled =>
@@ -2976,7 +2979,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Append romanized lyrics when available';
@override
String get downloadNeteaseIncludeRomanizationDisabled => 'Disabled';
String get downloadNeteaseIncludeRomanizationDisabled => 'Deaktiviert';
@override
String get downloadAppleQqMultiPerson => 'Apple/QQ Multi-Person Word-by-Word';
@@ -3008,10 +3011,10 @@ class AppLocalizationsDe extends AppLocalizations {
'Keep full Album Artist metadata value';
@override
String get downloadProvidersNoneEnabled => 'None enabled';
String get downloadProvidersNoneEnabled => 'Keine aktiviert';
@override
String get downloadMusixmatchLanguageCode => 'Language code';
String get downloadMusixmatchLanguageCode => 'Sprach-Code';
@override
String get downloadMusixmatchLanguageHint => 'auto / en / es / ja';
@@ -3024,7 +3027,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get downloadMusixmatchAuto => 'Auto';
@override
String get downloadNetworkAnySubtitle => 'WiFi + Mobile Data';
String get downloadNetworkAnySubtitle => 'WLAN + Mobile Daten';
@override
String get downloadNetworkWifiOnlySubtitle =>
@@ -3038,23 +3041,23 @@ class AppLocalizationsDe extends AppLocalizations {
String get snackbarUnsupportedAudioFormat => 'Unsupported audio format';
@override
String get cacheRefresh => 'Refresh';
String get cacheRefresh => 'Aktualisieren';
@override
String dialogDownloadPlaylistsMessage(int trackCount, int playlistCount) {
String _temp0 = intl.Intl.pluralLogic(
trackCount,
locale: localeName,
other: 'tracks',
one: 'track',
other: 'Titel',
one: 'Titel',
);
String _temp1 = intl.Intl.pluralLogic(
playlistCount,
locale: localeName,
other: 'playlists',
one: 'playlist',
other: 'Playlists',
one: 'Playlist',
);
return 'Download $trackCount $_temp0 from $playlistCount $_temp1?';
return 'Lade $trackCount $_temp0 von $playlistCount $_temp1?';
}
@override
@@ -3094,7 +3097,7 @@ class AppLocalizationsDe extends AppLocalizations {
'Select fields to fill automatically from online metadata';
@override
String get editMetadataAutoFillFetch => 'Fetch & Fill';
String get editMetadataAutoFillFetch => 'Abrufen & Ausfüllen';
@override
String get editMetadataAutoFillSearching => 'Searching online...';
@@ -3119,25 +3122,25 @@ class AppLocalizationsDe extends AppLocalizations {
'Select at least one field to auto-fill';
@override
String get editMetadataFieldTitle => 'Title';
String get editMetadataFieldTitle => 'Titel';
@override
String get editMetadataFieldArtist => 'Artist';
String get editMetadataFieldArtist => 'Künstler';
@override
String get editMetadataFieldAlbum => 'Album';
@override
String get editMetadataFieldAlbumArtist => 'Album Artist';
String get editMetadataFieldAlbumArtist => 'Album Künstler';
@override
String get editMetadataFieldDate => 'Date';
String get editMetadataFieldDate => 'Datum';
@override
String get editMetadataFieldTrackNum => 'Track #';
String get editMetadataFieldTrackNum => 'Titel #';
@override
String get editMetadataFieldDiscNum => 'Disc #';
String get editMetadataFieldDiscNum => 'Disk #';
@override
String get editMetadataFieldGenre => 'Genre';
@@ -3149,16 +3152,16 @@ class AppLocalizationsDe extends AppLocalizations {
String get editMetadataFieldLabel => 'Label';
@override
String get editMetadataFieldCopyright => 'Copyright';
String get editMetadataFieldCopyright => 'Urheberrecht';
@override
String get editMetadataFieldCover => 'Cover Art';
String get editMetadataFieldCover => 'Cover-Art';
@override
String get editMetadataSelectAll => 'All';
String get editMetadataSelectAll => 'Alle';
@override
String get editMetadataSelectEmpty => 'Empty only';
String get editMetadataSelectEmpty => 'Nur leer';
@override
String queueDownloadingCount(int count) {
@@ -3166,10 +3169,10 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get queueDownloadedHeader => 'Downloaded';
String get queueDownloadedHeader => 'Heruntergeladen';
@override
String get queueFilteringIndicator => 'Filtering...';
String get queueFilteringIndicator => 'Filtere...';
@override
String queueTrackCount(int count) {
@@ -3194,7 +3197,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get queueEmptyAlbums => 'No album downloads';
String get queueEmptyAlbums => 'Keine Album-Downloads';
@override
String get queueEmptyAlbumsSubtitle =>
@@ -3220,7 +3223,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get selectionTapPlaylistsToSelect => 'Tap playlists to select';
@override
String get selectionSelectPlaylistsToDelete => 'Select playlists to delete';
String get selectionSelectPlaylistsToDelete => 'Playlist zum Löschen wählen';
@override
String get audioAnalysisTitle => 'Audio Quality Analysis';
@@ -3230,37 +3233,37 @@ class AppLocalizationsDe extends AppLocalizations {
'Verify lossless quality with spectrum analysis';
@override
String get audioAnalysisAnalyzing => 'Analyzing audio...';
String get audioAnalysisAnalyzing => 'Audio wird analysiert...';
@override
String get audioAnalysisSampleRate => 'Sample Rate';
@override
String get audioAnalysisBitDepth => 'Bit Depth';
String get audioAnalysisBitDepth => 'Bit-Tiefe';
@override
String get audioAnalysisChannels => 'Channels';
String get audioAnalysisChannels => 'Kanäle';
@override
String get audioAnalysisDuration => 'Duration';
String get audioAnalysisDuration => 'Länge';
@override
String get audioAnalysisNyquist => 'Nyquist';
@override
String get audioAnalysisFileSize => 'Size';
String get audioAnalysisFileSize => 'Größe';
@override
String get audioAnalysisDynamicRange => 'Dynamic Range';
String get audioAnalysisDynamicRange => 'Dynamischer Bereich';
@override
String get audioAnalysisPeak => 'Peak';
String get audioAnalysisPeak => 'Maximum';
@override
String get audioAnalysisRms => 'RMS';
@override
String get audioAnalysisSamples => 'Samples';
String get audioAnalysisSamples => 'Proben';
@override
String extensionsSearchWith(String providerName) {
@@ -3268,7 +3271,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get extensionsHomeFeedProvider => 'Home Feed Provider';
String get extensionsHomeFeedProvider => 'Home Feed Anbieter';
@override
String get extensionsHomeFeedDescription =>
@@ -3296,7 +3299,7 @@ class AppLocalizationsDe extends AppLocalizations {
String get sortAlphaDesc => 'Z-A';
@override
String get cancelDownloadTitle => 'Cancel download?';
String get cancelDownloadTitle => 'Download abbrechen?';
@override
String cancelDownloadContent(String trackName) {
@@ -3304,7 +3307,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get cancelDownloadKeep => 'Keep';
String get cancelDownloadKeep => 'Behalten';
@override
String get metadataSaveFailedFfmpeg => 'Failed to save metadata via FFmpeg';
@@ -3319,22 +3322,22 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get errorLoadAlbum => 'Failed to load album';
String get errorLoadAlbum => 'Fehler beim Laden des Albums';
@override
String get errorLoadPlaylist => 'Failed to load playlist';
String get errorLoadPlaylist => 'Fehler beim Laden der Playlist';
@override
String get errorLoadArtist => 'Failed to load artist';
String get errorLoadArtist => 'Fehler beim Laden des Interpreten';
@override
String get notifChannelDownloadName => 'Download Progress';
String get notifChannelDownloadName => 'Download Fortschritt';
@override
String get notifChannelDownloadDesc => 'Shows download progress for tracks';
@override
String get notifChannelLibraryScanName => 'Library Scan';
String get notifChannelLibraryScanName => 'Bibliotheksscan';
@override
String get notifChannelLibraryScanDesc => 'Shows local library scan progress';
@@ -3358,7 +3361,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get notifAlreadyInLibrary => 'Already in Library';
String get notifAlreadyInLibrary => 'Bereits in der Bibliothek';
@override
String notifDownloadCompleteCount(int completed, int total) {
@@ -3366,7 +3369,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get notifDownloadComplete => 'Download Complete';
String get notifDownloadComplete => 'Download abgeschlossen';
@override
String notifDownloadsFinished(int completed, int failed) {
@@ -3408,12 +3411,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String notifLibraryScanExcluded(int count) {
return '$count excluded';
return '$count ausgeschlossen';
}
@override
String notifLibraryScanErrors(int count) {
return '$count errors';
return '$count Fehler';
}
@override
@@ -3436,7 +3439,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get notifUpdateReady => 'Update Ready';
String get notifUpdateReady => 'Update bereit';
@override
String notifUpdateReadyBody(String version) {
@@ -3444,7 +3447,7 @@ class AppLocalizationsDe extends AppLocalizations {
}
@override
String get notifUpdateFailed => 'Update Failed';
String get notifUpdateFailed => 'Update fehlgeschlagen';
@override
String get notifUpdateFailedBody =>
File diff suppressed because it is too large Load Diff
+35 -33
View File
@@ -21,13 +21,13 @@ class AppLocalizationsFr extends AppLocalizations {
String get navSettings => 'Paramètres';
@override
String get navStore => 'Magasin';
String get navStore => 'Repo';
@override
String get homeTitle => 'Accueil';
@override
String get homeSubtitle => 'Coller un lien Spotify ou rechercher par nom';
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeSupports => 'Supports: Piste, Album, Playlist, Artiste URLs';
@@ -218,10 +218,10 @@ class AppLocalizationsFr 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';
@@ -282,7 +282,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get extensionsTitle => 'Extensions';
@override
String get extensionsDisabled => 'Disabled';
String get extensionsDisabled => 'Désactivée';
@override
String extensionsVersion(String version) {
@@ -291,38 +291,38 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String extensionsAuthor(String author) {
return 'by $author';
return 'par $author';
}
@override
String get extensionsUninstall => 'Désinstaller';
@override
String get storeTitle => 'Magasin d\'extension';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => 'Recherche d\'extensions...';
@override
String get storeInstall => 'Install';
String get storeInstall => 'Installer';
@override
String get storeInstalled => 'Installed';
String get storeInstalled => 'Installé';
@override
String get storeUpdate => 'Update';
String get storeUpdate => 'Mettre à jour';
@override
String get aboutTitle => 'About';
String get aboutTitle => 'À propos de';
@override
String get aboutContributors => 'Contributors';
String get aboutContributors => 'Contributeurs';
@override
String get aboutMobileDeveloper => 'Mobile version developer';
String get aboutMobileDeveloper => 'Développeur de la version mobile';
@override
String get aboutOriginalCreator => 'Creator of the original SpotiFLAC';
String get aboutOriginalCreator => 'Créateur de SpotiFLAC original';
@override
String get aboutLogoArtist =>
@@ -362,7 +362,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get aboutTelegramChannel => 'Telegram Channel';
@override
String get aboutTelegramChannelSubtitle => 'Announcements and updates';
String get aboutTelegramChannelSubtitle => 'Annonces et mises à jour';
@override
String get aboutTelegramChat => 'Telegram Community';
@@ -520,10 +520,11 @@ class AppLocalizationsFr extends AppLocalizations {
'SpotiFLAC needs storage permission to save your downloaded music files.';
@override
String get setupNotificationGranted => 'Notification Permission Granted!';
String get setupNotificationGranted =>
'Autorisation de notifications accordée!';
@override
String get setupNotificationEnable => 'Enable Notifications';
String get setupNotificationEnable => 'Activer les notifications';
@override
String get setupFolderChoose => 'Choisissez le dossier pour télécharger';
@@ -533,39 +534,39 @@ class AppLocalizationsFr extends AppLocalizations {
'Sélectionnez un dossier dans lequel votre musique téléchargée sera enregistrée.';
@override
String get setupSelectFolder => 'Select Folder';
String get setupSelectFolder => 'Sélectionner un dossier';
@override
String get setupEnableNotifications => 'Enable Notifications';
String get setupEnableNotifications => 'Activer les notifications';
@override
String get setupNotificationBackgroundDescription =>
'Get notified about download progress and completion. This helps you track downloads when the app is in background.';
@override
String get setupSkipForNow => 'Skip for now';
String get setupSkipForNow => 'Ignorer pour le moment';
@override
String get setupNext => 'Next';
String get setupNext => 'Suivant';
@override
String get setupGetStarted => 'Get Started';
String get setupGetStarted => 'Démarrer';
@override
String get setupAllowAccessToManageFiles =>
'Please enable \"Allow access to manage all files\" in the next screen.';
'Veuillez activer \"Autoriser l\'accès à tous les fichiers\" sur l\'écran suivant.';
@override
String get dialogCancel => 'Cancel';
String get dialogCancel => 'Annuler';
@override
String get dialogSave => 'Save';
String get dialogSave => 'Sauvegarder';
@override
String get dialogDelete => 'Delete';
String get dialogDelete => 'Supprimer';
@override
String get dialogRetry => 'Retry';
String get dialogRetry => 'Réessayer';
@override
String get dialogClear => 'Clear';
@@ -577,7 +578,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get dialogImport => 'Import';
@override
String get dialogDownload => 'Download';
String get dialogDownload => 'Télécharger';
@override
String get dialogDiscard => 'Discard';
@@ -586,10 +587,10 @@ class AppLocalizationsFr extends AppLocalizations {
String get dialogRemove => 'Remove';
@override
String get dialogUninstall => 'Uninstall';
String get dialogUninstall => 'Désinstaller';
@override
String get dialogDiscardChanges => 'Discard Changes?';
String get dialogDiscardChanges => 'Ignorer les modifications ?';
@override
String get dialogUnsavedChanges =>
@@ -1338,7 +1339,7 @@ class AppLocalizationsFr extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Use built-in search';
@@ -2116,7 +2117,7 @@ class AppLocalizationsFr 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 =>
@@ -2397,7 +2398,8 @@ class AppLocalizationsFr extends AppLocalizations {
String get trackConvertFormat => 'Convert Format';
@override
String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Convert Audio';
+9 -8
View File
@@ -21,13 +21,13 @@ class AppLocalizationsHi extends AppLocalizations {
String get navSettings => 'विकल्प';
@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';
@@ -216,10 +216,10 @@ class AppLocalizationsHi 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';
@@ -296,7 +296,7 @@ class AppLocalizationsHi extends AppLocalizations {
String get extensionsUninstall => 'Uninstall';
@override
String get storeTitle => 'Extension Store';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => 'Search extensions...';
@@ -1336,7 +1336,7 @@ class AppLocalizationsHi extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Use built-in search';
@@ -2114,7 +2114,7 @@ class AppLocalizationsHi 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 =>
@@ -2395,7 +2395,8 @@ class AppLocalizationsHi extends AppLocalizations {
String get trackConvertFormat => 'Convert Format';
@override
String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Convert Audio';
+28 -29
View File
@@ -27,8 +27,7 @@ class AppLocalizationsId extends AppLocalizations {
String get homeTitle => 'Beranda';
@override
String get homeSubtitle =>
'Tempel URL yang didukung atau cari berdasarkan nama';
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeSupports => 'Mendukung: URL Track, Album, Playlist, Artis';
@@ -130,11 +129,11 @@ class AppLocalizationsId extends AppLocalizations {
}
@override
String get optionsDefaultSearchTab => 'Tab Pencarian Default';
String get optionsDefaultSearchTab => 'Default Search Tab';
@override
String get optionsDefaultSearchTabSubtitle =>
'Pilih tab yang dibuka lebih dulu untuk hasil pencarian baru.';
'Choose which tab opens first for new search results.';
@override
String get optionsSwitchBack =>
@@ -220,10 +219,10 @@ class AppLocalizationsId extends AppLocalizations {
'Unduhan paralel dapat memicu pembatasan rate';
@override
String get optionsExtensionStore => 'Repo Ekstensi';
String get optionsExtensionStore => 'Extension Repo';
@override
String get optionsExtensionStoreSubtitle => 'Tampilkan tab Repo di navigasi';
String get optionsExtensionStoreSubtitle => 'Show Repo tab in navigation';
@override
String get optionsCheckUpdates => 'Periksa Pembaruan';
@@ -299,7 +298,7 @@ class AppLocalizationsId extends AppLocalizations {
String get extensionsUninstall => 'Copot';
@override
String get storeTitle => 'Repo Ekstensi';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => 'Cari ekstensi...';
@@ -745,15 +744,15 @@ class AppLocalizationsId extends AppLocalizations {
String get errorNoTracksFound => 'Tidak ada lagu ditemukan';
@override
String get errorUrlNotRecognized => 'Link tidak dikenali';
String get errorUrlNotRecognized => 'Tautan tidak dikenali';
@override
String get errorUrlNotRecognizedMessage =>
'Link ini tidak didukung. Pastikan URL benar dan ekstensi yang kompatibel sudah terpasang.';
'Tautan ini tidak didukung. Pastikan URL sudah benar dan ekstensi yang kompatibel telah terpasang.';
@override
String get errorUrlFetchFailed =>
'Gagal memuat konten dari link ini. Silakan coba lagi.';
'Konten dari tautan ini gagal dimuat. Silakan coba lagi.';
@override
String errorMissingExtensionSource(String item) {
@@ -941,15 +940,15 @@ class AppLocalizationsId extends AppLocalizations {
'Jika lagu tidak tersedia di provider pertama, aplikasi akan otomatis mencoba yang berikutnya.';
@override
String get providerPriorityFallbackExtensionsTitle => 'Fallback Ekstensi';
String get providerPriorityFallbackExtensionsTitle => 'Extension Fallback';
@override
String get providerPriorityFallbackExtensionsDescription =>
'Pilih ekstensi unduhan terpasang mana yang boleh dipakai saat fallback otomatis. Provider bawaan tetap mengikuti urutan prioritas di atas.';
'Choose which installed download extensions can be used during automatic fallback. Built-in providers still follow the priority order above.';
@override
String get providerPriorityFallbackExtensionsHint =>
'Hanya ekstensi aktif dengan kemampuan download provider yang ditampilkan di sini.';
'Only enabled extensions with download-provider capability are listed here.';
@override
String get providerBuiltIn => 'Bawaan';
@@ -1334,7 +1333,7 @@ class AppLocalizationsId extends AppLocalizations {
String get storeNewRepoUrlLabel => 'New Repository URL';
@override
String get storeLoadError => 'Gagal memuat repo';
String get storeLoadError => 'Failed to load repository';
@override
String get storeEmptyNoExtensions => 'No extensions available';
@@ -1343,7 +1342,7 @@ class AppLocalizationsId extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Bawaan (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Gunakan pencarian bawaan';
@@ -1449,7 +1448,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get extensionsFallbackSubtitle =>
'Pilih ekstensi unduhan terpasang yang boleh dipakai saat fallback';
'Choose which installed download extensions can be used as fallback';
@override
String get extensionsNoDownloadProvider =>
@@ -2124,7 +2123,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get tutorialExtensionsTip1 =>
'Buka tab Repo untuk menemukan ekstensi yang berguna';
'Browse the Repo tab to discover useful extensions';
@override
String get tutorialExtensionsTip2 =>
@@ -2375,25 +2374,25 @@ class AppLocalizationsId extends AppLocalizations {
String get trackReEnrichFfmpegFailed => 'FFmpeg metadata embed failed';
@override
String get queueFlacAction => 'Antrekan FLAC';
String get queueFlacAction => 'Queue FLAC';
@override
String queueFlacConfirmMessage(int count) {
return 'Cari kecocokan online untuk track yang dipilih lalu antrekan download FLAC.\n\nFile yang sudah ada tidak akan diubah atau dihapus.\n\nHanya kecocokan dengan keyakinan tinggi yang akan diantrikan otomatis.\n\n$count dipilih';
return 'Search online matches for the selected tracks and queue FLAC downloads.\n\nExisting files will not be modified or deleted.\n\nOnly high-confidence matches are queued automatically.\n\n$count selected';
}
@override
String queueFlacFindingProgress(int current, int total) {
return 'Mencari kecocokan FLAC... ($current/$total)';
return 'Finding FLAC matches... ($current/$total)';
}
@override
String get queueFlacNoReliableMatches =>
'Tidak ada kecocokan online yang cukup meyakinkan untuk pilihan ini';
'No reliable online matches found for the selection';
@override
String queueFlacQueuedWithSkipped(int addedCount, int skippedCount) {
return 'Menambahkan $addedCount track ke antrean, melewati $skippedCount';
return 'Added $addedCount tracks to queue, skipped $skippedCount';
}
@override
@@ -2406,7 +2405,7 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get trackConvertFormatSubtitle =>
'Konversi ke MP3, Opus, ALAC, atau FLAC';
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Convert Audio';
@@ -2434,12 +2433,12 @@ class AppLocalizationsId extends AppLocalizations {
String sourceFormat,
String targetFormat,
) {
return 'Konversi dari $sourceFormat ke $targetFormat? (Lossless — tanpa kehilangan kualitas)\n\nFile asli akan dihapus setelah konversi.';
return 'Convert from $sourceFormat to $targetFormat? (Lossless — no quality loss)\n\nThe original file will be deleted after conversion.';
}
@override
String get trackConvertLosslessHint =>
'Konversi lossless — tanpa kehilangan kualitas';
'Lossless conversion — no quality loss';
@override
String get trackConvertConverting => 'Converting audio...';
@@ -2893,19 +2892,19 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get downloadCreatePlaylistSourceFolder =>
'Buat folder sumber playlist';
'Create playlist source folder';
@override
String get downloadCreatePlaylistSourceFolderEnabled =>
'Unduhan dari playlist memakai Playlist/ lalu struktur folder normal Anda.';
'Playlist downloads use Playlist/ plus your normal folder structure.';
@override
String get downloadCreatePlaylistSourceFolderDisabled =>
'Unduhan dari playlist hanya memakai struktur folder normal.';
'Playlist downloads use the normal folder structure only.';
@override
String get downloadCreatePlaylistSourceFolderRedundant =>
'Mode Berdasarkan Playlist sudah menaruh unduhan ke dalam folder playlist.';
'By Playlist already places downloads inside a playlist folder.';
@override
String get downloadSongLinkRegion => 'SongLink Region';
+9 -8
View File
@@ -21,13 +21,13 @@ class AppLocalizationsJa extends AppLocalizations {
String get navSettings => '設定';
@override
String get navStore => 'ストア';
String get navStore => 'Repo';
@override
String get homeTitle => 'ホーム';
@override
String get homeSubtitle => 'Spotify のリンクを貼り付けるか、名前で検索します';
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeSupports => 'サポート: トラック、アルバム、プレイリスト、アーティスト、URL';
@@ -214,10 +214,10 @@ class AppLocalizationsJa extends AppLocalizations {
'Parallel downloads may trigger rate limiting';
@override
String get optionsExtensionStore => '拡張ストア';
String get optionsExtensionStore => 'Extension Repo';
@override
String get optionsExtensionStoreSubtitle => 'ナビゲーションにストアタブを表示';
String get optionsExtensionStoreSubtitle => 'Show Repo tab in navigation';
@override
String get optionsCheckUpdates => '更新を確認';
@@ -293,7 +293,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get extensionsUninstall => 'アンインストール';
@override
String get storeTitle => '拡張ストア';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => '拡張を検索...';
@@ -1330,7 +1330,7 @@ class AppLocalizationsJa extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'デフォルト (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => '内蔵の検索を使用する';
@@ -2101,7 +2101,7 @@ class AppLocalizationsJa 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 =>
@@ -2382,7 +2382,8 @@ class AppLocalizationsJa extends AppLocalizations {
String get trackConvertFormat => '変換の形式';
@override
String get trackConvertFormatSubtitle => 'MP3 または Opus に変換';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'オーディオを変換';
+9 -8
View File
@@ -21,13 +21,13 @@ class AppLocalizationsKo extends AppLocalizations {
String get navSettings => 'Settings';
@override
String get navStore => 'Store';
String get navStore => 'Repo';
@override
String get homeTitle => 'Home';
@override
String get homeSubtitle => 'Spotify URL을 붙여 넣거나 검색';
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeSupports => '지원 항목: 트랙, 앨범, 플레이리스트, 아티스트 URLs';
@@ -209,10 +209,10 @@ class AppLocalizationsKo extends AppLocalizations {
String get optionsConcurrentWarning => '동시에 다수의 음반을 다운로드하면 속도 제한이 발생할 수 있습니다';
@override
String get optionsExtensionStore => '확장 기능 스토어';
String get optionsExtensionStore => 'Extension Repo';
@override
String get optionsExtensionStoreSubtitle => '탐색 메뉴에 스토어 탭 표시';
String get optionsExtensionStoreSubtitle => 'Show Repo tab in navigation';
@override
String get optionsCheckUpdates => '업데이트 확인';
@@ -286,7 +286,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get extensionsUninstall => '삭제';
@override
String get storeTitle => '확장 기능 스토어';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => '확장 기능 검색';
@@ -1316,7 +1316,7 @@ class AppLocalizationsKo extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Use built-in search';
@@ -2094,7 +2094,7 @@ class AppLocalizationsKo 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 =>
@@ -2375,7 +2375,8 @@ class AppLocalizationsKo extends AppLocalizations {
String get trackConvertFormat => 'Convert Format';
@override
String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Convert Audio';
+9 -8
View File
@@ -21,13 +21,13 @@ class AppLocalizationsNl 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';
@@ -216,10 +216,10 @@ class AppLocalizationsNl extends AppLocalizations {
'Parallel downloaden kan leiden tot 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';
@@ -296,7 +296,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get extensionsUninstall => 'Uninstall';
@override
String get storeTitle => 'Extension Store';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => 'Search extensions...';
@@ -1336,7 +1336,7 @@ class AppLocalizationsNl extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'Default (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle => 'Use built-in search';
@@ -2114,7 +2114,7 @@ class AppLocalizationsNl 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 =>
@@ -2395,7 +2395,8 @@ class AppLocalizationsNl extends AppLocalizations {
String get trackConvertFormat => 'Convert Format';
@override
String get trackConvertFormatSubtitle => 'Convert to MP3 or Opus';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Convert Audio';
File diff suppressed because it is too large Load Diff
+20 -20
View File
@@ -21,13 +21,13 @@ class AppLocalizationsRu extends AppLocalizations {
String get navSettings => 'Настройки';
@override
String get navStore => 'Магазин';
String get navStore => 'Repo';
@override
String get homeTitle => 'Главная';
@override
String get homeSubtitle => 'Вставьте ссылку Spotify или ищите по названию';
String get homeSubtitle => 'Paste a supported URL or search by name';
@override
String get homeSupports =>
@@ -221,11 +221,10 @@ class AppLocalizationsRu extends AppLocalizations {
'Параллельные загрузки могут вызвать ограничение скорости';
@override
String get optionsExtensionStore => 'Магазин расширений';
String get optionsExtensionStore => 'Extension Repo';
@override
String get optionsExtensionStoreSubtitle =>
'Показывать вкладку Магазин в гл. меню';
String get optionsExtensionStoreSubtitle => 'Show Repo tab in navigation';
@override
String get optionsCheckUpdates => 'Проверить обновления';
@@ -302,7 +301,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get extensionsUninstall => 'Удалить';
@override
String get storeTitle => 'Магазин расширений';
String get storeTitle => 'Extension Repo';
@override
String get storeSearch => 'Поиск расширений...';
@@ -636,9 +635,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Удалить $count $_temp0 из истории?\n\nЭто также удалит файлы из хранилища.';
}
@@ -691,9 +690,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Удалено $count $_temp0';
}
@@ -1161,9 +1160,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: '$count треков',
one: '1 трек',
many: '$count треков',
few: '$count трека',
one: '$count трек',
);
return '$_temp0';
}
@@ -1357,7 +1356,7 @@ class AppLocalizationsRu extends AppLocalizations {
String get storeEmptyNoResults => 'No extensions found';
@override
String get extensionDefaultProvider => 'По умолчанию (Deezer/Spotify)';
String get extensionDefaultProvider => 'Default (Deezer)';
@override
String get extensionDefaultProviderSubtitle =>
@@ -1670,9 +1669,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Удалить $count $_temp0 из этого альбома?\n\nЭто также удалит файлы из хранилища.';
}
@@ -1694,9 +1693,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Удалить $count $_temp0';
}
@@ -1927,9 +1926,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return '$_temp0';
}
@@ -2083,9 +2082,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: '$count минут',
one: '1 минуту',
many: '$count минут',
few: '$count минуты',
one: '$count минуту',
);
return '$_temp0 назад';
}
@@ -2096,9 +2095,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: '$count часов',
one: '1 час',
many: '$count часов',
few: '$count часа',
one: '$count час',
);
return '$_temp0 назад';
}
@@ -2164,7 +2163,7 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get tutorialExtensionsTip1 =>
'Просмотрите вкладку Магазина, чтобы найти полезные расширения';
'Browse the Repo tab to discover useful extensions';
@override
String get tutorialExtensionsTip2 =>
@@ -2448,7 +2447,8 @@ class AppLocalizationsRu extends AppLocalizations {
String get trackConvertFormat => 'Переконвертировать формат';
@override
String get trackConvertFormatSubtitle => 'Конвертировать в MP3 или Opus';
String get trackConvertFormatSubtitle =>
'Convert to MP3, Opus, ALAC, or FLAC';
@override
String get trackConvertTitle => 'Конвертировать аудио';
@@ -2579,9 +2579,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: '$count треков',
one: '1 трек',
many: '$count треков',
few: '$count трека',
one: '$count трек',
);
return '$_temp0';
}
@@ -2698,9 +2698,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Отправить $count $_temp0';
}
@@ -2715,9 +2715,9 @@ class AppLocalizationsRu extends AppLocalizations {
count,
locale: localeName,
other: 'треков',
one: 'трек',
many: 'треков',
few: 'трека',
one: 'трек',
);
return 'Конвертировать $count $_temp0';
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1332 -20
View File
File diff suppressed because it is too large Load Diff
+1352 -40
View File
File diff suppressed because it is too large Load Diff
+1345 -33
View File
File diff suppressed because it is too large Load Diff
+1321 -9
View File
File diff suppressed because it is too large Load Diff
+1252 -45
View File
File diff suppressed because it is too large Load Diff
+1321 -9
View File
File diff suppressed because it is too large Load Diff
+1321 -9
View File
File diff suppressed because it is too large Load Diff
+1321 -9
View File
File diff suppressed because it is too large Load Diff
+1327 -15
View File
File diff suppressed because it is too large Load Diff
+1332 -20
View File
File diff suppressed because it is too large Load Diff
+1591 -279
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+1322 -10
View File
File diff suppressed because it is too large Load Diff
+1322 -10
View File
File diff suppressed because it is too large Load Diff
+2
View File
@@ -20,6 +20,7 @@ const List<Locale> filteredSupportedLocales = <Locale>[
Locale('pt', 'PT'),
Locale('ja'),
Locale('tr'),
Locale('uk'),
];
/// Set of locale codes for quick lookup.
@@ -31,4 +32,5 @@ const Set<String> filteredLocaleCodes = <String>{
'pt_PT',
'ja',
'tr',
'uk',
};
+211 -10
View File
@@ -574,6 +574,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
if (trimmed.startsWith('content://')) return true;
return trimmed.endsWith('.flac') ||
trimmed.endsWith('.m4a') ||
trimmed.endsWith('.mp4') ||
trimmed.endsWith('.aac') ||
trimmed.endsWith('.mp3') ||
trimmed.endsWith('.opus') ||
@@ -595,6 +596,7 @@ class DownloadHistoryNotifier extends Notifier<DownloadHistoryState> {
!hasResolvedSpecs &&
(trimmedPath.endsWith('.flac') ||
trimmedPath.endsWith('.m4a') ||
trimmedPath.endsWith('.mp4') ||
trimmedPath.endsWith('.aac') ||
trimmedPath.startsWith('content://'));
@@ -2359,6 +2361,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final normalized = preferred.startsWith('.')
? preferred.toLowerCase()
: '.${preferred.toLowerCase()}';
if (normalized == '.mp4') {
return '.m4a';
}
const allowed = <String>{'.flac', '.m4a', '.mp3', '.opus'};
if (allowed.contains(normalized)) {
return normalized;
@@ -2369,6 +2374,21 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
return null;
}
bool _extensionPreservesNativeOutputExt(String service, String ext) {
final normalizedService = service.trim().toLowerCase();
final normalizedExt = ext.trim().toLowerCase();
if (normalizedService.isEmpty || normalizedExt.isEmpty) return false;
final extensionState = ref.read(extensionProvider);
return extensionState.extensions.any(
(ext) =>
ext.enabled &&
ext.hasDownloadProvider &&
ext.id.toLowerCase() == normalizedService &&
ext.preservedNativeOutputExtensions.contains(normalizedExt),
);
}
String _determineOutputExt(String quality, String service) {
final extensionPreferred = _extensionPreferredOutputExt(service);
if (extensionPreferred != null) {
@@ -2387,6 +2407,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
String _mimeTypeForExt(String ext) {
switch (ext.toLowerCase()) {
case '.m4a':
case '.mp4':
return 'audio/mp4';
case '.mp3':
return 'audio/mpeg';
@@ -3689,6 +3710,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
) {
final backendTrackNum = _parsePositiveInt(backendResult['track_number']);
final backendDiscNum = _parsePositiveInt(backendResult['disc_number']);
final backendTotalTracks = _parsePositiveInt(backendResult['total_tracks']);
final backendTotalDiscs = _parsePositiveInt(backendResult['total_discs']);
final backendYear = normalizeOptionalString(
backendResult['release_date'] as String?,
);
@@ -3716,7 +3739,9 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
backendIsrc != null ||
backendCoverUrl != null ||
backendAlbumArtist != null ||
backendComposer != null;
backendComposer != null ||
backendTotalTracks != null ||
backendTotalDiscs != null;
if (!hasOverrides) {
return baseTrack;
@@ -3735,12 +3760,12 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
isrc: backendIsrc ?? baseTrack.isrc,
trackNumber: backendTrackNum ?? baseTrack.trackNumber,
discNumber: backendDiscNum ?? baseTrack.discNumber,
totalDiscs: baseTrack.totalDiscs,
totalDiscs: backendTotalDiscs ?? baseTrack.totalDiscs,
releaseDate: backendYear ?? baseTrack.releaseDate,
deezerId: baseTrack.deezerId,
availability: baseTrack.availability,
albumType: baseTrack.albumType,
totalTracks: baseTrack.totalTracks,
totalTracks: backendTotalTracks ?? baseTrack.totalTracks,
composer: backendComposer ?? baseTrack.composer,
source: baseTrack.source,
);
@@ -4873,6 +4898,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final actualService =
((result['service'] as String?)?.toLowerCase()) ??
item.service.toLowerCase();
final preferredOutputExt = _extensionPreferredOutputExt(actualService);
final shouldPreserveNativeM4a =
preferredOutputExt == '.m4a' ||
_extensionPreservesNativeOutputExt(actualService, '.m4a');
final decryptionDescriptor =
DownloadDecryptionDescriptor.fromDownloadResult(result);
trackToDownload = _buildTrackForMetadataEmbedding(
@@ -4998,6 +5027,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final isM4aFile =
filePath != null &&
(filePath.endsWith('.m4a') ||
filePath.endsWith('.mp4') ||
(mimeType != null && mimeType.contains('mp4')));
final isFlacFile =
filePath != null &&
@@ -5013,7 +5043,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (shouldForceTidalSafM4aHandling) {
_log.w(
'Tidal SAF file is labeled FLAC but backend returned DASH/M4A stream; preserving it as M4A instead.',
'Tidal SAF file is labeled FLAC but backend returned DASH/M4A stream; converting it back to FLAC.',
);
}
@@ -5130,7 +5160,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
}
}
}
} else {
} else if (shouldPreserveNativeM4a) {
_log.d('M4A file detected (SAF), preserving native container...');
final tempPath = await _copySafToTemp(currentFilePath);
if (tempPath != null) {
@@ -5188,6 +5218,85 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
} catch (_) {}
}
}
} else {
_log.d('M4A file detected (SAF), converting to FLAC...');
final tempPath = await _copySafToTemp(currentFilePath);
if (tempPath != null) {
String? flacPath;
try {
final length = await File(tempPath).length();
if (length < 1024) {
_log.w('Temp M4A is too small (<1KB), skipping conversion');
} else {
updateItemStatus(
item.id,
DownloadStatus.finalizing,
progress: 0.95,
);
flacPath = await FFmpegService.convertM4aToFlac(tempPath);
if (flacPath != null) {
_log.d('Converted to FLAC (temp): $flacPath');
_log.d(
'Embedding metadata and cover to converted FLAC...',
);
final finalTrack = _buildTrackForMetadataEmbedding(
trackToDownload,
result,
resolvedAlbumArtist,
);
final backendGenre = result['genre'] as String?;
final backendLabel = result['label'] as String?;
final backendCopyright = result['copyright'] as String?;
await _embedMetadataToFile(
flacPath,
finalTrack,
format: 'flac',
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
writeExternalLrc: false,
);
final newFileName = '${safBaseName ?? 'track'}.flac';
final newUri = await _writeTempToSaf(
treeUri: settings.downloadTreeUri,
relativeDir: effectiveOutputDir,
fileName: newFileName,
mimeType: _mimeTypeForExt('.flac'),
srcPath: flacPath,
);
if (newUri != null) {
if (newUri != currentFilePath) {
await _deleteSafFile(currentFilePath);
}
filePath = newUri;
finalSafFileName = newFileName;
} else {
_log.w('Failed to write FLAC to SAF, keeping M4A');
}
} else {
_log.w(
'FFmpeg conversion returned null, keeping M4A file',
);
}
}
} catch (e) {
_log.w('SAF M4A->FLAC conversion failed: $e');
} finally {
try {
await File(tempPath).delete();
} catch (_) {}
if (flacPath != null) {
try {
await File(flacPath).delete();
} catch (_) {}
}
}
}
}
} else {
if (quality == 'HIGH') {
@@ -5264,7 +5373,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
_log.w('M4A conversion process failed: $e, keeping M4A file');
actualQuality = 'AAC 320kbps';
}
} else {
} else if (shouldPreserveNativeM4a) {
_log.d('M4A file detected, preserving native container...');
try {
@@ -5273,7 +5382,8 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
if (!await file.exists()) {
_log.e('File does not exist at path: $filePath');
} else {
if (!targetPath.toLowerCase().endsWith('.m4a')) {
if (!(targetPath.toLowerCase().endsWith('.m4a') ||
targetPath.toLowerCase().endsWith('.mp4'))) {
final renamedPath = targetPath.replaceAll(
RegExp(r'\.[^.]+$'),
'.m4a',
@@ -5318,6 +5428,84 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
} catch (e) {
_log.w('Native M4A handling failed: $e');
}
} else {
_log.d(
'M4A file detected (Hi-Res DASH stream), attempting conversion to FLAC...',
);
try {
final file = File(currentFilePath);
if (!await file.exists()) {
_log.e('File does not exist at path: $filePath');
} else {
final length = await file.length();
_log.i('File size before conversion: ${length / 1024} KB');
if (length < 1024) {
_log.w(
'File is too small (<1KB), skipping conversion. Download might be corrupt.',
);
} else {
updateItemStatus(
item.id,
DownloadStatus.finalizing,
progress: 0.95,
);
final flacPath = await FFmpegService.convertM4aToFlac(
currentFilePath,
);
if (flacPath != null) {
filePath = flacPath;
_log.d('Converted to FLAC: $flacPath');
_log.d(
'Embedding metadata and cover to converted FLAC...',
);
try {
final finalTrack = _buildTrackForMetadataEmbedding(
trackToDownload,
result,
resolvedAlbumArtist,
);
final backendGenre = result['genre'] as String?;
final backendLabel = result['label'] as String?;
final backendCopyright = result['copyright'] as String?;
if (backendGenre != null ||
backendLabel != null ||
backendCopyright != null) {
_log.d(
'Extended metadata from backend - Genre: $backendGenre, Label: $backendLabel, Copyright: $backendCopyright',
);
}
await _embedMetadataToFile(
flacPath,
finalTrack,
format: 'flac',
genre: backendGenre ?? genre,
label: backendLabel ?? label,
copyright: backendCopyright,
downloadService: item.service,
);
_log.d('Metadata and cover embedded successfully');
} catch (e) {
_log.w('Warning: Failed to embed metadata/cover: $e');
}
} else {
_log.w(
'FFmpeg conversion returned null, keeping M4A file',
);
}
}
}
} catch (e) {
_log.w(
'FFmpeg conversion process failed: $e, keeping M4A file',
);
}
}
}
} else if (metadataEmbeddingEnabled &&
@@ -5632,12 +5820,15 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
final backendYear = result['release_date'] as String?;
final backendTrackNum = result['track_number'] as int?;
final backendDiscNum = result['disc_number'] as int?;
final backendTotalTracks = result['total_tracks'] as int?;
final backendTotalDiscs = result['total_discs'] as int?;
final backendBitDepth = result['actual_bit_depth'] as int?;
final backendSampleRate = result['actual_sample_rate'] as int?;
final backendISRC = result['isrc'] as String?;
final backendGenre = result['genre'] as String?;
final backendLabel = result['label'] as String?;
final backendCopyright = result['copyright'] as String?;
final backendComposer = result['composer'] as String?;
final effectiveGenre =
normalizeOptionalString(backendGenre) ??
normalizeOptionalString(genre) ??
@@ -5658,6 +5849,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
filePath.startsWith('content://') ||
lowerFilePath.endsWith('.flac') ||
lowerFilePath.endsWith('.m4a') ||
lowerFilePath.endsWith('.mp4') ||
lowerFilePath.endsWith('.aac') ||
lowerFilePath.endsWith('.mp3') ||
lowerFilePath.endsWith('.opus') ||
@@ -5745,11 +5937,17 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
trackNumber: (backendTrackNum != null && backendTrackNum > 0)
? backendTrackNum
: trackToDownload.trackNumber,
totalTracks: trackToDownload.totalTracks,
totalTracks:
(backendTotalTracks != null && backendTotalTracks > 0)
? backendTotalTracks
: trackToDownload.totalTracks,
discNumber: (backendDiscNum != null && backendDiscNum > 0)
? backendDiscNum
: trackToDownload.discNumber,
totalDiscs: trackToDownload.totalDiscs,
totalDiscs:
(backendTotalDiscs != null && backendTotalDiscs > 0)
? backendTotalDiscs
: trackToDownload.totalDiscs,
duration: trackToDownload.duration,
releaseDate: (backendYear != null && backendYear.isNotEmpty)
? backendYear
@@ -5758,7 +5956,10 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
bitDepth: historyBitDepth,
sampleRate: historySampleRate,
genre: effectiveGenre,
composer: trackToDownload.composer,
composer:
(backendComposer != null && backendComposer.isNotEmpty)
? backendComposer
: trackToDownload.composer,
label: effectiveLabel,
copyright: effectiveCopyright,
),
+14
View File
@@ -179,6 +179,20 @@ class Extension {
final trimmed = value.trim();
return trimmed.isEmpty ? null : trimmed;
}
List<String> get preservedNativeOutputExtensions {
final value = capabilities['preserveNativeOutputExtensions'];
if (value is! List) return const [];
final normalized = <String>[];
for (final item in value) {
if (item is! String) continue;
final trimmed = item.trim().toLowerCase();
if (trimmed.isEmpty) continue;
normalized.add(trimmed.startsWith('.') ? trimmed : '.$trimmed');
}
return normalized;
}
}
class SearchFilter {
+7 -6
View File
@@ -609,17 +609,16 @@ class TrackNotifier extends Notifier<TrackState> {
.map((ext) => ext.id)
.firstOrNull;
}
resolvedProvider ??= 'tidal';
}
final isEnabledExtensionProvider =
resolvedProvider != null &&
resolvedProvider.isNotEmpty &&
extensionState.extensions.any(
(ext) => ext.enabled && ext.id == resolvedProvider,
);
if (resolvedProvider != null &&
resolvedProvider.isNotEmpty &&
if (resolvedProvider.isNotEmpty &&
resolvedProvider != 'tidal' &&
resolvedProvider != 'qobuz' &&
!isEnabledExtensionProvider &&
@@ -639,10 +638,10 @@ class TrackNotifier extends Notifier<TrackState> {
.where((ext) => ext.enabled && ext.hasCustomSearch)
.map((ext) => ext.id)
.firstOrNull;
resolvedProvider ??= 'tidal';
}
if (resolvedProvider != null &&
resolvedProvider.isNotEmpty &&
if (resolvedProvider.isNotEmpty &&
resolvedProvider != 'tidal' &&
resolvedProvider != 'qobuz' &&
extensionState.extensions.any(
@@ -663,7 +662,9 @@ class TrackNotifier extends Notifier<TrackState> {
final effectiveBuiltInProvider =
resolvedProvider == 'tidal' || resolvedProvider == 'qobuz'
? resolvedProvider
: builtInSearchProvider;
: (builtInSearchProvider?.isNotEmpty == true
? builtInSearchProvider
: 'tidal');
if (effectiveBuiltInProvider == null || effectiveBuiltInProvider.isEmpty) {
state = TrackState(
+25 -24
View File
@@ -480,7 +480,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
))) {
return explicit;
}
return _defaultSearchExtension(extensions)?.id;
return _defaultSearchExtension(extensions)?.id ?? 'tidal';
}
String? _sanitizeSearchFilterForProvider(
@@ -524,8 +524,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
_canonicalSearchFilterId(candidate.label!) ==
canonicalFilter) ||
(candidate.icon != null &&
_canonicalSearchFilterId(candidate.icon!) ==
canonicalFilter),
_canonicalSearchFilterId(candidate.icon!) == canonicalFilter),
)
.firstOrNull;
return match?.id;
@@ -1289,28 +1288,28 @@ class _HomeTabState extends ConsumerState<HomeTab>
(hasHomeFeedExtension || hasExploreContent) &&
hasExploreContent;
ref.listen<String>(
settingsProvider.select((s) => s.defaultSearchTab),
(previous, next) {
if (previous == next) return;
final selectedSearchFilter = ref.read(
trackProvider.select((s) => s.selectedSearchFilter),
);
if (selectedSearchFilter != null && selectedSearchFilter.isNotEmpty) {
return;
}
ref.listen<String>(settingsProvider.select((s) => s.defaultSearchTab), (
previous,
next,
) {
if (previous == next) return;
final selectedSearchFilter = ref.read(
trackProvider.select((s) => s.selectedSearchFilter),
);
if (selectedSearchFilter != null && selectedSearchFilter.isNotEmpty) {
return;
}
final text = _urlController.text.trim();
if (text.isEmpty || text.length < _minLiveSearchChars) return;
if (text.startsWith('http') || text.startsWith('spotify:')) return;
final text = _urlController.text.trim();
if (text.isEmpty || text.length < _minLiveSearchChars) return;
if (text.startsWith('http') || text.startsWith('spotify:')) return;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_lastSearchQuery = null;
_performSearch(text);
});
},
);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
_lastSearchQuery = null;
_performSearch(text);
});
});
if (hasActualResults &&
isShowingRecentAccess &&
@@ -3554,8 +3553,10 @@ class _SearchProviderDropdown extends ConsumerWidget {
.where((ext) => ext.enabled && ext.hasCustomSearch)
.toList();
final primarySearchExtension = _defaultSearchExtension(searchProviders);
final defaultProviderTarget =
primarySearchExtension?.displayName ?? 'Tidal';
final defaultProviderLabel =
primarySearchExtension?.displayName ?? 'Deezer';
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
final defaultProviderIconPath = primarySearchExtension?.iconPath;
final currentProvider =
rawCurrentProvider != null &&
+34
View File
@@ -4333,6 +4333,40 @@ class _QueueTabState extends ConsumerState<QueueTab> {
),
),
if (filterMode == 'all' &&
totalTrackCount == 0 &&
!showFilteringIndicator &&
(_activeFilterCount > 0 || unifiedItems.isNotEmpty))
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
const Spacer(),
if (!_isSelectionMode)
_buildFilterButton(context, unifiedItems),
],
),
),
),
if (filterMode == 'singles' &&
totalTrackCount == 0 &&
!showFilteringIndicator &&
(_activeFilterCount > 0 || unifiedItems.isNotEmpty))
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
child: Row(
children: [
const Spacer(),
if (!_isSelectionMode)
_buildFilterButton(context, unifiedItems),
],
),
),
),
if (historyItems.isNotEmpty && hasQueueItems)
SliverToBoxAdapter(
child: Padding(
@@ -735,6 +735,7 @@ class _LanguageSelector extends StatelessWidget {
('pt_PT', 'Português (Brasil)', Icons.language),
('ru', 'Русский', Icons.language),
('tr', 'Türkçe', Icons.language),
('uk', 'Українська', Icons.language),
('zh', '简体中文', Icons.language),
('zh_CN', '简体中文 (中国)', Icons.language),
('zh_TW', '繁體中文', Icons.language),
@@ -742,8 +742,10 @@ class _MetadataSourceSelector extends ConsumerWidget {
final rawSearchProvider = settings.searchProvider?.trim() ?? '';
final isValidBuiltIn = _builtInProviders.containsKey(rawSearchProvider);
final primarySearchExtension = _defaultSearchExtension(extState.extensions);
final defaultProviderTarget =
primarySearchExtension?.displayName ?? 'Tidal';
final defaultProviderLabel =
primarySearchExtension?.displayName ?? 'Deezer';
'${context.l10n.extensionsHomeFeedAuto} ($defaultProviderTarget)';
final searchProvider =
isValidBuiltIn ||
extState.extensions.any(
+6
View File
@@ -1978,8 +1978,14 @@ class FFmpegService {
break;
case 'DATE':
vorbis['DATE'] = value;
final yearMatch = RegExp(r'^(\d{4})').firstMatch(value);
if (yearMatch != null &&
(!vorbis.containsKey('YEAR') || vorbis['YEAR']!.isEmpty)) {
vorbis['YEAR'] = yearMatch.group(1)!;
}
break;
case 'YEAR':
vorbis['YEAR'] = value;
if (!vorbis.containsKey('DATE') || vorbis['DATE']!.isEmpty) {
vorbis['DATE'] = value;
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024"><path fill="#000000" d="M0 0h1024v1024H0z"/><g fill="none" stroke="#1DB954" stroke-linecap="round" stroke-linejoin="round" transform="matrix(.9 0 0 .9 51.2 1.2)"><path stroke-width="38" d="M512 148v592"/><path stroke-width="36.1" d="M422 217.4v241.2m180-241.2v241.2"/><path stroke-width="34.2" d="M341 290v96m342-96v96"/><path stroke-width="38" d="m420 642 92 110 92-110M290 762v90q0 72 72 72h300q72 0 72-72v-90"/></g></svg>

After

Width:  |  Height:  |  Size: 488 B

+1 -1
View File
@@ -1,7 +1,7 @@
name: spotiflac_android
description: Download Spotify tracks in FLAC from Tidal, Qobuz & Deezer
publish_to: "none"
version: 4.3.0+125
version: 4.3.1+126
environment:
sdk: ^3.10.0