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.

Merge pull request #40 from tsirysndr/feat/play-shuffle-favourites

webui: add 'Play/Shuffle' buttons to 'Likes' Page

authored by

Tsiry Sandratraina and committed by
GitHub
489ed012 bf1df998

+563 -40
+31
crates/graphql/src/schema/playback.rs
··· 288 288 289 289 Ok(0) 290 290 } 291 + 292 + async fn play_liked_tracks( 293 + &self, 294 + ctx: &Context<'_>, 295 + shuffle: Option<bool>, 296 + ) -> Result<i32, Error> { 297 + let pool = ctx.data::<Pool<Sqlite>>()?; 298 + let tracks = repo::favourites::all_tracks(pool.clone()) 299 + .await? 300 + .into_iter() 301 + .map(|t| t.path) 302 + .collect::<Vec<String>>(); 303 + 304 + let client = ctx.data::<reqwest::Client>().unwrap(); 305 + let body = serde_json::json!({ 306 + "tracks": tracks, 307 + }); 308 + 309 + let url = format!("{}/playlists", rockbox_url()); 310 + client.post(&url).json(&body).send().await?; 311 + 312 + if let Some(true) = shuffle { 313 + let url = format!("{}/playlists/shuffle", rockbox_url()); 314 + client.put(&url).send().await?; 315 + } 316 + 317 + let url = format!("{}/playlists/start", rockbox_url()); 318 + client.put(&url).send().await?; 319 + 320 + Ok(0) 321 + } 291 322 } 292 323 293 324 #[derive(Default)]
+7
crates/rpc/proto/rockbox/v1alpha1/playback.proto
··· 138 138 139 139 message PlayTrackResponse {} 140 140 141 + message PlayLikedTracksRequest { 142 + optional bool shuffle = 1; 143 + } 144 + 145 + message PlayLikedTracksResponse {} 146 + 141 147 service PlaybackService { 142 148 rpc Play(PlayRequest) returns (PlayResponse) {} 143 149 rpc Pause(PauseRequest) returns (PauseResponse) {} ··· 156 162 rpc PlayPlaylist(PlayPlaylistRequest) returns (PlayPlaylistResponse) {} 157 163 rpc PlayDirectory(PlayDirectoryRequest) returns (PlayDirectoryResponse) {} 158 164 rpc PlayTrack(PlayTrackRequest) returns (PlayTrackResponse) {} 165 + rpc PlayLikedTracks(PlayLikedTracksRequest) returns (PlayLikedTracksResponse) {} 159 166 }
+90
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 2043 2043 } 2044 2044 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2045 2045 pub struct PlayTrackResponse {} 2046 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2047 + pub struct PlayLikedTracksRequest { 2048 + #[prost(bool, optional, tag = "1")] 2049 + pub shuffle: ::core::option::Option<bool>, 2050 + } 2051 + #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2052 + pub struct PlayLikedTracksResponse {} 2046 2053 /// Generated client implementations. 2047 2054 pub mod playback_service_client { 2048 2055 #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] ··· 2570 2577 ); 2571 2578 self.inner.unary(req, path, codec).await 2572 2579 } 2580 + pub async fn play_liked_tracks( 2581 + &mut self, 2582 + request: impl tonic::IntoRequest<super::PlayLikedTracksRequest>, 2583 + ) -> std::result::Result< 2584 + tonic::Response<super::PlayLikedTracksResponse>, 2585 + tonic::Status, 2586 + > { 2587 + self.inner 2588 + .ready() 2589 + .await 2590 + .map_err(|e| { 2591 + tonic::Status::new( 2592 + tonic::Code::Unknown, 2593 + format!("Service was not ready: {}", e.into()), 2594 + ) 2595 + })?; 2596 + let codec = tonic::codec::ProstCodec::default(); 2597 + let path = http::uri::PathAndQuery::from_static( 2598 + "/rockbox.v1alpha1.PlaybackService/PlayLikedTracks", 2599 + ); 2600 + let mut req = request.into_request(); 2601 + req.extensions_mut() 2602 + .insert( 2603 + GrpcMethod::new( 2604 + "rockbox.v1alpha1.PlaybackService", 2605 + "PlayLikedTracks", 2606 + ), 2607 + ); 2608 + self.inner.unary(req, path, codec).await 2609 + } 2573 2610 } 2574 2611 } 2575 2612 /// Generated server implementations. ··· 2681 2718 request: tonic::Request<super::PlayTrackRequest>, 2682 2719 ) -> std::result::Result< 2683 2720 tonic::Response<super::PlayTrackResponse>, 2721 + tonic::Status, 2722 + >; 2723 + async fn play_liked_tracks( 2724 + &self, 2725 + request: tonic::Request<super::PlayLikedTracksRequest>, 2726 + ) -> std::result::Result< 2727 + tonic::Response<super::PlayLikedTracksResponse>, 2684 2728 tonic::Status, 2685 2729 >; 2686 2730 } ··· 3515 3559 let inner = self.inner.clone(); 3516 3560 let fut = async move { 3517 3561 let method = PlayTrackSvc(inner); 3562 + let codec = tonic::codec::ProstCodec::default(); 3563 + let mut grpc = tonic::server::Grpc::new(codec) 3564 + .apply_compression_config( 3565 + accept_compression_encodings, 3566 + send_compression_encodings, 3567 + ) 3568 + .apply_max_message_size_config( 3569 + max_decoding_message_size, 3570 + max_encoding_message_size, 3571 + ); 3572 + let res = grpc.unary(method, req).await; 3573 + Ok(res) 3574 + }; 3575 + Box::pin(fut) 3576 + } 3577 + "/rockbox.v1alpha1.PlaybackService/PlayLikedTracks" => { 3578 + #[allow(non_camel_case_types)] 3579 + struct PlayLikedTracksSvc<T: PlaybackService>(pub Arc<T>); 3580 + impl< 3581 + T: PlaybackService, 3582 + > tonic::server::UnaryService<super::PlayLikedTracksRequest> 3583 + for PlayLikedTracksSvc<T> { 3584 + type Response = super::PlayLikedTracksResponse; 3585 + type Future = BoxFuture< 3586 + tonic::Response<Self::Response>, 3587 + tonic::Status, 3588 + >; 3589 + fn call( 3590 + &mut self, 3591 + request: tonic::Request<super::PlayLikedTracksRequest>, 3592 + ) -> Self::Future { 3593 + let inner = Arc::clone(&self.0); 3594 + let fut = async move { 3595 + <T as PlaybackService>::play_liked_tracks(&inner, request) 3596 + .await 3597 + }; 3598 + Box::pin(fut) 3599 + } 3600 + } 3601 + let accept_compression_encodings = self.accept_compression_encodings; 3602 + let send_compression_encodings = self.send_compression_encodings; 3603 + let max_decoding_message_size = self.max_decoding_message_size; 3604 + let max_encoding_message_size = self.max_encoding_message_size; 3605 + let inner = self.inner.clone(); 3606 + let fut = async move { 3607 + let method = PlayLikedTracksSvc(inner); 3518 3608 let codec = tonic::codec::ProstCodec::default(); 3519 3609 let mut grpc = tonic::server::Grpc::new(codec) 3520 3610 .apply_compression_config(
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+40
crates/rpc/src/playback.rs
··· 393 393 394 394 Ok(tonic::Response::new(PlayTrackResponse::default())) 395 395 } 396 + 397 + async fn play_liked_tracks( 398 + &self, 399 + request: tonic::Request<PlayLikedTracksRequest>, 400 + ) -> Result<tonic::Response<PlayLikedTracksResponse>, tonic::Status> { 401 + let request = request.into_inner(); 402 + let shuffle = request.shuffle; 403 + let tracks = repo::favourites::all_tracks(self.pool.clone()) 404 + .await 405 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 406 + let body = serde_json::json!({ 407 + "tracks": tracks.into_iter().map(|t| t.path).collect::<Vec<String>>(), 408 + }); 409 + 410 + let url = format!("{}/playlists", rockbox_url()); 411 + self.client 412 + .post(&url) 413 + .json(&body) 414 + .send() 415 + .await 416 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 417 + 418 + if let Some(true) = shuffle { 419 + let url = format!("{}/playlists/shuffle", rockbox_url()); 420 + self.client 421 + .put(&url) 422 + .send() 423 + .await 424 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 425 + } 426 + 427 + let url = format!("{}/playlists/start", rockbox_url()); 428 + self.client 429 + .put(&url) 430 + .send() 431 + .await 432 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 433 + 434 + Ok(tonic::Response::new(PlayLikedTracksResponse::default())) 435 + } 396 436 }
+29
webui/rockbox/graphql.schema.json
··· 1322 1322 "deprecationReason": null 1323 1323 }, 1324 1324 { 1325 + "name": "playLikedTracks", 1326 + "description": null, 1327 + "args": [ 1328 + { 1329 + "name": "shuffle", 1330 + "description": null, 1331 + "type": { 1332 + "kind": "SCALAR", 1333 + "name": "Boolean", 1334 + "ofType": null 1335 + }, 1336 + "defaultValue": null, 1337 + "isDeprecated": false, 1338 + "deprecationReason": null 1339 + } 1340 + ], 1341 + "type": { 1342 + "kind": "NON_NULL", 1343 + "name": null, 1344 + "ofType": { 1345 + "kind": "SCALAR", 1346 + "name": "Int", 1347 + "ofType": null 1348 + } 1349 + }, 1350 + "isDeprecated": false, 1351 + "deprecationReason": null 1352 + }, 1353 + { 1325 1354 "name": "playPlaylist", 1326 1355 "description": null, 1327 1356 "args": [
+1 -1
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 470 470 </div> 471 471 </button> 472 472 <div 473 - class="css-wa9s5g" 473 + class="css-15d2ttn" 474 474 /> 475 475 <button 476 476 class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bo bp bq br bl bm bn"
+1 -1
webui/rockbox/src/Components/AlbumDetails/styles.tsx
··· 73 73 `; 74 74 75 75 export const Separator = styled.div` 76 - width: 26px; 76 + width: 20px; 77 77 `; 78 78 79 79 export const BackButton = styled.button`
+1 -1
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 436 436 </div> 437 437 </button> 438 438 <div 439 - class="css-wa9s5g" 439 + class="css-15d2ttn" 440 440 /> 441 441 <button 442 442 class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bo bp bq br bl bm bn"
+1 -1
webui/rockbox/src/Components/ArtistDetails/styles.tsx
··· 53 53 `; 54 54 55 55 export const Separator = styled.div` 56 - width: 26px; 56 + width: 20px; 57 57 `; 58 58 59 59 export const Label = styled.div`
+5
webui/rockbox/src/Components/Artists/Artists.tsx
··· 8 8 ArtistCover, 9 9 ArtistName, 10 10 Container, 11 + FilterContainer, 11 12 NoArtistCover, 12 13 Scrollable, 13 14 Title, 14 15 } from "./styles"; 15 16 import Artist from "../Icons/Artist"; 16 17 import { Link } from "react-router-dom"; 18 + import Filter from "../Filter"; 17 19 18 20 export type ArtistsProps = { 19 21 artists: any[]; ··· 30 32 <ControlBar /> 31 33 <Scrollable> 32 34 <Title>Artists</Title> 35 + <FilterContainer> 36 + <Filter placeholder="Search artists" onChange={() => {}} /> 37 + </FilterContainer> 33 38 <div style={{ marginBottom: 100 }}> 34 39 <Grid 35 40 gridColumns={[2, 3, 4]}
+50 -7
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 379 379 Artists 380 380 </div> 381 381 <div 382 + class="css-1ngvhjn" 383 + > 384 + <div 385 + class="ae af ag ah ai aj ak al am an ao ap aq ar as at au av aw ax ay az b0 b1 b2 b3 b4 b5 b6 b7 b8" 386 + data-baseweb="input" 387 + > 388 + <div 389 + class="af b9 ba bb ar as ax ay az b0 b7 bc bd b5 be bf" 390 + > 391 + <svg 392 + data-test="icon-ActionActiveSearch" 393 + fill="none" 394 + height="20" 395 + width="20" 396 + xmlns="http://www.w3.org/2000/svg" 397 + > 398 + <path 399 + clip-rule="evenodd" 400 + d="M4 9.5a5.5 5.5 0 1 1 11 0 5.5 5.5 0 0 1-11 0ZM9.5 2a7.5 7.5 0 1 0 4.549 13.463l2.744 2.744a1 1 0 0 0 1.414-1.414l-2.744-2.744A7.5 7.5 0 0 0 9.5 2Z" 401 + fill="#ACAAAA" 402 + fill-rule="evenodd" 403 + /> 404 + </svg> 405 + </div> 406 + <div 407 + class="af bg bh ar as ax ay az b0 bd b5" 408 + data-baseweb="base-input" 409 + > 410 + <input 411 + aria-invalid="false" 412 + aria-required="false" 413 + autocomplete="on" 414 + class="ae b5 bi bj bk bl bm bn bo bp bq bg br bs bt bu bv bw b6 bx ax by az b0 bz c0 c1" 415 + inputmode="text" 416 + name="filter" 417 + placeholder="Search artists" 418 + type="text" 419 + value="" 420 + /> 421 + </div> 422 + </div> 423 + </div> 424 + <div 382 425 style="margin-bottom: 100px;" 383 426 > 384 427 <div 385 - class="ae af" 428 + class="c2 c3" 386 429 > 387 430 <div 388 - class="ag ah ai af aj ak al am an ao ap aq ar as at au av aw ax ay" 431 + class="ae af c4 c3 c5 c6 c7 c8 c9 ca cb cc cd ce cf cg ch ci cj ck" 389 432 > 390 433 <div 391 - class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 434 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da" 392 435 > 393 436 <a 394 437 href="/artists/1" ··· 406 449 </a> 407 450 </div> 408 451 <div 409 - class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 452 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da" 410 453 > 411 454 <a 412 455 href="/artists/2" ··· 424 467 </a> 425 468 </div> 426 469 <div 427 - class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 470 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da" 428 471 > 429 472 <a 430 473 href="/artists/3" ··· 442 485 </a> 443 486 </div> 444 487 <div 445 - class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 488 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da" 446 489 > 447 490 <a 448 491 href="/artists/4" ··· 460 503 </a> 461 504 </div> 462 505 <div 463 - class="ag az b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb bc bd be bf bg bh bi bj bk bl bm bn bo bp" 506 + class="ae bg cl cm cn co cp cq cr cs ct cu cv cw cx cy cz d0 d1 d2 d3 d4 d5 d6 d7 d8 d9 da" 464 507 > 465 508 <a 466 509 href="/artists/4"
+6
webui/rockbox/src/Components/Artists/styles.tsx
··· 55 55 width: 194px; 56 56 color: #000; 57 57 `; 58 + 59 + export const FilterContainer = styled.div` 60 + margin-top: 30px; 61 + margin-bottom: 40px; 62 + margin-left: 20px; 63 + `;
+7
webui/rockbox/src/Components/ControlBar/ControlBar.stories.tsx
··· 17 17 onPrevious: { action: "onPrevious" }, 18 18 onShuffle: { action: "onShuffle" }, 19 19 onRepeat: { action: "onRepeat" }, 20 + onLike: { action: "onLike" }, 21 + onUnlike: { action: "onUnlike" }, 20 22 }, 21 23 } satisfies Meta<typeof ControlBar>; 22 24 ··· 32 34 onPrevious: fn(), 33 35 onShuffle: fn(), 34 36 onRepeat: fn(), 37 + onLike: fn(), 38 + onUnlike: fn(), 39 + liked: false, 35 40 }, 36 41 }; 37 42 ··· 54 59 onPrevious: fn(), 55 60 onShuffle: fn(), 56 61 onRepeat: fn(), 62 + onLike: fn(), 63 + onUnlike: fn(), 57 64 }, 58 65 };
+2
webui/rockbox/src/Components/ControlBar/ControlBar.test.tsx
··· 30 30 onPrevious={vi.fn()} 31 31 onShuffle={vi.fn()} 32 32 onRepeat={vi.fn()} 33 + onLike={vi.fn()} 34 + onUnlike={vi.fn()} 33 35 /> 34 36 </MockedProvider> 35 37 </MemoryRouter>
+9 -1
webui/rockbox/src/Components/ControlBar/ControlBar.tsx
··· 18 18 onPrevious: () => void; 19 19 onShuffle: () => void; 20 20 onRepeat: () => void; 21 + liked?: boolean; 22 + onLike: (trackId: string) => void; 23 + onUnlike: (trackId: string) => void; 21 24 }; 22 25 23 26 const ControlBar: FC<ControlBarProps> = (props) => { ··· 49 52 </Button> 50 53 </ControlsContainer> 51 54 </Controls> 52 - <CurrentTrack nowPlaying={props.nowPlaying} /> 55 + <CurrentTrack 56 + nowPlaying={props.nowPlaying} 57 + liked={props.liked} 58 + onLike={props.onLike} 59 + onUnlike={props.onUnlike} 60 + /> 53 61 <RightMenu /> 54 62 </Container> 55 63 );
+51
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 6 6 useGetLikedAlbumsQuery, 7 7 useGetLikedTracksQuery, 8 8 useGetPlaybackStatusQuery, 9 + useLikeTrackMutation, 9 10 useNextMutation, 10 11 usePauseMutation, 11 12 usePlaybackStatusSubscription, 12 13 usePreviousMutation, 13 14 useResumeMutation, 15 + useUnlikeTrackMutation, 14 16 } from "../../Hooks/GraphQL"; 15 17 import { CurrentTrack } from "../../Types/track"; 16 18 import _ from "lodash"; ··· 35 37 const { data: playbackStatus } = usePlaybackStatusSubscription(); 36 38 const { previousTracks, nextTracks } = usePlayQueue(); 37 39 const { resumePlaylistTrack } = useResumePlaylist(); 40 + const [likeTrack] = useLikeTrackMutation(); 41 + const [unlikeTrack] = useUnlikeTrackMutation(); 38 42 39 43 const [likes, setLikes] = useRecoilState(likesState); 40 44 const { data: likedTracksData, loading: likedTracksLoading } = ··· 98 102 if (_.get(playbackSubscription, "currentlyPlayingSong.length", 0) > 0) { 99 103 const currentSong = playbackSubscription?.currentlyPlayingSong; 100 104 setNowPlaying({ 105 + id: currentSong?.id || "", 101 106 album: currentSong?.album, 102 107 artist: currentSong?.artist, 103 108 title: currentSong?.title, ··· 127 132 } 128 133 129 134 setNowPlaying({ 135 + id: data.currentTrack?.id || "", 130 136 album: data.currentTrack?.album, 131 137 artist: data.currentTrack?.artist, 132 138 title: data.currentTrack?.title, ··· 202 208 }, 3000); 203 209 }; 204 210 211 + const onLike = async (trackId: string) => { 212 + if (!nowPlaying || !trackId) { 213 + return; 214 + } 215 + 216 + setLikes((state) => ({ 217 + ...state, 218 + [trackId]: true, 219 + })); 220 + 221 + try { 222 + await likeTrack({ 223 + variables: { 224 + trackId, 225 + }, 226 + }); 227 + } catch (e) { 228 + console.error(e); 229 + } 230 + }; 231 + 232 + const onUnlike = async (trackId: string) => { 233 + if (!nowPlaying || !trackId) { 234 + return; 235 + } 236 + 237 + setLikes((state) => ({ 238 + ...state, 239 + [trackId]: false, 240 + })); 241 + 242 + try { 243 + await unlikeTrack({ 244 + variables: { 245 + trackId, 246 + }, 247 + }); 248 + } catch (e) { 249 + console.error(e); 250 + } 251 + }; 252 + 205 253 return ( 206 254 <ControlBar 207 255 nowPlaying={nowPlaying} ··· 211 259 onPrevious={() => previous()} 212 260 onShuffle={() => {}} 213 261 onRepeat={() => {}} 262 + liked={likes[nowPlaying?.id || ""]} 263 + onLike={onLike} 264 + onUnlike={onUnlike} 214 265 /> 215 266 ); 216 267 };
+33 -6
webui/rockbox/src/Components/ControlBar/CurrentTrack/CurrentTrack.tsx
··· 1 1 import { FC } from "react"; 2 2 import { ProgressBar } from "baseui/progress-bar"; 3 3 import { 4 + Actions, 4 5 Album, 5 6 AlbumCover, 6 7 ArtistAlbum, 7 8 Container, 9 + Icon, 8 10 NoCover, 9 11 ProgressbarContainer, 10 12 Separator, ··· 17 19 import { useTimeFormat } from "../../../Hooks/useFormat"; 18 20 import { CurrentTrack as NowPlaying } from "../../../Types/track"; 19 21 import _ from "lodash"; 22 + import HeartOutline from "../../Icons/HeartOutline"; 23 + import Heart from "../../Icons/Heart"; 20 24 21 25 export type CurrentTrackProps = { 22 26 nowPlaying?: NowPlaying; 27 + liked?: boolean; 28 + onLike: (trackId: string) => void; 29 + onUnlike: (trackId: string) => void; 23 30 }; 24 31 25 - const CurrentTrack: FC<CurrentTrackProps> = ({ nowPlaying }) => { 32 + const CurrentTrack: FC<CurrentTrackProps> = ({ 33 + nowPlaying, 34 + liked, 35 + onLike, 36 + onUnlike, 37 + }) => { 26 38 const { formatTime } = useTimeFormat(); 27 39 const album = `${nowPlaying?.artist} - ${nowPlaying?.album}`; 28 40 return ( ··· 43 55 )} 44 56 {nowPlaying && nowPlaying?.duration > 0 && ( 45 57 <> 46 - <Title> 47 - {_.get(nowPlaying, "title.length", 0) > 75 48 - ? `${nowPlaying.title?.substring(0, 75)}...` 49 - : nowPlaying.title} 50 - </Title> 58 + <div style={{ display: "flex", flexDirection: "row" }}> 59 + <Actions /> 60 + <Title> 61 + {_.get(nowPlaying, "title.length", 0) > 75 62 + ? `${nowPlaying.title?.substring(0, 75)}...` 63 + : nowPlaying.title} 64 + </Title> 65 + <Actions> 66 + {!liked && ( 67 + <Icon onClick={() => onLike(nowPlaying!.id!)}> 68 + <HeartOutline color="#000" /> 69 + </Icon> 70 + )} 71 + {liked && ( 72 + <Icon onClick={() => onUnlike(nowPlaying!.id!)}> 73 + <Heart color="#fe09a3" /> 74 + </Icon> 75 + )} 76 + </Actions> 77 + </div> 51 78 <div 52 79 style={{ 53 80 display: "flex",
+16
webui/rockbox/src/Components/ControlBar/CurrentTrack/styles.tsx webui/rockbox/src/Components/ControlBar/CurrentTrack/styles.ts
··· 78 78 margin-right: 10px; 79 79 `; 80 80 81 + export const Actions = styled.div` 82 + width: 60px; 83 + display: flex; 84 + align-items: center; 85 + justify-content: center; 86 + margin-right: 5px; 87 + opacity: 0; 88 + &:hover { 89 + opacity: 1; 90 + } 91 + `; 92 + 93 + export const Icon = styled.div` 94 + cursor: pointer; 95 + `; 96 + 81 97 export const Album = styled(Link)` 82 98 text-decoration: none; 83 99 color: inherit;
+1 -1
webui/rockbox/src/Components/ControlBar/PlayQueue/styles.css
··· 11 11 display: none; 12 12 position: absolute; 13 13 left: 10px; 14 - top: 17px; 14 + top: 19px; 15 15 } 16 16 17 17 .album-cover-container:hover .floating-play {
+46 -2
webui/rockbox/src/Components/ControlBar/__snapshots__/ControlBar.test.tsx.snap
··· 116 116 class="css-rbpzuh" 117 117 > 118 118 <div 119 - class="css-1cojt9j" 119 + style="display: flex; flex-direction: row;" 120 120 > 121 - Disco on the Baltic Sea 121 + <div 122 + class="css-4fj3gy" 123 + /> 124 + <div 125 + class="css-1cojt9j" 126 + > 127 + Disco on the Baltic Sea 128 + </div> 129 + <div 130 + class="css-4fj3gy" 131 + > 132 + <div 133 + class="css-1h5x3dy" 134 + > 135 + <svg 136 + fill="none" 137 + height="20" 138 + style="padding-top: 1px;" 139 + width="20" 140 + xmlns="http://www.w3.org/2000/svg" 141 + > 142 + <g 143 + class="ionicon" 144 + style="fill: #000;" 145 + > 146 + <path 147 + d="M13.534 4C11.167 4 10 6.364 10 6.364S8.833 4 6.466 4C4.543 4 3.02 5.63 3 7.575c-.04 4.038 3.162 6.91 6.672 9.323a.578.578 0 0 0 .656 0c3.51-2.413 6.712-5.285 6.672-9.323C16.98 5.63 15.457 4 13.534 4Z" 148 + fill="none" 149 + stroke="currentColor" 150 + stroke-linecap="round" 151 + stroke-linejoin="round" 152 + style="fill: none;" 153 + /> 154 + <path 155 + class="stroke-shape" 156 + d="M13.534 4C11.167 4 10 6.364 10 6.364S8.833 4 6.466 4C4.543 4 3.02 5.63 3 7.575c-.04 4.038 3.162 6.91 6.672 9.323a.578.578 0 0 0 .656 0c3.51-2.413 6.712-5.285 6.672-9.323C16.98 5.63 15.457 4 13.534 4Z" 157 + stroke="currentColor" 158 + stroke-linecap="round" 159 + stroke-linejoin="round" 160 + style="fill: none; stroke-width: 2; stroke: #000; stroke-opacity: 1;" 161 + /> 162 + </g> 163 + </svg> 164 + </div> 165 + </div> 122 166 </div> 123 167 <div 124 168 style="display: flex; flex-direction: row; align-items: center; justify-content: space-between;"
+32 -6
webui/rockbox/src/Components/Likes/Likes.tsx
··· 13 13 FilterContainer, 14 14 Link, 15 15 Title, 16 + Separator, 17 + Label, 18 + HeaderWrapper, 16 19 } from "./styles"; 17 20 import { Track } from "../../Types/track"; 18 21 import Table from "../VirtualizedTable"; 19 22 import Filter from "../Filter"; 20 23 import TrackIcon from "../Icons/Track"; 21 - import { Play } from "@styled-icons/ionicons-sharp"; 22 24 import "./styles.css"; 23 25 import ContextMenu from "../ContextMenu"; 26 + import Button from "../Button"; 27 + import Play from "../Icons/Play"; 28 + import Shuffle from "../Icons/Shuffle"; 24 29 25 30 const columnHelper = createColumnHelper<Track>(); 26 31 27 32 export type TracksProps = { 28 33 tracks: Track[]; 29 34 onPlayTrack: (id: string) => void; 35 + onPlayAll: () => void; 36 + onShuffleAll: () => void; 30 37 }; 31 38 32 39 const Likes: FC<TracksProps> = (props) => { ··· 43 50 justifyContent: "center", 44 51 marginLeft: 5, 45 52 marginRight: 5, 53 + height: 48, 54 + marginTop: -10, 46 55 }} 47 56 > 48 57 {info.getValue()} ··· 65 74 onClick={() => props.onPlayTrack(info.row.original.id)} 66 75 className="floating-play" 67 76 > 68 - <Play size={16} color={info.getValue() ? "#fff" : "#000"} /> 77 + <Play small color={info.getValue() ? "#fff" : "#000"} /> 69 78 </div> 70 79 </div> 71 80 )} ··· 78 87 onClick={() => props.onPlayTrack(info.row.original.id)} 79 88 className="floating-play" 80 89 > 81 - <Play size={16} color={info.getValue() ? "#fff" : "#000"} /> 90 + <Play small color={info.getValue() ? "#fff" : "#000"} /> 82 91 </div> 83 92 </div> 84 93 )} ··· 179 188 <ControlBar /> 180 189 <ContentWrapper ref={containerRef}> 181 190 <Title>Likes</Title> 182 - <FilterContainer> 183 - <Filter placeholder="Search song" onChange={() => {}} /> 184 - </FilterContainer> 191 + <HeaderWrapper> 192 + <ButtonGroup> 193 + <Button onClick={props.onPlayAll} kind="primary"> 194 + <Label> 195 + <Play small color="#fff" /> 196 + <div style={{ marginLeft: 7 }}>Play</div> 197 + </Label> 198 + </Button> 199 + <Separator /> 200 + <Button onClick={props.onShuffleAll} kind="secondary"> 201 + <Label> 202 + <Shuffle color="#fe099c" /> 203 + <div style={{ marginLeft: 7 }}>Shuffle</div> 204 + </Label> 205 + </Button> 206 + </ButtonGroup> 207 + <FilterContainer> 208 + <Filter placeholder="Search song" onChange={() => {}} /> 209 + </FilterContainer> 210 + </HeaderWrapper> 185 211 <div style={{ marginBottom: 60 }}> 186 212 {props.tracks.length > 0 && ( 187 213 <Table
+25 -2
webui/rockbox/src/Components/Likes/LikesWithData.tsx
··· 1 1 import { FC, useEffect } from "react"; 2 2 import Likes from "./Likes"; 3 - import { useGetLikedTracksQuery } from "../../Hooks/GraphQL"; 3 + import { 4 + useGetLikedTracksQuery, 5 + usePlayLikedTracksMutation, 6 + } from "../../Hooks/GraphQL"; 4 7 import { useTimeFormat } from "../../Hooks/useFormat"; 5 8 import { useRecoilState } from "recoil"; 6 9 import { likedTracks, likesState } from "./LikesState"; ··· 11 14 fetchPolicy: "network-only", 12 15 }); 13 16 const [tracks, setTracks] = useRecoilState(likedTracks); 17 + const [playLikedTracks] = usePlayLikedTracksMutation(); 14 18 const { formatTime } = useTimeFormat(); 15 19 16 20 useEffect(() => { ··· 49 53 console.log(">>", trackId); 50 54 }; 51 55 52 - return <Likes tracks={tracks} onPlayTrack={onPlayTrack} />; 56 + const onPlayAll = () => { 57 + playLikedTracks(); 58 + }; 59 + 60 + const onShuffleAll = () => { 61 + playLikedTracks({ 62 + variables: { 63 + shuffle: true, 64 + }, 65 + }); 66 + }; 67 + 68 + return ( 69 + <Likes 70 + tracks={tracks} 71 + onPlayTrack={onPlayTrack} 72 + onPlayAll={onPlayAll} 73 + onShuffleAll={onShuffleAll} 74 + /> 75 + ); 53 76 }; 54 77 55 78 export default LikesWithData;
+2 -2
webui/rockbox/src/Components/Likes/styles.css
··· 16 16 .album-cover-container .floating-play { 17 17 display: none; 18 18 position: absolute; 19 - left: 17px; 20 - top: 10px; 19 + left: 14px; 20 + top: 14px; 21 21 } 22 22 23 23 .album-cover-container:hover .floating-play {
+17
webui/rockbox/src/Components/Likes/styles.tsx
··· 100 100 text-decoration: underline; 101 101 } 102 102 `; 103 + 104 + export const Label = styled.div` 105 + display: flex; 106 + flex-direction: row; 107 + align-items: center; 108 + `; 109 + 110 + export const Separator = styled.div` 111 + width: 20px; 112 + `; 113 + 114 + export const HeaderWrapper = styled.div` 115 + display: flex; 116 + flex-direction: row; 117 + align-items: center; 118 + justify-content: space-between; 119 + `;
+3 -2
webui/rockbox/src/Components/Tracks/Tracks.tsx
··· 43 43 justifyContent: "center", 44 44 marginLeft: 5, 45 45 marginRight: 5, 46 + marginTop: -6, 46 47 }} 47 48 > 48 49 {info.getValue()} ··· 55 56 cell: (info) => ( 56 57 <> 57 58 {info.getValue() && ( 58 - <div className="album-cover-container"> 59 + <div className="album-cover-container songs"> 59 60 <AlbumCover 60 61 src={info.getValue()!} 61 62 alt="album art" ··· 70 71 </div> 71 72 )} 72 73 {!info.getValue() && ( 73 - <div className="album-cover-container"> 74 + <div className="album-cover-container songs"> 74 75 <AlbumCoverAlt> 75 76 <TrackIcon width={28} height={28} color="#a4a3a3" /> 76 77 </AlbumCoverAlt>
+7 -7
webui/rockbox/src/Components/Tracks/styles.css
··· 2 2 border-spacing: 0; 3 3 } 4 4 5 - .album-cover-container { 6 - height: 64px; 5 + .album-cover-container.songs { 6 + height: 48px; 7 7 justify-content: center; 8 8 align-items: center; 9 9 display: flex; ··· 12 12 margin-right: 10px; 13 13 } 14 14 15 - .album-cover-container .floating-play { 15 + .album-cover-container.songs .floating-play { 16 16 display: none; 17 17 position: absolute; 18 - left: 19px; 19 - top: 17px; 18 + left: 17px; 19 + top: 10px; 20 20 } 21 21 22 - .album-cover-container:hover .floating-play { 22 + .album-cover-container.songs:hover .floating-play { 23 23 display: block; 24 24 } 25 25 26 - .album-cover-container:hover img { 26 + .album-cover-container.songs:hover img { 27 27 opacity: 0.4; 28 28 }
+6
webui/rockbox/src/GraphQL/Playback/Mutation.ts
··· 53 53 playTrack(path: $path) 54 54 } 55 55 `; 56 + 57 + export const PLAY_LIKED_TRACKS = gql` 58 + mutation PlayLikedTracks($shuffle: Boolean) { 59 + playLikedTracks(shuffle: $shuffle) 60 + } 61 + `;
+44
webui/rockbox/src/Hooks/GraphQL.tsx
··· 93 93 playAlbum: Scalars['Int']['output']; 94 94 playArtistTracks: Scalars['Int']['output']; 95 95 playDirectory: Scalars['Int']['output']; 96 + playLikedTracks: Scalars['Int']['output']; 96 97 playPlaylist: Scalars['Int']['output']; 97 98 playTrack: Scalars['Int']['output']; 98 99 playlistCreate: Scalars['Int']['output']; ··· 186 187 export type MutationPlayDirectoryArgs = { 187 188 path: Scalars['String']['input']; 188 189 recurse?: InputMaybe<Scalars['Boolean']['input']>; 190 + shuffle?: InputMaybe<Scalars['Boolean']['input']>; 191 + }; 192 + 193 + 194 + export type MutationPlayLikedTracksArgs = { 189 195 shuffle?: InputMaybe<Scalars['Boolean']['input']>; 190 196 }; 191 197 ··· 682 688 683 689 684 690 export type PlayTrackMutation = { __typename?: 'Mutation', playTrack: number }; 691 + 692 + export type PlayLikedTracksMutationVariables = Exact<{ 693 + shuffle?: InputMaybe<Scalars['Boolean']['input']>; 694 + }>; 695 + 696 + 697 + export type PlayLikedTracksMutation = { __typename?: 'Mutation', playLikedTracks: number }; 685 698 686 699 export type GetCurrentTrackQueryVariables = Exact<{ [key: string]: never; }>; 687 700 ··· 1616 1629 export type PlayTrackMutationHookResult = ReturnType<typeof usePlayTrackMutation>; 1617 1630 export type PlayTrackMutationResult = Apollo.MutationResult<PlayTrackMutation>; 1618 1631 export type PlayTrackMutationOptions = Apollo.BaseMutationOptions<PlayTrackMutation, PlayTrackMutationVariables>; 1632 + export const PlayLikedTracksDocument = gql` 1633 + mutation PlayLikedTracks($shuffle: Boolean) { 1634 + playLikedTracks(shuffle: $shuffle) 1635 + } 1636 + `; 1637 + export type PlayLikedTracksMutationFn = Apollo.MutationFunction<PlayLikedTracksMutation, PlayLikedTracksMutationVariables>; 1638 + 1639 + /** 1640 + * __usePlayLikedTracksMutation__ 1641 + * 1642 + * To run a mutation, you first call `usePlayLikedTracksMutation` within a React component and pass it any options that fit your needs. 1643 + * When your component renders, `usePlayLikedTracksMutation` returns a tuple that includes: 1644 + * - A mutate function that you can call at any time to execute the mutation 1645 + * - An object with fields that represent the current status of the mutation's execution 1646 + * 1647 + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; 1648 + * 1649 + * @example 1650 + * const [playLikedTracksMutation, { data, loading, error }] = usePlayLikedTracksMutation({ 1651 + * variables: { 1652 + * shuffle: // value for 'shuffle' 1653 + * }, 1654 + * }); 1655 + */ 1656 + export function usePlayLikedTracksMutation(baseOptions?: Apollo.MutationHookOptions<PlayLikedTracksMutation, PlayLikedTracksMutationVariables>) { 1657 + const options = {...defaultOptions, ...baseOptions} 1658 + return Apollo.useMutation<PlayLikedTracksMutation, PlayLikedTracksMutationVariables>(PlayLikedTracksDocument, options); 1659 + } 1660 + export type PlayLikedTracksMutationHookResult = ReturnType<typeof usePlayLikedTracksMutation>; 1661 + export type PlayLikedTracksMutationResult = Apollo.MutationResult<PlayLikedTracksMutation>; 1662 + export type PlayLikedTracksMutationOptions = Apollo.BaseMutationOptions<PlayLikedTracksMutation, PlayLikedTracksMutationVariables>; 1619 1663 export const GetCurrentTrackDocument = gql` 1620 1664 query GetCurrentTrack { 1621 1665 currentTrack {