mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-24 16:54:03 +02:00
chore: remove redundant comments and update donor list
This commit is contained in:
@@ -163,10 +163,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
"sm-t225",
|
||||
"hammerhead",
|
||||
)
|
||||
/**
|
||||
* Check if device should use Skia instead of Impeller.
|
||||
* Returns true for devices with old/problematic GPUs or old Android versions.
|
||||
*/
|
||||
private fun shouldDisableImpeller(): Boolean {
|
||||
val hardware = Build.HARDWARE.lowercase(Locale.ROOT)
|
||||
val board = Build.BOARD.lowercase(Locale.ROOT)
|
||||
@@ -215,7 +211,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get GPU renderer string.
|
||||
* Note: This may return empty on some devices before OpenGL context is created.
|
||||
*/
|
||||
private fun getGpuRenderer(): String {
|
||||
|
||||
+7
-29
@@ -80,13 +80,11 @@ func readAPETagAtOffset(f *os.File, fileSize, footerOffset int64) (*APETag, erro
|
||||
return nil, fmt.Errorf("invalid footer offset")
|
||||
}
|
||||
|
||||
// Read the 32-byte footer/header
|
||||
footer := make([]byte, apeTagHeaderSize)
|
||||
if _, err := f.ReadAt(footer, footerOffset); err != nil {
|
||||
return nil, fmt.Errorf("failed to read APE footer: %w", err)
|
||||
}
|
||||
|
||||
// Verify preamble
|
||||
if string(footer[0:8]) != apeTagPreamble {
|
||||
return nil, fmt.Errorf("APE preamble not found")
|
||||
}
|
||||
@@ -96,7 +94,6 @@ func readAPETagAtOffset(f *os.File, fileSize, footerOffset int64) (*APETag, erro
|
||||
itemCount := binary.LittleEndian.Uint32(footer[16:20])
|
||||
flags := binary.LittleEndian.Uint32(footer[20:24])
|
||||
|
||||
// Sanity checks
|
||||
if version != apeTagVersion2 && version != 1000 {
|
||||
return nil, fmt.Errorf("unsupported APE tag version: %d", version)
|
||||
}
|
||||
@@ -113,7 +110,6 @@ func readAPETagAtOffset(f *os.File, fileSize, footerOffset int64) (*APETag, erro
|
||||
return nil, fmt.Errorf("expected APE footer but found header")
|
||||
}
|
||||
|
||||
// Calculate where the items data starts.
|
||||
// tagSize includes items + footer (32 bytes), but NOT the header.
|
||||
itemsSize := int64(tagSize) - apeTagHeaderSize
|
||||
if itemsSize < 0 {
|
||||
@@ -125,13 +121,11 @@ func readAPETagAtOffset(f *os.File, fileSize, footerOffset int64) (*APETag, erro
|
||||
return nil, fmt.Errorf("APE tag items extend before file start")
|
||||
}
|
||||
|
||||
// Read all items data
|
||||
itemsData := make([]byte, itemsSize)
|
||||
if _, err := f.ReadAt(itemsData, itemsOffset); err != nil {
|
||||
return nil, fmt.Errorf("failed to read APE items: %w", err)
|
||||
}
|
||||
|
||||
// Parse individual items
|
||||
items, err := parseAPEItems(itemsData, int(itemCount))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse APE items: %w", err)
|
||||
@@ -167,9 +161,8 @@ func parseAPEItems(data []byte, count int) ([]APETagItem, error) {
|
||||
}
|
||||
|
||||
key := string(data[pos:keyEnd])
|
||||
pos = keyEnd + 1 // skip null terminator
|
||||
pos = keyEnd + 1
|
||||
|
||||
// Read value
|
||||
if pos+valueSize > len(data) {
|
||||
break
|
||||
}
|
||||
@@ -190,19 +183,16 @@ func parseAPEItems(data []byte, count int) ([]APETagItem, error) {
|
||||
// If the file already has APEv2 tags, they are replaced.
|
||||
// The tag is written with both header and footer.
|
||||
func WriteAPETags(filePath string, tag *APETag) error {
|
||||
// First, read existing file to find and strip any existing APE tag
|
||||
existingSize, err := findExistingAPETagSize(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check existing APE tag: %w", err)
|
||||
}
|
||||
|
||||
// Build the new tag data
|
||||
tagData, err := marshalAPETag(tag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal APE tag: %w", err)
|
||||
}
|
||||
|
||||
// If there's an existing tag, we need to truncate the file first
|
||||
if existingSize > 0 {
|
||||
fi, err := os.Stat(filePath)
|
||||
if err != nil {
|
||||
@@ -214,7 +204,6 @@ func WriteAPETags(filePath string, tag *APETag) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Append the new tag
|
||||
f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open file for writing: %w", err)
|
||||
@@ -243,7 +232,6 @@ func findExistingAPETagSize(filePath string) (int64, error) {
|
||||
}
|
||||
fileSize := fi.Size()
|
||||
|
||||
// Try to read footer
|
||||
offsets := []int64{fileSize - apeTagHeaderSize}
|
||||
if fileSize > apeTagHeaderSize+128 {
|
||||
offsets = append(offsets, fileSize-apeTagHeaderSize-128)
|
||||
@@ -263,7 +251,7 @@ func findExistingAPETagSize(filePath string) (int64, error) {
|
||||
|
||||
flags := binary.LittleEndian.Uint32(footer[20:24])
|
||||
if (flags & apeTagFlagHeader) != 0 {
|
||||
continue // This is a header, not footer
|
||||
continue
|
||||
}
|
||||
|
||||
tagSize := int64(binary.LittleEndian.Uint32(footer[12:16]))
|
||||
@@ -292,7 +280,6 @@ func marshalAPETag(tag *APETag) ([]byte, error) {
|
||||
return nil, fmt.Errorf("empty APE tag")
|
||||
}
|
||||
|
||||
// Build items data
|
||||
var itemsData []byte
|
||||
for _, item := range tag.Items {
|
||||
keyBytes := []byte(item.Key)
|
||||
@@ -309,7 +296,7 @@ func marshalAPETag(tag *APETag) ([]byte, error) {
|
||||
itemsData = append(itemsData, sizeBuf...)
|
||||
itemsData = append(itemsData, flagsBuf...)
|
||||
itemsData = append(itemsData, keyBytes...)
|
||||
itemsData = append(itemsData, 0) // null terminator
|
||||
itemsData = append(itemsData, 0)
|
||||
itemsData = append(itemsData, valueBytes...)
|
||||
}
|
||||
|
||||
@@ -322,12 +309,10 @@ func marshalAPETag(tag *APETag) ([]byte, error) {
|
||||
version = tag.Version
|
||||
}
|
||||
|
||||
// Build header
|
||||
// flags: bit 29 = 1 (is header), bit 31 = 1 (contains header)
|
||||
headerFlags := uint32(apeTagFlagHeader | (1 << 31))
|
||||
header := buildAPEHeaderFooter(version, tagSize, itemCount, headerFlags)
|
||||
|
||||
// Build footer
|
||||
// flags: bit 29 = 0 (is footer), bit 31 = 1 (contains header)
|
||||
footerFlags := uint32(1 << 31)
|
||||
footer := buildAPEHeaderFooter(version, tagSize, itemCount, footerFlags)
|
||||
@@ -463,7 +448,6 @@ func AudioMetadataToAPEItems(metadata *AudioMetadata) []APETagItem {
|
||||
// the metadata fields map sent by the editor. This is used during merge to
|
||||
// ensure that even empty (cleared) fields override old values.
|
||||
func apeKeysFromFields(fields map[string]string) map[string]struct{} {
|
||||
// Map from fields-map key → APE tag key.
|
||||
mapping := map[string]string{
|
||||
"title": "TITLE",
|
||||
"artist": "ARTIST",
|
||||
@@ -490,29 +474,25 @@ func apeKeysFromFields(fields map[string]string) map[string]struct{} {
|
||||
result[strings.ToUpper(apeKey)] = struct{}{}
|
||||
}
|
||||
}
|
||||
// The reader accepts both YEAR and DATE for the date field; the writer
|
||||
// always emits "Year". Ensure both variants are overridden so that an
|
||||
// old "DATE" tag from another tagger is removed when the user edits date.
|
||||
// Some fields have reader aliases that must also be cleared when the
|
||||
// canonical key is updated (e.g. "Year" writer ↔ DATE/YEAR reader,
|
||||
// DISC ↔ DISCNUMBER, TRACK ↔ TRACKNUMBER, "ALBUM ARTIST" ↔ ALBUMARTIST,
|
||||
// LABEL ↔ PUBLISHER, LYRICS ↔ UNSYNCEDLYRICS).
|
||||
if _, present := fields["date"]; present {
|
||||
result["DATE"] = struct{}{}
|
||||
}
|
||||
// Similarly, DISCNUMBER is an alias for DISC in the reader.
|
||||
if _, present := fields["disc_number"]; present {
|
||||
result["DISCNUMBER"] = struct{}{}
|
||||
}
|
||||
// TRACKNUMBER is an alias for TRACK in the reader.
|
||||
if _, present := fields["track_number"]; present {
|
||||
result["TRACKNUMBER"] = struct{}{}
|
||||
}
|
||||
// ALBUMARTIST is an alias for ALBUM ARTIST in the reader.
|
||||
if _, present := fields["album_artist"]; present {
|
||||
result["ALBUMARTIST"] = struct{}{}
|
||||
}
|
||||
// PUBLISHER is an alias for LABEL in the reader.
|
||||
if _, present := fields["label"]; present {
|
||||
result["PUBLISHER"] = struct{}{}
|
||||
}
|
||||
// UNSYNCEDLYRICS is an alias for LYRICS in the reader.
|
||||
if _, present := fields["lyrics"]; present {
|
||||
result["UNSYNCEDLYRICS"] = struct{}{}
|
||||
}
|
||||
@@ -538,7 +518,6 @@ func MergeAPEItems(existing, newItems []APETagItem, overrideKeys map[string]stru
|
||||
combined[strings.ToUpper(item.Key)] = struct{}{}
|
||||
}
|
||||
|
||||
// Start with existing items whose keys are NOT in the combined set
|
||||
var merged []APETagItem
|
||||
for _, item := range existing {
|
||||
if _, overwritten := combined[strings.ToUpper(item.Key)]; !overwritten {
|
||||
@@ -546,7 +525,6 @@ func MergeAPEItems(existing, newItems []APETagItem, overrideKeys map[string]stru
|
||||
}
|
||||
}
|
||||
|
||||
// Append all new items
|
||||
merged = append(merged, newItems...)
|
||||
|
||||
return merged
|
||||
|
||||
@@ -62,7 +62,6 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) {
|
||||
return trackURL, nil
|
||||
}
|
||||
|
||||
// Try SongLink
|
||||
spotifyID := strings.TrimSpace(req.SpotifyID)
|
||||
if spotifyID != "" && isLikelySpotifyTrackID(spotifyID) {
|
||||
songlink := NewSongLinkClient()
|
||||
@@ -82,7 +81,6 @@ func resolveDeezerTrackURL(req DownloadRequest) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try ISRC
|
||||
isrc := strings.TrimSpace(req.ISRC)
|
||||
if isrc != "" {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), SongLinkTimeout)
|
||||
|
||||
+17
-27
@@ -171,10 +171,6 @@ func applyReEnrichTrackMetadata(req *reEnrichRequest, track ExtTrackMetadata) {
|
||||
req.SpotifyID = track.ID
|
||||
}
|
||||
|
||||
if req.shouldUpdateField("basic_tags") {
|
||||
// Title and Artist are not overwritten — they are used for search matching
|
||||
// and should remain as the user's original values.
|
||||
}
|
||||
if req.shouldUpdateField("basic_tags") {
|
||||
if track.AlbumName != "" {
|
||||
req.AlbumName = track.AlbumName
|
||||
@@ -768,8 +764,7 @@ func DownloadTrack(requestJSON string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
// DownloadByStrategy routes a unified download request to the appropriate flow.
|
||||
// Routing priority: YouTube service > extension fallback > built-in fallback > direct service.
|
||||
// DownloadByStrategy routes download requests with priority: YouTube > extension fallback > built-in fallback > direct service.
|
||||
func DownloadByStrategy(requestJSON string) (string, error) {
|
||||
var req DownloadRequest
|
||||
if err := json.Unmarshal([]byte(requestJSON), &req); err != nil {
|
||||
@@ -1067,7 +1062,6 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
result["copyright"] = metadata.Copyright
|
||||
result["composer"] = metadata.Composer
|
||||
result["comment"] = metadata.Comment
|
||||
// ReplayGain fields
|
||||
result["replaygain_track_gain"] = metadata.ReplayGainTrackGain
|
||||
result["replaygain_track_peak"] = metadata.ReplayGainTrackPeak
|
||||
result["replaygain_album_gain"] = metadata.ReplayGainAlbumGain
|
||||
@@ -1214,8 +1208,7 @@ func ReadFileMetadata(filePath string) (string, error) {
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
// ParseCueSheet parses a .cue file and returns JSON with split information.
|
||||
// This is called from Dart to get track listing and timing data for CUE splitting.
|
||||
// ParseCueSheet is called from Dart to get track listing and timing data for CUE splitting.
|
||||
// audioDir, if non-empty, overrides the directory used for resolving the
|
||||
// referenced audio file (useful for SAF temp file scenarios).
|
||||
func ParseCueSheet(cuePath string, audioDir string) (string, error) {
|
||||
@@ -1260,9 +1253,7 @@ func ScanCueSheetForLibraryWithCoverCacheKey(cuePath, audioDir, virtualPathPrefi
|
||||
return string(jsonBytes), nil
|
||||
}
|
||||
|
||||
// EditFileMetadata writes metadata to an audio file.
|
||||
// For FLAC files, uses native Go FLAC library.
|
||||
// For MP3/Opus, returns the metadata map so Dart can use FFmpeg.
|
||||
// EditFileMetadata writes audio file tags: FLAC via native Go library, MP3/Opus returns map for Dart/FFmpeg.
|
||||
func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
var fields map[string]string
|
||||
if err := json.Unmarshal([]byte(metadataJSON), &fields); err != nil {
|
||||
@@ -1299,20 +1290,19 @@ func EditFileMetadata(filePath, metadataJSON string) (string, error) {
|
||||
}
|
||||
|
||||
meta := &AudioMetadata{
|
||||
Title: fields["title"],
|
||||
Artist: fields["artist"],
|
||||
Album: fields["album"],
|
||||
AlbumArtist: fields["album_artist"],
|
||||
Date: fields["date"],
|
||||
TrackNumber: trackNum,
|
||||
DiscNumber: discNum,
|
||||
ISRC: fields["isrc"],
|
||||
Genre: fields["genre"],
|
||||
Label: fields["label"],
|
||||
Copyright: fields["copyright"],
|
||||
Composer: fields["composer"],
|
||||
Comment: fields["comment"],
|
||||
// ReplayGain fields
|
||||
Title: fields["title"],
|
||||
Artist: fields["artist"],
|
||||
Album: fields["album"],
|
||||
AlbumArtist: fields["album_artist"],
|
||||
Date: fields["date"],
|
||||
TrackNumber: trackNum,
|
||||
DiscNumber: discNum,
|
||||
ISRC: fields["isrc"],
|
||||
Genre: fields["genre"],
|
||||
Label: fields["label"],
|
||||
Copyright: fields["copyright"],
|
||||
Composer: fields["composer"],
|
||||
Comment: fields["comment"],
|
||||
ReplayGainTrackGain: fields["replaygain_track_gain"],
|
||||
ReplayGainTrackPeak: fields["replaygain_track_peak"],
|
||||
ReplayGainAlbumGain: fields["replaygain_album_gain"],
|
||||
@@ -2917,7 +2907,7 @@ func CustomSearchWithExtensionJSON(extensionID, query string, optionsJSON string
|
||||
"album_name": track.AlbumName,
|
||||
"album_artist": track.AlbumArtist,
|
||||
"duration_ms": track.DurationMS,
|
||||
"images": track.ResolvedCoverURL(), // Use helper to get cover URL from either field
|
||||
"images": track.ResolvedCoverURL(),
|
||||
"release_date": track.ReleaseDate,
|
||||
"track_number": track.TrackNumber,
|
||||
"disc_number": track.DiscNumber,
|
||||
|
||||
@@ -104,7 +104,6 @@ func RunWithTimeout(vm *goja.Runtime, script string, timeout time.Duration) (goj
|
||||
func RunWithTimeoutAndRecover(vm *goja.Runtime, script string, timeout time.Duration) (goja.Value, error) {
|
||||
result, err := RunWithTimeout(vm, script, timeout)
|
||||
|
||||
// Clear any interrupt state so VM can be reused
|
||||
if vm != nil {
|
||||
vm.ClearInterrupt()
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ func GetLogBuffer() *LogBuffer {
|
||||
globalLogBuffer = &LogBuffer{
|
||||
entries: make([]LogEntry, 0, defaultLogBufferSize),
|
||||
maxSize: defaultLogBufferSize,
|
||||
loggingEnabled: false, // Default: disabled for performance (user can enable in settings)
|
||||
loggingEnabled: false,
|
||||
}
|
||||
})
|
||||
return globalLogBuffer
|
||||
|
||||
@@ -309,7 +309,6 @@ func ReadMetadata(filePath string) (*Metadata, error) {
|
||||
metadata.Composer = getComment(cmt, "COMPOSER")
|
||||
metadata.Comment = getComment(cmt, "COMMENT")
|
||||
|
||||
// ReplayGain tags
|
||||
metadata.ReplayGainTrackGain = getComment(cmt, "REPLAYGAIN_TRACK_GAIN")
|
||||
metadata.ReplayGainTrackPeak = getComment(cmt, "REPLAYGAIN_TRACK_PEAK")
|
||||
metadata.ReplayGainAlbumGain = getComment(cmt, "REPLAYGAIN_ALBUM_GAIN")
|
||||
@@ -350,7 +349,7 @@ func EditFlacFields(filePath string, fields map[string]string) error {
|
||||
cmt = flacvorbis.New()
|
||||
}
|
||||
|
||||
artistMode := fields["artist_tag_mode"] // may be ""
|
||||
artistMode := fields["artist_tag_mode"]
|
||||
|
||||
// Mapping from fields-map key → one or more Vorbis Comment keys.
|
||||
// Each entry is handled with set-or-clear semantics.
|
||||
@@ -448,12 +447,10 @@ func EditFlacFields(filePath string, fields map[string]string) error {
|
||||
f.Meta = append(f.Meta, &cmtBlock)
|
||||
}
|
||||
|
||||
// Cover art
|
||||
coverPath := strings.TrimSpace(fields["cover_path"])
|
||||
if coverPath != "" && fileExists(coverPath) {
|
||||
coverData, err := os.ReadFile(coverPath)
|
||||
if err == nil && len(coverData) > 0 {
|
||||
// Remove existing pictures
|
||||
for i := len(f.Meta) - 1; i >= 0; i-- {
|
||||
if f.Meta[i].Type == flac.Picture {
|
||||
f.Meta = append(f.Meta[:i], f.Meta[i+1:]...)
|
||||
|
||||
@@ -175,7 +175,6 @@ func (s *SongLinkClient) doResolveRequest(payload []byte) (map[string]songLinkPl
|
||||
return nil, fmt.Errorf("resolve API returned success=false")
|
||||
}
|
||||
|
||||
// Map resolve API keys to SongLink-compatible platform keys
|
||||
keyMap := map[string]string{
|
||||
"Spotify": "spotify",
|
||||
"Deezer": "deezer",
|
||||
@@ -509,8 +508,6 @@ func extractYouTubeIDFromURL(youtubeURL string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
// isNumeric is defined in library_scan.go
|
||||
|
||||
func (s *SongLinkClient) GetDeezerIDFromSpotify(spotifyTrackID string) (string, error) {
|
||||
availability, err := s.CheckTrackAvailability(spotifyTrackID, "")
|
||||
if err != nil {
|
||||
|
||||
@@ -2473,7 +2473,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
_locallyCancelledItemIds.remove(id);
|
||||
}
|
||||
|
||||
// Clean accumulator entry for non-completed items.
|
||||
if (item.status != DownloadStatus.completed) {
|
||||
final key = _albumRgKey(item.track);
|
||||
final accumulator = _albumRgData[key];
|
||||
@@ -2499,7 +2498,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
void clearCompleted() {
|
||||
// Purge accumulator entries for failed/skipped items being removed.
|
||||
final removedItems = state.items.where(
|
||||
(item) =>
|
||||
item.status == DownloadStatus.completed ||
|
||||
@@ -2760,7 +2758,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
}
|
||||
|
||||
void clearFailedDownloads() {
|
||||
// Purge accumulator entries for failed items before removing them.
|
||||
final failedItems = state.items
|
||||
.where((item) => item.status == DownloadStatus.failed)
|
||||
.toList();
|
||||
@@ -2883,7 +2880,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
final key = _albumRgKey(track);
|
||||
final accumulator = _albumRgData[key];
|
||||
if (accumulator == null) return;
|
||||
// Find the entry for this track and update its file path in-place.
|
||||
for (final entry in accumulator.entries) {
|
||||
if (entry.trackId == track.id) {
|
||||
entry.filePath = finalPath;
|
||||
@@ -2969,7 +2965,6 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
||||
'Album ReplayGain for "$key": gain=$albumGain, peak=$albumPeak (${validEntries.length} tracks, album LUFS=${albumLufs.toStringAsFixed(1)})',
|
||||
);
|
||||
|
||||
// Write album gain to every completed track file.
|
||||
for (final entry in validEntries) {
|
||||
try {
|
||||
await _writeAlbumReplayGain(entry.filePath, albumGain, albumPeak);
|
||||
|
||||
@@ -496,7 +496,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if live search is available (extension is set as search provider)
|
||||
bool _isLiveSearchEnabled() {
|
||||
final settings = ref.read(settingsProvider);
|
||||
final extState = ref.read(extensionProvider);
|
||||
@@ -564,7 +563,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
}
|
||||
}
|
||||
|
||||
/// Built-in search providers that are not extensions
|
||||
static const _builtInSearchProviders = {'tidal', 'qobuz'};
|
||||
|
||||
Future<void> _performSearch(String query, {String? filterOverride}) async {
|
||||
@@ -599,7 +597,6 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
.read(trackProvider.notifier)
|
||||
.customSearch(searchProvider, query, options: options);
|
||||
} else if (isBuiltInProvider) {
|
||||
// Use built-in Tidal or Qobuz search
|
||||
await ref
|
||||
.read(trackProvider.notifier)
|
||||
.search(
|
||||
@@ -1122,7 +1119,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
title: Text(
|
||||
context.l10n.homeTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 20 + (14 * expandRatio), // 20 -> 34
|
||||
fontSize: 20 + (14 * expandRatio),
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
@@ -1496,7 +1493,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
) {
|
||||
final hasGreeting = greeting != null && greeting.isNotEmpty;
|
||||
final sectionOffset = hasGreeting ? 1 : 0;
|
||||
final totalCount = sections.length + sectionOffset + 1; // + bottom padding
|
||||
final totalCount = sections.length + sectionOffset + 1;
|
||||
|
||||
return [
|
||||
SliverList(
|
||||
@@ -2939,7 +2936,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
albumId: album.id,
|
||||
albumName: album.name,
|
||||
coverUrl: album.imageUrl,
|
||||
tracks: const [], // Will be fetched by AlbumScreen
|
||||
tracks: const [],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -2965,7 +2962,7 @@ class _HomeTabState extends ConsumerState<HomeTab>
|
||||
builder: (context) => PlaylistScreen(
|
||||
playlistName: playlist.name,
|
||||
coverUrl: playlist.imageUrl,
|
||||
tracks: const [], // Will be fetched
|
||||
tracks: const [],
|
||||
playlistId: playlist.id,
|
||||
),
|
||||
),
|
||||
@@ -3694,7 +3691,7 @@ class _TrackItemWithStatus extends ConsumerWidget {
|
||||
thickness: 1,
|
||||
indent:
|
||||
thumbWidth +
|
||||
24, // Adjust divider indent based on thumbnail width
|
||||
24,
|
||||
endIndent: 12,
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||
),
|
||||
|
||||
@@ -1132,11 +1132,11 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
String? _filterCacheFormat;
|
||||
String? _filterCacheMetadata;
|
||||
String _filterCacheSortMode = 'latest';
|
||||
String? _filterSource; // null = all, 'downloaded', 'local'
|
||||
String? _filterQuality; // null = all, 'hires', 'cd', 'lossy'
|
||||
String? _filterFormat; // null = all, 'flac', 'mp3', 'm4a', 'opus', 'ogg'
|
||||
String? _filterMetadata; // null = all, 'complete', 'missing-*'
|
||||
String _sortMode = 'latest'; // 'latest', 'oldest', 'a-z', 'z-a'
|
||||
String? _filterSource;
|
||||
String? _filterQuality;
|
||||
String? _filterFormat;
|
||||
String? _filterMetadata;
|
||||
String _sortMode = 'latest';
|
||||
|
||||
double _effectiveTextScale() {
|
||||
final textScale = MediaQuery.textScalerOf(context).scale(1.0);
|
||||
@@ -2036,7 +2036,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return quality.split('/').first;
|
||||
}
|
||||
|
||||
// Supports "MP3 320k", "Opus 256kbps", etc.
|
||||
final bitrateTextMatch = RegExp(
|
||||
r'(\d+)\s*k(?:bps)?',
|
||||
caseSensitive: false,
|
||||
@@ -2045,7 +2044,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
return '${bitrateTextMatch.group(1)}k';
|
||||
}
|
||||
|
||||
// Supports legacy quality IDs like "opus_256" / "mp3_320".
|
||||
final bitrateIdMatch = RegExp(r'_(\d+)$').firstMatch(q);
|
||||
if (bitrateIdMatch != null) {
|
||||
return '${bitrateIdMatch.group(1)}k';
|
||||
@@ -2301,7 +2299,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
|
||||
List<UnifiedLibraryItem> _applySorting(List<UnifiedLibraryItem> items) {
|
||||
if (_sortMode == 'latest') {
|
||||
return items; // Already sorted newest first from _getUnifiedItems
|
||||
return items;
|
||||
}
|
||||
final sorted = List<UnifiedLibraryItem>.of(items);
|
||||
switch (_sortMode) {
|
||||
@@ -3364,7 +3362,6 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
final selectionItems = getFilterData(
|
||||
historyFilterMode,
|
||||
).filteredUnifiedItems;
|
||||
// Only sync overlays when selection mode is active
|
||||
if (_isSelectionMode || _isPlaylistSelectionMode) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_isSelectionMode) {
|
||||
|
||||
@@ -164,13 +164,7 @@ class _RecentDonorsCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
const donorNames = <String>[
|
||||
'McNuggets Jimmy',
|
||||
'zcc09',
|
||||
'micahRichie',
|
||||
'a fan',
|
||||
'CJBGR',
|
||||
];
|
||||
const donorNames = <String>['R4ND0MIZ3D', 'Isra', 'bigJr48'];
|
||||
|
||||
// Match SettingsGroup color logic
|
||||
final cardColor = isDark
|
||||
|
||||
@@ -2210,7 +2210,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
final baseName = _buildSaveBaseName();
|
||||
|
||||
if (_isSafFile) {
|
||||
// SAF file: save to temp, then copy to SAF tree
|
||||
final tempDir = await Directory.systemTemp.createTemp('cover_');
|
||||
final tempOutput =
|
||||
'${tempDir.path}${Platform.pathSeparator}$baseName.jpg';
|
||||
@@ -2293,7 +2292,6 @@ class _TrackMetadataScreenState extends ConsumerState<TrackMetadataScreen> {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// No SAF tree info, keep in temp
|
||||
try {
|
||||
await Directory(tempDir.path).delete(recursive: true);
|
||||
} catch (_) {}
|
||||
@@ -5192,7 +5190,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
final method = result['method'] as String?;
|
||||
|
||||
if (method == 'ffmpeg') {
|
||||
// MP3/Opus: use FFmpeg to write metadata
|
||||
// For SAF files, Kotlin returns temp_path + saf_uri
|
||||
final tempPath = result['temp_path'] as String?;
|
||||
final safUri = result['saf_uri'] as String?;
|
||||
@@ -5280,7 +5277,6 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> {
|
||||
} catch (_) {}
|
||||
}
|
||||
} catch (_) {
|
||||
// No cover to preserve, continue without
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -892,7 +892,6 @@ class FFmpegService {
|
||||
///
|
||||
/// Returns a [ReplayGainResult] on success, or null if the scan fails.
|
||||
static Future<ReplayGainResult?> scanReplayGain(String filePath) async {
|
||||
// Run FFmpeg with ebur128 filter + astats for true peak.
|
||||
// -nostats suppresses the interactive progress line.
|
||||
// ebur128=peak=true prints integrated loudness + true peak.
|
||||
// framelog=quiet suppresses per-frame measurements (very verbose),
|
||||
@@ -941,7 +940,6 @@ class FFmpegService {
|
||||
}
|
||||
}
|
||||
|
||||
// ReplayGain reference level: -18 LUFS
|
||||
const replayGainReferenceLufs = -18.0;
|
||||
final gainDb = replayGainReferenceLufs - integratedLufs;
|
||||
|
||||
@@ -949,13 +947,11 @@ class FFmpegService {
|
||||
// If no true peak was found, fall back to 1.0 (0 dBFS).
|
||||
double peakLinear;
|
||||
if (truePeakDbfs != null) {
|
||||
// 10^(dBFS/20) converts dBFS to linear amplitude
|
||||
peakLinear = math.pow(10, truePeakDbfs / 20.0).toDouble();
|
||||
} else {
|
||||
peakLinear = 1.0;
|
||||
}
|
||||
|
||||
// Format to standard ReplayGain precision
|
||||
final trackGain =
|
||||
'${gainDb >= 0 ? "+" : ""}${gainDb.toStringAsFixed(2)} dB';
|
||||
final trackPeak = peakLinear.toStringAsFixed(6);
|
||||
@@ -1791,7 +1787,6 @@ class FFmpegService {
|
||||
vorbis['LYRICS'] = value;
|
||||
vorbis['UNSYNCEDLYRICS'] = value;
|
||||
break;
|
||||
// ReplayGain fields
|
||||
case 'REPLAYGAINTRACKGAIN':
|
||||
vorbis['REPLAYGAIN_TRACK_GAIN'] = value;
|
||||
break;
|
||||
|
||||
@@ -354,7 +354,7 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
/// Sets the lyrics provider order. Providers not in the list are disabled.
|
||||
/// Providers not in the list are disabled.
|
||||
static Future<void> setLyricsProviders(List<String> providers) async {
|
||||
final providersJSON = jsonEncode(providers);
|
||||
await _channel.invokeMethod('setLyricsProviders', {
|
||||
@@ -362,14 +362,12 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns the current lyrics provider order.
|
||||
static Future<List<String>> getLyricsProviders() async {
|
||||
final result = await _channel.invokeMethod('getLyricsProviders');
|
||||
final List<dynamic> decoded = jsonDecode(result as String) as List<dynamic>;
|
||||
return decoded.cast<String>();
|
||||
}
|
||||
|
||||
/// Returns metadata about all available lyrics providers.
|
||||
static Future<List<Map<String, dynamic>>>
|
||||
getAvailableLyricsProviders() async {
|
||||
final result = await _channel.invokeMethod('getAvailableLyricsProviders');
|
||||
@@ -387,7 +385,6 @@ class PlatformBridge {
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns current advanced lyrics fetch options.
|
||||
static Future<Map<String, dynamic>> getLyricsFetchOptions() async {
|
||||
final result = await _channel.invokeMethod('getLyricsFetchOptions');
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
|
||||
Reference in New Issue
Block a user