v.shebanov 4 anni fa
parent
commit
7a5bcb4e88

+ 33 - 26
back/db.lisp

@@ -184,6 +184,19 @@
         (integer track)
         (string (parse-integer track :junk-allowed t))))))
 
+(defun slot< (a b)
+  (declare #.*standard-optimize-settings*
+           (type simple-string a b))
+  (when (xor (emptyp a) (emptyp b))
+    (return-from slot< (when (emptyp b) 0)))
+  (let ((aa (alpha-char-p (elt a 0)))
+        (ba (alpha-char-p (elt b 0))))
+    (cond
+      ((and aa ba) (string< a b))
+      (aa 0)
+      (ba)
+      (t (string< a b)))))
+
 (defparameter +album-type-order+ '("album" "lp" "ep" "single" "compilation" "live" "soundtrack"
                                    "spokenword" "remix" "mixed" "dj-mix" "mixtape" "broadcast")
   "Half-arbitrary album type order")
@@ -194,22 +207,26 @@
       (let ((slot-a (slot-value a slot))
             (slot-b (slot-value b slot)))
         (when (xor (null slot-a) (null slot-b))
-          (return-from info<> (if (null slot-b) 1 -1)))
-        (case slot
-          (type
-           (setf slot-a (or (position slot-a (the list +album-type-order+) :test 'string-equal) 0)
-                 slot-b (or (position slot-b (the list +album-type-order+) :test 'string-equal) 0)))
-          (no
-           (setf slot-a (clear-track-no slot-a)
-                 slot-b (clear-track-no slot-b))))
-        (unless (or (and (null slot-a) (null slot-b))
-                    (case slot
-                      ((type year no) (= (the fixnum slot-a) (the fixnum slot-b)))
-                      (t (string-equal slot-a slot-b))))
-          (return-from info<> (case slot
-                                ((type year no) (- (the fixnum slot-a) (the fixnum slot-b)))
-                                (t (if (string< slot-a slot-b) -1 1)))))))))
-(defparameter +album<>+ (gen-comparator '(artist type original-date year album)))
+          (return-from info<> (if (null slot-b) -1 1)))
+        (unless (and (null slot-a) (null slot-b))
+          (case slot
+            (type
+             (let ((cmp (if (slot< slot-a slot-b) -1 1)))
+               (setf slot-a (or (position slot-a (the list +album-type-order+) :test 'string-equal)
+                                (+ 100 cmp))
+                     slot-b (or (position slot-b (the list +album-type-order+) :test 'string-equal)
+                                (- 100 cmp)))))
+            (no
+             (setf slot-a (clear-track-no slot-a)
+                   slot-b (clear-track-no slot-b))))
+          (unless (case slot
+                    ((type year no) (= (the fixnum slot-a) (the fixnum slot-b)))
+                    (t (string-equal slot-a slot-b)))
+            (return-from info<> (case slot
+                                  ((type year no) (- (the fixnum slot-a) (the fixnum slot-b)))
+                                  (t (if (slot< slot-a slot-b) -1 1))))))))))
+
+(defparameter +album<>+ (gen-comparator '(artist type year album)))
 (defun album< (a b)
   (declare #.*standard-optimize-settings*
            (type function +album<>+))
@@ -231,16 +248,6 @@
         (< (the fixnum (funcall +track<>+ a b)) 0)
         albs)))
 
-(defun slot< (a b)
-  (declare #.*standard-optimize-settings*
-           (type simple-string a b))
-  (let ((aa (alpha-char-p (elt a 0)))
-        (ba (alpha-char-p (elt b 0))))
-    (cond
-      ((and aa ba) (string< a b))
-      (aa 0)
-      (ba)
-      (t (string< a b)))))
 
 (defun match-filter (data category filter)
   (declare #.*standard-optimize-settings*)

+ 1 - 1
back/server.lisp

@@ -53,7 +53,7 @@
     (maybe-key-value "publisher" (album-publisher album))
     (maybe-key-value "country" (album-country album))
     (maybe-key-value "genre" (album-genre album))
-    (maybe-key-value "type" (or (album-type album) "album"))
+    (maybe-key-value "type" (or (album-type album) "release"))
     (maybe-key-value "status" (album-status album))
     (maybe-key-value "mb_id" (album-mb-id album))
     (maybe-key-value "track_count" (album-track-count album))

+ 1 - 1
web/public/index.html

@@ -2,7 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <link rel="icon" href="{{ copy "./favicon.ico" }}" />
+    <link rel="icon" href="{{ copy "./logo256.png" }}" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
     <meta

+ 1 - 1
web/public/index_old.html

@@ -2,7 +2,7 @@
 <html lang="en">
   <head>
     <meta charset="utf-8" />
-    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
+    <link rel="icon" href="%PUBLIC_URL%/logo256.png" />
     <meta name="viewport" content="width=device-width, initial-scale=1" />
     <meta name="theme-color" content="#000000" />
     <meta

BIN
web/public/logo256.png


+ 4 - 14
web/public/manifest.json

@@ -1,21 +1,11 @@
 {
-  "short_name": "React App",
-  "name": "Create React App Sample",
+  "short_name": "Chad Music",
+  "name": "Chad music streaming service",
   "icons": [
     {
-      "src": "favicon.ico",
-      "sizes": "64x64 32x32 24x24 16x16",
-      "type": "image/x-icon"
-    },
-    {
-      "src": "logo192.png",
-      "type": "image/png",
-      "sizes": "192x192"
-    },
-    {
-      "src": "logo512.png",
+      "src": "logo256.png",
       "type": "image/png",
-      "sizes": "512x512"
+      "sizes": "256x256"
     }
   ],
   "start_url": ".",

+ 4 - 4
web/src/components/AlbumList/AlbumList.css → web/src/components/Album/Album.css

@@ -1,9 +1,9 @@
 .Album{
   display: grid;
-  grid: "cover year    title     actions" 1fr
-        "cover country publisher actions" 1fr
-        "cover tracks  genre     actions" 1fr
-        / auto 1fr 7fr 1fr;
+  grid: "cover year    title     actions" auto
+        "cover country publisher actions" auto
+        "cover tracks  genre     actions" auto
+        / auto auto 1fr auto;
   grid-column-gap: 8px;
   padding-bottom: 10px;
 }

+ 1 - 0
web/src/components/Album/Album.js

@@ -1,3 +1,4 @@
+import './Album.css';
 import logo from 'src/logo.svg';
 import { Link } from 'react-router5'
 import {formatDuration, slugify} from 'src/utils';

+ 0 - 1
web/src/components/AlbumList/AlbumList.js

@@ -1,4 +1,3 @@
-import './AlbumList.css';
 import InfiniteScroll from 'react-infinite-scroller';
 import {useEffect} from 'react';
 import {getAlbums, request} from 'src/Api';

+ 6 - 0
web/src/components/AlbumPage/AlbumPage.css

@@ -4,3 +4,9 @@
   height: 24px;
   vertical-align: bottom;
 }
+
+@media screen and (max-width: 800px) {
+  .AlbumPage h1 { margin: 5px 0; }
+  .AlbumPage h2 { margin: 2px 0; }
+  .AlbumPage h3 { margin: 1px 0; }
+}

+ 36 - 0
web/src/components/App/App.css

@@ -38,6 +38,7 @@
 .App-intro {
   font-size: large;
 }
+.App-panels { display: none; }
 
 .App main {
   grid-area: browser;
@@ -51,3 +52,38 @@
 .App footer {
   grid-area: footer;
 }
+
+@media screen and (max-width: 800px) {
+  .App {
+    grid: "header" auto
+          "panels" auto
+          "main"   1fr
+          "footer" auto
+          / 1fr;
+  }
+  .App main {
+    grid-area: main;
+    padding: 0 8px;
+  }
+  .App-panels {
+    grid-area: panels;
+    display: grid;
+    grid: auto / 1fr 1fr;
+    text-align: center;
+    margin-bottom: 4px;
+  }
+  .App-panels input { display: none; }
+
+  .App-panels label {
+    background-color: var(--controls-background);
+  }
+  .App-panels #panel-main:checked ~ label[for='panel-main'],
+  .App-panels #panel-side:checked ~ label[for='panel-side'] {
+    background-color: var(--background);
+  }
+  .App aside { grid-area: main; }
+  .App[data-panel='side'] main { display: none; }
+  .App[data-panel='main'] aside { display: none; }
+
+  .ArtistName { margin: 0; }
+}

+ 15 - 6
web/src/components/App/App.js

@@ -11,7 +11,7 @@ function storeSettings(settings) {
 }
 function loadSettings() {
   const settings = localStorage.getItem('settings');
-  return (settings && JSON.parse(settings)) || {login:{}, theme:'light', layout:'two-col'};
+  return (settings && JSON.parse(settings)) || {login:{}, theme:'light', layout:'two-col', panel:'main'};
 }
 
 function settingsReducer(state, action) {
@@ -20,14 +20,17 @@ function settingsReducer(state, action) {
   case 'LOGIN_SUCCESS':
     storeToken(action.payload.token);
     return {...state, login: {loading: false, error: null}};
-  case 'LOGIN_INIT':    return {...state, login: {loading: true, error: null}};
-  case 'LOGIN_FAIL':    return {...state, login: {loading: false, error: action.payload}};
-  case 'LAYOUT_SET':    return storeSettings({...state, layout: action.payload});
-  case 'THEME_SET':     return storeSettings({...state, theme: action.payload});
+  case 'LOGIN_INIT': return {...state, login: {loading: true, error: null}};
+  case 'LOGIN_FAIL': return {...state, login: {loading: false, error: action.payload}};
+  case 'LAYOUT_SET': return storeSettings({...state, layout: action.payload});
+  case 'THEME_SET':  return storeSettings({...state, theme: action.payload});
+  case 'PANEL_SET':  return storeSettings({...state, panel: action.payload});
   default: throw new Error(`Bad action ${action}`);
   }
 }
 
+const handleRadio = (dispatch, type) => (e) => dispatch({type:`${e.target.name.toUpperCase()}_SET`, payload: e.target.value});
+
 function App() {
   const [settings, dispatch] = useReducer(settingsReducer, loadSettings());
   if(!loadToken()) {
@@ -35,7 +38,13 @@ function App() {
   }
 
   return (
-    <div className="App" data-theme={settings.theme} data-layout={settings.layout}>
+    <div className="App" data-theme={settings.theme} data-layout={settings.layout} data-panel={settings.panel || 'main'}>
+      <div className="App-panels">
+        <input id="panel-main" type="radio" name="panel" value="main" checked={settings.panel === 'main'} onChange={handleRadio(dispatch)} />
+        <input id="panel-side" type="radio" name="panel" value="side" checked={settings.panel === 'side'} onChange={handleRadio(dispatch)} />
+        <label htmlFor="panel-main">Browse</label>
+        <label htmlFor="panel-side">Queue</label>
+      </div>
       <Header settings={settings} dispatch={dispatch} />
       <Player />
     </div>

+ 1 - 1
web/src/components/ArtistPage.js

@@ -13,7 +13,7 @@ export default function ArtistPage({dispatch, scrollRef}) {
 
   return (
     <>
-      <h1>{artist}</h1><Link routeName="browser">Browse</Link>
+      <h1 className="ArtistName">{artist}</h1><Link routeName="browser">Browse</Link>
       <AlbumList {...state} dispatch={dispatchBrowser} playerDispatch={dispatch} scrollRef={scrollRef} noTitle noArtist/>
     </>
   );

+ 29 - 0
web/src/components/Controls/Controls.css

@@ -124,3 +124,32 @@
   height: 48px;
   width: 48px;
 }
+
+@media screen and (max-width: 800px) {
+  .Controls {
+    grid: "progress progress progress" auto
+          "cover    buttons time" auto
+          "cover    title artist" auto
+          "album    album album" auto
+          / auto auto 1fr;
+    grid-column-gap: 4px;
+  }
+  .Controls-buttons {
+    grid-column-gap: 10px;
+  }
+  .Controls progress {
+    height: 4px;
+  }
+  .Controls-album {
+    padding: 0 8px;
+  }
+  .Controls-artist {
+    min-width: 25vw;
+    padding: 0 8px;
+    text-align: right;
+  }
+  .Controls-time {
+    justify-self: right;
+    padding-right: 8px;
+  }
+}

+ 28 - 2
web/src/components/Filters/Filters.css

@@ -1,12 +1,38 @@
 .Filters {
+  padding: 8px 0;
   display: grid;
   grid-template-columns: repeat(3, 1fr);
   grid-column-gap: 10px;
+  grid-row-gap: 4px;
 }
 .Filters h3{
   font-size: 1em;
 }
+.Filters .Latest {
+  align-self: center;
+}
+.Filters .Latest label {
+  color: hsl(0, 0%, 50%);
+}
+.Filters .Latest input {
+  margin-left: 8px;
+}
+
 .Filters .Filter {
-  grid-column-end: span 2;
+  grid-column-end: span 3;
+}
+.Filters .Filter input {
+  width: 100%;
+  height: 30px;
+  border-radius: 4px;
+  border-width: 1px;
+  padding-left: 8px;
+  font-size: 16px;
+}
+
+@media screen and (max-width: 800px) {
+  .Filters {
+    grid-template-columns: repeat(2, 1fr);
+  }
+  .Filters .Filter { display: none; }
 }
-.Filters .Filter input { width: 100%; }

+ 7 - 9
web/src/components/Filters/Filters.js

@@ -41,10 +41,10 @@ export default function Filters({dispatch, filters, filter}) {
     <div className="Filters">
       {CATEGORIES.map(({cat, title, hidden}) => (!hidden &&
         <div className="section" key={cat}>
-          <h3>{title}</h3>
           <AsyncSelect
             isClearable
             cacheOptions
+            placeholder={title}
             defaultValue={filters[cat] && {item: filters[cat]}}
             defaultOptions
             loadOptions={(q) => handleLoadOptions(cat, q)}
@@ -54,21 +54,19 @@ export default function Filters({dispatch, filters, filter}) {
             />
         </div>
       ))}
+      <div className="Latest">
+      <label>Latest<input
+             type="checkbox"
+             checked={ !filters.alpha }
+             onChange={(e) => handleCategoryChange(router, 'alpha', !e.target.checked)} /></label>
+      </div>
       <div className="Filter">
-        <h3>Search</h3>
         <input
           type="text"
           placeholder="Search albums"
           value={filter || ''}
           onChange={(e)=>handleFilterChange(dispatch, router, timeout, e.target.value)} />
       </div>
-      <div>
-      <h3>Latest</h3>
-          <input className="Latest"
-             type="checkbox"
-             checked={ !filters.alpha }
-             onChange={(e) => handleCategoryChange(router, 'alpha', !e.target.checked)} />
-      </div>
     </div>
   );
 }

+ 29 - 2
web/src/components/Header/Header.css

@@ -2,17 +2,44 @@
   grid-area: header;
   background-color: var(--header-background);
   display: grid;
-  grid: 1fr / auto 1fr 2fr 2fr auto;
+  grid: "logo title search settings user" auto
+        / auto auto 1fr auto auto;
   align-items: center;
+  grid-column-gap: 10px;
 }
 
 .Header-logo {
+  grid-area: logo;
+  height: 80px;
+}
+.Header-logo img {
   min-width: 64px;
   height: 64px;
   padding: 8px;
 }
-
 .Header-title {
+  grid-area: title;
   color: var(--header-color);
   font-size: 1.5em;
 }
+.Header-search {
+  grid-area: search;
+  max-width: 600px;
+}
+
+@media screen and (max-width: 800px) {
+  .Header {
+    grid: "logo title  user" auto
+          "logo search user" auto
+          / auto 1fr auto;
+    grid-column-gap: 2px;
+  }
+  .Header-title {
+    margin: 0;
+    justify-self: center;
+  }
+  .Header-search {
+    height: 20px;
+    margin: 6px 0;
+  }
+}

+ 2 - 2
web/src/components/Header/Header.js

@@ -14,8 +14,8 @@ export default function Header({settings, dispatch}) {
 
   return (
     <header className="Header">
-      <BaseLink router={router} routeName="browser" routeOptions={{ reload: true }}>
-        <img src={logo} className="Header-logo" alt="Chad music" />
+      <BaseLink className="Header-logo" router={router} routeName="browser" routeOptions={{ reload: true }}>
+        <img src={logo} alt="Chad music" />
       </BaseLink>
       <h1 className="Header-title">Chad Music</h1>
       <input className="Header-search" type="text"

+ 2 - 2
web/src/components/Queue/Queue.js

@@ -21,7 +21,7 @@ export default function Queue({queue, dispatch}) {
                   <svg viewBox="0 0 24 24"><g><path d="M8 5v14l11-7z"/></g></svg>
               </button></td>
             </tr>))}
-    </tbody></table>
-      </aside>
+      </tbody></table>
+    </aside>
   );
 }

+ 14 - 3
web/src/components/Settings/Settings.css

@@ -1,9 +1,20 @@
 .Settings {
+  grid-area: settings;
   justify-self: right;
-  padding-right: 64px;
 
   display: grid;
-  grid: auto / 1fr 1fr;
-  width: 384px;
+  grid: auto / auto auto;
   grid-column-gap: 10px;
 }
+
+.Settings-theme {
+  width: 100px;
+}
+
+.Settings-layout {
+  width: 170px;
+}
+
+@media screen and (max-width: 800px) {
+  .Settings { display: none; }
+}

+ 2 - 2
web/src/components/Settings/Settings.js

@@ -18,8 +18,8 @@ export default function Settings({state, dispatch}) {
 
   return (
     <div className="Settings">
-      <Select defaultValue={theme} options={THEMES} onChange={(e)=>dispatch({type: 'THEME_SET', payload: e.value})} />
-      <Select defaultValue={layout} options={LAYOUTS} onChange={(e)=>dispatch({type: 'LAYOUT_SET', payload: e.value})} />
+      <Select className="Settings-theme" defaultValue={theme} options={THEMES} onChange={(e)=>dispatch({type: 'THEME_SET', payload: e.value})} />
+      <Select className="Settings-layout" defaultValue={layout} options={LAYOUTS} onChange={(e)=>dispatch({type: 'LAYOUT_SET', payload: e.value})} />
     </div>
   );
 }

+ 4 - 0
web/src/components/UserInfo/UserInfo.css

@@ -1,4 +1,5 @@
 .UserInfo {
+  grid-area: user;
   justify-self: right;
   display: grid;
   grid: 1fr / 1fr auto;
@@ -14,3 +15,6 @@
   padding: 8px;
   border-radius: 64px;
 }
+@media screen and (max-width: 800px) {
+  .UserInfo span { display: none; }
+}