Version 1.5: Groups

This commit is contained in:
2026-04-16 05:48:51 +02:00
parent ae706bc8bc
commit e9462f6d91
8 changed files with 959 additions and 203 deletions
+123 -1
View File
@@ -9,12 +9,13 @@ import SwiftUI
import UIKit
enum FavoritesTab: CaseIterable {
case artists, albums, radios, podcasts
case artists, albums, songs, radios, podcasts
var title: LocalizedStringKey {
switch self {
case .artists: return "Artists"
case .albums: return "Albums"
case .songs: return "Songs"
case .radios: return "Radios"
case .podcasts: return "Podcasts"
}
@@ -38,6 +39,7 @@ struct FavoritesView: View {
switch selectedTab {
case .artists: FavoriteArtistsSection()
case .albums: FavoriteAlbumsSection()
case .songs: FavoriteSongsSection()
case .radios: FavoriteRadiosSection()
case .podcasts: FavoritePodcastsSection()
}
@@ -237,6 +239,126 @@ private struct FavoriteAlbumsSection: View {
}
}
// MARK: - Favorite Songs
private struct FavoriteSongsSection: View {
@Environment(MAService.self) private var service
@State private var tracks: [MAMediaItem] = []
@State private var isLoading = true
@State private var errorMessage: String?
@State private var showError = false
@State private var selectedTrackIndex: Int?
@State private var showPlayerPicker = false
private var players: [MAPlayer] {
Array(service.playerManager.players.values)
.filter { $0.available }
.sorted { $0.name < $1.name }
}
private var nowPlayingURIs: Set<String> {
Set(service.playerManager.playerQueues.values.compactMap {
$0.currentItem?.mediaItem?.uri
})
}
var body: some View {
Group {
if isLoading {
ProgressView()
} else if tracks.isEmpty {
ContentUnavailableView(
"No Favorite Songs",
systemImage: "heart.slash",
description: Text("Tap the heart icon on any song to add it here.")
)
} else {
List {
ForEach(Array(tracks.enumerated()), id: \.element.id) { index, track in
Button {
handleTrackTap(index: index)
} label: {
TrackRow(
track: track,
trackNumber: index + 1,
isPlaying: nowPlayingURIs.contains(track.uri)
)
}
.buttonStyle(.plain)
.listRowSeparator(.visible)
}
}
.listStyle(.plain)
}
}
.task {
await loadTracks()
}
.refreshable {
await loadTracks()
}
.alert("Error", isPresented: $showError) {
Button("OK", role: .cancel) { }
} message: {
if let errorMessage { Text(errorMessage) }
}
.sheet(isPresented: $showPlayerPicker) {
EnhancedPlayerPickerView(
players: players,
showNowPlayingOnSelect: true,
onSelect: { player in
if let index = selectedTrackIndex {
Task { await playFrom(index: index, on: player) }
}
}
)
}
}
private func handleTrackTap(index: Int) {
if players.count == 1 {
Task { await playFrom(index: index, on: players.first!) }
} else {
selectedTrackIndex = index
showPlayerPicker = true
}
}
private func loadTracks() async {
isLoading = true
errorMessage = nil
do {
// Fetch all favorited tracks from the server directly
var allTracks: [MAMediaItem] = []
var offset = 0
let pageSize = 50
var hasMore = true
while hasMore {
let page = try await service.getTracks(favorite: true, limit: pageSize, offset: offset)
allTracks.append(contentsOf: page)
offset += page.count
hasMore = page.count >= pageSize
}
tracks = allTracks.sorted { $0.name.lowercased() < $1.name.lowercased() }
} catch {
errorMessage = error.localizedDescription
showError = true
}
isLoading = false
}
private func playFrom(index: Int, on player: MAPlayer) async {
let uris = tracks[index...].map { $0.uri }
do {
try await service.playerManager.playMedia(playerId: player.playerId, uris: uris)
} catch {
errorMessage = error.localizedDescription
showError = true
}
}
}
// MARK: - Favorite Radios
private struct FavoriteRadiosSection: View {