Rockbox open source high quality audio player as a Music Player Daemon
mpris rockbox mpd libadwaita audio rust zig deno
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add playback toolbar and PlayButtons component

Integrates play/shuffle actions into Songs and Likes views via a
toolbar, including a song count and toolbar background. LikesListView
gains a loading state and now renders a header + song rows; both views
call the async play endpoints and surface gRPC errors in an alert.

+213 -60
macos/Rockbox.xcodeproj/project.xcworkspace/xcuserdata/tsirysandratraina.xcuserdatad/UserInterfaceState.xcuserstate

This is a binary file and will not be displayed.

+65
macos/Rockbox/Views/Components/PlayButtons.swift
··· 1 + // 2 + // PlayButtons.swift 3 + // Rockbox 4 + // 5 + // Created by Tsiry Sandratraina on 20/12/2025. 6 + // 7 + 8 + import SwiftUI 9 + 10 + struct PlayShuffleButtons: View { 11 + var onPlay: () -> Void 12 + var onShuffle: () -> Void 13 + var songCount: Int 14 + 15 + @State private var isHoveringPlay = false 16 + @State private var isHoveringShuffle = false 17 + 18 + var body: some View { 19 + HStack(spacing: 12) { 20 + Button(action: onPlay) { 21 + HStack(spacing: 6) { 22 + Image(systemName: "play.fill") 23 + .font(.system(size: 12)) 24 + Text("Play") 25 + .font(.system(size: 13, weight: .medium)) 26 + } 27 + .foregroundStyle(.white) 28 + .padding(.horizontal, 16) 29 + .padding(.vertical, 8) 30 + .background( 31 + RoundedRectangle(cornerRadius: 6) 32 + .fill(isHoveringPlay ? Color.accentColor.opacity(0.8) : Color.accentColor) 33 + ) 34 + } 35 + .buttonStyle(.plain) 36 + .onHover { isHoveringPlay = $0 } 37 + 38 + Button(action: onShuffle) { 39 + HStack(spacing: 6) { 40 + Image(systemName: "shuffle") 41 + .font(.system(size: 12)) 42 + Text("Shuffle") 43 + .font(.system(size: 13, weight: .medium)) 44 + } 45 + .foregroundStyle(.primary) 46 + .padding(.horizontal, 16) 47 + .padding(.vertical, 8) 48 + .background( 49 + RoundedRectangle(cornerRadius: 6) 50 + .fill(isHoveringShuffle ? Color.secondary.opacity(0.2) : Color.secondary.opacity(0.1)) 51 + ) 52 + } 53 + .buttonStyle(.plain) 54 + .onHover { isHoveringShuffle = $0 } 55 + 56 + Spacer() 57 + 58 + Text("\(songCount) songs") 59 + .font(.system(size: 12)) 60 + .foregroundStyle(.secondary) 61 + } 62 + .padding(.horizontal, 20) 63 + .padding(.vertical, 12) 64 + } 65 + }
+97 -44
macos/Rockbox/Views/Likes/LikesListView.swift
··· 7 7 8 8 import SwiftUI 9 9 10 - 11 10 struct LikesListView: View { 12 11 @State private var likedSongs: [Song] = [] 13 12 @State private var errorText: String? 13 + @State private var isLoading = true 14 14 @ObservedObject var library: MusicLibrary 15 15 16 16 var body: some View { 17 - if likedSongs.isEmpty { 18 - VStack(spacing: 12) { 19 - Image(systemName: "heart.slash") 20 - .font(.system(size: 48)) 21 - .foregroundStyle(.tertiary) 22 - 23 - Text("No liked songs yet") 24 - .font(.title3) 25 - .foregroundStyle(.secondary) 26 - 27 - Text("Tap the heart icon on any song to add it here") 28 - .font(.subheadline) 29 - .foregroundStyle(.tertiary) 17 + VStack(spacing: 0) { 18 + if isLoading { 19 + ProgressView() 20 + .frame(maxWidth: .infinity, maxHeight: .infinity) 21 + } else if likedSongs.isEmpty { 22 + VStack(spacing: 12) { 23 + Image(systemName: "heart.slash") 24 + .font(.system(size: 48)) 25 + .foregroundStyle(.tertiary) 26 + 27 + Text("No liked songs yet") 28 + .font(.title3) 29 + .foregroundStyle(.secondary) 30 + 31 + Text("Tap the heart icon on any song to add it here") 32 + .font(.subheadline) 33 + .foregroundStyle(.tertiary) 34 + } 35 + .frame(maxWidth: .infinity, maxHeight: .infinity) 36 + } else { 37 + // Song list 38 + ScrollView { 39 + LazyVStack(spacing: 0) { 40 + // Header row 41 + SongHeaderRow(showLike: true) 42 + 43 + Divider() 44 + 45 + // Liked song rows 46 + ForEach(Array(likedSongs.enumerated()), id: \.element.id) { index, song in 47 + SongRowView( 48 + song: song, 49 + index: index + 1, 50 + isEven: index % 2 == 0, 51 + showLike: true, 52 + isLikesScreen: true, 53 + library: library 54 + ) 55 + } 56 + } 57 + } 30 58 } 31 - .frame(maxWidth: .infinity, maxHeight: .infinity) 32 - .task { 33 - do { 34 - let data = try await fetchLikedTracks() 35 - likedSongs = [] 36 - for track in data { 37 - let song = Song(cuid: track.id, title: track.title, artist: track.artist, album: track.album, albumArt: URL(string: "http://localhost:6062/covers/" + track.albumArt), duration: TimeInterval(track.length / 1000), trackNumber: Int(track.trackNumber), discNumber: Int(track.discNumber),color: .gray.opacity(0.3)) 38 - library.likedSongIds.insert(song.cuid) 39 - likedSongs.append(song) 59 + } 60 + .toolbar { 61 + ToolbarItemGroup(placement: .automatic) { 62 + if !likedSongs.isEmpty { 63 + Button(action: { Task { 64 + do { 65 + try await playLikedTracks(shuffle: false) 66 + } catch { 67 + errorText = String(describing: error) 68 + } 69 + } }) { 70 + Label("Play", systemImage: "play.fill") 71 + } 72 + 73 + Button(action: { Task { 74 + do { 75 + try await playLikedTracks(shuffle: true) 76 + } catch { 77 + errorText = String(describing: error) 78 + } 79 + } }) { 80 + Label("Shuffle", systemImage: "shuffle") 40 81 } 41 82 42 - } catch { 43 - errorText = String(describing: error) 83 + Spacer() 84 + 85 + Text("\(likedSongs.count) songs") 86 + .foregroundStyle(.secondary) 44 87 } 45 88 } 46 - .alert("gRPC Error", isPresented: .constant(errorText != nil)) { 47 - Button("OK") { errorText = nil } 48 - } message: { 49 - Text(errorText ?? "") 50 - } 51 - 52 - } else { 53 - ScrollView { 54 - LazyVStack(spacing: 0) { 55 - // Header row 56 - SongHeaderRow(showLike: true) 57 - 58 - Divider() 59 - 60 - // Liked song rows 61 - ForEach(Array(likedSongs.enumerated()), id: \.element.id) { index, song in 62 - SongRowView(song: song, index: index + 1, isEven: index % 2 == 0, showLike: true, isLikesScreen: true, library: library) 63 - } 89 + } 90 + .toolbarBackground(.ultraThinMaterial, for: .windowToolbar) 91 + .task { 92 + do { 93 + let data = try await fetchLikedTracks() 94 + likedSongs = [] 95 + for track in data { 96 + let song = Song( 97 + cuid: track.id, 98 + title: track.title, 99 + artist: track.artist, 100 + album: track.album, 101 + albumArt: URL(string: "http://localhost:6062/covers/" + track.albumArt), 102 + duration: TimeInterval(track.length / 1000), 103 + trackNumber: Int(track.trackNumber), 104 + discNumber: Int(track.discNumber), 105 + color: .gray.opacity(0.3) 106 + ) 107 + library.likedSongIds.insert(song.cuid) 108 + likedSongs.append(song) 64 109 } 110 + isLoading = false 111 + } catch { 112 + errorText = String(describing: error) 113 + isLoading = false 65 114 } 66 115 } 116 + .alert("gRPC Error", isPresented: .constant(errorText != nil)) { 117 + Button("OK") { errorText = nil } 118 + } message: { 119 + Text(errorText ?? "") 120 + } 67 121 } 68 122 } 69 -
+51 -16
macos/Rockbox/Views/Songs/SongsListView.swift
··· 11 11 @State private var songs: [Song] = [] 12 12 @State private var errorText: String? 13 13 @ObservedObject var library: MusicLibrary 14 + @State private var isHoveringPlay = false 15 + @State private var isHoveringShuffle = false 14 16 15 17 var body: some View { 16 - ScrollView { 17 - LazyVStack(spacing: 0) { 18 - // Header row 19 - SongHeaderRow(showLike: true) 20 - 21 - Divider() 22 - 23 - // Song rows 24 - ForEach(Array(songs.enumerated()), id: \.element.id) { index, song in 25 - SongRowView(song: song, index: index + 1, isEven: index % 2 == 0, showLike: true, library: library) 18 + VStack(spacing: 0) { 19 + // Scrollable song list 20 + ScrollView { 21 + LazyVStack(spacing: 0) { 22 + // Header row 23 + SongHeaderRow(showLike: true) 24 + 25 + Divider() 26 + 27 + // Song rows 28 + ForEach(Array(songs.enumerated()), id: \.element.id) { index, song in 29 + SongRowView(song: song, index: index + 1, isEven: index % 2 == 0, showLike: true, library: library) 30 + } 31 + } 32 + } 33 + } 34 + .toolbar { 35 + ToolbarItemGroup(placement: .automatic) { 36 + if !songs.isEmpty { 37 + Button(action: { Task { 38 + do { 39 + try await playAllTracks(shuffle: false) 40 + } catch { 41 + errorText = String(describing: error) 42 + } 43 + } }) { 44 + Label("Play", systemImage: "play.fill") 45 + } 46 + 47 + Button(action: { Task { 48 + do { 49 + try await playAllTracks(shuffle: true) 50 + } catch { 51 + errorText = String(describing: error) 52 + } 53 + } }) { 54 + Label("Shuffle", systemImage: "shuffle") 55 + } 56 + 57 + Spacer() 58 + 59 + Text("\(songs.count) songs") 60 + .foregroundStyle(.secondary) 26 61 } 27 62 } 28 63 } 64 + .toolbarBackground(.ultraThinMaterial, for: .windowToolbar) 29 65 .task { 30 66 do { 31 67 let data = try await fetchTracks() 32 68 songs = [] 33 69 for track in data { 34 - songs.append(Song(cuid: track.id, title: track.title, artist: track.artist, album: track.album, albumArt: URL(string: "http://localhost:6062/covers/" + track.albumArt), duration: TimeInterval(track.length / 1000), trackNumber: Int(track.trackNumber), discNumber: Int(track.discNumber),color: .gray.opacity(0.3))) 70 + songs.append(Song(cuid: track.id, title: track.title, artist: track.artist, album: track.album, albumArt: URL(string: "http://localhost:6062/covers/" + track.albumArt), duration: TimeInterval(track.length / 1000), trackNumber: Int(track.trackNumber), discNumber: Int(track.discNumber), color: .gray.opacity(0.3))) 35 71 } 36 72 37 73 let likes = try await fetchLikedTracks() ··· 45 81 } 46 82 } 47 83 .alert("gRPC Error", isPresented: .constant(errorText != nil)) { 48 - Button("OK") { errorText = nil } 49 - } message: { 50 - Text(errorText ?? "") 51 - } 52 - 84 + Button("OK") { errorText = nil } 85 + } message: { 86 + Text(errorText ?? "") 87 + } 53 88 } 54 89 }