my music view fix

This commit is contained in:
Dallas Groot 2026-04-10 20:44:23 -07:00
parent 228b186569
commit 55820fdb38
2 changed files with 136 additions and 20 deletions

View file

@ -12,6 +12,8 @@ struct MyMusicView: View {
@State private var recentAlbums: [Album] = []
@State private var allAlbums: [Album] = []
@State private var allSongs: [Song] = []
@State private var songsLoaded = false
@State private var isLoadingSongs = false
@State private var playlists: [Playlist] = []
@State private var artists: [ArtistIndex] = []
@State private var genres: [Genre] = []
@ -735,28 +737,104 @@ struct MyMusicView: View {
}
// MARK: - Songs Tab (list)
/// First letter of a song title for section headers, collapsing digits/symbols to "#"
private func sectionKey(for song: Song) -> String {
let first = song.title.first ?? "#"
return first.isLetter ? String(first).uppercased() : "#"
}
private var songsTab: some View {
let filtered = searchText.isEmpty ? allSongs : allSongs.filter {
$0.title.localizedCaseInsensitiveContains(searchText) ||
($0.artist ?? "").localizedCaseInsensitiveContains(searchText) ||
($0.album ?? "").localizedCaseInsensitiveContains(searchText)
}
return LazyVStack(spacing: 0) {
ForEach(Array(filtered.enumerated()), id: \.element.id) { index, song in
songRow(song: song, index: index, allSongs: filtered)
Divider()
.background(Color.white.opacity(0.08))
.padding(.leading, 72)
let filtered: [Song] = {
let base = allSongs
guard !searchText.isEmpty else { return base }
return base.filter {
$0.title.localizedCaseInsensitiveContains(searchText) ||
($0.artist ?? "").localizedCaseInsensitiveContains(searchText) ||
($0.album ?? "").localizedCaseInsensitiveContains(searchText)
}
if filtered.isEmpty && !isLoading {
}()
// Group into alphabetical sections
let grouped: [(String, [Song])] = {
var dict: [String: [Song]] = [:]
for song in filtered {
let key = sectionKey(for: song)
dict[key, default: []].append(song)
}
// Letters A-Z then # for digits/symbols
let letters = dict.keys.filter { $0 != "#" }.sorted()
let keys = dict["#"] != nil ? letters + ["#"] : letters
return keys.compactMap { k in dict[k].map { (k, $0) } }
}()
return LazyVStack(spacing: 0, pinnedViews: .sectionHeaders) {
if isLoadingSongs {
ProgressView()
.tint(accentPink)
.frame(maxWidth: .infinity)
.padding(.top, 60)
} else if filtered.isEmpty {
emptyState("No songs found")
} else {
ForEach(grouped, id: \.0) { (letter, songs) in
Section {
ForEach(Array(songs.enumerated()), id: \.element.id) { idx, song in
songRow(song: song, index: idx, allSongs: Array(filtered))
Divider()
.background(Color.white.opacity(0.08))
.padding(.leading, 72)
}
} header: {
Text(letter)
.font(.system(size: 13, weight: .semibold))
.foregroundColor(.gray)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 16)
.padding(.vertical, 6)
.background(Color.black.opacity(0.85))
}
}
}
}
.padding(.top, 4)
.task {
guard !songsLoaded, !isLoadingSongs else { return }
await loadAllSongs()
}
}
private func loadAllSongs() async {
// Cache hit instant display
if let cached = LibraryCache.shared.load([Song].self, key: "all_songs_sorted") {
allSongs = cached
songsLoaded = true
return
}
isLoadingSongs = true
do {
var collected: [Song] = []
var offset = 0
while true {
let result = try await serverManager.client.search3(
query: "", artistCount: 0, artistOffset: 0,
albumCount: 0, albumOffset: 0,
songCount: 500, songOffset: offset)
let page = result?.song ?? []
collected.append(contentsOf: page)
if page.count < 500 { break }
offset += 500
}
let sorted = collected.sorted {
$0.title.localizedCaseInsensitiveCompare($1.title) == .orderedAscending
}
LibraryCache.shared.save(sorted, key: "all_songs_sorted")
allSongs = sorted
songsLoaded = true
isLoadingSongs = false
} catch {
isLoadingSongs = false
}
}
@ViewBuilder
@ -1121,7 +1199,6 @@ struct MyMusicView: View {
self.artists = artistList
self.allAlbums = albumsFull
self.genres = genreList
self.allSongs = [] // Songs loaded on-demand via search
if let starredSongs = starred?.song {
self.favouriteSongs = starredSongs
}

View file

@ -21,7 +21,8 @@ class AlbumColorExtractor: ObservableObject {
private var lastCoverArtId: String?
private var coverStoreObserver: AnyCancellable?
private var radioCoverStoreObserver: AnyCancellable?
private init() {
// Re-extract colors when custom album cover changes
coverStoreObserver = AlbumCoverStore.shared.$updateTrigger
@ -31,6 +32,28 @@ class AlbumColorExtractor: ObservableObject {
self.lastCoverArtId = nil // force re-extract
self.extract(from: id)
}
// Re-extract when a radio cover changes
radioCoverStoreObserver = RadioCoverStore.shared.$updateTrigger
.dropFirst()
.sink { [weak self] _ in
guard let self = self else { return }
self.lastCoverArtId = nil // force re-extract on next call
}
}
/// Extract colors directly from a UIImage used for radio stations whose
/// custom cover lives in RadioCoverStore rather than AlbumCoverStore.
func extractDirect(from image: UIImage, id: String) {
guard id != lastCoverArtId else { return }
lastCoverArtId = id
let colors = Self.dominantColors(from: image)
withAnimation(.easeInOut(duration: 1.0)) {
self.primaryColor = colors.0
self.secondaryColor = colors.1
self.currentImage = image
self.isLoaded = true
}
self.preferredStatusBarStyle = Self.statusBarStyle(for: colors.0)
}
func extract(from coverArtId: String?) {
@ -230,6 +253,18 @@ struct NowPlayingView: View {
private var isRadio: Bool {
audioPlayer.isRadioStream || audioPlayer.currentSong?.album == "Radio"
}
/// Routes color extraction to RadioCoverStore for radio stations with custom covers,
/// otherwise falls through to the standard AlbumColorExtractor path.
private func extractAlbumColors() {
if isRadio,
let song = audioPlayer.currentSong,
let customImg = RadioCoverStore.shared.loadCover(for: song.id) {
albumColors.extractDirect(from: customImg, id: song.id)
} else {
albumColors.extract(from: audioPlayer.currentSong?.coverArt)
}
}
var body: some View {
GeometryReader { screenGeo in
@ -309,11 +344,15 @@ struct NowPlayingView: View {
.onAppear {
dragOffset = 0
isStarred = audioPlayer.currentSong?.starred != nil
albumColors.extract(from: audioPlayer.currentSong?.coverArt)
extractAlbumColors()
}
.onChange(of: audioPlayer.currentSong?.id) { _, _ in
isStarred = audioPlayer.currentSong?.starred != nil
albumColors.extract(from: audioPlayer.currentSong?.coverArt)
extractAlbumColors()
}
.onChange(of: RadioCoverStore.shared.updateTrigger) { _, _ in
// Radio cover changed re-extract if a radio station is playing
if isRadio { extractAlbumColors() }
}
.onChange(of: audioPlayer.currentSong?.starred) { _, newVal in
isStarred = newVal != nil