mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 02:33:39 +02:00
Move all source files to src folder
This commit is contained in:
parent
cdf3785f82
commit
47c37e198c
20 changed files with 24 additions and 9 deletions
34
src/cljs/airsonic_ui/api.cljs
Normal file
34
src/cljs/airsonic_ui/api.cljs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
(ns airsonic-ui.api
|
||||
(:require [clojure.string :as str]
|
||||
[airsonic-ui.config :as config]))
|
||||
|
||||
(defn- encode [c]
|
||||
(js/encodeURIComponent c))
|
||||
|
||||
(defn url
|
||||
"Returns an absolute url to an API endpoint"
|
||||
[server endpoint params]
|
||||
(let [query (->> (assoc params
|
||||
:f "json"
|
||||
:c "airsonic-ui-cljs"
|
||||
:v "1.15.0")
|
||||
(map (fn [[k v]] (str (encode (name k)) "=" (encode v))))
|
||||
(str/join "&"))]
|
||||
(str server (when-not (str/ends-with? server "/") "/") "rest/" endpoint "?" query)))
|
||||
|
||||
(defn song-url [server credentials song]
|
||||
(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?
|
||||
"We need to look at the message body because the subsonic api always responds
|
||||
with status 200"
|
||||
[response]
|
||||
(= "failed" (-> response :subsonic-response :status)))
|
||||
|
||||
(defn- error-message
|
||||
[response]
|
||||
(let [{:keys [code message]} (-> response :subsonic-response :error)]
|
||||
(str "Code " code ": " message)))
|
||||
39
src/cljs/airsonic_ui/audio.cljs
Normal file
39
src/cljs/airsonic_ui/audio.cljs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
(ns airsonic-ui.audio
|
||||
(:require [re-frame.core :as re-frame]))
|
||||
|
||||
;; TODO: Manage buffering
|
||||
|
||||
(defonce audio (atom nil))
|
||||
|
||||
(defn ->status
|
||||
"Takes an audio object and returns a map describing its current status"
|
||||
[elem]
|
||||
{:ended? (.-ended elem)
|
||||
:loop? (.-loop elem)
|
||||
:muted? (.-muted elem)
|
||||
:paused? (.-paused elem)
|
||||
:current-src (.-currentSrc elem)
|
||||
:current-time (.-currentTime elem)})
|
||||
|
||||
; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics
|
||||
(defn attach-listeners! [el]
|
||||
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause"]]
|
||||
(.addEventListener el event #(re-frame/dispatch [:audio/update (->status el)]))))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:play-song
|
||||
(fn [song-url]
|
||||
(when-not @audio
|
||||
(reset! audio (js/Audio.))
|
||||
(attach-listeners! @audio))
|
||||
(.pause @audio)
|
||||
(set! (.-src @audio) song-url)
|
||||
(.play @audio)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:toggle-play-pause
|
||||
(fn [_]
|
||||
(let [a @audio]
|
||||
(if (.-paused a)
|
||||
(.play a)
|
||||
(.pause a)))))
|
||||
4
src/cljs/airsonic_ui/config.cljs
Normal file
4
src/cljs/airsonic_ui/config.cljs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
(ns airsonic-ui.config)
|
||||
|
||||
(def debug?
|
||||
^boolean goog.DEBUG)
|
||||
24
src/cljs/airsonic_ui/core.cljs
Normal file
24
src/cljs/airsonic_ui/core.cljs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
(ns airsonic-ui.core
|
||||
(:require [reagent.core :as reagent]
|
||||
[re-frame.core :as re-frame]
|
||||
[day8.re-frame.http-fx]
|
||||
[airsonic-ui.audio] ; <- just registers effects
|
||||
[airsonic-ui.routes :as routes]
|
||||
[airsonic-ui.events :as events]
|
||||
[airsonic-ui.views :as views]
|
||||
[airsonic-ui.config :as config]))
|
||||
|
||||
(defn dev-setup []
|
||||
(when config/debug?
|
||||
(enable-console-print!)
|
||||
(println "dev mode")))
|
||||
|
||||
(defn mount-root []
|
||||
(re-frame/clear-subscription-cache!)
|
||||
(reagent/render [views/main-panel] (.getElementById js/document "app")))
|
||||
|
||||
(defn ^:export init []
|
||||
(routes/start-routing!)
|
||||
(re-frame/dispatch-sync [::events/initialize-db])
|
||||
(dev-setup)
|
||||
(mount-root))
|
||||
7
src/cljs/airsonic_ui/db.cljs
Normal file
7
src/cljs/airsonic_ui/db.cljs
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
(ns airsonic-ui.db
|
||||
(:require [airsonic-ui.routes :as routes]))
|
||||
|
||||
(def default-db
|
||||
{:active-requests 0
|
||||
;; because navigate! executes asynchronously we force to display the login screen first
|
||||
:current-route [routes/default-route]})
|
||||
147
src/cljs/airsonic_ui/events.cljs
Normal file
147
src/cljs/airsonic_ui/events.cljs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
(ns airsonic-ui.events
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[ajax.core :as ajax]
|
||||
[airsonic-ui.routes :as routes]
|
||||
[airsonic-ui.db :as db]
|
||||
[airsonic-ui.api :as api]
|
||||
[day8.re-frame.tracing :refer-macros [fn-traced]])) ; <- useful to debug handlers
|
||||
|
||||
;; this is where all of the event handling takes place; the names put the events into
|
||||
;; the following categories:
|
||||
;; ::events/something-happening -> relevant to only this app
|
||||
;; :single-colon/something -> coming from external sources (e.g. :audio/... or :routes/...) that are potentially reusable
|
||||
|
||||
;; database reset / init
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::initialize-db
|
||||
(fn [_]
|
||||
db/default-db))
|
||||
|
||||
;; this is called with user and password to try and see if the credentials are
|
||||
;; correct; if yes, ::auth-success will be fired
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::authenticate
|
||||
(fn [{:keys [db]} [_ user pass server]]
|
||||
{:db (-> (update db :active-requests inc)
|
||||
(assoc :server server))
|
||||
:http-xhrio {:method :get
|
||||
:uri (api/url server "ping" {:u user :p pass})
|
||||
:response-format (ajax/json-response-format {:keywords? true})
|
||||
:on-success [::auth-success user pass]
|
||||
:on-failure [::api-failure]}}))
|
||||
|
||||
;; TODO: Test that credentials are associated
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::auth-success
|
||||
(fn [{:keys [db]} [_ user pass response]]
|
||||
;; TODO: Handle failures differently
|
||||
(let [login {:u user :p pass}]
|
||||
{:routes/set-credentials login
|
||||
:db (-> (update db :active-requests #(max (dec %) 0))
|
||||
(assoc :login login))
|
||||
: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
|
||||
(re-frame/reg-event-fx
|
||||
::logged-in
|
||||
(fn [_ _]
|
||||
{:routes/navigate [::routes/main]
|
||||
:show-nav-bar nil}))
|
||||
|
||||
;; TODO: Test that credentials are actually taken
|
||||
;; TODO: Move these in the future? events.cljs should just do wiring. We could
|
||||
;; implement api.cljs as a completely independent module.
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
:api-request
|
||||
(fn [{:keys [db]} [_ endpoint k params]]
|
||||
{:http-xhrio {:method :get
|
||||
:uri (api/url (:server db) endpoint (merge params (:login db)))
|
||||
:response-format (ajax/json-response-format {:keywords? true})
|
||||
:on-success [::api-success k]
|
||||
:on-failure [::api-failure]}}))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::api-success
|
||||
(fn [db [_ k response]]
|
||||
; we "unwrap" the responses
|
||||
(assoc db :response (-> response :subsonic-response k))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::api-failure
|
||||
(fn [db event]
|
||||
(println "api call gone bad; CORS headers missing? check for :status 0" event)
|
||||
db))
|
||||
|
||||
;; musique
|
||||
|
||||
; TODO: Make play, next and previous a bit prettier and more DRY
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
; sets up the db, starts to play a song and adds the rest to a playlist
|
||||
::play-songs
|
||||
(fn [{:keys [db]} [_ songs song]]
|
||||
{:play-song (api/song-url (:server db) (:login db) song)
|
||||
:db (-> db
|
||||
(assoc-in [:currently-playing :item] song)
|
||||
(assoc-in [:currently-playing :playlist] songs))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::next-song
|
||||
(fn [{:keys [db]} _]
|
||||
(let [playlist (-> db :currently-playing :playlist)
|
||||
current (-> db :currently-playing :item)
|
||||
next (first (rest (drop-while #(not= % current) playlist)))]
|
||||
(when next
|
||||
{:play-song (api/song-url (:server db) (:login db) next)
|
||||
:db (assoc-in db [:currently-playing :item] next)}))))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::previous-song
|
||||
(fn [{:keys [db]} _]
|
||||
(let [playlist (-> db :currently-playing :playlist)
|
||||
current (-> db :currently-playing :item)
|
||||
previous (last (take-while #(not= % current) playlist))]
|
||||
(when previous
|
||||
{:play-song (api/song-url (:server db) (:login db) previous)
|
||||
:db (assoc-in db [:currently-playing :item] previous)}))))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::toggle-play-pause
|
||||
(fn [_ _]
|
||||
{:toggle-play-pause nil}))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
:audio/update
|
||||
(fn [db [_ status]]
|
||||
; we receive this from the player once it's playing
|
||||
(assoc-in db [:currently-playing :status] status)))
|
||||
|
||||
;; routing
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
:routes/navigation
|
||||
(fn [{:keys [db]} [_ route params query]]
|
||||
;; all the naviagation logic is in routes.cljs; all we need to do here
|
||||
;; is say what actually happens once we've navigated succesfully
|
||||
{:db (assoc db :current-route [route params query])
|
||||
:dispatch (routes/route-data route params query)}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
:routes/unauthorized
|
||||
(fn [fx _]
|
||||
;; log out on 403
|
||||
{:routes/navigate [routes/default-route]
|
||||
:routes/unset-credentials nil
|
||||
:db db/default-db}))
|
||||
78
src/cljs/airsonic_ui/routes.cljs
Normal file
78
src/cljs/airsonic_ui/routes.cljs
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
(ns airsonic-ui.routes
|
||||
(:require [bide.core :as r]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(def default-route ::login)
|
||||
|
||||
(def router
|
||||
(r/router [["/" ::login]
|
||||
["/hello" ::main]
|
||||
["/artist/:id" ::artist-view]
|
||||
["/album/:id" ::album-view]]))
|
||||
|
||||
; use this in views to construct a url
|
||||
(defn url-for
|
||||
([k] (url-for k {}))
|
||||
([k params] (str "#" (r/resolve router k params))))
|
||||
|
||||
; which routes need valid login credentials?
|
||||
(def protected-routes #{::main ::artist-view ::album-view})
|
||||
|
||||
; which data should be requested for which route? can either be a vector or a function returning a vector
|
||||
|
||||
(defmulti route-data
|
||||
"Returns the events that take care of correct data being fetched."
|
||||
(fn [route-id & _] route-id))
|
||||
|
||||
(defmethod route-data :default [route-id params query] []) ; no data
|
||||
|
||||
(defmethod route-data ::main
|
||||
[route-id params query]
|
||||
[:api-request "getAlbumList2" :albumList2 {:type "recent"}])
|
||||
|
||||
(defmethod route-data ::artist-view
|
||||
[route-id params query]
|
||||
[:api-request "getArtist" :artist (select-keys params [:id])])
|
||||
|
||||
(defmethod route-data ::album-view
|
||||
[route-id params query]
|
||||
[:api-request "getAlbum" :album (select-keys params [:id])])
|
||||
|
||||
;; shouldn't need to change anything below
|
||||
|
||||
;; these are helper effects we can use to navigate; the first two manage an atom
|
||||
;; holding credentials, which is necessary to restrict certain routes, and the
|
||||
;; last one is used for actual navigation
|
||||
|
||||
(def credentials (atom nil))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:routes/set-credentials
|
||||
(fn [credentials']
|
||||
(reset! credentials credentials')))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:routes/unset-credentials
|
||||
(fn []
|
||||
(reset! credentials nil)))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:routes/navigate
|
||||
(fn [[route-id params query]]
|
||||
(println "calling ::navigate with" route-id params query)
|
||||
(r/navigate! router route-id params query)))
|
||||
|
||||
(defn can-access? [route]
|
||||
(or (not (protected-routes route)) @credentials))
|
||||
|
||||
(defn on-navigate
|
||||
[route-id params query]
|
||||
(if (can-access? route-id)
|
||||
(re-frame/dispatch [:routes/navigation route-id params query])
|
||||
(re-frame/dispatch [:routes/unauthorized route-id params query])))
|
||||
|
||||
(defn start-routing!
|
||||
"Initializes the router and makes sure the correct events get dispatched."
|
||||
[]
|
||||
(r/start! router {:default default-route
|
||||
:on-navigate on-navigate}))
|
||||
37
src/cljs/airsonic_ui/subs.cljs
Normal file
37
src/cljs/airsonic_ui/subs.cljs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
(ns airsonic-ui.subs
|
||||
(:require [re-frame.core :as re-frame]))
|
||||
|
||||
;; can be used to query the user's credentials
|
||||
;; TODO: Organize login credentials and server location differently (i.e. together)
|
||||
|
||||
(re-frame/reg-sub
|
||||
::login
|
||||
(fn [db]
|
||||
(:login db)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::server
|
||||
(fn [db]
|
||||
(:server db)))
|
||||
|
||||
;; current hashbang
|
||||
|
||||
(re-frame/reg-sub
|
||||
::current-route
|
||||
(fn [db]
|
||||
(:current-route db)))
|
||||
|
||||
;; ---
|
||||
|
||||
;; TODO: Make this nice and clean
|
||||
|
||||
(re-frame/reg-sub
|
||||
::current-content
|
||||
(fn [db]
|
||||
(-> 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)))
|
||||
52
src/cljs/airsonic_ui/views.cljs
Normal file
52
src/cljs/airsonic_ui/views.cljs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
(ns airsonic-ui.views
|
||||
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||
[airsonic-ui.config :as config]
|
||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||
[airsonic-ui.events :as events]
|
||||
[airsonic-ui.subs :as subs]
|
||||
|
||||
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
|
||||
[airsonic-ui.views.bottom-bar :refer [bottom-bar]]
|
||||
[airsonic-ui.views.login :refer [login-form]]
|
||||
[airsonic-ui.views.album :as album]
|
||||
[airsonic-ui.views.song :as song]))
|
||||
|
||||
;; TODO: Find better names and places for these.
|
||||
|
||||
(defn album-detail [content]
|
||||
[:div
|
||||
[:h2.title (str (:artist content) " - " (:name content))]
|
||||
[song/listing (:song content)]])
|
||||
|
||||
(defn artist-detail [content]
|
||||
[:div
|
||||
[:h2.title (:name content)]
|
||||
[album/listing (:album content)]])
|
||||
|
||||
(defn most-recent [content]
|
||||
[:div
|
||||
[:h2.title "Recently played"]
|
||||
[album/listing (:album content)]])
|
||||
|
||||
;; putting everything together
|
||||
|
||||
(defn app [route params query]
|
||||
(let [login @(subscribe [::subs/login])
|
||||
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])]
|
||||
[bottom-bar]]))
|
||||
|
||||
(defn main-panel []
|
||||
(let [[route params query] @(subscribe [::subs/current-route])]
|
||||
(case route
|
||||
::routes/login [login-form]
|
||||
[app route params query])))
|
||||
23
src/cljs/airsonic_ui/views/album.cljs
Normal file
23
src/cljs/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/cljs/airsonic_ui/views/bottom_bar.cljs
Normal file
41
src/cljs/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/cljs/airsonic_ui/views/breadcrumbs.cljs
Normal file
37
src/cljs/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/cljs/airsonic_ui/views/cover.cljs
Normal file
15
src/cljs/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/cljs/airsonic_ui/views/login.cljs
Normal file
42
src/cljs/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/cljs/airsonic_ui/views/song.cljs
Normal file
23
src/cljs/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