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 #31 from tsirysndr/feat/file-browsing

webui: implement file browser

authored by

Tsiry Sandratraina and committed by
GitHub
94593eaf 57788d68

+924 -93
+5 -2
crates/ext/src/browse/mod.rs
··· 22 22 23 23 #[op2(async)] 24 24 #[serde] 25 - pub async fn op_tree_get_entries(#[string] path: String) -> Result<Vec<Entry>, AnyError> { 25 + pub async fn op_tree_get_entries(#[string] path: Option<String>) -> Result<Vec<Entry>, AnyError> { 26 26 let client = reqwest::Client::new(); 27 - let url = format!("{}/browse/tree-entries?q={}", rockbox_url(), path); 27 + let url = match path { 28 + Some(path) => format!("{}/browse/tree-entries?q={}", rockbox_url(), path), 29 + None => format!("{}/browse/tree-entries", rockbox_url()), 30 + }; 28 31 let response = client.get(&url).send().await?; 29 32 let entries = response.json::<Vec<Entry>>().await?; 30 33 Ok(entries)
+5
crates/graphql/src/lib.rs
··· 5 5 pub mod server; 6 6 pub type RockboxSchema = Schema<Query, Mutation, EmptySubscription>; 7 7 8 + pub const AUDIO_EXTENSIONS: [&str; 17] = [ 9 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 10 + "spx", "sid", "ape", "wma", 11 + ]; 12 + 8 13 pub fn rockbox_url() -> String { 9 14 let port = std::env::var("ROCKBOX_TCP_PORT").unwrap_or_else(|_| "6063".to_string()); 10 15 format!("http://127.0.0.1:{}", port)
+56 -7
crates/graphql/src/schema/browse.rs
··· 1 + use std::{env, fs}; 2 + 1 3 use async_graphql::*; 2 4 3 - use crate::{rockbox_url, schema::objects::entry::Entry}; 5 + use crate::{schema::objects::entry::Entry, AUDIO_EXTENSIONS}; 4 6 5 7 #[derive(Default)] 6 8 pub struct BrowseQuery; 7 9 8 10 #[Object] 9 11 impl BrowseQuery { 10 - async fn tree_get_entries(&self, ctx: &Context<'_>, path: String) -> Result<Vec<Entry>, Error> { 11 - let client = ctx.data::<reqwest::Client>().unwrap(); 12 - let url = format!("{}/browse/tree-entries?q={}", rockbox_url(), path); 13 - let response = client.get(&url).send().await?; 14 - let response = response.json::<Vec<Entry>>().await?; 15 - Ok(response) 12 + async fn tree_get_entries( 13 + &self, 14 + _ctx: &Context<'_>, 15 + path: Option<String>, 16 + ) -> Result<Vec<Entry>, Error> { 17 + let show_hidden = false; 18 + let home = env::var("HOME").unwrap(); 19 + let music_library = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 20 + 21 + let path = match path { 22 + Some(path) => path, 23 + None => music_library, 24 + }; 25 + 26 + if !fs::metadata(&path)?.is_dir() { 27 + return Err(Error::new("Path is not a directory")); 28 + } 29 + 30 + let mut entries = vec![]; 31 + 32 + for file in fs::read_dir(&path)? { 33 + let file = file?; 34 + 35 + if file.metadata()?.is_file() 36 + && !AUDIO_EXTENSIONS.iter().any(|ext| { 37 + file.path() 38 + .to_string_lossy() 39 + .ends_with(&format!(".{}", ext)) 40 + }) 41 + { 42 + continue; 43 + } 44 + 45 + if file.file_name().to_string_lossy().starts_with(".") && !show_hidden { 46 + continue; 47 + } 48 + 49 + entries.push(Entry { 50 + name: file.path().to_string_lossy().to_string(), 51 + time_write: file 52 + .metadata()? 53 + .modified()? 54 + .duration_since(std::time::SystemTime::UNIX_EPOCH)? 55 + .as_secs() as u32, 56 + attr: match file.metadata()?.is_dir() { 57 + true => 0x10, 58 + false => 0, 59 + }, 60 + ..Default::default() 61 + }); 62 + } 63 + 64 + Ok(entries) 16 65 } 17 66 }
+1 -1
crates/rpc/proto/rockbox/v1alpha1/browse.proto
··· 11 11 message TreeGetContextResponse {} 12 12 13 13 message TreeGetEntriesRequest { 14 - string path = 1; 14 + optional string path = 1; 15 15 } 16 16 17 17 message Entry {
+2 -2
crates/rpc/src/api/rockbox.v1alpha1.rs
··· 9 9 pub struct TreeGetContextResponse {} 10 10 #[derive(Clone, PartialEq, ::prost::Message)] 11 11 pub struct TreeGetEntriesRequest { 12 - #[prost(string, tag = "1")] 13 - pub path: ::prost::alloc::string::String, 12 + #[prost(string, optional, tag = "1")] 13 + pub path: ::core::option::Option<::prost::alloc::string::String>, 14 14 } 15 15 #[derive(Clone, PartialEq, ::prost::Message)] 16 16 pub struct Entry {
crates/rpc/src/api/rockbox_descriptor.bin

This is a binary file and will not be displayed.

+52 -16
crates/rpc/src/browse.rs
··· 1 + use std::{env, fs}; 2 + 1 3 use crate::{ 2 4 api::rockbox::v1alpha1::{browse_service_server::BrowseService, *}, 3 - rockbox_url, 5 + rockbox_url, AUDIO_EXTENSIONS, 4 6 }; 5 7 use rockbox_sys as rb; 6 8 ··· 22 24 request: tonic::Request<TreeGetEntriesRequest>, 23 25 ) -> Result<tonic::Response<TreeGetEntriesResponse>, tonic::Status> { 24 26 let path = request.into_inner().path; 25 - let url = format!("{}/browse/tree-entries?q={}", rockbox_url(), path); 26 - let response = self 27 - .client 28 - .get(url) 29 - .send() 30 - .await 31 - .map_err(|e| tonic::Status::internal(e.to_string()))?; 32 - let data = response 33 - .json::<Vec<rb::types::tree::Entry>>() 34 - .await 35 - .map_err(|e| tonic::Status::internal(e.to_string()))?; 36 - let entries = data 37 - .into_iter() 38 - .map(|entry| Entry::from(entry)) 39 - .collect::<Vec<Entry>>(); 27 + 28 + let show_hidden = false; 29 + let home = env::var("HOME").unwrap(); 30 + let music_library = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 31 + 32 + let path = match path { 33 + Some(path) => path, 34 + None => music_library, 35 + }; 36 + 37 + if !fs::metadata(&path)?.is_dir() { 38 + return Err(tonic::Status::internal("Path is not a directory")); 39 + } 40 + 41 + let mut entries = vec![]; 42 + 43 + for file in fs::read_dir(&path)? { 44 + let file = file?; 45 + 46 + if file.metadata()?.is_file() 47 + && !AUDIO_EXTENSIONS.iter().any(|ext| { 48 + file.path() 49 + .to_string_lossy() 50 + .ends_with(&format!(".{}", ext)) 51 + }) 52 + { 53 + continue; 54 + } 55 + 56 + if file.file_name().to_string_lossy().starts_with(".") && !show_hidden { 57 + continue; 58 + } 59 + 60 + entries.push(Entry { 61 + name: file.path().to_string_lossy().to_string(), 62 + time_write: file 63 + .metadata()? 64 + .modified()? 65 + .duration_since(std::time::SystemTime::UNIX_EPOCH) 66 + .map_err(|e| tonic::Status::internal(e.to_string()))? 67 + .as_secs() as u32, 68 + attr: match file.metadata()?.is_dir() { 69 + true => 0x10, 70 + false => 0, 71 + }, 72 + ..Default::default() 73 + }); 74 + } 75 + 40 76 Ok(tonic::Response::new(TreeGetEntriesResponse { entries })) 41 77 } 42 78 }
+5
crates/rpc/src/lib.rs
··· 8 8 pub mod sound; 9 9 pub mod system; 10 10 11 + pub const AUDIO_EXTENSIONS: [&str; 17] = [ 12 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 13 + "spx", "sid", "ape", "wma", 14 + ]; 15 + 11 16 pub mod api { 12 17 #[path = ""] 13 18 pub mod rockbox {
+51
crates/server/src/cache.rs
··· 1 + use std::{fs, thread}; 2 + 3 + use anyhow::Error; 4 + use rockbox_sys::types::tree::Entry; 5 + 6 + use crate::{http::Context, AUDIO_EXTENSIONS}; 7 + 8 + pub fn update_cache(ctx: &Context, path: &str, show_hidden: bool) { 9 + let fs_cache = ctx.fs_cache.clone(); 10 + let path = path.to_string(); 11 + thread::spawn(move || { 12 + let rt = tokio::runtime::Runtime::new().unwrap(); 13 + let mut fs_cache = rt.block_on(fs_cache.lock()); 14 + let mut entries = vec![]; 15 + 16 + for file in fs::read_dir(&path)? { 17 + let file = file?; 18 + 19 + if file.metadata()?.is_file() 20 + && !AUDIO_EXTENSIONS.iter().any(|ext| { 21 + file.path() 22 + .to_string_lossy() 23 + .ends_with(&format!(".{}", ext)) 24 + }) 25 + { 26 + continue; 27 + } 28 + 29 + if file.file_name().to_string_lossy().starts_with(".") && !show_hidden { 30 + continue; 31 + } 32 + 33 + entries.push(Entry { 34 + name: file.path().to_string_lossy().to_string(), 35 + time_write: file 36 + .metadata()? 37 + .modified()? 38 + .duration_since(std::time::SystemTime::UNIX_EPOCH)? 39 + .as_secs() as u32, 40 + attr: match file.metadata()?.is_dir() { 41 + true => 0x10, 42 + false => 0, 43 + }, 44 + ..Default::default() 45 + }); 46 + } 47 + 48 + fs_cache.insert(path, entries.clone()); 49 + Ok::<(), Error>(()) 50 + }); 51 + }
+25 -10
crates/server/src/handlers/browse.rs
··· 1 - use std::fs; 1 + use std::{env, fs}; 2 2 3 - use crate::http::{Context, Request, Response}; 3 + use crate::{ 4 + cache::update_cache, 5 + http::{Context, Request, Response}, 6 + AUDIO_EXTENSIONS, 7 + }; 4 8 use anyhow::Error; 5 9 use rockbox_sys::types::tree::Entry; 6 10 7 - const AUDIO_EXTENSIONS: [&str; 17] = [ 8 - "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 9 - "spx", "sid", "ape", "wma", 10 - ]; 11 - 12 11 pub async fn get_tree_entries( 13 - _ctx: &Context, 12 + ctx: &Context, 14 13 req: &Request, 15 14 res: &mut Response, 16 15 ) -> Result<(), Error> { 16 + let home = env::var("HOME").unwrap(); 17 + let music_library = env::var("ROCKBOX_LIBRARY").unwrap_or(format!("{}/Music", home)); 18 + 17 19 let path = match req.query_params.get("q") { 18 - Some(path) => path.as_str().unwrap_or("/"), 19 - None => "/", 20 + Some(path) => path.as_str().unwrap_or(&music_library), 21 + None => &music_library, 20 22 }; 21 23 let show_hidden = match req.query_params.get("show_hidden") { 22 24 Some(show_hidden) => show_hidden.as_str().unwrap_or("false") == "true", ··· 26 28 if !fs::metadata(path)?.is_dir() { 27 29 res.set_status(500); 28 30 res.text("Path is not a directory"); 31 + return Ok(()); 32 + } 33 + 34 + let mut fs_cache = ctx.fs_cache.lock().await; 35 + if let Some(entries) = fs_cache.get(path.into()) { 36 + update_cache(ctx, path, show_hidden); 37 + res.json(entries); 29 38 return Ok(()); 30 39 } 31 40 ··· 55 64 .modified()? 56 65 .duration_since(std::time::SystemTime::UNIX_EPOCH)? 57 66 .as_secs() as u32, 67 + attr: match file.metadata()?.is_dir() { 68 + true => 0x10, 69 + false => 0, 70 + }, 58 71 ..Default::default() 59 72 }); 60 73 } 74 + 75 + fs_cache.insert(path.to_string(), entries.clone()); 61 76 62 77 res.json(&entries); 63 78 Ok(())
+7 -2
crates/server/src/http.rs
··· 1 1 use anyhow::Error; 2 2 use owo_colors::OwoColorize; 3 - use rockbox_sys as rb; 3 + use rockbox_sys::{self as rb, types::tree::Entry}; 4 4 use serde::Serialize; 5 5 use serde_json::Value; 6 6 use sqlx::Sqlite; ··· 18 18 19 19 pub struct Context { 20 20 pub pool: sqlx::Pool<Sqlite>, 21 + pub fs_cache: Arc<tokio::sync::Mutex<HashMap<String, Vec<Entry>>>>, 21 22 } 22 23 23 24 #[derive(Debug)] ··· 246 247 let active_connections = Arc::new(Mutex::new(0)); 247 248 let rt = tokio::runtime::Runtime::new()?; 248 249 let db_pool = rt.block_on(rockbox_library::create_connection_pool())?; 250 + let fs_cache = Arc::new(tokio::sync::Mutex::new(HashMap::new())); 249 251 250 252 loop { 251 253 match listener.accept() { ··· 257 259 *active_connections += 1; 258 260 } 259 261 let mut cloned_self = self.clone(); 262 + let cloned_fs_cache = fs_cache.clone(); 260 263 pool.execute(move || { 261 264 let mut buf_reader = BufReader::new(&stream); 262 265 let mut request = String::new(); ··· 320 323 stream, 321 324 db_pool, 322 325 req_body, 326 + cloned_fs_cache, 323 327 ); 324 328 } 325 329 ··· 361 365 mut stream: TcpStream, 362 366 pool: sqlx::Pool<Sqlite>, 363 367 body: Option<String>, 368 + fs_cache: Arc<tokio::sync::Mutex<HashMap<String, Vec<Entry>>>>, 364 369 ) { 365 370 println!("{} {}", method.bright_cyan(), path); 366 371 match self.router.route(method, path) { 367 372 Some((handler, params)) => { 368 373 let mut response = Response::new(); 369 - let context = Context { pool }; 374 + let context = Context { pool, fs_cache }; 370 375 let request = Request { 371 376 method: method.to_string(), 372 377 params,
+6
crates/server/src/lib.rs
··· 8 8 thread, 9 9 }; 10 10 11 + pub mod cache; 11 12 pub mod handlers; 12 13 pub mod http; 13 14 pub mod types; 15 + 16 + pub const AUDIO_EXTENSIONS: [&str; 17] = [ 17 + "mp3", "ogg", "flac", "m4a", "aac", "mp4", "alac", "wav", "wv", "mpc", "aiff", "ac3", "opus", 18 + "spx", "sid", "ape", "wma", 19 + ]; 14 20 15 21 #[no_mangle] 16 22 pub extern "C" fn debugfn(args: *const c_char) {
+1 -1
crates/sys/src/types/tree.rs
··· 93 93 } 94 94 } 95 95 96 - #[derive(Debug, Default, Serialize, Deserialize)] 96 + #[derive(Debug, Default, Serialize, Deserialize, Clone)] 97 97 pub struct Entry { 98 98 pub name: String, // char* name 99 99 pub attr: i32, // int attr (FAT attributes + file type flags)
+3 -7
webui/rockbox/graphql.schema.json
··· 1739 1739 "name": "path", 1740 1740 "description": null, 1741 1741 "type": { 1742 - "kind": "NON_NULL", 1743 - "name": null, 1744 - "ofType": { 1745 - "kind": "SCALAR", 1746 - "name": "String", 1747 - "ofType": null 1748 - } 1742 + "kind": "SCALAR", 1743 + "name": "String", 1744 + "ofType": null 1749 1745 }, 1750 1746 "defaultValue": null, 1751 1747 "isDeprecated": false,
+3
webui/rockbox/src/Components/Files/Files.stories.tsx
··· 2 2 3 3 import Files from "./Files"; 4 4 import { files } from "./mocks"; 5 + import { fn } from "@storybook/test"; 5 6 6 7 // More on how to set up stories at: https://storybook.js.org/docs/writing-stories#default-export 7 8 const meta = { ··· 19 20 export const Default: Story = { 20 21 args: { 21 22 files, 23 + canGoBack: true, 24 + onGoBack: fn(), 22 25 }, 23 26 };
+2 -1
webui/rockbox/src/Components/Files/Files.test.tsx
··· 3 3 import Providers from "../../Providers"; 4 4 import { files } from "./mocks"; 5 5 import { MemoryRouter } from "react-router-dom"; 6 + import { vi } from "vitest"; 6 7 7 8 describe("Files", () => { 8 9 it("should render", () => { 9 10 const { container } = render( 10 11 <MemoryRouter initialEntries={["/"]}> 11 12 <Providers> 12 - <Files files={files} /> 13 + <Files files={files} canGoBack={true} onGoBack={vi.fn()} /> 13 14 </Providers> 14 15 </MemoryRouter> 15 16 );
+41 -8
webui/rockbox/src/Components/Files/Files.tsx
··· 5 5 import ControlBar from "../ControlBar"; 6 6 import { Folder2, MusicNoteBeamed } from "@styled-icons/bootstrap"; 7 7 import { 8 + AudioFile, 8 9 BackButton, 9 10 ButtonGroup, 10 11 Container, ··· 17 18 } from "./styles"; 18 19 import { EllipsisHorizontal } from "@styled-icons/ionicons-sharp"; 19 20 import { File } from "../../Types/file"; 20 - import Table from "../VirtualizedTable"; 21 + import Table from "../Table"; 21 22 import "./styles.css"; 22 23 import ArrowBack from "../Icons/ArrowBack"; 24 + import { Spinner } from "baseui/spinner"; 23 25 24 26 const columnHelper = createColumnHelper<File>(); 25 27 const columns = [ ··· 32 34 display: "flex", 33 35 alignItems: "center", 34 36 justifyContent: "center", 37 + marginLeft: 10, 35 38 }} 36 39 > 37 40 {info.row.original.isDirectory && <Folder2 size={20} />} ··· 41 44 }), 42 45 columnHelper.accessor("name", { 43 46 header: "", 44 - cell: (info) => <Directory href="#">{info.getValue()}</Directory>, 47 + cell: (info) => ( 48 + <> 49 + {info.row.original.isDirectory && ( 50 + <Directory to={`/files?q=${info.row.original.path}`}> 51 + {info.getValue()} 52 + </Directory> 53 + )} 54 + {!info.row.original.isDirectory && ( 55 + <AudioFile>{info.getValue()}</AudioFile> 56 + )} 57 + </> 58 + ), 45 59 }), 46 60 columnHelper.accessor("name", { 47 61 header: "", ··· 60 74 61 75 export type FilesProps = { 62 76 files: File[]; 77 + canGoBack: boolean; 78 + onGoBack: () => void; 79 + refetching?: boolean; 63 80 }; 64 81 65 82 const Files: FC<FilesProps> = (props) => { ··· 69 86 <MainView> 70 87 <ControlBar /> 71 88 <ContentWrapper> 72 - <BackButton onClick={() => {}}> 73 - <div style={{ marginTop: 2 }}> 74 - <ArrowBack color={"#000"} /> 75 - </div> 76 - </BackButton> 89 + {props.canGoBack && ( 90 + <BackButton onClick={() => props.onGoBack()}> 91 + <div style={{ marginTop: 2 }}> 92 + <ArrowBack color={"#000"} /> 93 + </div> 94 + </BackButton> 95 + )} 77 96 <Title>Files</Title> 78 - <Table columns={columns as any} tracks={props.files} /> 97 + {!props.refetching && ( 98 + <Table columns={columns as any} tracks={props.files as any} /> 99 + )} 100 + {props.refetching && ( 101 + <div 102 + style={{ 103 + display: "flex", 104 + alignItems: "center", 105 + justifyContent: "center", 106 + height: "calc(100vh - 200px)", 107 + }} 108 + > 109 + <Spinner $size={"30px"} $borderWidth={"4px"} /> 110 + </div> 111 + )} 79 112 </ContentWrapper> 80 113 </MainView> 81 114 </Container>
+37 -3
webui/rockbox/src/Components/Files/FilesWithData.tsx
··· 1 - import { FC } from "react"; 1 + import { FC, useEffect, useState } from "react"; 2 2 import Files from "./Files"; 3 - import { files } from "./mocks"; 3 + import { useGetEntriesQuery } from "../../Hooks/GraphQL"; 4 + import { useNavigate, useSearchParams } from "react-router-dom"; 4 5 5 6 const FilesWithData: FC = () => { 6 - return <Files files={files} />; 7 + const navigate = useNavigate(); 8 + const [refetching, setRefetching] = useState(false); 9 + const { data, refetch, loading } = useGetEntriesQuery(); 10 + const [params] = useSearchParams(); 11 + const path = params.get("q"); 12 + const canGoBack = !!path; 13 + 14 + const files = 15 + data?.treeGetEntries.map((x) => ({ 16 + name: x.name.split("/").pop()!, 17 + isDirectory: x.attr === 16, 18 + path: x.name, 19 + })) || []; 20 + 21 + const onGoBack = () => navigate(-1); 22 + 23 + useEffect(() => { 24 + setRefetching(true); 25 + refetch({ 26 + path, 27 + }) 28 + .then(() => setRefetching(false)) 29 + .catch(() => setRefetching(false)); 30 + // eslint-disable-next-line react-hooks/exhaustive-deps 31 + }, [path]); 32 + 33 + return ( 34 + <Files 35 + files={files} 36 + canGoBack={canGoBack} 37 + onGoBack={onGoBack} 38 + refetching={refetching} 39 + /> 40 + ); 7 41 }; 8 42 9 43 export default FilesWithData;
+579 -25
webui/rockbox/src/Components/Files/__snapshots__/Files.test.tsx.snap
··· 323 323 </div> 324 324 </div> 325 325 <div 326 - class="css-bd0ywd" 326 + class="css-b767jv" 327 327 > 328 328 <button 329 329 class="css-t20qhr" ··· 358 358 > 359 359 Files 360 360 </div> 361 - <div 362 - style="overflow: auto; position: relative; height: 600px;" 361 + <table 362 + style="width: 100%; margin-top: 31px;" 363 363 > 364 - <table 365 - style="width: 100%;" 366 - > 367 - <thead> 368 - <tr 369 - style="height: 36px; color: rgba(0, 0, 0, 0.54);" 364 + <thead> 365 + <tr 366 + style="height: 36px; color: rgba(0, 0, 0, 0.54);" 367 + > 368 + <th 369 + style="text-align: left; width: 20px;" 370 + /> 371 + <th 372 + style="text-align: left; width: 150px;" 373 + /> 374 + <th 375 + style="text-align: left; width: 150px;" 376 + /> 377 + </tr> 378 + </thead> 379 + <tbody> 380 + <tr 381 + style="height: 48px;" 382 + > 383 + <td 384 + style="width: 20px; overflow: hidden;" 385 + > 386 + <div 387 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 388 + > 389 + <svg 390 + aria-hidden="true" 391 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 392 + fill="currentColor" 393 + focusable="false" 394 + height="20" 395 + viewBox="0 0 16 16" 396 + width="20" 397 + xmlns="http://www.w3.org/2000/svg" 398 + > 399 + <path 400 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 401 + /> 402 + </svg> 403 + </div> 404 + </td> 405 + <td 406 + style="width: 150px; overflow: hidden;" 407 + > 408 + <a 409 + class="css-1e3wasx" 410 + href="/files?q=/home/tsirysndr/Music/Big K.R.I.T." 411 + > 412 + Big K.R.I.T. 413 + </a> 414 + </td> 415 + <td 416 + style="width: 150px; overflow: hidden;" 417 + > 418 + <div 419 + class="css-14zx4vh" 420 + style="justify-content: flex-end; align-items: center;" 421 + > 422 + <button 423 + class="css-10asy2d" 424 + > 425 + <button 426 + class="css-k309af" 427 + > 428 + <svg 429 + aria-hidden="true" 430 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 431 + fill="currentColor" 432 + focusable="false" 433 + height="24" 434 + viewBox="0 0 512 512" 435 + width="24" 436 + xmlns="http://www.w3.org/2000/svg" 437 + > 438 + <circle 439 + cx="256" 440 + cy="256" 441 + r="48" 442 + /> 443 + <circle 444 + cx="416" 445 + cy="256" 446 + r="48" 447 + /> 448 + <circle 449 + cx="96" 450 + cy="256" 451 + r="48" 452 + /> 453 + </svg> 454 + </button> 455 + </button> 456 + </div> 457 + </td> 458 + </tr> 459 + <tr 460 + style="height: 48px;" 461 + > 462 + <td 463 + style="width: 20px; overflow: hidden;" 464 + > 465 + <div 466 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 467 + > 468 + <svg 469 + aria-hidden="true" 470 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 471 + fill="currentColor" 472 + focusable="false" 473 + height="20" 474 + viewBox="0 0 16 16" 475 + width="20" 476 + xmlns="http://www.w3.org/2000/svg" 477 + > 478 + <path 479 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 480 + /> 481 + </svg> 482 + </div> 483 + </td> 484 + <td 485 + style="width: 150px; overflow: hidden;" 370 486 > 371 - <th 372 - style="text-align: left; width: 20px;" 373 - /> 374 - <th 375 - style="text-align: left; width: 150px;" 376 - /> 377 - <th 378 - style="text-align: left; width: 150px;" 379 - /> 380 - </tr> 381 - </thead> 382 - <tbody 383 - style="height: 231px; position: relative;" 384 - /> 385 - </table> 386 - </div> 487 + <a 488 + class="css-1e3wasx" 489 + href="/files?q=/home/tsirysndr/Music/BoyWithUke, Oliver Tree" 490 + > 491 + BoyWithUke, Oliver Tree 492 + </a> 493 + </td> 494 + <td 495 + style="width: 150px; overflow: hidden;" 496 + > 497 + <div 498 + class="css-14zx4vh" 499 + style="justify-content: flex-end; align-items: center;" 500 + > 501 + <button 502 + class="css-10asy2d" 503 + > 504 + <button 505 + class="css-k309af" 506 + > 507 + <svg 508 + aria-hidden="true" 509 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 510 + fill="currentColor" 511 + focusable="false" 512 + height="24" 513 + viewBox="0 0 512 512" 514 + width="24" 515 + xmlns="http://www.w3.org/2000/svg" 516 + > 517 + <circle 518 + cx="256" 519 + cy="256" 520 + r="48" 521 + /> 522 + <circle 523 + cx="416" 524 + cy="256" 525 + r="48" 526 + /> 527 + <circle 528 + cx="96" 529 + cy="256" 530 + r="48" 531 + /> 532 + </svg> 533 + </button> 534 + </button> 535 + </div> 536 + </td> 537 + </tr> 538 + <tr 539 + style="height: 48px;" 540 + > 541 + <td 542 + style="width: 20px; overflow: hidden;" 543 + > 544 + <div 545 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 546 + > 547 + <svg 548 + aria-hidden="true" 549 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 550 + fill="currentColor" 551 + focusable="false" 552 + height="20" 553 + viewBox="0 0 16 16" 554 + width="20" 555 + xmlns="http://www.w3.org/2000/svg" 556 + > 557 + <path 558 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 559 + /> 560 + </svg> 561 + </div> 562 + </td> 563 + <td 564 + style="width: 150px; overflow: hidden;" 565 + > 566 + <a 567 + class="css-1e3wasx" 568 + href="/files?q=/home/tsirysndr/Music/Cage The Elephant" 569 + > 570 + Cage The Elephant 571 + </a> 572 + </td> 573 + <td 574 + style="width: 150px; overflow: hidden;" 575 + > 576 + <div 577 + class="css-14zx4vh" 578 + style="justify-content: flex-end; align-items: center;" 579 + > 580 + <button 581 + class="css-10asy2d" 582 + > 583 + <button 584 + class="css-k309af" 585 + > 586 + <svg 587 + aria-hidden="true" 588 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 589 + fill="currentColor" 590 + focusable="false" 591 + height="24" 592 + viewBox="0 0 512 512" 593 + width="24" 594 + xmlns="http://www.w3.org/2000/svg" 595 + > 596 + <circle 597 + cx="256" 598 + cy="256" 599 + r="48" 600 + /> 601 + <circle 602 + cx="416" 603 + cy="256" 604 + r="48" 605 + /> 606 + <circle 607 + cx="96" 608 + cy="256" 609 + r="48" 610 + /> 611 + </svg> 612 + </button> 613 + </button> 614 + </div> 615 + </td> 616 + </tr> 617 + <tr 618 + style="height: 48px;" 619 + > 620 + <td 621 + style="width: 20px; overflow: hidden;" 622 + > 623 + <div 624 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 625 + > 626 + <svg 627 + aria-hidden="true" 628 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 629 + fill="currentColor" 630 + focusable="false" 631 + height="20" 632 + viewBox="0 0 16 16" 633 + width="20" 634 + xmlns="http://www.w3.org/2000/svg" 635 + > 636 + <path 637 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 638 + /> 639 + </svg> 640 + </div> 641 + </td> 642 + <td 643 + style="width: 150px; overflow: hidden;" 644 + > 645 + <a 646 + class="css-1e3wasx" 647 + href="/files?q=/home/tsirysndr/Music/Cannons" 648 + > 649 + Cannons 650 + </a> 651 + </td> 652 + <td 653 + style="width: 150px; overflow: hidden;" 654 + > 655 + <div 656 + class="css-14zx4vh" 657 + style="justify-content: flex-end; align-items: center;" 658 + > 659 + <button 660 + class="css-10asy2d" 661 + > 662 + <button 663 + class="css-k309af" 664 + > 665 + <svg 666 + aria-hidden="true" 667 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 668 + fill="currentColor" 669 + focusable="false" 670 + height="24" 671 + viewBox="0 0 512 512" 672 + width="24" 673 + xmlns="http://www.w3.org/2000/svg" 674 + > 675 + <circle 676 + cx="256" 677 + cy="256" 678 + r="48" 679 + /> 680 + <circle 681 + cx="416" 682 + cy="256" 683 + r="48" 684 + /> 685 + <circle 686 + cx="96" 687 + cy="256" 688 + r="48" 689 + /> 690 + </svg> 691 + </button> 692 + </button> 693 + </div> 694 + </td> 695 + </tr> 696 + <tr 697 + style="height: 48px;" 698 + > 699 + <td 700 + style="width: 20px; overflow: hidden;" 701 + > 702 + <div 703 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 704 + > 705 + <svg 706 + aria-hidden="true" 707 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 708 + fill="currentColor" 709 + focusable="false" 710 + height="20" 711 + viewBox="0 0 16 16" 712 + width="20" 713 + xmlns="http://www.w3.org/2000/svg" 714 + > 715 + <path 716 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 717 + /> 718 + </svg> 719 + </div> 720 + </td> 721 + <td 722 + style="width: 150px; overflow: hidden;" 723 + > 724 + <a 725 + class="css-1e3wasx" 726 + href="/files?q=/home/tsirysndr/Music/Cold War Kids" 727 + > 728 + Cold War Kids 729 + </a> 730 + </td> 731 + <td 732 + style="width: 150px; overflow: hidden;" 733 + > 734 + <div 735 + class="css-14zx4vh" 736 + style="justify-content: flex-end; align-items: center;" 737 + > 738 + <button 739 + class="css-10asy2d" 740 + > 741 + <button 742 + class="css-k309af" 743 + > 744 + <svg 745 + aria-hidden="true" 746 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 747 + fill="currentColor" 748 + focusable="false" 749 + height="24" 750 + viewBox="0 0 512 512" 751 + width="24" 752 + xmlns="http://www.w3.org/2000/svg" 753 + > 754 + <circle 755 + cx="256" 756 + cy="256" 757 + r="48" 758 + /> 759 + <circle 760 + cx="416" 761 + cy="256" 762 + r="48" 763 + /> 764 + <circle 765 + cx="96" 766 + cy="256" 767 + r="48" 768 + /> 769 + </svg> 770 + </button> 771 + </button> 772 + </div> 773 + </td> 774 + </tr> 775 + <tr 776 + style="height: 48px;" 777 + > 778 + <td 779 + style="width: 20px; overflow: hidden;" 780 + > 781 + <div 782 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 783 + > 784 + <svg 785 + aria-hidden="true" 786 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 787 + fill="currentColor" 788 + focusable="false" 789 + height="20" 790 + viewBox="0 0 16 16" 791 + width="20" 792 + xmlns="http://www.w3.org/2000/svg" 793 + > 794 + <path 795 + d="M1 3.5A1.5 1.5 0 0 1 2.5 2h2.764c.958 0 1.76.56 2.311 1.184C7.985 3.648 8.48 4 9 4h4.5A1.5 1.5 0 0 1 15 5.5v7a1.5 1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 12.5v-9zM2.5 3a.5.5 0 0 0-.5.5V6h12v-.5a.5.5 0 0 0-.5-.5H9c-.964 0-1.71-.629-2.174-1.154C6.374 3.334 5.82 3 5.264 3H2.5zM14 7H2v5.5a.5.5 0 0 0 .5.5h11a.5.5 0 0 0 .5-.5V7z" 796 + /> 797 + </svg> 798 + </div> 799 + </td> 800 + <td 801 + style="width: 150px; overflow: hidden;" 802 + > 803 + <a 804 + class="css-1e3wasx" 805 + href="/files?q=/home/tsirysndr/Music/Crazy Town" 806 + > 807 + Crazy Town 808 + </a> 809 + </td> 810 + <td 811 + style="width: 150px; overflow: hidden;" 812 + > 813 + <div 814 + class="css-14zx4vh" 815 + style="justify-content: flex-end; align-items: center;" 816 + > 817 + <button 818 + class="css-10asy2d" 819 + > 820 + <button 821 + class="css-k309af" 822 + > 823 + <svg 824 + aria-hidden="true" 825 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 826 + fill="currentColor" 827 + focusable="false" 828 + height="24" 829 + viewBox="0 0 512 512" 830 + width="24" 831 + xmlns="http://www.w3.org/2000/svg" 832 + > 833 + <circle 834 + cx="256" 835 + cy="256" 836 + r="48" 837 + /> 838 + <circle 839 + cx="416" 840 + cy="256" 841 + r="48" 842 + /> 843 + <circle 844 + cx="96" 845 + cy="256" 846 + r="48" 847 + /> 848 + </svg> 849 + </button> 850 + </button> 851 + </div> 852 + </td> 853 + </tr> 854 + <tr 855 + style="height: 48px;" 856 + > 857 + <td 858 + style="width: 20px; overflow: hidden;" 859 + > 860 + <div 861 + style="display: flex; align-items: center; justify-content: center; margin-left: 10px;" 862 + > 863 + <svg 864 + aria-hidden="true" 865 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 866 + fill="currentColor" 867 + focusable="false" 868 + height="20" 869 + viewBox="0 0 16 16" 870 + width="20" 871 + xmlns="http://www.w3.org/2000/svg" 872 + > 873 + <path 874 + d="M6 13c0 1.105-1.12 2-2.5 2S1 14.105 1 13c0-1.104 1.12-2 2.5-2s2.5.896 2.5 2zm9-2c0 1.105-1.12 2-2.5 2s-2.5-.895-2.5-2 1.12-2 2.5-2 2.5.895 2.5 2z" 875 + /> 876 + <path 877 + d="M14 11V2h1v9h-1zM6 3v10H5V3h1z" 878 + fill-rule="evenodd" 879 + /> 880 + <path 881 + d="M5 2.905a1 1 0 0 1 .9-.995l8-.8a1 1 0 0 1 1.1.995V3L5 4V2.905z" 882 + /> 883 + </svg> 884 + </div> 885 + </td> 886 + <td 887 + style="width: 150px; overflow: hidden;" 888 + > 889 + <div 890 + class="css-xpicpn" 891 + > 892 + Missez - Love Song ft. Pimp C-lIzJ7fmG-JA.mp4 893 + </div> 894 + </td> 895 + <td 896 + style="width: 150px; overflow: hidden;" 897 + > 898 + <div 899 + class="css-14zx4vh" 900 + style="justify-content: flex-end; align-items: center;" 901 + > 902 + <button 903 + class="css-10asy2d" 904 + > 905 + <button 906 + class="css-k309af" 907 + > 908 + <svg 909 + aria-hidden="true" 910 + class="StyledIconBase-sc-ea9ulj-0 dmvaRK" 911 + fill="currentColor" 912 + focusable="false" 913 + height="24" 914 + viewBox="0 0 512 512" 915 + width="24" 916 + xmlns="http://www.w3.org/2000/svg" 917 + > 918 + <circle 919 + cx="256" 920 + cy="256" 921 + r="48" 922 + /> 923 + <circle 924 + cx="416" 925 + cy="256" 926 + r="48" 927 + /> 928 + <circle 929 + cx="96" 930 + cy="256" 931 + r="48" 932 + /> 933 + </svg> 934 + </button> 935 + </button> 936 + </div> 937 + </td> 938 + </tr> 939 + </tbody> 940 + </table> 387 941 </div> 388 942 </div> 389 943 </div>
+7
webui/rockbox/src/Components/Files/mocks.tsx
··· 1 1 export const files = [ 2 2 { 3 3 name: "Big K.R.I.T.", 4 + path: "/home/tsirysndr/Music/Big K.R.I.T.", 4 5 isDirectory: true, 5 6 }, 6 7 { 7 8 name: "BoyWithUke, Oliver Tree", 9 + path: "/home/tsirysndr/Music/BoyWithUke, Oliver Tree", 8 10 isDirectory: true, 9 11 }, 10 12 { 11 13 name: "Cage The Elephant", 14 + path: "/home/tsirysndr/Music/Cage The Elephant", 12 15 isDirectory: true, 13 16 }, 14 17 { 15 18 name: "Cannons", 19 + path: "/home/tsirysndr/Music/Cannons", 16 20 isDirectory: true, 17 21 }, 18 22 { 19 23 name: "Cold War Kids", 24 + path: "/home/tsirysndr/Music/Cold War Kids", 20 25 isDirectory: true, 21 26 }, 22 27 { 23 28 name: "Crazy Town", 29 + path: "/home/tsirysndr/Music/Crazy Town", 24 30 isDirectory: true, 25 31 }, 26 32 { 27 33 name: "Missez - Love Song ft. Pimp C-lIzJ7fmG-JA.mp4", 34 + path: "/home/tsirysndr/Music/Missez - Love Song ft. Pimp C-lIzJ7fmG-JA.mp4", 28 35 isDirectory: false, 29 36 }, 30 37 ];
+27 -2
webui/rockbox/src/Components/Files/styles.tsx
··· 1 1 import styled from "@emotion/styled"; 2 + import { Link } from "react-router-dom"; 2 3 3 4 export const Container = styled.div` 4 5 display: flex; ··· 54 55 55 56 export const ContentWrapper = styled.div` 56 57 overflow-y: auto; 57 - height: calc(100vh - 60px); 58 + height: calc(100vh - 100px); 58 59 padding-left: 20px; 59 60 padding-right: 20px; 60 61 `; ··· 64 65 width: 48px; 65 66 `; 66 67 67 - export const Directory = styled.a` 68 + export const Directory = styled(Link)` 68 69 color: #000; 70 + margin-left: 10px; 69 71 text-decoration: none; 70 72 font-family: RockfordSansRegular; 73 + width: calc(100vw - 500px); 74 + max-width: calc(100vw - 500px); 75 + text-overflow: ellipsis; 76 + overflow: hidden; 77 + white-space: nowrap; 78 + display: block; 79 + &:hover { 80 + text-decoration: underline; 81 + } 82 + `; 83 + 84 + export const AudioFile = styled.div` 85 + color: #000; 86 + margin-left: 10px; 87 + text-decoration: none; 88 + font-family: RockfordSansRegular; 89 + width: calc(100vw - 500px); 90 + max-width: calc(100vw - 500px); 91 + text-overflow: ellipsis; 92 + overflow: hidden; 93 + white-space: nowrap; 94 + display: block; 95 + cursor: pointer; 71 96 &:hover { 72 97 text-decoration: underline; 73 98 }
+2 -1
webui/rockbox/src/GraphQL/Browse/Query.tsx
··· 1 1 import { gql } from "@apollo/client"; 2 2 3 3 export const GET_ENTRIES = gql` 4 - query GetEntries($path: String!) { 4 + query GetEntries($path: String) { 5 5 treeGetEntries(path: $path) { 6 6 name 7 + attr 7 8 timeWrite 8 9 } 9 10 }
+6 -5
webui/rockbox/src/Hooks/GraphQL.tsx
··· 185 185 186 186 187 187 export type QueryTreeGetEntriesArgs = { 188 - path: Scalars['String']['input']; 188 + path?: InputMaybe<Scalars['String']['input']>; 189 189 }; 190 190 191 191 export type ReplaygainSettings = { ··· 441 441 }; 442 442 443 443 export type GetEntriesQueryVariables = Exact<{ 444 - path: Scalars['String']['input']; 444 + path?: InputMaybe<Scalars['String']['input']>; 445 445 }>; 446 446 447 447 448 - export type GetEntriesQuery = { __typename?: 'Query', treeGetEntries: Array<{ __typename?: 'Entry', name: string, timeWrite: number }> }; 448 + export type GetEntriesQuery = { __typename?: 'Query', treeGetEntries: Array<{ __typename?: 'Entry', name: string, attr: number, timeWrite: number }> }; 449 449 450 450 export type GetAlbumsQueryVariables = Exact<{ [key: string]: never; }>; 451 451 ··· 483 483 484 484 485 485 export const GetEntriesDocument = gql` 486 - query GetEntries($path: String!) { 486 + query GetEntries($path: String) { 487 487 treeGetEntries(path: $path) { 488 488 name 489 + attr 489 490 timeWrite 490 491 } 491 492 } ··· 507 508 * }, 508 509 * }); 509 510 */ 510 - export function useGetEntriesQuery(baseOptions: Apollo.QueryHookOptions<GetEntriesQuery, GetEntriesQueryVariables> & ({ variables: GetEntriesQueryVariables; skip?: boolean; } | { skip: boolean; }) ) { 511 + export function useGetEntriesQuery(baseOptions?: Apollo.QueryHookOptions<GetEntriesQuery, GetEntriesQueryVariables>) { 511 512 const options = {...defaultOptions, ...baseOptions} 512 513 return Apollo.useQuery<GetEntriesQuery, GetEntriesQueryVariables>(GetEntriesDocument, options); 513 514 }
+1
webui/rockbox/src/Types/file.ts
··· 1 1 export type File = { 2 2 name: string; 3 + path: string; 3 4 isDirectory: boolean; 4 5 };