1
0
Fork 0
mirror of https://github.com/heyarne/airsonic-ui.git synced 2026-05-06 18:33:38 +02:00

Polish and lipstick (#7)

* Add dark sidebar

* Add generated covers for items that have none

* Fix small spacing issue with generated covers

* Set up different sidebar sections and improve styling of bottom bar

* Add open-iconic and use icons for playback control buttons

* Make sure sidebar always extends to complete height

* Simplify album listing view function, add text-overflow to thumbs

* Use better identifier for generated covers

Makes sure that covers look the same, no matter if generated from an album or
individual track

* Move shadow-cljs to devDependencies

* Display all album titles in a table

* Make progress bar take up all available space
This commit is contained in:
Arne Schlüter 2018-06-03 17:05:52 +02:00 committed by GitHub
commit c8849810db
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 236 additions and 52 deletions

View file

@ -28,7 +28,8 @@
(defmethod route-data ::main
[route-id params query]
[:api-request "getAlbumList2" :albumList2 {:type "recent"}])
[:api-request "getAlbumList2" :albumList2 {:type "recent"
:size 18}])
(defmethod route-data ::artist-view
[route-id params query]

View file

@ -9,6 +9,11 @@
(fn [db]
(select-keys (:credentials db) [:u :p])))
(re-frame/reg-sub
::user
(fn [{:keys [credentials]}]
{:name (:u credentials)}))
(re-frame/reg-sub
::server
(fn [db]
@ -28,10 +33,19 @@
(re-frame/reg-sub
::current-content
(fn [db]
(-> db :response)))
(db :response)))
(re-frame/reg-sub
; returns info on the current song as is (basically the metadata you can read from the file system)
::currently-playing
(fn [db]
(-> db :currently-playing)))
(db :currently-playing)))
(re-frame/reg-sub
::is-playing?
(fn [query-v _]
[(re-frame/subscribe [::currently-playing])])
(fn [[currently-playing]]
(let [status (:status currently-playing)]
(and (not (:paused? status))
(not (:ended? status))))))

View file

@ -28,21 +28,40 @@
[:h2.title "Recently played"]
[album/listing (:album content)]])
(defn sidebar [user]
[:aside.menu.section
[:p.menu-label "Music"]
[:ul.menu-list
[:li [:a "By artist"]]
[:li [:a "Top rated"]]
[:li [:a "Most played"]]]
[:p.menu-label "Playlists"]
[:p.menu-label "Shares"]
[:p.menu-label "Podcasts"]
[:p.menu-label "User area"]
[:ul.menu-list
[:li [:a "Settings"]]
;; FIXME: Create proper logout event
[:li [:a
{:on-click #(dispatch [::events/initialize-db]) :href "#"}
(str "Logout (" (:name user) ")")]]]])
;; putting everything together
(defn app [route params query]
(let [login @(subscribe [::subs/login])
(let [user @(subscribe [::subs/user])
content @(subscribe [::subs/current-content])]
[:div
[:section.section>div.container
[:div.level
[:div.level-left [:span (str "Currently logged in as " (:u login))]]
[:div.level-right [:a {:on-click #(dispatch [::events/initialize-db]) :href "#"} "Logout"]]]
[breadcrumbs content]
(case route
::routes/main [most-recent content]
::routes/artist-view [artist-detail content]
::routes/album-view [album-detail content])]
[:main.columns
[:div.column.is-2.sidebar
[sidebar user]]
[:div.column
[:section.section
[breadcrumbs content]
(case route
::routes/main [most-recent content]
::routes/artist-view [artist-detail content]
::routes/album-view [album-detail content])]]]
[bottom-bar]]))
(defn main-panel []

View file

@ -17,7 +17,7 @@
(defn listing [albums]
;; always show 5 in a row
[:div
(for [albums (partition-all 5 albums)]
[:div.columns
(for [[idx album] (map-indexed vector albums)]
[:div.column {:key idx} [preview album]])])])
[:div.columns.is-multiline.is-mobile
(for [[idx album] (map-indexed vector albums)]
^{:key idx} [:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-one-half-mobile
[preview album]])]])

View file

@ -2,40 +2,46 @@
(:require [re-frame.core :refer [dispatch subscribe]]
[airsonic-ui.events :as events]
[airsonic-ui.subs :as subs]
[airsonic-ui.views.cover :refer [cover]]))
[airsonic-ui.views.cover :refer [cover]]
[airsonic-ui.views.icon :refer [icon]]))
;; currently playing / coming next / audio controls...
(defn current-song-info [{:keys [item status]}]
[:article
[:div (:artist item) " - " (:title item)]
;; FIXME: Sometimes items don't have a duration
[:progress.progress.is-tiny {:value (:current-time status)
:max (:duration item)}]])
:max (:duration item)}]])
(defn playback-controls []
(defn playback-controls [is-playing?]
;; TODO: Toggle play pause icon based on playback status
[:div.field.has-addons
(let [buttons [["previous" ::events/previous-song]
["play / pause" ::events/toggle-play-pause]
["next" ::events/next-song]]]
(map (fn [[label event]]
[:p.control>button.button.is-light {:on-click #(dispatch [event])} label])
(let [buttons [[:media-step-backward ::events/previous-song]
[(if is-playing? :media-pause :media-play) ::events/toggle-play-pause]
[:media-step-forward ::events/next-song]]]
(map (fn [[icon-glyph event]]
^{:key icon-glyph} [:p.control>button.button.is-light
{:on-click #(dispatch [event])}
[icon icon-glyph]])
buttons))])
(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-dark-350x100.png")
(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-light-350x100.png")
(defn bottom-bar []
(let [currently-playing @(subscribe [::subs/currently-playing])]
[:nav.navbar.is-fixed-bottom
(let [currently-playing @(subscribe [::subs/currently-playing])
is-playing? @(subscribe [::subs/is-playing?])]
[:nav.navbar.is-fixed-bottom.playback-area
[:div.navbar-brand
[:div.navbar-item
[:img {:src logo-url}]]]
[:div.navbar-menu.is-active
(if currently-playing
;; show song info
[:section.level
[:section.level.audio-interaction
[:div.level-left>article.media
[:div.media-left [cover (:item currently-playing) 48]]
[:div.media-content [current-song-info currently-playing]]]
[:div.level-right [playback-controls]]]
[:div.level-right [playback-controls is-playing?]]]
;; not playing anything
[:span "Currently no song selected"])]]))
[:p.idle-notification "Currently no song selected"])]]))

View file

@ -1,15 +1,69 @@
(ns airsonic-ui.views.cover
(:require [re-frame.core :refer [subscribe]]
(:require [clojure.string :as str]
[re-frame.core :refer [subscribe]]
[reagent.core :as reagent]
[airsonic-ui.subs :as subs]
[airsonic-ui.utils.api :as api]))
[airsonic-ui.utils.api :as api]
["@hugojosefson/color-hash" :as ColorHash]))
(def color-hash (ColorHash.))
(defn palette
"Generate a hsl palette of two colors that's unique for a given item"
[item]
(let [identifier (str (:artistId item) "-" (or (:albumId item) (:id item)))
[h s l] (js->clj (.hsl color-hash identifier))
s (str (* 100 s) "%")
l (str (* 100 l) "%")]
(->>
[[h s l]
[(mod (+ h (* h 0.3) 10) 360) s l]]
(map #(str "hsl(" (str/join "," %) ")")))))
;; FIXME: The direct dependency on these subs is a bit ugly
(defn generate-cover [canvas item]
(let [ctx (.getContext canvas "2d")
size (.-clientWidth canvas)
[a b] (palette item)
pad (* 0.02 size)
gradient (doto (.createLinearGradient ctx pad 0 (- size pad) size)
(.addColorStop 0 a)
(.addColorStop 1 b))]
(set! (.-fillStyle ctx) gradient)
(.fillRect ctx 0 0 size size)))
(defn missing-cover
[item size]
(let [dom-node (reagent/atom nil)]
(reagent/create-class
{:component-did-update
(fn [this]
(let [canvas @dom-node]
(set! (.. canvas -style -width) "100%")
(set! (. canvas -width) (.-offsetWidth canvas))
(set! (. canvas -height) (.-offsetWidth canvas))
(generate-cover canvas item)))
:component-did-mount
(fn [this]
(reset! dom-node (reagent/dom-node this)))
:reagent-render
(fn []
@dom-node
[:canvas.missing-cover])})))
(defn has-cover? [item]
(:coverArt item))
(defn cover
[item size]
(let [server @(subscribe [::subs/server])
login @(subscribe [::subs/login])
url (partial api/cover-url server login item)]
[:figure {:class-name (str "image is-" size "x" size)}
[:img {:src (url size)
:srcset (str (url size) ", " (url (* 2 size)) " 2x")}]]))
(if (has-cover? item)
[:img {:src (url size)
:srcSet (str (url size) ", " (url (* 2 size)) " 2x")}]
[missing-cover item size])]))

View file

@ -0,0 +1,4 @@
(ns airsonic-ui.views.icon)
(defn icon [glyph]
[:span.icon [:span.oi {:data-glyph (name glyph)}]])

View file

@ -1,7 +1,8 @@
(ns airsonic-ui.views.song
(:require [re-frame.core :refer [dispatch]]
[airsonic-ui.events :as events]
[airsonic-ui.routes :as routes :refer [url-for]]))
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.views.icon :refer [icon]]))
(defn item [songs song]
(let [artist-id (:artistId song)]
@ -19,5 +20,10 @@
;; FIXME: This is very similar to album-listing
(defn listing [songs]
[:ul (for [[idx song] (map-indexed vector songs)]
[:li {:key idx} [item songs song]])])
[:table.table.is-striped.is-hoverable.is-fullwidth>tbody
(for [[idx song] (map-indexed vector songs)]
^{:key idx} [:tr
[:td.grow [item songs song]]
;; FIXME: Not implemented yet
[:td>a {:title "Play next"} [icon :plus]]
[:td>a {:title "Play last"} [icon :arrow-thick-right]]])])

View file

@ -1,16 +1,75 @@
@import "../../node_modules/bulma/bulma"
@import "../../node_modules/open-iconic/font/css/open-iconic.scss"
// area holding content & side navi
#app
main
margin-bottom: 0
// navi on the left side
.sidebar
min-height: 100vh
background: $dark
a
color: $light
.has-navbar-fixed-bottom .sidebar
// 2.5 = 3.25 ($navbar-height) - 0.75 ($padding)
min-height: calc(100vh - 2.5rem)
// bottom bar
.playback-area
background: $dark
color: $light
.navbar-menu
align-items: center
.audio-interaction
flex-grow: 1
.level-left
flex-grow: 1
flex-shrink: 0
.level-right
flex-grow: 0
flex-shrink: 1
padding-left: .5rem
padding-left: .5rem
padding-right: .5rem
.media
flex-grow: 1
align-items: center
progress
width: 100%
.progress.is-tiny
height: 0.25rem
height: .25rem
// cover images
.image.is-256x256
// for cover images
width: 256px
height: 256px
.album-preview .image.is-256x256
// make sure the grid doesn't overflow
width: auto
height: auto
max-width: 256px
max-height: 256px
.missing-cover
display: block
.album-preview
.title,
.subtitle
overflow: hidden
white-space: nowrap
text-overflow: ellipsis
.image.is-256x256
width: auto
height: auto
max-width: 256px
max-height: 256px
// occurs in album detail view
.table
.grow
width: 100%