mirror of
https://github.com/tsirysndr/music-player.git
synced 2026-01-09 13:18:05 -05:00
feat(webui): implement search
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3114,6 +3114,7 @@ name = "music-player-storage"
|
||||
version = "0.1.1"
|
||||
dependencies = [
|
||||
"itertools",
|
||||
"md5",
|
||||
"music-player-settings",
|
||||
"music-player-types",
|
||||
"sea-orm",
|
||||
|
||||
@@ -26,6 +26,9 @@ pub struct Track {
|
||||
pub album: Album,
|
||||
pub artist: String,
|
||||
pub cover: Option<String>,
|
||||
pub artist_id: String,
|
||||
pub album_id: String,
|
||||
pub album_title: String,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
@@ -69,6 +72,18 @@ impl Track {
|
||||
async fn cover(&self) -> Option<String> {
|
||||
self.cover.clone()
|
||||
}
|
||||
|
||||
async fn artist_id(&self) -> &str {
|
||||
&self.artist_id
|
||||
}
|
||||
|
||||
async fn album_id(&self) -> &str {
|
||||
&self.album_id
|
||||
}
|
||||
|
||||
async fn album_title(&self) -> &str {
|
||||
&self.album_title
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Model> for Track {
|
||||
@@ -108,6 +123,9 @@ impl From<TrackType> for Track {
|
||||
artist: song.artist,
|
||||
duration: Some(song.duration.as_secs_f32()),
|
||||
cover: song.cover,
|
||||
artist_id: song.artist_id,
|
||||
album_id: song.album_id,
|
||||
album_title: song.album,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,3 +21,4 @@ itertools = "0.10.5"
|
||||
sea-orm = { version = "0.9.2", features = ["runtime-tokio-rustls", "sqlx-sqlite"] }
|
||||
tantivy = "0.18.0"
|
||||
tempfile = "3.3.0"
|
||||
md5 = "0.7.0"
|
||||
|
||||
@@ -29,6 +29,8 @@ impl TrackSearcher {
|
||||
schema_builder.add_text_field("genre", TEXT);
|
||||
schema_builder.add_text_field("cover", STRING | STORED);
|
||||
schema_builder.add_i64_field("duration", STORED);
|
||||
schema_builder.add_text_field("artistId", STRING | STORED);
|
||||
schema_builder.add_text_field("albumId", STRING | STORED);
|
||||
|
||||
let schema: Schema = schema_builder.build();
|
||||
let dir = MmapDirectory::open(&index_path).unwrap();
|
||||
@@ -64,6 +66,8 @@ impl TrackSearcher {
|
||||
let genre = self.schema.get_field("genre").unwrap();
|
||||
let cover = self.schema.get_field("cover").unwrap();
|
||||
let duration = self.schema.get_field("duration").unwrap();
|
||||
let artist_id = self.schema.get_field("artistId").unwrap();
|
||||
let album_id = self.schema.get_field("albumId").unwrap();
|
||||
|
||||
let time = song.duration.as_secs_f32() as i64;
|
||||
|
||||
@@ -75,6 +79,8 @@ impl TrackSearcher {
|
||||
genre => song.genre.clone(),
|
||||
cover => song.cover.unwrap_or_default().clone(),
|
||||
duration => time,
|
||||
artist_id => format!("{:x}", md5::compute(song.album_artist.to_owned())),
|
||||
album_id => format!("{:x}", md5::compute(song.album.to_owned()))
|
||||
);
|
||||
|
||||
index_writer.add_document(doc)?;
|
||||
|
||||
@@ -33,6 +33,8 @@ pub struct SimplifiedSong {
|
||||
pub genre: String,
|
||||
pub duration: Duration,
|
||||
pub cover: Option<String>,
|
||||
pub artist_id: String,
|
||||
pub album_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -82,9 +84,15 @@ impl From<Document> for Album {
|
||||
.to_string();
|
||||
let year = Some(doc.get_first(year_field).unwrap().as_i64().unwrap() as u32);
|
||||
let cover = match doc.get_first(cover_field) {
|
||||
Some(cover) => Some(cover.as_text().unwrap().to_string()),
|
||||
Some(cover) => cover.as_text(),
|
||||
None => None,
|
||||
};
|
||||
let cover = match cover {
|
||||
Some("") => None,
|
||||
Some(cover) => Some(cover.to_string()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
@@ -133,8 +141,10 @@ impl From<Document> for SimplifiedSong {
|
||||
let artist_field = schema_builder.add_text_field("artist", TEXT | STORED);
|
||||
let album_field = schema_builder.add_text_field("album", TEXT | STORED);
|
||||
let genre_field = schema_builder.add_text_field("genre", TEXT);
|
||||
let duration_field = schema_builder.add_i64_field("duration", STORED);
|
||||
let cover_field = schema_builder.add_text_field("cover", STRING | STORED);
|
||||
let duration_field = schema_builder.add_i64_field("duration", STORED);
|
||||
let artist_id_field = schema_builder.add_text_field("artist_id", STRING | STORED);
|
||||
let album_id_field = schema_builder.add_text_field("album_id", STRING | STORED);
|
||||
|
||||
let id = doc
|
||||
.get_first(id_field)
|
||||
@@ -164,9 +174,26 @@ impl From<Document> for SimplifiedSong {
|
||||
None => Duration::from_secs(0),
|
||||
};
|
||||
let cover = match doc.get_first(cover_field) {
|
||||
Some(cover) => Some(cover.as_text().unwrap_or_default().to_string()),
|
||||
Some(cover) => cover.as_text(),
|
||||
None => None,
|
||||
};
|
||||
let cover = match cover {
|
||||
Some("") => None,
|
||||
Some(cover) => Some(cover.to_string()),
|
||||
None => None,
|
||||
};
|
||||
let artist_id = doc
|
||||
.get_first(artist_id_field)
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
let album_id = doc
|
||||
.get_first(album_id_field)
|
||||
.unwrap()
|
||||
.as_text()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
Self {
|
||||
id,
|
||||
title,
|
||||
@@ -175,6 +202,8 @@ impl From<Document> for SimplifiedSong {
|
||||
genre,
|
||||
duration,
|
||||
cover,
|
||||
artist_id,
|
||||
album_id,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"files": {
|
||||
"main.css": "/static/css/main.6627e378.css",
|
||||
"main.js": "/static/js/main.b5171ddd.js",
|
||||
"main.css": "/static/css/main.56b69c4b.css",
|
||||
"main.js": "/static/js/main.1a39bb8b.js",
|
||||
"static/js/787.26bf0a29.chunk.js": "/static/js/787.26bf0a29.chunk.js",
|
||||
"static/media/RockfordSans-ExtraBold.otf": "/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf",
|
||||
"static/media/RockfordSans-Bold.otf": "/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf",
|
||||
@@ -9,12 +9,12 @@
|
||||
"static/media/RockfordSans-Regular.otf": "/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf",
|
||||
"static/media/RockfordSans-Light.otf": "/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf",
|
||||
"index.html": "/index.html",
|
||||
"main.6627e378.css.map": "/static/css/main.6627e378.css.map",
|
||||
"main.b5171ddd.js.map": "/static/js/main.b5171ddd.js.map",
|
||||
"main.56b69c4b.css.map": "/static/css/main.56b69c4b.css.map",
|
||||
"main.1a39bb8b.js.map": "/static/js/main.1a39bb8b.js.map",
|
||||
"787.26bf0a29.chunk.js.map": "/static/js/787.26bf0a29.chunk.js.map"
|
||||
},
|
||||
"entrypoints": [
|
||||
"static/css/main.6627e378.css",
|
||||
"static/js/main.b5171ddd.js"
|
||||
"static/css/main.56b69c4b.css",
|
||||
"static/js/main.1a39bb8b.js"
|
||||
]
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Music Player</title><script defer="defer" src="/static/js/main.b5171ddd.js"></script><link href="/static/css/main.6627e378.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site created using create-react-app"/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>Music Player</title><script defer="defer" src="/static/js/main.1a39bb8b.js"></script><link href="/static/css/main.56b69c4b.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
|
||||
@@ -1,2 +1,2 @@
|
||||
@font-face{font-family:RockfordSansLight;src:local("RockfordSansLight"),url(/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf) format("opentype")}@font-face{font-family:RockfordSansRegular;src:local("RockfordSans"),url(/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf) format("opentype")}@font-face{font-family:RockfordSansMedium;src:local("RockfordSans"),url(/static/media/RockfordSans-Medium.e10344a796535b513215.otf) format("opentype")}@font-face{font-family:RockfordSansBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf) format("opentype")}@font-face{font-family:RockfordSansExtraBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf) format("opentype")}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:RockfordSansRegular,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0;overflow-y:hidden}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}[data-baseweb=modal]{z-index:1}:focus{outline:none!important}.play{display:none}.floating-play{display:none;left:40px;position:absolute}tr:hover td div .floating-play,tr:hover td div .play{cursor:pointer;display:block}tr:hover td div .tracknumber{display:none}tr:hover td div .album-cover{opacity:.4}tr:hover td div button{color:#000}tr td div div{overflow:hidden;text-overflow:ellipsis}a{color:#000;color:initial;text-decoration:none;text-decoration:initial}a:hover{text-decoration:underline}.album-cover-container{position:relative}.album-cover-container .floating-play{display:none;left:19px;position:absolute;top:12px}.album-cover-container:hover .floating-play{display:block}.album-cover-container:hover img{opacity:.4}
|
||||
/*# sourceMappingURL=main.6627e378.css.map*/
|
||||
@font-face{font-family:RockfordSansLight;src:local("RockfordSansLight"),url(/static/media/RockfordSans-Light.b4a12e8abb38f7d105c4.otf) format("opentype")}@font-face{font-family:RockfordSansRegular;src:local("RockfordSans"),url(/static/media/RockfordSans-Regular.652654f28f1c111914b9.otf) format("opentype")}@font-face{font-family:RockfordSansMedium;src:local("RockfordSans"),url(/static/media/RockfordSans-Medium.e10344a796535b513215.otf) format("opentype")}@font-face{font-family:RockfordSansBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-Bold.c9f599ae01b13e565598.otf) format("opentype")}@font-face{font-family:RockfordSansExtraBold;font-weight:900;src:local("RockfordSans"),url(/static/media/RockfordSans-ExtraBold.1513e8fd97078bfb7708.otf) format("opentype")}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;font-family:RockfordSansRegular,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;margin:0;overflow-y:hidden}code{font-family:source-code-pro,Menlo,Monaco,Consolas,Courier New,monospace}[data-baseweb=modal]{z-index:1}:focus{outline:none!important}.play{display:none}.floating-play{display:none;left:40px;position:absolute}tr:hover td div .floating-play,tr:hover td div .play{cursor:pointer;display:block}tr:hover td div .tracknumber{display:none}tr:hover td div .album-cover{opacity:.4}tr:hover td div button{color:#000}tr td div div{overflow:hidden;text-overflow:ellipsis}a{color:#000;color:initial;text-decoration:none;text-decoration:initial}a:hover{text-decoration:underline}.album-cover-container{position:relative}.album-cover-container .floating-play{display:none;left:19px;position:absolute;top:12px}.album-cover-container:hover .floating-play{display:block}.album-cover-container:hover img{opacity:.4}[data-baseweb=tab-panel]{padding-left:0!important;padding-right:0!important}
|
||||
/*# sourceMappingURL=main.56b69c4b.css.map*/
|
||||
1
webui/musicplayer/build/static/css/main.56b69c4b.css.map
Normal file
1
webui/musicplayer/build/static/css/main.56b69c4b.css.map
Normal file
@@ -0,0 +1 @@
|
||||
{"version":3,"file":"static/css/main.56b69c4b.css","mappings":"AACA,WACE,6BAAgC,CAChC,gHACF,CAEA,WACE,+BAAkC,CAClC,6GACF,CAEA,WACE,8BAAiC,CACjC,4GACF,CAEA,WACE,4BAA+B,CAC7B,eAAgB,CAChB,0GACJ,CAEA,WACE,iCAAoC,CAClC,eAAgB,CAChB,+GACJ,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,uJAEY,CAHZ,QAAS,CAMT,iBACF,CAEA,KACE,uEAEF,CAEA,qBACE,SACF,CAEA,OAAS,sBAAuB,CAEhC,MACE,YACF,CAEA,eACE,YAAa,CAEb,SAAU,CADV,iBAEF,CAOA,qDACE,cAAe,CACf,aACF,CAGA,6BACC,YACD,CAEA,6BACE,UACD,CAED,uBACE,UACD,CAEA,cACC,eAAgB,CAChB,sBACD,CAEA,EACG,UAAc,CAAd,aAAc,CACd,oBAAwB,CAAxB,uBACH,CAEA,QACC,yBACD,CAEA,uBACC,iBACD,CAEA,sCACC,YAAa,CAEb,SAAU,CADV,iBAAkB,CAElB,QACD,CAEA,4CACC,aACD,CAEA,iCACC,UACD,CAEA,yBACC,wBAA0B,CAC1B,yBACD","sources":["index.css"],"sourcesContent":["\n@font-face {\n font-family: 'RockfordSansLight';\n src: local('RockfordSansLight'), url(./Assets/fonts/RockfordSans-Light.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansRegular';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Regular.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansMedium';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Medium.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Bold.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansExtraBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-ExtraBold.otf) format('opentype');\n}\n\nbody {\n margin: 0;\n font-family: RockfordSansRegular, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overflow-y: hidden;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n[data-baseweb=\"modal\"] {\n z-index: 1;\n}\n\n*:focus {outline:none !important}\n\n.play {\n display: none;\n}\n\n.floating-play {\n display: none;\n position: absolute;\n left: 40px;\n}\n\ntr:hover td div .play {\n cursor: pointer;\n display: block;\n}\n\ntr:hover td div .floating-play {\n cursor: pointer;\n display: block;\n}\n\n\ntr:hover td div .tracknumber {\n display: none;\n}\n\ntr:hover td div .album-cover {\n opacity: 0.4;\n }\n\ntr:hover td div button {\n color: #000;\n }\n\n tr td div div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n a {\n color: initial;\n text-decoration: initial;\n }\n\n a:hover {\n text-decoration: underline;\n }\n\n .album-cover-container {\n position: relative;\n }\n\n .album-cover-container .floating-play {\n display: none;\n position: absolute;\n left: 19px;\n top: 12px;\n }\n\n .album-cover-container:hover .floating-play {\n display: block;\n }\n\n .album-cover-container:hover img {\n opacity: 0.4;\n }\n\n [data-baseweb=\"tab-panel\"] {\n padding-left: 0 !important;\n padding-right: 0 !important;\n }"],"names":[],"sourceRoot":""}
|
||||
@@ -1 +0,0 @@
|
||||
{"version":3,"file":"static/css/main.6627e378.css","mappings":"AACA,WACE,6BAAgC,CAChC,gHACF,CAEA,WACE,+BAAkC,CAClC,6GACF,CAEA,WACE,8BAAiC,CACjC,4GACF,CAEA,WACE,4BAA+B,CAC7B,eAAgB,CAChB,0GACJ,CAEA,WACE,iCAAoC,CAClC,eAAgB,CAChB,+GACJ,CAEA,KAKE,kCAAmC,CACnC,iCAAkC,CAJlC,uJAEY,CAHZ,QAAS,CAMT,iBACF,CAEA,KACE,uEAEF,CAEA,qBACE,SACF,CAEA,OAAS,sBAAuB,CAEhC,MACE,YACF,CAEA,eACE,YAAa,CAEb,SAAU,CADV,iBAEF,CAOA,qDACE,cAAe,CACf,aACF,CAGA,6BACC,YACD,CAEA,6BACE,UACD,CAED,uBACE,UACD,CAEA,cACC,eAAgB,CAChB,sBACD,CAEA,EACG,UAAc,CAAd,aAAc,CACd,oBAAwB,CAAxB,uBACH,CAEA,QACC,yBACD,CAEA,uBACC,iBACD,CAEA,sCACC,YAAa,CAEb,SAAU,CADV,iBAAkB,CAElB,QACD,CAEA,4CACC,aACD,CAEA,iCACC,UACD","sources":["index.css"],"sourcesContent":["\n@font-face {\n font-family: 'RockfordSansLight';\n src: local('RockfordSansLight'), url(./Assets/fonts/RockfordSans-Light.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansRegular';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Regular.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansMedium';\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Medium.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-Bold.otf) format('opentype');\n}\n\n@font-face {\n font-family: 'RockfordSansExtraBold';\n font-weight: 900;\n src: local('RockfordSans'), url(./Assets/fonts/RockfordSans-ExtraBold.otf) format('opentype');\n}\n\nbody {\n margin: 0;\n font-family: RockfordSansRegular, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',\n 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',\n sans-serif;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n overflow-y: hidden;\n}\n\ncode {\n font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',\n monospace;\n}\n\n[data-baseweb=\"modal\"] {\n z-index: 1;\n}\n\n*:focus {outline:none !important}\n\n.play {\n display: none;\n}\n\n.floating-play {\n display: none;\n position: absolute;\n left: 40px;\n}\n\ntr:hover td div .play {\n cursor: pointer;\n display: block;\n}\n\ntr:hover td div .floating-play {\n cursor: pointer;\n display: block;\n}\n\n\ntr:hover td div .tracknumber {\n display: none;\n}\n\ntr:hover td div .album-cover {\n opacity: 0.4;\n }\n\ntr:hover td div button {\n color: #000;\n }\n\n tr td div div {\n overflow: hidden;\n text-overflow: ellipsis;\n }\n\n a {\n color: initial;\n text-decoration: initial;\n }\n\n a:hover {\n text-decoration: underline;\n }\n\n .album-cover-container {\n position: relative;\n }\n\n .album-cover-container .floating-play {\n display: none;\n position: absolute;\n left: 19px;\n top: 12px;\n }\n\n .album-cover-container:hover .floating-play {\n display: block;\n }\n\n .album-cover-container:hover img {\n opacity: 0.4;\n }"],"names":[],"sourceRoot":""}
|
||||
3
webui/musicplayer/build/static/js/main.1a39bb8b.js
Normal file
3
webui/musicplayer/build/static/js/main.1a39bb8b.js
Normal file
File diff suppressed because one or more lines are too long
1
webui/musicplayer/build/static/js/main.1a39bb8b.js.map
Normal file
1
webui/musicplayer/build/static/js/main.1a39bb8b.js.map
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1626,13 +1626,30 @@
|
||||
{
|
||||
"name": "search",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"args": [
|
||||
{
|
||||
"name": "keyword",
|
||||
"description": null,
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"defaultValue": null,
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "Boolean",
|
||||
"kind": "OBJECT",
|
||||
"name": "SearchResult",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
@@ -1718,6 +1735,89 @@
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "OBJECT",
|
||||
"name": "SearchResult",
|
||||
"description": null,
|
||||
"fields": [
|
||||
{
|
||||
"name": "albums",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Album",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "artists",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Artist",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "tracks",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "LIST",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "OBJECT",
|
||||
"name": "Track",
|
||||
"ofType": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
}
|
||||
],
|
||||
"inputFields": null,
|
||||
"interfaces": [],
|
||||
"enumValues": null,
|
||||
"possibleTypes": null
|
||||
},
|
||||
{
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
@@ -1749,6 +1849,38 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "albumId",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "albumTitle",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "artist",
|
||||
"description": null,
|
||||
@@ -1765,6 +1897,22 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "artistId",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "NON_NULL",
|
||||
"name": null,
|
||||
"ofType": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
}
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "artists",
|
||||
"description": null,
|
||||
@@ -1789,6 +1937,18 @@
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "cover",
|
||||
"description": null,
|
||||
"args": [],
|
||||
"type": {
|
||||
"kind": "SCALAR",
|
||||
"name": "String",
|
||||
"ofType": null
|
||||
},
|
||||
"isDeprecated": false,
|
||||
"deprecationReason": null
|
||||
},
|
||||
{
|
||||
"name": "discNumber",
|
||||
"description": null,
|
||||
|
||||
@@ -4,6 +4,7 @@ import ArtistsPage from "./Containers/Artists";
|
||||
import TracksPage from "./Containers/Tracks";
|
||||
import AlbumDetailsPage from "./Containers/AlbumDetails";
|
||||
import ArtistDetailsPage from "./Containers/ArtistDetails";
|
||||
import SearchPage from "./Containers/Search";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -15,6 +16,7 @@ function App() {
|
||||
<Route path="/albums" element={<AlbumsPage />} />
|
||||
<Route path="/albums/:id" element={<AlbumDetailsPage />} />
|
||||
<Route path="/artists/:id" element={<ArtistDetailsPage />} />
|
||||
<Route path="/search" element={<SearchPage />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
@@ -139,24 +139,18 @@ export type AlbumDetailsProps = {
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const AlbumDetails: FC<AlbumDetailsProps> = (props) => {
|
||||
const {
|
||||
onBack,
|
||||
onClickLibraryItem,
|
||||
album,
|
||||
nowPlaying,
|
||||
onPlayAlbum,
|
||||
onPlayNext,
|
||||
} = props;
|
||||
const { onBack, album, nowPlaying, onPlayAlbum, onPlayNext } = props;
|
||||
const coverUrl = _.startsWith(album.cover, "https://")
|
||||
? album.cover
|
||||
: `/covers/${album.cover}`;
|
||||
const { cover } = useCover(coverUrl);
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active="albums" onClickLibraryItem={onClickLibraryItem} />
|
||||
<Sidebar active="albums" {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<MainContent displayHeader={false}>
|
||||
|
||||
@@ -81,6 +81,7 @@ export type AlbumsProps = {
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
export type AlbumProps = {
|
||||
@@ -105,10 +106,10 @@ const Album: FC<AlbumProps> = ({ onClick, album }) => {
|
||||
};
|
||||
|
||||
const Albums: FC<AlbumsProps> = (props) => {
|
||||
const { albums, onClickAlbum, onClickLibraryItem } = props;
|
||||
const { albums, onClickAlbum } = props;
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active="albums" onClickLibraryItem={onClickLibraryItem} />
|
||||
<Sidebar active="albums" {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<Scrollable>
|
||||
|
||||
@@ -160,21 +160,15 @@ export type ArtistDetailsProps = {
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const ArtistDetails: FC<ArtistDetailsProps> = (props) => {
|
||||
const {
|
||||
onBack,
|
||||
onClickLibraryItem,
|
||||
artist,
|
||||
tracks,
|
||||
albums,
|
||||
onPlayArtistTracks,
|
||||
onPlayNext,
|
||||
} = props;
|
||||
const { onBack, artist, tracks, albums, onPlayArtistTracks, onPlayNext } =
|
||||
props;
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active="artists" onClickLibraryItem={onClickLibraryItem} />
|
||||
<Sidebar active="artists" {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<MainContent displayHeader={false}>
|
||||
|
||||
@@ -73,13 +73,14 @@ export type ArtistsProps = {
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const Artists: FC<ArtistsProps> = (props) => {
|
||||
const { onClickLibraryItem, onClickArtist, artists } = props;
|
||||
const { onClickArtist, artists } = props;
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active="artists" onClickLibraryItem={onClickLibraryItem} />
|
||||
<Sidebar active="artists" {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<Scrollable>
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
import { Input } from "baseui/input";
|
||||
import { FC } from "react";
|
||||
|
||||
const Search = () => {
|
||||
export type SearchProps = {
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const Search: FC<SearchProps> = ({ onSearch }) => {
|
||||
const handleKeyPress = (
|
||||
event: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
|
||||
) => {
|
||||
if (event.key === "Enter") {
|
||||
onSearch(event.currentTarget.value);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Search"
|
||||
onKeyPress={handleKeyPress}
|
||||
overrides={{
|
||||
Root: {
|
||||
style: {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { Cell, Grid } from "baseui/layout-grid";
|
||||
import { FC } from "react";
|
||||
import { useCover } from "../../../Hooks/useCover";
|
||||
import AlbumIcon from "../../Icons/AlbumCover";
|
||||
|
||||
const AlbumCover = styled.img`
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const NoAlbumCover = styled.div`
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #ddaefb14;
|
||||
`;
|
||||
|
||||
const Artist = styled.div`
|
||||
color: #828282;
|
||||
margin-bottom: 56px;
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Title = styled.div`
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-top: 34px;
|
||||
`;
|
||||
|
||||
export type AlbumProps = {
|
||||
onClick: (item: any) => void;
|
||||
album: any;
|
||||
};
|
||||
|
||||
const Album: FC<AlbumProps> = ({ onClick, album }) => {
|
||||
const { cover } = useCover(album.cover);
|
||||
return (
|
||||
<Wrapper>
|
||||
{cover && <AlbumCover src={cover} onClick={() => onClick(album)} />}
|
||||
{!cover && (
|
||||
<NoAlbumCover onClick={() => onClick(album)}>
|
||||
<AlbumIcon />
|
||||
</NoAlbumCover>
|
||||
)}
|
||||
<Title onClick={() => onClick(album)}>{album.title}</Title>
|
||||
<Artist>{album.artist}</Artist>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export type AlbumsProps = {
|
||||
albums: any[];
|
||||
onClickAlbum: (album: any) => void;
|
||||
};
|
||||
|
||||
const Albums: FC<AlbumsProps> = (props) => {
|
||||
const { albums, onClickAlbum } = props;
|
||||
return (
|
||||
<>
|
||||
<Grid gridColumns={[2, 3, 4, 6]} gridMargins={[8, 16, 18]}>
|
||||
{albums.map((item) => (
|
||||
<Cell key={item.id}>
|
||||
<Album onClick={onClickAlbum} album={item} />
|
||||
</Cell>
|
||||
))}
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Albums;
|
||||
@@ -0,0 +1,71 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { Cell, Grid } from "baseui/layout-grid";
|
||||
import { FC } from "react";
|
||||
import Artist from "../../Icons/Artist";
|
||||
|
||||
const ArtistCover = styled.img`
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
border-radius: 110px;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
margin-top: 34px;
|
||||
`;
|
||||
|
||||
const NoArtistCover = styled.div`
|
||||
height: 220px;
|
||||
width: 220px;
|
||||
border-radius: 110px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #f3f3f3b9;
|
||||
`;
|
||||
|
||||
const ArtistName = styled.div`
|
||||
font-size: 14px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 18px;
|
||||
text-align: center;
|
||||
width: 220px;
|
||||
`;
|
||||
|
||||
export type ArtistsProps = {
|
||||
artists: any[];
|
||||
onClickArtist: (artist: any) => void;
|
||||
};
|
||||
|
||||
const Artists: FC<ArtistsProps> = (props) => {
|
||||
const { artists, onClickArtist } = props;
|
||||
return (
|
||||
<Wrapper>
|
||||
<Grid gridColumns={[2, 3, 4]} gridMargins={[8, 16, 18]}>
|
||||
{artists.map((item) => (
|
||||
<Cell key={item.id}>
|
||||
{item.cover && (
|
||||
<ArtistCover
|
||||
src={item.cover}
|
||||
onClick={() => onClickArtist(item)}
|
||||
/>
|
||||
)}
|
||||
{!item.cover && (
|
||||
<NoArtistCover onClick={() => onClickArtist(item)}>
|
||||
<Artist width={75} height={75} color="#a4a3a3" />
|
||||
</NoArtistCover>
|
||||
)}
|
||||
<ArtistName>{item.name}</ArtistName>
|
||||
</Cell>
|
||||
))}
|
||||
</Grid>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default Artists;
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FC } from "react";
|
||||
|
||||
export type PlaylistsProps = {};
|
||||
|
||||
const Playlists: FC<PlaylistsProps> = (props) => {
|
||||
return <></>;
|
||||
};
|
||||
|
||||
export default Playlists;
|
||||
103
webui/musicplayer/src/Components/SearchResults/SearchResults.tsx
Normal file
103
webui/musicplayer/src/Components/SearchResults/SearchResults.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { FC, useState } from "react";
|
||||
import { Track } from "../../Types";
|
||||
import ControlBar from "../ControlBar";
|
||||
import Sidebar from "../Sidebar";
|
||||
import { Tabs, Tab } from "baseui/tabs-motion";
|
||||
import Tracks from "./Tracks";
|
||||
import Albums from "./Albums";
|
||||
import Artists from "./Artists";
|
||||
import Playlists from "./Playlists";
|
||||
|
||||
const Container = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
`;
|
||||
|
||||
const Results = styled.div`
|
||||
overflow-y: auto;
|
||||
height: calc(100vh - 146px);
|
||||
`;
|
||||
|
||||
export type SearchResultsProps = {
|
||||
tracks: any[];
|
||||
albums: any[];
|
||||
artists: any[];
|
||||
onClickAlbum: (album: any) => void;
|
||||
onClickArtist: (artist: any) => void;
|
||||
onClickLibraryItem: (item: string) => void;
|
||||
onPlay: () => void;
|
||||
onPause: () => void;
|
||||
onNext: () => void;
|
||||
onPrevious: () => void;
|
||||
onShuffle: () => void;
|
||||
onRepeat: () => void;
|
||||
nowPlaying: any;
|
||||
onPlayTrack: (id: string, postion?: number) => void;
|
||||
nextTracks: Track[];
|
||||
previousTracks: Track[];
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const SearchResults: FC<SearchResultsProps> = (props) => {
|
||||
const [activeKey, setActiveKey] = useState<React.Key>(0);
|
||||
const { tracks, nowPlaying, onPlayTrack, onPlayNext } = props;
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active={""} {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<div>
|
||||
<Tabs
|
||||
activeKey={activeKey}
|
||||
onChange={({ activeKey }) => setActiveKey(activeKey)}
|
||||
overrides={{
|
||||
TabList: {
|
||||
style: {
|
||||
marginLeft: "26px",
|
||||
marginRight: "26px",
|
||||
},
|
||||
},
|
||||
TabBorder: {
|
||||
style: {
|
||||
marginLeft: "26px",
|
||||
marginRight: "26px",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab title="Tracks">
|
||||
<Results>
|
||||
<Tracks {...props} />
|
||||
</Results>
|
||||
</Tab>
|
||||
<Tab title="Albums">
|
||||
<Results>
|
||||
<Albums {...props} />
|
||||
</Results>
|
||||
</Tab>
|
||||
<Tab title="Artists">
|
||||
<Results>
|
||||
<Artists {...props} />
|
||||
</Results>
|
||||
</Tab>
|
||||
<Tab title="Playlists">
|
||||
<Playlists />
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
</Content>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchResults;
|
||||
@@ -0,0 +1,30 @@
|
||||
import styled from "@emotion/styled";
|
||||
import { FC } from "react";
|
||||
import TracksTable from "../../TracksTable";
|
||||
|
||||
export type TracksProps = {
|
||||
tracks: any[];
|
||||
nowPlaying: any;
|
||||
onPlayTrack: (id: string, postion?: number) => void;
|
||||
onPlayNext: (id: string) => void;
|
||||
};
|
||||
|
||||
const Tracks: FC<TracksProps> = (props) => {
|
||||
const { tracks, nowPlaying, onPlayTrack, onPlayNext } = props;
|
||||
return (
|
||||
<TracksTable
|
||||
tracks={tracks}
|
||||
currentTrackId={nowPlaying.id}
|
||||
isPlaying={nowPlaying.isPlaying}
|
||||
onPlayTrack={onPlayTrack}
|
||||
onPlayNext={onPlayNext}
|
||||
maxHeight="initial"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Tracks.defaultProps = {
|
||||
tracks: [],
|
||||
};
|
||||
|
||||
export default Tracks;
|
||||
3
webui/musicplayer/src/Components/SearchResults/index.tsx
Normal file
3
webui/musicplayer/src/Components/SearchResults/index.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
import SearchResults from "./SearchResults";
|
||||
|
||||
export default SearchResults;
|
||||
@@ -16,12 +16,13 @@ const Container = styled.div`
|
||||
export type SidebarProps = {
|
||||
active?: string;
|
||||
onClickLibraryItem: (item: string) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const Sidebar: FC<SidebarProps> = (props) => {
|
||||
return (
|
||||
<Container>
|
||||
<Search />
|
||||
<Search {...props} />
|
||||
<Library {...props} />
|
||||
<Playlists />
|
||||
</Container>
|
||||
|
||||
@@ -33,14 +33,14 @@ export type TracksProps = {
|
||||
onPlayNext: (id: string) => void;
|
||||
onPlayTrackAt: (position: number) => void;
|
||||
onRemoveTrackAt: (position: number) => void;
|
||||
onSearch: (query: string) => void;
|
||||
};
|
||||
|
||||
const Tracks: FC<TracksProps> = (props) => {
|
||||
const { onClickLibraryItem, tracks, nowPlaying, onPlayTrack, onPlayNext } =
|
||||
props;
|
||||
const { tracks, nowPlaying, onPlayTrack, onPlayNext } = props;
|
||||
return (
|
||||
<Container>
|
||||
<Sidebar active="tracks" onClickLibraryItem={onClickLibraryItem} />
|
||||
<Sidebar active="tracks" {...props} />
|
||||
<Content>
|
||||
<ControlBar {...props} />
|
||||
<MainContent title="Tracks">
|
||||
|
||||
@@ -4,6 +4,7 @@ import AlbumDetails from "../../Components/AlbumDetails";
|
||||
import { useGetAlbumQuery } from "../../Hooks/GraphQL";
|
||||
import { useTimeFormat } from "../../Hooks/useFormat";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const AlbumDetailsPage = () => {
|
||||
const params = useParams();
|
||||
@@ -33,6 +34,7 @@ const AlbumDetailsPage = () => {
|
||||
playTrackAt,
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const { onSearch } = useSearch();
|
||||
const album =
|
||||
!loading && data
|
||||
? {
|
||||
@@ -70,6 +72,7 @@ const AlbumDetailsPage = () => {
|
||||
onPlayNext={(trackId) => playNext({ variables: { trackId } })}
|
||||
onPlayTrackAt={(position) => playTrackAt({ variables: { position } })}
|
||||
onRemoveTrackAt={(position) => removeTrackAt({ variables: { position } })}
|
||||
onSearch={(query) => navigate(`/search?q=${query}`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import Albums from "../../Components/Albums";
|
||||
import { useGetAlbumsQuery } from "../../Hooks/GraphQL";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const AlbumsPage = () => {
|
||||
const { data, loading } = useGetAlbumsQuery({
|
||||
@@ -20,6 +21,7 @@ const AlbumsPage = () => {
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const navigate = useNavigate();
|
||||
const { onSearch } = useSearch();
|
||||
const albums = !loading && data ? data.albums : [];
|
||||
return (
|
||||
<Albums
|
||||
@@ -43,6 +45,7 @@ const AlbumsPage = () => {
|
||||
onPlayNext={(trackId) => playNext({ variables: { trackId } })}
|
||||
onPlayTrackAt={(position) => playTrackAt({ variables: { position } })}
|
||||
onRemoveTrackAt={(position) => removeTrackAt({ variables: { position } })}
|
||||
onSearch={(query) => navigate(`/search?q=${query}`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import ArtistDetails from "../../Components/ArtistDetails";
|
||||
import { useGetArtistQuery } from "../../Hooks/GraphQL";
|
||||
import { useTimeFormat } from "../../Hooks/useFormat";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const ArtistDetailsPage = () => {
|
||||
const params = useParams();
|
||||
@@ -33,6 +34,7 @@ const ArtistDetailsPage = () => {
|
||||
playTrackAt,
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const { onSearch } = useSearch();
|
||||
const artist = !loading && data ? data.artist : {};
|
||||
const tracks =
|
||||
!loading && data
|
||||
@@ -79,6 +81,7 @@ const ArtistDetailsPage = () => {
|
||||
onPlayNext={(trackId) => playNext({ variables: { trackId } })}
|
||||
onPlayTrackAt={(position) => playTrackAt({ variables: { position } })}
|
||||
onRemoveTrackAt={(position) => removeTrackAt({ variables: { position } })}
|
||||
onSearch={(query) => navigate(`/search?q=${query}`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import Artists from "../../Components/Artists";
|
||||
import { useGetArtistsQuery } from "../../Hooks/GraphQL";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const ArtistsPage = () => {
|
||||
const { data, loading } = useGetArtistsQuery();
|
||||
@@ -18,6 +19,7 @@ const ArtistsPage = () => {
|
||||
playTrackAt,
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const { onSearch } = useSearch();
|
||||
const artists = !loading && data ? data.artists : [];
|
||||
return (
|
||||
<Artists
|
||||
@@ -40,6 +42,7 @@ const ArtistsPage = () => {
|
||||
onPlayNext={(trackId) => playNext({ variables: { trackId } })}
|
||||
onPlayTrackAt={(position) => playTrackAt({ variables: { position } })}
|
||||
onRemoveTrackAt={(position) => removeTrackAt({ variables: { position } })}
|
||||
onSearch={(query) => navigate(`/search?q=${query}`)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
66
webui/musicplayer/src/Containers/Search/index.tsx
Normal file
66
webui/musicplayer/src/Containers/Search/index.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import SearchResults from "../../Components/SearchResults";
|
||||
import { useTimeFormat } from "../../Hooks/useFormat";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const SearchPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { formatTime } = useTimeFormat();
|
||||
const {
|
||||
play,
|
||||
pause,
|
||||
next,
|
||||
previous,
|
||||
nowPlaying,
|
||||
nextTracks,
|
||||
previousTracks,
|
||||
playNext,
|
||||
playTrackAt,
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const [params] = useSearchParams();
|
||||
const { onSearch, results, query } = useSearch();
|
||||
const q = useMemo(() => params.get("q"), [params]);
|
||||
|
||||
useEffect(() => {
|
||||
if (q && q !== null) {
|
||||
onSearch(q);
|
||||
}
|
||||
}, [q]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SearchResults
|
||||
tracks={results.tracks.map((x) => ({
|
||||
...x,
|
||||
time: formatTime(x.duration * 1000),
|
||||
}))}
|
||||
albums={results.albums}
|
||||
artists={results.artists}
|
||||
onClickAlbum={({ id }) => navigate(`/album/${id}`)}
|
||||
onClickArtist={({ id }) => navigate(`/artists/${id}`)}
|
||||
onClickLibraryItem={(item) => navigate(`/${item}`)}
|
||||
onPlay={() => play()}
|
||||
onPause={() => pause()}
|
||||
onNext={() => next()}
|
||||
onPrevious={() => previous()}
|
||||
onShuffle={() => {}}
|
||||
onRepeat={() => {}}
|
||||
nowPlaying={nowPlaying}
|
||||
onPlayTrack={(id, position) => {}}
|
||||
nextTracks={nextTracks}
|
||||
previousTracks={previousTracks}
|
||||
onPlayNext={(trackId) => playNext({ variables: { trackId } })}
|
||||
onPlayTrackAt={(position) => playTrackAt({ variables: { position } })}
|
||||
onRemoveTrackAt={(position) =>
|
||||
removeTrackAt({ variables: { position } })
|
||||
}
|
||||
onSearch={onSearch}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchPage;
|
||||
@@ -3,6 +3,7 @@ import Tracks from "../../Components/Tracks";
|
||||
import { useGetTracksQuery } from "../../Hooks/GraphQL";
|
||||
import { useTimeFormat } from "../../Hooks/useFormat";
|
||||
import { usePlayback } from "../../Hooks/usePlayback";
|
||||
import { useSearch } from "../../Hooks/useSearch";
|
||||
|
||||
const TracksPage = () => {
|
||||
const { data, loading } = useGetTracksQuery({
|
||||
@@ -22,6 +23,7 @@ const TracksPage = () => {
|
||||
playTrackAt,
|
||||
removeTrackAt,
|
||||
} = usePlayback();
|
||||
const { onSearch } = useSearch();
|
||||
const tracks = !loading && data ? data.tracks : [];
|
||||
return (
|
||||
<>
|
||||
@@ -52,6 +54,7 @@ const TracksPage = () => {
|
||||
onRemoveTrackAt={(position) =>
|
||||
removeTrackAt({ variables: { position } })
|
||||
}
|
||||
onSearch={(query) => navigate(`/search?q=${query}`)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -89,3 +89,31 @@ export const GET_ALBUM = gql`
|
||||
}
|
||||
${ALBUM_FRAGMENT}
|
||||
`;
|
||||
|
||||
export const SEARCH = gql`
|
||||
query Search($keyword: String!) {
|
||||
search(keyword: $keyword) {
|
||||
artists {
|
||||
id
|
||||
name
|
||||
picture
|
||||
}
|
||||
albums {
|
||||
id
|
||||
title
|
||||
artist
|
||||
cover
|
||||
}
|
||||
tracks {
|
||||
id
|
||||
title
|
||||
artist
|
||||
duration
|
||||
cover
|
||||
artistId
|
||||
albumId
|
||||
albumTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -188,7 +188,7 @@ export type Query = {
|
||||
getVolume: Scalars['Int'];
|
||||
playlist: Scalars['Boolean'];
|
||||
playlists: Scalars['Boolean'];
|
||||
search: Scalars['Boolean'];
|
||||
search: SearchResult;
|
||||
track: Track;
|
||||
tracklistTracks: Tracklist;
|
||||
tracks: Array<Track>;
|
||||
@@ -210,15 +210,31 @@ export type QueryGetPlaylistTracksArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QuerySearchArgs = {
|
||||
keyword: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryTrackArgs = {
|
||||
id: Scalars['ID'];
|
||||
};
|
||||
|
||||
export type SearchResult = {
|
||||
__typename?: 'SearchResult';
|
||||
albums: Array<Album>;
|
||||
artists: Array<Artist>;
|
||||
tracks: Array<Track>;
|
||||
};
|
||||
|
||||
export type Track = {
|
||||
__typename?: 'Track';
|
||||
album: Album;
|
||||
albumId: Scalars['String'];
|
||||
albumTitle: Scalars['String'];
|
||||
artist: Scalars['String'];
|
||||
artistId: Scalars['String'];
|
||||
artists: Array<Artist>;
|
||||
cover?: Maybe<Scalars['String']>;
|
||||
discNumber: Scalars['Int'];
|
||||
duration?: Maybe<Scalars['Float']>;
|
||||
id: Scalars['String'];
|
||||
@@ -277,6 +293,13 @@ export type GetAlbumQueryVariables = Exact<{
|
||||
|
||||
export type GetAlbumQuery = { __typename?: 'Query', album: { __typename?: 'Album', id: string, title: string, artist: string, year?: number | null, cover?: string | null, tracks: Array<{ __typename?: 'Track', id: string, trackNumber?: number | null, title: string, artist: string, duration?: number | null, artists: Array<{ __typename?: 'Artist', id: string, name: string }> }> } };
|
||||
|
||||
export type SearchQueryVariables = Exact<{
|
||||
keyword: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResult', artists: Array<{ __typename?: 'Artist', id: string, name: string, picture: string }>, albums: Array<{ __typename?: 'Album', id: string, title: string, artist: string, cover?: string | null }>, tracks: Array<{ __typename?: 'Track', id: string, title: string, artist: string, duration?: number | null, cover?: string | null, artistId: string, albumId: string, albumTitle: string }> } };
|
||||
|
||||
export type NextMutationVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
@@ -591,6 +614,61 @@ export function useGetAlbumLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<G
|
||||
export type GetAlbumQueryHookResult = ReturnType<typeof useGetAlbumQuery>;
|
||||
export type GetAlbumLazyQueryHookResult = ReturnType<typeof useGetAlbumLazyQuery>;
|
||||
export type GetAlbumQueryResult = Apollo.QueryResult<GetAlbumQuery, GetAlbumQueryVariables>;
|
||||
export const SearchDocument = gql`
|
||||
query Search($keyword: String!) {
|
||||
search(keyword: $keyword) {
|
||||
artists {
|
||||
id
|
||||
name
|
||||
picture
|
||||
}
|
||||
albums {
|
||||
id
|
||||
title
|
||||
artist
|
||||
cover
|
||||
}
|
||||
tracks {
|
||||
id
|
||||
title
|
||||
artist
|
||||
duration
|
||||
cover
|
||||
artistId
|
||||
albumId
|
||||
albumTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useSearchQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useSearchQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useSearchQuery` returns an object from Apollo Client that contains loading, error, and data properties
|
||||
* you can use to render your UI.
|
||||
*
|
||||
* @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;
|
||||
*
|
||||
* @example
|
||||
* const { data, loading, error } = useSearchQuery({
|
||||
* variables: {
|
||||
* keyword: // value for 'keyword'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useSearchQuery(baseOptions: Apollo.QueryHookOptions<SearchQuery, SearchQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<SearchQuery, SearchQueryVariables>(SearchDocument, options);
|
||||
}
|
||||
export function useSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<SearchQuery, SearchQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<SearchQuery, SearchQueryVariables>(SearchDocument, options);
|
||||
}
|
||||
export type SearchQueryHookResult = ReturnType<typeof useSearchQuery>;
|
||||
export type SearchLazyQueryHookResult = ReturnType<typeof useSearchLazyQuery>;
|
||||
export type SearchQueryResult = Apollo.QueryResult<SearchQuery, SearchQueryVariables>;
|
||||
export const NextDocument = gql`
|
||||
mutation Next {
|
||||
next
|
||||
|
||||
55
webui/musicplayer/src/Hooks/useSearch.tsx
Normal file
55
webui/musicplayer/src/Hooks/useSearch.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { useState } from "react";
|
||||
import { useSearchLazyQuery } from "./GraphQL";
|
||||
import { Album, Artist, Track } from "../Types";
|
||||
|
||||
export const useSearch = () => {
|
||||
const [query, setQuery] = useState("");
|
||||
const [results, setResults] = useState<{
|
||||
tracks: Track[];
|
||||
artists: Artist[];
|
||||
albums: Album[];
|
||||
playlists: any[];
|
||||
}>({
|
||||
albums: [],
|
||||
artists: [],
|
||||
tracks: [],
|
||||
playlists: [],
|
||||
});
|
||||
|
||||
const [search] = useSearchLazyQuery();
|
||||
|
||||
const onSearch = async (keyword: string) => {
|
||||
setQuery(query);
|
||||
const { data } = await search({
|
||||
variables: {
|
||||
keyword,
|
||||
},
|
||||
});
|
||||
|
||||
setResults({
|
||||
albums:
|
||||
data?.search.albums.map((x) => ({
|
||||
...x,
|
||||
cover: x.cover ? `/covers/${x.cover}` : undefined,
|
||||
year: 0,
|
||||
})) || [],
|
||||
artists: data?.search.artists || [],
|
||||
tracks:
|
||||
data?.search.tracks.map((x) => ({
|
||||
...x,
|
||||
cover: x.cover ? `/covers/${x.cover}` : undefined,
|
||||
duration: x.duration!,
|
||||
album: x.albumTitle,
|
||||
artistId: x.artistId,
|
||||
albumId: x.albumId,
|
||||
})) || [],
|
||||
playlists: [],
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
query,
|
||||
results,
|
||||
onSearch,
|
||||
};
|
||||
};
|
||||
@@ -111,4 +111,9 @@ tr:hover td div button {
|
||||
|
||||
.album-cover-container:hover img {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
[data-baseweb="tab-panel"] {
|
||||
padding-left: 0 !important;
|
||||
padding-right: 0 !important;
|
||||
}
|
||||
@@ -114,6 +114,7 @@ pub async fn start_webui(
|
||||
.route("/albums", web::get().to(index_spa))
|
||||
.route("/artists/{_:.*}", web::get().to(index_spa))
|
||||
.route("/albums/{_:.*}", web::get().to(index_spa))
|
||||
.route("/search", web::get().to(index_spa))
|
||||
.service(dist)
|
||||
})
|
||||
.bind(addr)?
|
||||
|
||||
Reference in New Issue
Block a user