mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-06 18:33:38 +02:00
Put on some lipgloss
commit 9fa1a611e2fa093819b332791c28783a4f92a6dc
Author: Arne Schlüter <arne@schlueter.is>
Date: Fri May 18 00:00:31 2018 +0200
Add album previews
commit 317a6632b898039e370e3f8d52627e08a8c8186c
Author: Arne Schlüter <arne@schlueter.is>
Date: Fri May 18 00:00:06 2018 +0200
Fix cover art url
commit 0ba09903b96f5241853f003f679c0f407243f12a
Author: Arne Schlüter <arne@schlueter.is>
Date: Thu May 17 22:59:23 2018 +0200
Add bulma breadcrumbs
commit d21c7c8acc802101ff8ec096d5c6fad90e4f8ea7
Author: Arne Schlüter <arne@schlueter.is>
Date: Wed May 16 18:55:45 2018 +0200
Add basic styling to bottom bar
commit 23b37984ca9e8af84767e073492a42bf6c5924ea
Author: Arne Schlüter <arne@schlueter.is>
Date: Wed May 16 18:21:27 2018 +0200
Add retina version of cover component
commit d86a44bc5f7b7472f0084c47b691b0f7d151f497
Author: Arne Schlüter <arne@schlueter.is>
Date: Wed May 16 18:20:47 2018 +0200
Add info about server address to README
commit 4114581c259e17e3d0342755124e4fa56cd5dd3a
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue May 8 11:51:07 2018 +0200
Add cover image
commit e6cb0745b366cbce3c25f225d8e008f12fcaae8b
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue May 8 10:49:26 2018 +0200
Move views to their own namespaces
commit 5c24a47cc07b347beedd972e32ec145348a82d65
Author: Arne Schlüter <arne@schlueter.is>
Date: Mon May 7 18:10:00 2018 +0200
Add login styling
commit e04e0505f684d99316bdb3e875c403af2c28c127
Author: Arne Schlüter <arne@schlueter.is>
Date: Sun May 6 11:19:08 2018 +0200
Add gh-pages deploy script
commit e1a4cb4bb646def28989100f2084990863160dd9
Author: Arne Schlüter <arne@schlueter.is>
Date: Sun May 6 00:43:51 2018 +0200
Add bulma
This commit is contained in:
parent
94d6c25d9e
commit
8d24c1b42a
15 changed files with 2544 additions and 612 deletions
|
|
@ -21,7 +21,7 @@ To build the project make sure you have Node.js (v6.0.0), npm and Java 8 install
|
||||||
```
|
```
|
||||||
# after cloning the project, first install all dependencies
|
# after cloning the project, first install all dependencies
|
||||||
$ npm install
|
$ npm install
|
||||||
# start a continuous build with hot-code-reloading; first build takes a while
|
# start a continuous build with hot-code-reloading; first build takes a while. open http://localhost:8080
|
||||||
$ npm run dev
|
$ npm run dev
|
||||||
# build and optimize the code once for production
|
# build and optimize the code once for production
|
||||||
$ npm run build
|
$ npm run build
|
||||||
|
|
|
||||||
2623
package-lock.json
generated
2623
package-lock.json
generated
File diff suppressed because it is too large
Load diff
20
package.json
20
package.json
|
|
@ -5,19 +5,33 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "shadow-cljs watch app",
|
"build:cljs": "shadow-cljs release app",
|
||||||
"build": "rm -r public/app/js; shadow-cljs release app"
|
"build:sass": "node-sass --output-style compressed sass/app.sass public/app/style.css | sed 's/^/sass - /'",
|
||||||
|
"build": "rm -r public/app/*; run-p build:*; sed -i '' 's/\"\\/app\\//\".\\/app\\//g' public/index.html",
|
||||||
|
"dev:cljs": "shadow-cljs watch app",
|
||||||
|
"dev:sass": "{ node-sass sass/app.sass public/app/style.css; node-sass -w sass/app.sass public/app/style.css; } | sed 's/^/sass - /'",
|
||||||
|
"dev": "sed -i '' 's/\"\\.\\/app\\//\"\\/app\\//g' public/index.html; run-p dev:*",
|
||||||
|
"deploy": "gh-pages -d public"
|
||||||
},
|
},
|
||||||
"author": "Arne Schlüter",
|
"author": "Arne Schlüter",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git://github.com/heyarne/airsonic-ui.git"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"bulma": "^0.7.1",
|
||||||
"create-react-class": "^15.6.3",
|
"create-react-class": "^15.6.3",
|
||||||
"react": "^16.3.2",
|
"react": "^16.3.2",
|
||||||
"react-dom": "^16.3.2",
|
"react-dom": "^16.3.2",
|
||||||
"shadow-cljs": "^2.3.19"
|
"shadow-cljs": "^2.3.19"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"gh-pages": "^1.1.0",
|
||||||
|
"node-sass": "^4.9.0",
|
||||||
|
"npm-run-all": "^4.1.2",
|
||||||
"react-flip-move": "^3.0.1",
|
"react-flip-move": "^3.0.1",
|
||||||
"react-highlight.js": "^1.0.7"
|
"react-highlight.js": "^1.0.7",
|
||||||
|
"sass": "^1.3.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8">
|
||||||
<title>Airsonic UI</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Airsonic</title>
|
||||||
|
<link rel="stylesheet" href="/app/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"> </div>
|
<div id="app"></div>
|
||||||
<script src="/app/js/main.js"></script>
|
<script src="/app/js/main.js"></script>
|
||||||
<script>airsonic_ui.core.init()</script>
|
<script>airsonic_ui.core.init()</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
16
sass/app.sass
Normal file
16
sass/app.sass
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
@import "../node_modules/bulma/bulma"
|
||||||
|
|
||||||
|
.progress.is-tiny
|
||||||
|
height: 0.25rem
|
||||||
|
|
||||||
|
.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
|
||||||
|
|
@ -17,7 +17,10 @@
|
||||||
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
|
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
|
||||||
|
|
||||||
(defn song-url [server credentials song]
|
(defn song-url [server credentials song]
|
||||||
(url server "stream" (merge {:id (:id song)} credentials)))
|
(url server "stream" (merge (select-keys song [:id]) credentials)))
|
||||||
|
|
||||||
|
(defn cover-url [server credentials item size]
|
||||||
|
(url server "getCoverArt" (merge {:id (:coverArt item) :size size} credentials)))
|
||||||
|
|
||||||
(defn- api-error?
|
(defn- api-error?
|
||||||
"We need to look at the message body because the subsonic api always responds
|
"We need to look at the message body because the subsonic api always responds
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,20 @@
|
||||||
(assoc :login login))
|
(assoc :login login))
|
||||||
:dispatch [::logged-in]})))
|
:dispatch [::logged-in]})))
|
||||||
|
|
||||||
|
;; TODO: We have to find another solution for this once we have routes that
|
||||||
|
;; don't require a login but have the bottom controls
|
||||||
|
|
||||||
|
(re-frame/reg-fx
|
||||||
|
:show-nav-bar
|
||||||
|
(fn [_]
|
||||||
|
(.. js/document -documentElement -classList (add "has-navbar-fixed-bottom"))))
|
||||||
|
|
||||||
;; we do this in two steps to make sure the credentials are set once we navigate
|
;; we do this in two steps to make sure the credentials are set once we navigate
|
||||||
(re-frame/reg-event-fx
|
(re-frame/reg-event-fx
|
||||||
::logged-in
|
::logged-in
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{:routes/navigate [::routes/main]}))
|
{:routes/navigate [::routes/main]
|
||||||
|
:show-nav-bar nil}))
|
||||||
|
|
||||||
;; TODO: Test that credentials are actually taken
|
;; TODO: Test that credentials are actually taken
|
||||||
;; TODO: Move these in the future? events.cljs should just do wiring. We could
|
;; TODO: Move these in the future? events.cljs should just do wiring. We could
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,18 @@
|
||||||
(:require [re-frame.core :as re-frame]))
|
(:require [re-frame.core :as re-frame]))
|
||||||
|
|
||||||
;; can be used to query the user's credentials
|
;; can be used to query the user's credentials
|
||||||
|
;; TODO: Organize login credentials and server location differently (i.e. together)
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
::login
|
::login
|
||||||
(fn [db]
|
(fn [db]
|
||||||
(:login db)))
|
(:login db)))
|
||||||
|
|
||||||
|
(re-frame/reg-sub
|
||||||
|
::server
|
||||||
|
(fn [db]
|
||||||
|
(:server db)))
|
||||||
|
|
||||||
;; current hashbang
|
;; current hashbang
|
||||||
|
|
||||||
(re-frame/reg-sub
|
(re-frame/reg-sub
|
||||||
|
|
|
||||||
|
|
@ -1,138 +1,32 @@
|
||||||
(ns airsonic-ui.views
|
(ns airsonic-ui.views
|
||||||
(:require [re-frame.core :refer [dispatch subscribe]]
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
[reagent.core :as r]
|
|
||||||
[airsonic-ui.config :as config]
|
[airsonic-ui.config :as config]
|
||||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
[airsonic-ui.events :as events]
|
[airsonic-ui.events :as events]
|
||||||
[airsonic-ui.subs :as subs]))
|
[airsonic-ui.subs :as subs]
|
||||||
|
|
||||||
(defn- >reset!
|
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
|
||||||
"Sends all target values to the given atom"
|
[airsonic-ui.views.bottom-bar :refer [bottom-bar]]
|
||||||
[atom]
|
[airsonic-ui.views.login :refer [login-form]]
|
||||||
#(reset! atom (.. % -target -value)))
|
[airsonic-ui.views.album :as album]
|
||||||
|
[airsonic-ui.views.song :as song]))
|
||||||
|
|
||||||
;; login form
|
;; TODO: Find better names and places for these.
|
||||||
|
|
||||||
(defn login-form []
|
|
||||||
(let [user (r/atom "")
|
|
||||||
pass (r/atom "")
|
|
||||||
server (r/atom config/server)]
|
|
||||||
(fn []
|
|
||||||
[:div
|
|
||||||
[:div
|
|
||||||
[:span "User"]
|
|
||||||
[:input {:type "text"
|
|
||||||
:name "user"
|
|
||||||
:on-change (>reset! user)}]]
|
|
||||||
[:div
|
|
||||||
[:span "Password"]
|
|
||||||
[:input {:type "password"
|
|
||||||
:name "pass"
|
|
||||||
:on-change (>reset! pass)}]]
|
|
||||||
[:div
|
|
||||||
[:span "Server"]
|
|
||||||
[:input {:type "text"
|
|
||||||
:name "server"
|
|
||||||
:on-change (>reset! server)
|
|
||||||
:value @server}]]
|
|
||||||
[:div
|
|
||||||
[:button {:on-click #(dispatch [::events/authenticate @user @pass @server])} "Submit"]]])))
|
|
||||||
|
|
||||||
;; single album
|
|
||||||
|
|
||||||
(defn song-item [songs song]
|
|
||||||
(let [artist-id (:artistId song)]
|
|
||||||
[:div
|
|
||||||
[:a
|
|
||||||
(when artist-id {:href (url-for ::routes/artist-view {:id artist-id})})
|
|
||||||
(:artist song)]
|
|
||||||
" - "
|
|
||||||
[:a
|
|
||||||
{:href "#" :on-click (fn [e]
|
|
||||||
(.preventDefault e)
|
|
||||||
(dispatch [::events/play-songs songs song]))}
|
|
||||||
(:title song)]]))
|
|
||||||
|
|
||||||
(defn album-detail [content]
|
(defn album-detail [content]
|
||||||
[:div
|
[:div
|
||||||
[:h2 (str (:artist content) " - " (:name content))]
|
[:h2.title (str (:artist content) " - " (:name content))]
|
||||||
(let [songs (:song content)]
|
[song/listing (:song content)]])
|
||||||
[:ul (for [[idx song] (map-indexed vector songs)]
|
|
||||||
[:li {:key idx} [song-item songs song]])])])
|
|
||||||
|
|
||||||
;; single artist
|
|
||||||
|
|
||||||
(defn album-item [album]
|
|
||||||
(let [{:keys [artist artistId name coverArt year id]} album]
|
|
||||||
[:div
|
|
||||||
;; link to artist page
|
|
||||||
[:a {:href (url-for ::routes/artist-view {:id artistId})} artist]
|
|
||||||
" - "
|
|
||||||
;; link to album
|
|
||||||
[:a {:href (url-for ::routes/album-view {:id id})} name] (when year (str " (" year ")"))]))
|
|
||||||
|
|
||||||
(defn artist-detail [content]
|
(defn artist-detail [content]
|
||||||
[:div
|
[:div
|
||||||
[:h2 (:name content)]
|
[:h2.title (:name content)]
|
||||||
[:ul (for [[idx album] (map-indexed vector (:album content))]
|
[album/listing (:album content)]])
|
||||||
[:li {:key idx} [album-item album]])]])
|
|
||||||
|
|
||||||
;; TODO: album-list shouldn't know about the structure of content and should just get a list
|
|
||||||
|
|
||||||
(defn most-recent [content]
|
(defn most-recent [content]
|
||||||
[:div
|
[:div
|
||||||
[:h2 "Recently played"]
|
[:h2.title "Recently played"]
|
||||||
[:ul (for [[idx album] (map-indexed vector (:album content))]
|
[album/listing (:album content)]])
|
||||||
[:li {:key idx} [album-item album]])]])
|
|
||||||
|
|
||||||
;; top navigation
|
|
||||||
|
|
||||||
(defn content-type
|
|
||||||
"Helper to see what kind of server response"
|
|
||||||
[content]
|
|
||||||
(cond
|
|
||||||
(and (vector? (:album content)) (:id content)) :artist
|
|
||||||
(vector? (:song content)) :album
|
|
||||||
:else :unknown-content))
|
|
||||||
|
|
||||||
(defmulti breadcrumbs content-type)
|
|
||||||
|
|
||||||
(defmethod breadcrumbs :default [content]
|
|
||||||
[:div [:span "Start"]])
|
|
||||||
|
|
||||||
(defmethod breadcrumbs :artist [content]
|
|
||||||
[:div
|
|
||||||
[:span [:a {:href (url-for ::routes/main)} "Start"]]
|
|
||||||
[:span " · " (:name content)]])
|
|
||||||
|
|
||||||
(defmethod breadcrumbs :album [content]
|
|
||||||
[:div
|
|
||||||
[:span [:a {:href (url-for ::routes/main)} "Start"]]
|
|
||||||
[:span " · " [:a {:href (url-for ::routes/artist-view {:id (:artistId content)})} (:artist content)]]
|
|
||||||
[:span " · " (:name content)]])
|
|
||||||
|
|
||||||
;; currently playing / coming next / audio controls...
|
|
||||||
|
|
||||||
(defn current-song-info [{:keys [item status]}]
|
|
||||||
[:div
|
|
||||||
[:b "Currently playing: "]
|
|
||||||
[:div (:artist item) " - " (:title item)]
|
|
||||||
[:div (:current-time status) "s / " (:duration item) "s"]])
|
|
||||||
|
|
||||||
(defn playback-controls []
|
|
||||||
[:div
|
|
||||||
[:button {:on-click #(dispatch [::events/previous-song])} "previous"]
|
|
||||||
[:button {:on-click #(dispatch [::events/toggle-play-pause])} "play / pause"]
|
|
||||||
[:button {:on-click #(dispatch [::events/next-song])} "next"]
|
|
||||||
[:label [:input {:type "checkbox"}] "shuffle"]
|
|
||||||
[:label [:input {:type "checkbox"}] "repeat"]])
|
|
||||||
|
|
||||||
(defn bottom-bar []
|
|
||||||
[:div
|
|
||||||
(if-let [currently-playing @(subscribe [::subs/currently-playing])]
|
|
||||||
[current-song-info currently-playing]
|
|
||||||
[:span "Currently no song selected"])
|
|
||||||
[playback-controls]])
|
|
||||||
|
|
||||||
;; putting everything together
|
;; putting everything together
|
||||||
|
|
||||||
|
|
@ -140,19 +34,19 @@
|
||||||
(let [login @(subscribe [::subs/login])
|
(let [login @(subscribe [::subs/login])
|
||||||
content @(subscribe [::subs/current-content])]
|
content @(subscribe [::subs/current-content])]
|
||||||
[:div
|
[:div
|
||||||
[:span (str "Currently logged in as " (:u login))]
|
[:section.section>div.container
|
||||||
[breadcrumbs content]
|
[:div.level
|
||||||
(case route
|
[:div.level-left [:span (str "Currently logged in as " (:u login))]]
|
||||||
::routes/main [most-recent content]
|
[:div.level-right [:a {:on-click #(dispatch [::events/initialize-db]) :href "#"} "Logout"]]]
|
||||||
::routes/artist-view [artist-detail content]
|
[breadcrumbs content]
|
||||||
::routes/album-view [album-detail content])
|
(case route
|
||||||
[:a {:on-click #(dispatch [::events/initialize-db]) :href "#"} "Logout"]
|
::routes/main [most-recent content]
|
||||||
|
::routes/artist-view [artist-detail content]
|
||||||
|
::routes/album-view [album-detail content])]
|
||||||
[bottom-bar]]))
|
[bottom-bar]]))
|
||||||
|
|
||||||
(defn main-panel []
|
(defn main-panel []
|
||||||
(let [[route params query] @(subscribe [::subs/current-route])]
|
(let [[route params query] @(subscribe [::subs/current-route])]
|
||||||
[:div
|
(case route
|
||||||
[:h1 "Airsonic"]
|
::routes/login [login-form]
|
||||||
(case route
|
[app route params query])))
|
||||||
::routes/login [login-form]
|
|
||||||
[app route params query])]))
|
|
||||||
|
|
|
||||||
23
src/airsonic_ui/views/album.cljs
Normal file
23
src/airsonic_ui/views/album.cljs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
(ns airsonic-ui.views.album
|
||||||
|
(:require [airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
|
[airsonic-ui.views.cover :refer [cover]]))
|
||||||
|
|
||||||
|
(defn preview [album]
|
||||||
|
(let [{:keys [artist artistId name coverArt id]} album]
|
||||||
|
[:article.card.album-preview
|
||||||
|
[:div.card-image
|
||||||
|
[:a {:href (url-for ::routes/album-view {:id id})} [cover album 256]]]
|
||||||
|
[:div.card-content
|
||||||
|
;; link to album
|
||||||
|
[:div.title.is-5
|
||||||
|
[:a {:href (url-for ::routes/album-view {:id id})} name]]
|
||||||
|
;; link to artist page
|
||||||
|
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})} artist]]]]))
|
||||||
|
|
||||||
|
(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]])])])
|
||||||
41
src/airsonic_ui/views/bottom_bar.cljs
Normal file
41
src/airsonic_ui/views/bottom_bar.cljs
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
(ns airsonic-ui.views.bottom-bar
|
||||||
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
|
[airsonic-ui.events :as events]
|
||||||
|
[airsonic-ui.subs :as subs]
|
||||||
|
[airsonic-ui.views.cover :refer [cover]]))
|
||||||
|
|
||||||
|
;; currently playing / coming next / audio controls...
|
||||||
|
|
||||||
|
(defn current-song-info [{:keys [item status]}]
|
||||||
|
[:article
|
||||||
|
[:div (:artist item) " - " (:title item)]
|
||||||
|
[:progress.progress.is-tiny {:value (:current-time status)
|
||||||
|
:max (:duration item)}]])
|
||||||
|
|
||||||
|
(defn playback-controls []
|
||||||
|
[: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])
|
||||||
|
buttons))])
|
||||||
|
|
||||||
|
(def logo-url "https://airsonic.github.io/airsonic-ui/assets/images/logo/airsonic-dark-350x100.png")
|
||||||
|
|
||||||
|
(defn bottom-bar []
|
||||||
|
(let [currently-playing @(subscribe [::subs/currently-playing])]
|
||||||
|
[:nav.navbar.is-fixed-bottom
|
||||||
|
[:div.navbar-brand
|
||||||
|
[:div.navbar-item
|
||||||
|
[:img {:src logo-url}]]]
|
||||||
|
[:div.navbar-menu.is-active
|
||||||
|
(if currently-playing
|
||||||
|
;; show song info
|
||||||
|
[:section.level
|
||||||
|
[: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]]]
|
||||||
|
;; not playing anything
|
||||||
|
[:span "Currently no song selected"])]]))
|
||||||
37
src/airsonic_ui/views/breadcrumbs.cljs
Normal file
37
src/airsonic_ui/views/breadcrumbs.cljs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
(ns airsonic-ui.views.breadcrumbs
|
||||||
|
(:require [airsonic-ui.routes :as routes :refer [url-for]]))
|
||||||
|
|
||||||
|
;; Breadcrumbs are implemented in such a way that they provide a stringent
|
||||||
|
;; hierarchy no matter how you came to the url. They should allow easy
|
||||||
|
;; navigation upwards that hierarchy (e.g. album -> artist)
|
||||||
|
|
||||||
|
(defn content-type
|
||||||
|
"Helper to see what kind of server response"
|
||||||
|
[content]
|
||||||
|
(cond
|
||||||
|
(and (vector? (:album content)) (:id content)) :artist
|
||||||
|
(vector? (:song content)) :album
|
||||||
|
:else :unknown-content))
|
||||||
|
|
||||||
|
(defn- bulma-breadcrumbs [& items]
|
||||||
|
[:nav.breadcrumb {:aria-label "breadcrumbs"}
|
||||||
|
[:ul
|
||||||
|
(for [[idx [href label]] (map-indexed vector (butlast items))]
|
||||||
|
[:li {:key idx} [:a {:href href} label]])
|
||||||
|
[:li.is-active>a (last items)]]])
|
||||||
|
|
||||||
|
(defmulti breadcrumbs content-type)
|
||||||
|
|
||||||
|
(defmethod breadcrumbs :default [content]
|
||||||
|
[bulma-breadcrumbs "Start"])
|
||||||
|
|
||||||
|
(defmethod breadcrumbs :artist [content]
|
||||||
|
[bulma-breadcrumbs
|
||||||
|
[(url-for ::routes/main) "Start"]
|
||||||
|
(:name content)])
|
||||||
|
|
||||||
|
(defmethod breadcrumbs :album [content]
|
||||||
|
[bulma-breadcrumbs
|
||||||
|
[(url-for ::routes/main) "Start"]
|
||||||
|
[(url-for ::routes/artist-view {:id (:artistId content)}) (:artist content)]
|
||||||
|
(:name content)])
|
||||||
15
src/airsonic_ui/views/cover.cljs
Normal file
15
src/airsonic_ui/views/cover.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns airsonic-ui.views.cover
|
||||||
|
(:require [re-frame.core :refer [subscribe]]
|
||||||
|
[airsonic-ui.subs :as subs]
|
||||||
|
[airsonic-ui.api :as api]))
|
||||||
|
|
||||||
|
;; FIXME: The direct dependency on these subs is a bit ugly
|
||||||
|
|
||||||
|
(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")}]]))
|
||||||
42
src/airsonic_ui/views/login.cljs
Normal file
42
src/airsonic_ui/views/login.cljs
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
(ns airsonic-ui.views.login
|
||||||
|
(:require [reagent.core :as r]
|
||||||
|
[re-frame.core :refer [dispatch]]
|
||||||
|
[airsonic-ui.events :as events]))
|
||||||
|
|
||||||
|
(defn- >reset!
|
||||||
|
"Sends an event's target values to the given atom"
|
||||||
|
[atom]
|
||||||
|
#(reset! atom (.. % -target -value)))
|
||||||
|
|
||||||
|
;; login form
|
||||||
|
|
||||||
|
(defn login-form []
|
||||||
|
(let [user (r/atom "")
|
||||||
|
pass (r/atom "")
|
||||||
|
server (r/atom (.. js/window -location -origin))
|
||||||
|
submit (fn [e]
|
||||||
|
(.preventDefault e)
|
||||||
|
(dispatch [::events/authenticate @user @pass @server]))]
|
||||||
|
(fn []
|
||||||
|
[:section.hero.is-fullheight>div.hero-body
|
||||||
|
[:div.container.has-text-centered>div.column.is-4.is-offset-4
|
||||||
|
[:h3.title.has-text-grey "Airsonic"]
|
||||||
|
[:p.subtitle.has-text-grey "Please login to proceed"]
|
||||||
|
[:div.box
|
||||||
|
[:form {:on-submit submit}
|
||||||
|
[:div.field>div.control
|
||||||
|
[:input.input.is-large {:type "text"
|
||||||
|
:name "user"
|
||||||
|
:placeholder "Username"
|
||||||
|
:on-change (>reset! user)}]]
|
||||||
|
[:div.field>div.control
|
||||||
|
[:input.input.is-large {:type "password"
|
||||||
|
:name "pass"
|
||||||
|
:placeholder "Password"
|
||||||
|
:on-change (>reset! pass)}]]
|
||||||
|
[:div.field>div.control
|
||||||
|
[:input.input.is-large {:type "text"
|
||||||
|
:name "server"
|
||||||
|
:on-change (>reset! server)
|
||||||
|
:value @server}]]
|
||||||
|
[:button.button.is-block.is-info.is-large.is-fullwidth {:type "submit"} "Submit"]]]]])))
|
||||||
23
src/airsonic_ui/views/song.cljs
Normal file
23
src/airsonic_ui/views/song.cljs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
(ns airsonic-ui.views.song
|
||||||
|
(:require [re-frame.core :refer [dispatch]]
|
||||||
|
[airsonic-ui.events :as events]
|
||||||
|
[airsonic-ui.routes :as routes :refer [url-for]]))
|
||||||
|
|
||||||
|
(defn item [songs song]
|
||||||
|
(let [artist-id (:artistId song)]
|
||||||
|
[:div
|
||||||
|
[:a
|
||||||
|
(when artist-id {:href (url-for ::routes/artist-view {:id artist-id})})
|
||||||
|
(:artist song)]
|
||||||
|
" - "
|
||||||
|
[:a
|
||||||
|
{:href "#" :on-click (fn [e]
|
||||||
|
(.preventDefault e)
|
||||||
|
(dispatch [::events/play-songs songs song]))}
|
||||||
|
(:title song)]]))
|
||||||
|
|
||||||
|
;; 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]])])
|
||||||
Loading…
Add table
Add a link
Reference in a new issue