A modern Music Player Daemon based on Rockbox open source high quality audio player
libadwaita audio rust zig deno mpris rockbox mpd

webui: implement 'resume playback' feature

+443 -101
+1
crates/graphql/src/lib.rs
··· 4 4 pub mod schema; 5 5 pub mod server; 6 6 pub mod simplebroker; 7 + pub mod types; 7 8 8 9 pub type RockboxSchema = Schema<Query, Mutation, Subscription>; 9 10
+9 -8
crates/graphql/src/schema/playlist.rs
··· 7 7 types::{playlist_amount::PlaylistAmount, playlist_info::PlaylistInfo}, 8 8 }; 9 9 10 - use crate::{rockbox_url, schema::objects::playlist::Playlist, simplebroker::SimpleBroker}; 10 + use crate::{ 11 + rockbox_url, schema::objects::playlist::Playlist, simplebroker::SimpleBroker, types::StatusCode, 12 + }; 11 13 12 14 #[derive(Default)] 13 15 pub struct PlaylistQuery; ··· 61 63 62 64 #[Object] 63 65 impl PlaylistMutation { 64 - async fn playlist_resume(&self, ctx: &Context<'_>) -> Result<String, Error> { 65 - ctx.data::<Arc<Mutex<Sender<RockboxCommand>>>>() 66 - .unwrap() 67 - .lock() 68 - .unwrap() 69 - .send(RockboxCommand::PlaylistResume)?; 70 - Ok("".to_string()) 66 + async fn playlist_resume(&self, _ctx: &Context<'_>) -> Result<i32, Error> { 67 + let client = reqwest::Client::new(); 68 + let url = format!("{}/playlists/resume", rockbox_url()); 69 + let response = client.put(&url).send().await?; 70 + let response = response.json::<StatusCode>().await?; 71 + Ok(response.code) 71 72 } 72 73 73 74 async fn resume_track(&self, ctx: &Context<'_>) -> Result<String, Error> {
+6
crates/graphql/src/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, Serialize, Deserialize)] 4 + pub struct StatusCode { 5 + pub code: i32, 6 + }
+3 -1
crates/rpc/proto/rockbox/v1alpha1/playlist.proto
··· 41 41 42 42 message PlaylistResumeRequest {} 43 43 44 - message PlaylistResumeResponse {} 44 + message PlaylistResumeResponse { 45 + int32 code = 1; 46 + } 45 47 46 48 message ResumeTrackRequest { 47 49 int32 start_index = 1;
+4 -1
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 2654 2654 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2655 2655 pub struct PlaylistResumeRequest {} 2656 2656 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2657 - pub struct PlaylistResumeResponse {} 2657 + pub struct PlaylistResumeResponse { 2658 + #[prost(int32, tag = "1")] 2659 + pub code: i32, 2660 + } 2658 2661 #[derive(Clone, Copy, PartialEq, ::prost::Message)] 2659 2662 pub struct ResumeTrackRequest { 2660 2663 #[prost(int32, tag = "1")]
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+1
crates/rpc/src/lib.rs
··· 7 7 pub mod settings; 8 8 pub mod sound; 9 9 pub mod system; 10 + pub mod types; 10 11 11 12 pub const AUDIO_EXTENSIONS: [&str; 17] = [ 12 13 "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus",
+15 -6
crates/rpc/src/playlist.rs
··· 8 8 use crate::{ 9 9 api::rockbox::v1alpha1::{playlist_service_server::PlaylistService, *}, 10 10 rockbox_url, 11 + types::StatusCode, 11 12 }; 12 13 13 14 pub struct Playlist { ··· 107 108 &self, 108 109 _request: tonic::Request<PlaylistResumeRequest>, 109 110 ) -> Result<tonic::Response<PlaylistResumeResponse>, tonic::Status> { 110 - self.cmd_tx 111 - .lock() 112 - .unwrap() 113 - .send(RockboxCommand::PlaylistResume) 114 - .map_err(|_| tonic::Status::internal("Failed to send command"))?; 115 - Ok(tonic::Response::new(PlaylistResumeResponse::default())) 111 + let url = format!("{}/playlists/resume", rockbox_url()); 112 + let response = self 113 + .client 114 + .put(&url) 115 + .send() 116 + .await 117 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 118 + let response = response 119 + .json::<StatusCode>() 120 + .await 121 + .map_err(|e| tonic::Status::internal(e.to_string()))?; 122 + Ok(tonic::Response::new(PlaylistResumeResponse { 123 + code: response.code, 124 + })) 116 125 } 117 126 118 127 async fn resume_track(
+6
crates/rpc/src/types.rs
··· 1 + use serde::{Deserialize, Serialize}; 2 + 3 + #[derive(Debug, Serialize, Deserialize)] 4 + pub struct StatusCode { 5 + pub code: i32, 6 + }
+4 -3
crates/server/src/handlers/playlists.rs
··· 1 1 use crate::{ 2 2 http::{Context, Request, Response}, 3 - types::{DeleteTracks, InsertTracks, NewPlaylist}, 3 + types::{DeleteTracks, InsertTracks, NewPlaylist, StatusCode}, 4 4 }; 5 5 use anyhow::Error; 6 6 use rockbox_library::repo; ··· 88 88 pub async fn resume_playlist( 89 89 _ctx: &Context, 90 90 _req: &Request, 91 - _res: &mut Response, 91 + res: &mut Response, 92 92 ) -> Result<(), Error> { 93 - rb::playlist::resume(); 93 + let code = rb::playlist::resume(); 94 + res.json(&StatusCode { code }); 94 95 Ok(()) 95 96 } 96 97
+5
crates/server/src/types.rs
··· 18 18 pub struct DeleteTracks { 19 19 pub positions: Vec<i32>, 20 20 } 21 + 22 + #[derive(Debug, Serialize, Deserialize)] 23 + pub struct StatusCode { 24 + pub code: i32, 25 + }
+1 -1
webui/rockbox/graphql.schema.json
··· 1109 1109 "name": null, 1110 1110 "ofType": { 1111 1111 "kind": "SCALAR", 1112 - "name": "String", 1112 + "name": "Int", 1113 1113 "ofType": null 1114 1114 } 1115 1115 },
+5 -2
webui/rockbox/src/Components/AlbumDetails/AlbumDetails.tsx
··· 3 3 import { createColumnHelper } from "@tanstack/react-table"; 4 4 import Sidebar from "../Sidebar"; 5 5 import ControlBar from "../ControlBar"; 6 + import MainView from "../MainView/MainView"; 6 7 import { 7 8 Container, 8 - MainView, 9 9 AlbumCover, 10 10 ContentWrapper, 11 11 AlbumTitle, ··· 45 45 tracks: Track[]; 46 46 album?: Album | null; 47 47 volumes: Track[][]; 48 + enableBlur?: boolean; 48 49 }; 49 50 50 51 const AlbumDetails: FC<AlbumDetailsProps> = (props) => { ··· 127 128 return ( 128 129 <Container> 129 130 <Sidebar active="albums" /> 130 - <MainView> 131 + <MainView 132 + cover={props.enableBlur ? (props.album?.albumArt as any) : undefined} 133 + > 131 134 <ControlBar /> 132 135 <ContentWrapper> 133 136 <BackButton onClick={() => props.onGoBack()}>
+4
webui/rockbox/src/Components/AlbumDetails/AlbumDetailsWithData.tsx
··· 4 4 import { useGetAlbumQuery } from "../../Hooks/GraphQL"; 5 5 import { useTimeFormat } from "../../Hooks/useFormat"; 6 6 import { Track } from "../../Types/track"; 7 + import { useRecoilValue } from "recoil"; 8 + import { settingsState } from "../Settings/SettingsState"; 7 9 8 10 const AlbumDetailsWithData: FC = () => { 11 + const { enableBlur } = useRecoilValue(settingsState); 9 12 const [volumes, setVolumes] = useState<Track[][]>([]); 10 13 const [tracks, setTracks] = useState<Track[]>([]); 11 14 const { formatTime } = useTimeFormat(); ··· 88 91 // eslint-disable-next-line @typescript-eslint/no-explicit-any 89 92 album={album as any} 90 93 volumes={volumes} 94 + enableBlur={enableBlur} 91 95 /> 92 96 ); 93 97 };
+2 -2
webui/rockbox/src/Components/AlbumDetails/__snapshots__/AlbumDetails.test.tsx.snap
··· 6 6 class="css-e1989k" 7 7 > 8 8 <div 9 - class="css-1fkc5kv" 9 + class="css-1tlxqlf" 10 10 > 11 11 <a 12 12 href="/" ··· 137 137 </a> 138 138 </div> 139 139 <div 140 - class="css-v8cdaf" 140 + class="css-y9r6ap" 141 141 > 142 142 <div 143 143 class="css-14bvc2y"
-7
webui/rockbox/src/Components/AlbumDetails/styles.tsx
··· 8 8 height: 100%; 9 9 `; 10 10 11 - export const MainView = styled.div` 12 - display: flex; 13 - flex: 1; 14 - flex-direction: column; 15 - width: calc(100% - 240px); 16 - `; 17 - 18 11 export const AlbumCover = styled.img` 19 12 height: 240px; 20 13 width: 240px;
+1 -1
webui/rockbox/src/Components/Albums/Albums.tsx
··· 1 1 /* eslint-disable @typescript-eslint/no-explicit-any */ 2 2 import { FC } from "react"; 3 3 import { Cell, Grid } from "baseui/layout-grid"; 4 + import MainView from "../MainView"; 4 5 import Sidebar from "../Sidebar"; 5 6 import ControlBar from "../ControlBar"; 6 7 import AlbumArt from "../../Assets/albumart.svg"; ··· 13 14 FilterContainer, 14 15 FloatingButton, 15 16 Hover, 16 - MainView, 17 17 Scrollable, 18 18 Title, 19 19 Year,
+2 -2
webui/rockbox/src/Components/Albums/__snapshots__/Albums.test.tsx.snap
··· 9 9 class="css-e1989k" 10 10 > 11 11 <div 12 - class="css-1fkc5kv" 12 + class="css-1tlxqlf" 13 13 > 14 14 <a 15 15 href="/" ··· 140 140 </a> 141 141 </div> 142 142 <div 143 - class="css-v8cdaf" 143 + class="css-y9r6ap" 144 144 > 145 145 <div 146 146 class="css-14bvc2y"
-7
webui/rockbox/src/Components/Albums/styles.tsx
··· 9 9 height: 100%; 10 10 `; 11 11 12 - export const MainView = styled.div` 13 - display: flex; 14 - flex: 1; 15 - flex-direction: column; 16 - width: calc(100% - 240px); 17 - `; 18 - 19 12 export const Title = styled.div` 20 13 font-size: 24px; 21 14 font-family: RockfordSansMedium;
+1 -1
webui/rockbox/src/Components/ArtistDetails/ArtistDetails.tsx
··· 1 1 /* eslint-disable @typescript-eslint/no-explicit-any */ 2 2 import { FC } from "react"; 3 - import Sidebar from "../Sidebar"; 3 + import Sidebar from "../Sidebar/Sidebar"; 4 4 import ControlBar from "../ControlBar"; 5 5 import { 6 6 SmallAlbumCover,
+1 -1
webui/rockbox/src/Components/ArtistDetails/__snapshots__/ArtistDetails.test.tsx.snap
··· 9 9 class="css-e1989k" 10 10 > 11 11 <div 12 - class="css-1fkc5kv" 12 + class="css-1tlxqlf" 13 13 > 14 14 <a 15 15 href="/"
+1 -1
webui/rockbox/src/Components/Artists/Artists.tsx
··· 2 2 import { FC } from "react"; 3 3 import { Cell, Grid } from "baseui/layout-grid"; 4 4 import Sidebar from "../Sidebar"; 5 + import MainView from "../MainView"; 5 6 import ControlBar from "../ControlBar"; 6 7 import { 7 8 ArtistCover, 8 9 ArtistName, 9 10 Container, 10 - MainView, 11 11 NoArtistCover, 12 12 Scrollable, 13 13 Title,
+2 -2
webui/rockbox/src/Components/Artists/__snapshots__/Artists.test.tsx.snap
··· 9 9 class="css-e1989k" 10 10 > 11 11 <div 12 - class="css-1fkc5kv" 12 + class="css-1tlxqlf" 13 13 > 14 14 <a 15 15 href="/" ··· 140 140 </a> 141 141 </div> 142 142 <div 143 - class="css-v8cdaf" 143 + class="css-y9r6ap" 144 144 > 145 145 <div 146 146 class="css-14bvc2y"
-7
webui/rockbox/src/Components/Artists/styles.tsx
··· 7 7 height: 100%; 8 8 `; 9 9 10 - export const MainView = styled.div` 11 - display: flex; 12 - flex: 1; 13 - flex-direction: column; 14 - width: calc(100% - 240px); 15 - `; 16 - 17 10 export const Title = styled.div` 18 11 font-size: 24px; 19 12 font-family: RockfordSansMedium;
+1 -1
webui/rockbox/src/Components/ControlBar/ControlBar.tsx
··· 12 12 13 13 export type ControlBarProps = { 14 14 nowPlaying?: NowPlaying; 15 - onPlay: () => void; 15 + onPlay: () => Promise<void>; 16 16 onPause: () => void; 17 17 onNext: () => void; 18 18 onPrevious: () => void;
+2
webui/rockbox/src/Components/ControlBar/ControlBarState.tsx
··· 6 6 locked?: boolean; 7 7 previousTracks?: Track[]; 8 8 nextTracks?: Track[]; 9 + resumeIndex: number; 9 10 }>({ 10 11 key: "controlBarState", 11 12 default: { ··· 13 14 locked: false, 14 15 previousTracks: [], 15 16 nextTracks: [], 17 + resumeIndex: -1, 16 18 }, 17 19 });
+26 -2
webui/rockbox/src/Components/ControlBar/ControlBarWithData.tsx
··· 15 15 import { useRecoilState } from "recoil"; 16 16 import { controlBarState } from "./ControlBarState"; 17 17 import { usePlayQueue } from "../../Hooks/usePlayQueue"; 18 + import { useResumePlaylist } from "../../Hooks/useResumePlaylist"; 18 19 19 20 const ControlBarWithData: FC = () => { 20 - const [{ nowPlaying, locked }, setControlBarState] = 21 + const [{ nowPlaying, locked, resumeIndex }, setControlBarState] = 21 22 useRecoilState(controlBarState); 22 23 const { data, loading } = useGetCurrentTrackQuery(); 23 24 const { data: playback } = useGetPlaybackStatusQuery({ ··· 30 31 const { data: playbackSubscription } = useCurrentlyPlayingSongSubscription(); 31 32 const { data: playbackStatus } = usePlaybackStatusSubscription(); 32 33 const { previousTracks, nextTracks } = usePlayQueue(); 34 + const { resumePlaylistTrack } = useResumePlaylist(); 33 35 34 36 const setNowPlaying = (nowPlaying: CurrentTrack) => { 35 37 setControlBarState((state) => ({ ··· 96 98 // eslint-disable-next-line react-hooks/exhaustive-deps 97 99 }, [data, loading, playback]); 98 100 99 - const onPlay = () => { 101 + const onPlay = async () => { 100 102 setControlBarState((state) => ({ 101 103 ...state, 102 104 nowPlaying: { ··· 105 107 }, 106 108 locked: true, 107 109 })); 110 + 111 + if (resumeIndex > -1) { 112 + try { 113 + await resumePlaylistTrack(); 114 + } catch (e) { 115 + console.error(e); 116 + } 117 + 118 + setControlBarState((state) => ({ 119 + ...state, 120 + resumeIndex: -1, 121 + })); 122 + 123 + setTimeout(() => { 124 + setControlBarState((state) => ({ 125 + ...state, 126 + locked: false, 127 + })); 128 + }, 3000); 129 + return; 130 + } 108 131 resume(); 132 + 109 133 setTimeout(() => { 110 134 setControlBarState((state) => ({ 111 135 ...state,
+1 -1
webui/rockbox/src/Components/Files/Files.tsx
··· 13 13 Directory, 14 14 Hover, 15 15 IconButton, 16 - MainView, 17 16 Title, 18 17 } from "./styles"; 19 18 import { EllipsisHorizontal } from "@styled-icons/ionicons-sharp"; ··· 22 21 import "./styles.css"; 23 22 import ArrowBack from "../Icons/ArrowBack"; 24 23 import { Spinner } from "baseui/spinner"; 24 + import MainView from "../MainView"; 25 25 26 26 const columnHelper = createColumnHelper<File>(); 27 27 const columns = [
+2 -2
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 9 9 class="css-e1989k" 10 10 > 11 11 <div 12 - class="css-1fkc5kv" 12 + class="css-1tlxqlf" 13 13 > 14 14 <a 15 15 href="/" ··· 140 140 </a> 141 141 </div> 142 142 <div 143 - class="css-g1mxd4" 143 + class="css-y9r6ap" 144 144 > 145 145 <div 146 146 class="css-14bvc2y"
+2 -2
webui/rockbox/src/Components/Files/styles.css
··· 1 - tr:nth-child(even) { 2 - background-color: #f9f9f9; 1 + tbody > tr:hover { 2 + background-color: #f9f9f97a; 3 3 } 4 4 5 5 table {
+1 -8
webui/rockbox/src/Components/Files/styles.tsx
··· 1 1 import styled from "@emotion/styled"; 2 + import { css } from "@emotion/react"; 2 3 import { Link } from "react-router-dom"; 3 4 4 5 export const Container = styled.div` ··· 6 7 flex-direction: row; 7 8 width: 100%; 8 9 height: 100%; 9 - `; 10 - 11 - export const MainView = styled.div` 12 - display: flex; 13 - flex: 1; 14 - flex-direction: column; 15 - position: relative; 16 - width: calc(100% - 240px); 17 10 `; 18 11 19 12 export const Title = styled.div`
+18
webui/rockbox/src/Components/MainView/MainView.tsx
··· 1 + import { FC, ReactNode } from "react"; 2 + import { Container, Blur } from "./styles"; 3 + 4 + export type MainViewProps = { 5 + cover?: string; 6 + children?: ReactNode; 7 + }; 8 + 9 + const MainView: FC<MainViewProps> = ({ cover, children }) => { 10 + return ( 11 + <Container cover={cover}> 12 + {cover && <Blur>{children}</Blur>} 13 + {!cover && children} 14 + </Container> 15 + ); 16 + }; 17 + 18 + export default MainView;
+21
webui/rockbox/src/Components/MainView/MainViewWithData.tsx
··· 1 + import { FC, ReactNode } from "react"; 2 + import MainView from "./MainView"; 3 + import { useRecoilValue } from "recoil"; 4 + import { controlBarState } from "../ControlBar/ControlBarState"; 5 + import { settingsState } from "../Settings/SettingsState"; 6 + 7 + export type MainWithDataProps = { 8 + children?: ReactNode; 9 + }; 10 + 11 + const MainWithData: FC<MainWithDataProps> = ({ children }) => { 12 + const { nowPlaying } = useRecoilValue(controlBarState); 13 + const { enableBlur } = useRecoilValue(settingsState); 14 + return ( 15 + <MainView cover={enableBlur ? nowPlaying?.cover : undefined}> 16 + {children} 17 + </MainView> 18 + ); 19 + }; 20 + 21 + export default MainWithData;
+3
webui/rockbox/src/Components/MainView/index.tsx
··· 1 + import MainView from "./MainViewWithData"; 2 + 3 + export default MainView;
+22
webui/rockbox/src/Components/MainView/styles.tsx
··· 1 + import styled from "@emotion/styled"; 2 + import { css } from "@emotion/react"; 3 + 4 + export const Container = styled.div<{ cover?: string }>` 5 + display: flex; 6 + flex: 1; 7 + flex-direction: column; 8 + position: relative; 9 + width: calc(100% - 240px); 10 + background-position: center; 11 + background-repeat: no-repeat; 12 + background-size: cover; 13 + ${({ cover }) => css` 14 + background-image: url(${cover}); 15 + `} 16 + `; 17 + 18 + export const Blur = styled.div` 19 + background: rgba(256, 256, 256, 0.8); 20 + backdrop-filter: blur(30px); 21 + height: 100vh; 22 + `;
+10
webui/rockbox/src/Components/Settings/SettingsState.ts
··· 1 + import { atom } from "recoil"; 2 + 3 + export const settingsState = atom<{ 4 + enableBlur: boolean; 5 + }>({ 6 + key: "settings", 7 + default: { 8 + enableBlur: false, 9 + }, 10 + });
+3 -2
webui/rockbox/src/Components/Sidebar/Sidebar.tsx
··· 8 8 9 9 export type SidebarProps = { 10 10 active: string; 11 + cover?: string; 11 12 }; 12 13 13 - const Sidebar: FC<SidebarProps> = ({ active }) => { 14 + const Sidebar: FC<SidebarProps> = ({ active, cover }) => { 14 15 return ( 15 - <SidebarContainer> 16 + <SidebarContainer cover={cover}> 16 17 <a href="/" style={{ textDecoration: "none" }}> 17 18 <img 18 19 src={RockboxLogo}
+15
webui/rockbox/src/Components/Sidebar/SidebarWithData.tsx
··· 1 + import { FC } from "react"; 2 + import Sidebar from "./Sidebar"; 3 + import { controlBarState } from "../ControlBar/ControlBarState"; 4 + import { useRecoilValue } from "recoil"; 5 + import { settingsState } from "../Settings/SettingsState"; 6 + 7 + const SidebarWithData: FC<{ active: string }> = (props) => { 8 + const { nowPlaying } = useRecoilValue(controlBarState); 9 + const { enableBlur } = useRecoilValue(settingsState); 10 + return ( 11 + <Sidebar {...props} cover={enableBlur ? nowPlaying?.cover : undefined} /> 12 + ); 13 + }; 14 + 15 + export default SidebarWithData;
+1 -1
webui/rockbox/src/Components/Sidebar/__snapshots__/Sidebar.test.tsx.snap
··· 6 6 class="" 7 7 > 8 8 <div 9 - class="css-1fkc5kv" 9 + class="css-1tlxqlf" 10 10 > 11 11 <a 12 12 href="/"
+1 -1
webui/rockbox/src/Components/Sidebar/index.tsx
··· 1 - import Sidebar from "./Sidebar"; 1 + import Sidebar from "./SidebarWithData"; 2 2 3 3 export default Sidebar;
+6 -1
webui/rockbox/src/Components/Sidebar/styles.tsx
··· 2 2 import { css } from "@emotion/react"; 3 3 import { Link } from "react-router-dom"; 4 4 5 - export const SidebarContainer = styled.div` 5 + export const SidebarContainer = styled.div<{ cover?: string }>` 6 6 display: flex; 7 7 flex-direction: column; 8 8 height: 100vh; 9 9 width: 222px; 10 10 background-color: #f6f9fc; 11 11 padding: 20px; 12 + ${(props) => 13 + props.cover && 14 + css` 15 + background-color: #fff; 16 + `} 12 17 `; 13 18 14 19 export const MenuItem = styled(Link)<{ color?: string }>`
+1 -1
webui/rockbox/src/Components/Tracks/Tracks.tsx
··· 3 3 import { createColumnHelper } from "@tanstack/react-table"; 4 4 import Sidebar from "../Sidebar"; 5 5 import ControlBar from "../ControlBar"; 6 + import MainView from "../MainView"; 6 7 import { 7 8 AlbumCover, 8 9 AlbumCoverAlt, ··· 13 14 Hover, 14 15 IconButton, 15 16 Link, 16 - MainView, 17 17 Title, 18 18 } from "./styles"; 19 19 import { EllipsisHorizontal } from "@styled-icons/ionicons-sharp";
+2 -2
webui/rockbox/src/Components/Tracks/__snapshots__/Tracks.test.tsx.snap
··· 9 9 class="css-e1989k" 10 10 > 11 11 <div 12 - class="css-1fkc5kv" 12 + class="css-1tlxqlf" 13 13 > 14 14 <a 15 15 href="/" ··· 140 140 </a> 141 141 </div> 142 142 <div 143 - class="css-v8cdaf" 143 + class="css-y9r6ap" 144 144 > 145 145 <div 146 146 class="css-14bvc2y"
-7
webui/rockbox/src/Components/Tracks/styles.tsx
··· 9 9 height: 100%; 10 10 `; 11 11 12 - export const MainView = styled.div` 13 - display: flex; 14 - flex: 1; 15 - flex-direction: column; 16 - width: calc(100% - 240px); 17 - `; 18 - 19 12 export const Title = styled.div` 20 13 font-size: 24px; 21 14 font-family: RockfordSansMedium;
+12
webui/rockbox/src/GraphQL/Playlist/Mutation.ts
··· 1 1 import { gql } from "@apollo/client"; 2 2 3 + export const RESUME_PLAYLIST = gql` 4 + mutation ResumePlaylist { 5 + playlistResume 6 + } 7 + `; 8 + 9 + export const RESUME_PLAYLIST_TRACK = gql` 10 + mutation ResumePlaylistTrack { 11 + resumeTrack 12 + } 13 + `; 14 + 3 15 export const PLAYLIST_REMOVE_TRACK = gql` 4 16 mutation PlaylistRemoveTrack($index: Int!) { 5 17 playlistRemoveTrack(index: $index)
+2
webui/rockbox/src/GraphQL/Playlist/Query.ts
··· 14 14 artistId 15 15 albumId 16 16 path 17 + album 18 + length 17 19 } 18 20 } 19 21 }
+11
webui/rockbox/src/GraphQL/System/Query.ts
··· 5 5 rockboxVersion 6 6 } 7 7 `; 8 + 9 + export const GET_GLOBAL_STATUS = gql` 10 + query GetGlobalStatus { 11 + globalStatus { 12 + resumeIndex 13 + resumeCrc32 14 + resumeOffset 15 + resumeElapsed 16 + } 17 + } 18 + `;
+122 -3
webui/rockbox/src/Hooks/GraphQL.tsx
··· 90 90 playlistInsertTracks: Scalars['Int']['output']; 91 91 playlistRemoveAllTracks: Scalars['Int']['output']; 92 92 playlistRemoveTrack: Scalars['Int']['output']; 93 - playlistResume: Scalars['String']['output']; 93 + playlistResume: Scalars['Int']['output']; 94 94 playlistSetModified: Scalars['String']['output']; 95 95 playlistStart: Scalars['Int']['output']; 96 96 playlistSync: Scalars['String']['output']; ··· 561 561 562 562 export type PlaybackStatusSubscription = { __typename?: 'Subscription', playbackStatus: { __typename?: 'AudioStatus', status: number } }; 563 563 564 + export type ResumePlaylistMutationVariables = Exact<{ [key: string]: never; }>; 565 + 566 + 567 + export type ResumePlaylistMutation = { __typename?: 'Mutation', playlistResume: number }; 568 + 569 + export type ResumePlaylistTrackMutationVariables = Exact<{ [key: string]: never; }>; 570 + 571 + 572 + export type ResumePlaylistTrackMutation = { __typename?: 'Mutation', resumeTrack: string }; 573 + 564 574 export type PlaylistRemoveTrackMutationVariables = Exact<{ 565 575 index: Scalars['Int']['input']; 566 576 }>; ··· 580 590 export type GetCurrentPlaylistQueryVariables = Exact<{ [key: string]: never; }>; 581 591 582 592 583 - export type GetCurrentPlaylistQuery = { __typename?: 'Query', playlistGetCurrent: { __typename?: 'Playlist', index: number, amount: number, maxPlaylistSize: number, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, path: string }> } }; 593 + export type GetCurrentPlaylistQuery = { __typename?: 'Query', playlistGetCurrent: { __typename?: 'Playlist', index: number, amount: number, maxPlaylistSize: number, tracks: Array<{ __typename?: 'Track', id?: string | null, title: string, artist: string, albumArt?: string | null, artistId?: string | null, albumId?: string | null, path: string, album: string, length: number }> } }; 584 594 585 595 export type PlaylistChangedSubscriptionVariables = Exact<{ [key: string]: never; }>; 586 596 ··· 591 601 592 602 593 603 export type GetRockboxVersionQuery = { __typename?: 'Query', rockboxVersion: string }; 604 + 605 + export type GetGlobalStatusQueryVariables = Exact<{ [key: string]: never; }>; 606 + 607 + 608 + export type GetGlobalStatusQuery = { __typename?: 'Query', globalStatus: { __typename?: 'SystemStatus', resumeIndex: number, resumeCrc32: number, resumeOffset: number, resumeElapsed: number } }; 594 609 595 610 596 611 export const GetEntriesDocument = gql` ··· 1259 1274 } 1260 1275 export type PlaybackStatusSubscriptionHookResult = ReturnType<typeof usePlaybackStatusSubscription>; 1261 1276 export type PlaybackStatusSubscriptionResult = Apollo.SubscriptionResult<PlaybackStatusSubscription>; 1277 + export const ResumePlaylistDocument = gql` 1278 + mutation ResumePlaylist { 1279 + playlistResume 1280 + } 1281 + `; 1282 + export type ResumePlaylistMutationFn = Apollo.MutationFunction<ResumePlaylistMutation, ResumePlaylistMutationVariables>; 1283 + 1284 + /** 1285 + * __useResumePlaylistMutation__ 1286 + * 1287 + * To run a mutation, you first call `useResumePlaylistMutation` within a React component and pass it any options that fit your needs. 1288 + * When your component renders, `useResumePlaylistMutation` returns a tuple that includes: 1289 + * - A mutate function that you can call at any time to execute the mutation 1290 + * - An object with fields that represent the current status of the mutation's execution 1291 + * 1292 + * @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; 1293 + * 1294 + * @example 1295 + * const [resumePlaylistMutation, { data, loading, error }] = useResumePlaylistMutation({ 1296 + * variables: { 1297 + * }, 1298 + * }); 1299 + */ 1300 + export function useResumePlaylistMutation(baseOptions?: Apollo.MutationHookOptions<ResumePlaylistMutation, ResumePlaylistMutationVariables>) { 1301 + const options = {...defaultOptions, ...baseOptions} 1302 + return Apollo.useMutation<ResumePlaylistMutation, ResumePlaylistMutationVariables>(ResumePlaylistDocument, options); 1303 + } 1304 + export type ResumePlaylistMutationHookResult = ReturnType<typeof useResumePlaylistMutation>; 1305 + export type ResumePlaylistMutationResult = Apollo.MutationResult<ResumePlaylistMutation>; 1306 + export type ResumePlaylistMutationOptions = Apollo.BaseMutationOptions<ResumePlaylistMutation, ResumePlaylistMutationVariables>; 1307 + export const ResumePlaylistTrackDocument = gql` 1308 + mutation ResumePlaylistTrack { 1309 + resumeTrack 1310 + } 1311 + `; 1312 + export type ResumePlaylistTrackMutationFn = Apollo.MutationFunction<ResumePlaylistTrackMutation, ResumePlaylistTrackMutationVariables>; 1313 + 1314 + /** 1315 + * __useResumePlaylistTrackMutation__ 1316 + * 1317 + * To run a mutation, you first call `useResumePlaylistTrackMutation` within a React component and pass it any options that fit your needs. 1318 + * When your component renders, `useResumePlaylistTrackMutation` returns a tuple that includes: 1319 + * - A mutate function that you can call at any time to execute the mutation 1320 + * - An object with fields that represent the current status of the mutation's execution 1321 + * 1322 + * @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; 1323 + * 1324 + * @example 1325 + * const [resumePlaylistTrackMutation, { data, loading, error }] = useResumePlaylistTrackMutation({ 1326 + * variables: { 1327 + * }, 1328 + * }); 1329 + */ 1330 + export function useResumePlaylistTrackMutation(baseOptions?: Apollo.MutationHookOptions<ResumePlaylistTrackMutation, ResumePlaylistTrackMutationVariables>) { 1331 + const options = {...defaultOptions, ...baseOptions} 1332 + return Apollo.useMutation<ResumePlaylistTrackMutation, ResumePlaylistTrackMutationVariables>(ResumePlaylistTrackDocument, options); 1333 + } 1334 + export type ResumePlaylistTrackMutationHookResult = ReturnType<typeof useResumePlaylistTrackMutation>; 1335 + export type ResumePlaylistTrackMutationResult = Apollo.MutationResult<ResumePlaylistTrackMutation>; 1336 + export type ResumePlaylistTrackMutationOptions = Apollo.BaseMutationOptions<ResumePlaylistTrackMutation, ResumePlaylistTrackMutationVariables>; 1262 1337 export const PlaylistRemoveTrackDocument = gql` 1263 1338 mutation PlaylistRemoveTrack($index: Int!) { 1264 1339 playlistRemoveTrack(index: $index) ··· 1337 1412 artistId 1338 1413 albumId 1339 1414 path 1415 + album 1416 + length 1340 1417 } 1341 1418 } 1342 1419 } ··· 1449 1526 export type GetRockboxVersionQueryHookResult = ReturnType<typeof useGetRockboxVersionQuery>; 1450 1527 export type GetRockboxVersionLazyQueryHookResult = ReturnType<typeof useGetRockboxVersionLazyQuery>; 1451 1528 export type GetRockboxVersionSuspenseQueryHookResult = ReturnType<typeof useGetRockboxVersionSuspenseQuery>; 1452 - export type GetRockboxVersionQueryResult = Apollo.QueryResult<GetRockboxVersionQuery, GetRockboxVersionQueryVariables>; 1529 + export type GetRockboxVersionQueryResult = Apollo.QueryResult<GetRockboxVersionQuery, GetRockboxVersionQueryVariables>; 1530 + export const GetGlobalStatusDocument = gql` 1531 + query GetGlobalStatus { 1532 + globalStatus { 1533 + resumeIndex 1534 + resumeCrc32 1535 + resumeOffset 1536 + resumeElapsed 1537 + } 1538 + } 1539 + `; 1540 + 1541 + /** 1542 + * __useGetGlobalStatusQuery__ 1543 + * 1544 + * To run a query within a React component, call `useGetGlobalStatusQuery` and pass it any options that fit your needs. 1545 + * When your component renders, `useGetGlobalStatusQuery` returns an object from Apollo Client that contains loading, error, and data properties 1546 + * you can use to render your UI. 1547 + * 1548 + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; 1549 + * 1550 + * @example 1551 + * const { data, loading, error } = useGetGlobalStatusQuery({ 1552 + * variables: { 1553 + * }, 1554 + * }); 1555 + */ 1556 + export function useGetGlobalStatusQuery(baseOptions?: Apollo.QueryHookOptions<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>) { 1557 + const options = {...defaultOptions, ...baseOptions} 1558 + return Apollo.useQuery<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>(GetGlobalStatusDocument, options); 1559 + } 1560 + export function useGetGlobalStatusLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>) { 1561 + const options = {...defaultOptions, ...baseOptions} 1562 + return Apollo.useLazyQuery<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>(GetGlobalStatusDocument, options); 1563 + } 1564 + export function useGetGlobalStatusSuspenseQuery(baseOptions?: Apollo.SkipToken | Apollo.SuspenseQueryHookOptions<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>) { 1565 + const options = baseOptions === Apollo.skipToken ? baseOptions : {...defaultOptions, ...baseOptions} 1566 + return Apollo.useSuspenseQuery<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>(GetGlobalStatusDocument, options); 1567 + } 1568 + export type GetGlobalStatusQueryHookResult = ReturnType<typeof useGetGlobalStatusQuery>; 1569 + export type GetGlobalStatusLazyQueryHookResult = ReturnType<typeof useGetGlobalStatusLazyQuery>; 1570 + export type GetGlobalStatusSuspenseQueryHookResult = ReturnType<typeof useGetGlobalStatusSuspenseQuery>; 1571 + export type GetGlobalStatusQueryResult = Apollo.QueryResult<GetGlobalStatusQuery, GetGlobalStatusQueryVariables>;
+13 -12
webui/rockbox/src/Hooks/usePlayQueue.tsx
··· 4 4 usePlaylistChangedSubscription, 5 5 } from "./GraphQL"; 6 6 import _ from "lodash"; 7 + import { useRecoilValue } from "recoil"; 8 + import { controlBarState } from "../Components/ControlBar/ControlBarState"; 7 9 8 10 export const usePlayQueue = () => { 11 + const { resumeIndex } = useRecoilValue(controlBarState); 9 12 const { data: playlistSubscription } = usePlaylistChangedSubscription({ 10 13 fetchPolicy: "network-only", 11 14 }); ··· 14 17 }); 15 18 const previousTracks = useMemo(() => { 16 19 if (playlistSubscription?.playlistChanged) { 17 - const currentTrackIndex = _.get( 18 - playlistSubscription, 19 - "playlistChanged.index", 20 - 0 21 - ); 20 + const currentTrackIndex = 21 + resumeIndex > -1 22 + ? resumeIndex 23 + : _.get(playlistSubscription, "playlistChanged.index", 0); 22 24 const tracks = _.get(playlistSubscription, "playlistChanged.tracks", []); 23 25 return tracks.slice(0, currentTrackIndex + 1).map((x, index) => ({ 24 26 ...x, ··· 37 39 ? `http://localhost:6062/covers/${x.albumArt}` 38 40 : undefined, 39 41 })); 40 - }, [data, playlistSubscription]); 42 + }, [data, playlistSubscription, resumeIndex]); 41 43 42 44 const nextTracks = useMemo(() => { 43 45 if (playlistSubscription?.playlistChanged) { 44 - const currentTrackIndex = _.get( 45 - playlistSubscription, 46 - "playlistChanged.index", 47 - 0 48 - ); 46 + const currentTrackIndex = 47 + resumeIndex > -1 48 + ? resumeIndex 49 + : _.get(playlistSubscription, "playlistChanged.index", 0); 49 50 const tracks = _.get(playlistSubscription, "playlistChanged.tracks", []); 50 51 return tracks.slice(currentTrackIndex + 1).map((x, index) => ({ 51 52 ...x, ··· 64 65 ? `http://localhost:6062/covers/${x.albumArt}` 65 66 : undefined, 66 67 })); 67 - }, [data, playlistSubscription]); 68 + }, [data, playlistSubscription, resumeIndex]); 68 69 69 70 return { previousTracks, nextTracks }; 70 71 };
+69
webui/rockbox/src/Hooks/useResumePlaylist.tsx
··· 1 + import { useEffect } from "react"; 2 + import { 3 + useGetCurrentPlaylistQuery, 4 + useGetGlobalStatusQuery, 5 + useResumePlaylistMutation, 6 + useResumePlaylistTrackMutation, 7 + } from "./GraphQL"; 8 + import { useRecoilState } from "recoil"; 9 + import { controlBarState } from "../Components/ControlBar/ControlBarState"; 10 + 11 + export const useResumePlaylist = () => { 12 + const [{ resumeIndex }, setControlBarState] = useRecoilState(controlBarState); 13 + const { data: globalStatusData } = useGetGlobalStatusQuery(); 14 + const { 15 + data: currentPlaylistData, 16 + loading, 17 + refetch: refetchCurrentPlaylist, 18 + } = useGetCurrentPlaylistQuery(); 19 + const [resumePlaylist] = useResumePlaylistMutation(); 20 + const [resumePlaylistTrack] = useResumePlaylistTrackMutation(); 21 + 22 + useEffect(() => { 23 + if (loading || !currentPlaylistData || !globalStatusData) { 24 + return; 25 + } 26 + 27 + if (currentPlaylistData.playlistGetCurrent.tracks.length === 0) { 28 + resumePlaylist() 29 + .then((res) => { 30 + if (res.data?.playlistResume === 0) { 31 + return refetchCurrentPlaylist(); 32 + } 33 + }) 34 + .catch((e) => console.error(e)); 35 + return; 36 + } 37 + 38 + if ( 39 + currentPlaylistData.playlistGetCurrent.tracks.length > 0 && 40 + resumeIndex < 0 41 + ) { 42 + const currentSong = 43 + currentPlaylistData.playlistGetCurrent.tracks[ 44 + globalStatusData.globalStatus.resumeIndex 45 + ]; 46 + setControlBarState((state) => ({ 47 + ...state, 48 + nowPlaying: { 49 + album: currentSong?.album, 50 + artist: currentSong?.artist, 51 + title: currentSong?.title, 52 + cover: currentSong?.albumArt 53 + ? currentSong?.albumArt.startsWith("http") 54 + ? currentSong.albumArt 55 + : `http://localhost:6062/covers/${currentSong?.albumArt}` 56 + : "", 57 + duration: currentSong?.length || 0, 58 + progress: globalStatusData.globalStatus.resumeElapsed, 59 + isPlaying: false, 60 + albumId: currentSong?.albumId, 61 + }, 62 + resumeIndex: globalStatusData.globalStatus.resumeIndex, 63 + })); 64 + } 65 + // eslint-disable-next-line react-hooks/exhaustive-deps 66 + }, [loading, currentPlaylistData, globalStatusData]); 67 + 68 + return { resumePlaylistTrack }; 69 + };
+2 -2
webui/rockbox/src/Theme.tsx webui/rockbox/src/Theme.ts
··· 42 42 text: "#000", 43 43 background: "#fff", 44 44 icon: "#000", 45 - searchBackground: "#f7f7f7", 46 - searchBackgroundAlt: "#fff", 45 + searchBackground: "rgba(247, 247, 247, 0.2)", 46 + searchBackgroundAlt: "rgba(255, 255, 255, 0.2)", 47 47 secondaryBackground: "#fbf5ff", 48 48 secondaryText: "rgba(0, 0, 0, 0.542)", 49 49 backButton: "#f7f7f8",