mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-07 02:33:39 +02:00
Merge feature/search
Squashed commit of the following:
commit 8a19df91f8daa1b791d40cc910947c94355a8d0d
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Aug 28 16:06:35 2018 +0200
Implement search UI (closes #19)
commit bf661dd25ec9f1d5569df88a8a87f94c1bc1b317
Author: Arne Schlüter <arne@schlueter.is>
Date: Tue Aug 28 11:09:46 2018 +0200
Re-add subscription for single endpoint and move helpers to a different location
This commit is contained in:
parent
d26decb2ff
commit
7653af5dd1
22 changed files with 236 additions and 49 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
(ns airsonic-ui.api.helpers
|
(ns airsonic-ui.api.helpers
|
||||||
(:require [clojure.string :as str]))
|
(:require [clojure.string :as str]
|
||||||
|
[clojure.set :as set]))
|
||||||
|
|
||||||
(def default-params {:f "json"
|
(def default-params {:f "json"
|
||||||
:c "airsonic-ui-cljs"
|
:c "airsonic-ui-cljs"
|
||||||
|
|
@ -41,11 +42,22 @@
|
||||||
"Retrieves the actual response body"
|
"Retrieves the actual response body"
|
||||||
[response]
|
[response]
|
||||||
(if (is-error? response)
|
(if (is-error? response)
|
||||||
(let [error (:error response)]
|
(throw (->exception response))
|
||||||
(throw (->exception response)))
|
|
||||||
(unwrap-response* response)))
|
(unwrap-response* response)))
|
||||||
|
|
||||||
(defn error-msg
|
(defn error-msg
|
||||||
[exception-info]
|
[exception-info]
|
||||||
(let [{:keys [code message]} (ex-data exception-info)]
|
(let [{:keys [code message]} (ex-data exception-info)]
|
||||||
(str "Error " code ": " message)))
|
(str "Error " code ": " message)))
|
||||||
|
|
||||||
|
(defn content-type
|
||||||
|
"Given some piece of data returned by the api, returns a keyword that
|
||||||
|
describes what we look at"
|
||||||
|
[data]
|
||||||
|
(keyword :content-type
|
||||||
|
(condp set/subset? (set (keys data))
|
||||||
|
#{:path} :song
|
||||||
|
#{:artistId :name :songCount :artist} :album
|
||||||
|
#{:id :name :albumCount} :artist
|
||||||
|
:unknown)))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,13 @@
|
||||||
(:require [clojure.string :as str]
|
(:require [clojure.string :as str]
|
||||||
[re-frame.core :refer [reg-sub]]))
|
[re-frame.core :refer [reg-sub]]))
|
||||||
|
|
||||||
|
(defn response-for
|
||||||
|
"Returns the cached response for a single endpoint"
|
||||||
|
[db [_ endpoint params]]
|
||||||
|
(get-in db [:api/responses [endpoint params]]))
|
||||||
|
|
||||||
|
(reg-sub :api/response-for response-for)
|
||||||
|
|
||||||
(defn endpoint->kw
|
(defn endpoint->kw
|
||||||
"Given an endpoint like `getAlbumList2`, returns a cleaned keyword like
|
"Given an endpoint like `getAlbumList2`, returns a cleaned keyword like
|
||||||
`:album-list``.
|
`:album-list``.
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
"Implements playlist queues that support different kinds of repetition and
|
"Implements playlist queues that support different kinds of repetition and
|
||||||
song ordering."
|
song ordering."
|
||||||
(:refer-clojure :exclude [peek])
|
(:refer-clojure :exclude [peek])
|
||||||
(:require [airsonic-ui.utils.helpers :refer [find-where]]))
|
(:require [airsonic-ui.helpers :refer [find-where]]))
|
||||||
|
|
||||||
(defrecord Playlist [queue playback-mode repeat-mode]
|
(defrecord Playlist [queue playback-mode repeat-mode]
|
||||||
cljs.core/ICounted
|
cljs.core/ICounted
|
||||||
|
|
|
||||||
15
src/cljs/airsonic_ui/components/search/events.cljs
Normal file
15
src/cljs/airsonic_ui/components/search/events.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns airsonic-ui.components.search.events
|
||||||
|
(:require [re-frame.core :refer [reg-event-fx reg-event-db]]
|
||||||
|
[airsonic-ui.routes :as routes]))
|
||||||
|
|
||||||
|
(reg-event-db
|
||||||
|
;; this is called on navigation and handled in routes.cljs; the reason is that
|
||||||
|
;; when we're navigating to search?query=foo we don't have the term in our db.
|
||||||
|
:search/restore-term-from-param
|
||||||
|
(fn [db [_ term]]
|
||||||
|
(assoc-in db [:search :term] term)))
|
||||||
|
|
||||||
|
(reg-event-fx
|
||||||
|
:search/do-search
|
||||||
|
(fn do-search [fx [_ term]]
|
||||||
|
{:dispatch [:routes/do-navigation [::routes/search {} {:query term}]]}))
|
||||||
5
src/cljs/airsonic_ui/components/search/subs.cljs
Normal file
5
src/cljs/airsonic_ui/components/search/subs.cljs
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
(ns airsonic-ui.components.search.subs
|
||||||
|
(:require [re-frame.core :refer [reg-sub subscribe]]))
|
||||||
|
|
||||||
|
(reg-sub :search/current-term (fn current-term [db _]
|
||||||
|
(get-in db [:search :term])))
|
||||||
67
src/cljs/airsonic_ui/components/search/views.cljs
Normal file
67
src/cljs/airsonic_ui/components/search/views.cljs
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
(ns airsonic-ui.components.search.views
|
||||||
|
(:require [clojure.pprint :refer [pprint]]
|
||||||
|
[re-frame.core :refer [dispatch subscribe]]
|
||||||
|
[goog.functions :refer [debounce]]
|
||||||
|
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
|
[airsonic-ui.views.song :as song]
|
||||||
|
[airsonic-ui.views.cover :refer [card]]))
|
||||||
|
|
||||||
|
(defn form []
|
||||||
|
(let [search-term @(subscribe [:search/current-term])
|
||||||
|
throttled-search (debounce #(dispatch [:search/do-search (.. % -target -value)]) 100)]
|
||||||
|
(fn []
|
||||||
|
[:form {:on-submit #(.preventDefault %)}
|
||||||
|
[:div.feld>p.control
|
||||||
|
[:input.input {:on-change (fn [e]
|
||||||
|
;; the event might be gone when we the dispatched
|
||||||
|
;; function is fired, we need to persist it
|
||||||
|
(.persist e)
|
||||||
|
(throttled-search e))
|
||||||
|
:default-value search-term
|
||||||
|
:placeholder "Search"}]]])))
|
||||||
|
|
||||||
|
(defn artist-results [{:keys [artist]}]
|
||||||
|
[:div.columns.is-multiline.is-mobile
|
||||||
|
(for [[idx artist] (map-indexed vector artist)]
|
||||||
|
(let [url #(url-for ::routes/artist-view (select-keys % [:id]))]
|
||||||
|
^{:key idx} [:div.column.is-2
|
||||||
|
[card artist
|
||||||
|
:url-fn url
|
||||||
|
:content [:div>a
|
||||||
|
{:href (url artist), :title (:name artist)}
|
||||||
|
(:name artist)]]]))])
|
||||||
|
|
||||||
|
(defn album-results [{:keys [album]}]
|
||||||
|
[:div.columns.is-multiline.is-mobile
|
||||||
|
(for [[idx album] (map-indexed vector album)]
|
||||||
|
(let [url #(url-for ::routes/album-view (select-keys % [:id]))
|
||||||
|
title (str (:name album) " (" (:artist album) ")")]
|
||||||
|
^{:key idx} [:div.column.is-2 [card album
|
||||||
|
:url-fn url
|
||||||
|
:content [:div>a
|
||||||
|
{:href (url album), :title title}
|
||||||
|
title]]]))])
|
||||||
|
|
||||||
|
(defn song-results [{:keys [song]}]
|
||||||
|
[song/listing song])
|
||||||
|
|
||||||
|
(defn results [{:keys [search]}]
|
||||||
|
(let [term @(subscribe [:search/current-term])]
|
||||||
|
[:div
|
||||||
|
[:h2.title (str "Search results for \"" term "\"")]
|
||||||
|
(if (empty? search)
|
||||||
|
[:p "The server returned no results."]
|
||||||
|
[:div.content
|
||||||
|
(when-not (empty? (:artist search))
|
||||||
|
[:section.section.is-small
|
||||||
|
[:h3.subtitle.is-5 "Artists"]
|
||||||
|
[artist-results search]])
|
||||||
|
(when-not (empty? (:album search))
|
||||||
|
[:section.section.is-small
|
||||||
|
[:h3.subtitle.is-5 "Albums"]
|
||||||
|
[album-results search]])
|
||||||
|
(when-not (empty? (:song search))
|
||||||
|
[:section.section.is-small
|
||||||
|
[:h3.subtitle.is-5 "Songs"]
|
||||||
|
[song-results search]])])
|
||||||
|
[:pre (with-out-str (pprint search))]]))
|
||||||
|
|
@ -10,6 +10,8 @@
|
||||||
[airsonic-ui.audio.core]
|
[airsonic-ui.audio.core]
|
||||||
[airsonic-ui.api.events]
|
[airsonic-ui.api.events]
|
||||||
[airsonic-ui.api.subs]
|
[airsonic-ui.api.subs]
|
||||||
|
[airsonic-ui.components.search.events]
|
||||||
|
[airsonic-ui.components.search.subs]
|
||||||
[airsonic-ui.events :as events]
|
[airsonic-ui.events :as events]
|
||||||
[airsonic-ui.views :as views]
|
[airsonic-ui.views :as views]
|
||||||
[airsonic-ui.config :as config]))
|
[airsonic-ui.config :as config]))
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(ns airsonic-ui.utils.helpers
|
(ns airsonic-ui.helpers
|
||||||
"Assorted helper functions"
|
"Assorted helper functions"
|
||||||
(:require [re-frame.core :as rf]))
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
|
@ -16,3 +16,9 @@
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(.preventDefault e)
|
(.preventDefault e)
|
||||||
(rf/dispatch ev)))
|
(rf/dispatch ev)))
|
||||||
|
|
||||||
|
(defn add-classes
|
||||||
|
"Adds one or more classes to a hiccup keyword"
|
||||||
|
[elem & classes]
|
||||||
|
(keyword (apply str (name elem) (->> (filter identity classes)
|
||||||
|
(map #(str "." (name %)))))))
|
||||||
|
|
@ -5,11 +5,12 @@
|
||||||
|
|
||||||
(def default-route ::login)
|
(def default-route ::login)
|
||||||
|
|
||||||
(def router
|
(defonce router
|
||||||
(r/router [["/" ::login]
|
(r/router [["/" ::login]
|
||||||
["/main" ::main]
|
["/main" ::main]
|
||||||
["/artist/:id" ::artist-view]
|
["/artist/:id" ::artist-view]
|
||||||
["/album/:id" ::album-view]]))
|
["/album/:id" ::album-view]
|
||||||
|
["/search" ::search]]))
|
||||||
|
|
||||||
;; use this in views to construct a url
|
;; use this in views to construct a url
|
||||||
(defn url-for
|
(defn url-for
|
||||||
|
|
@ -17,7 +18,7 @@
|
||||||
([k params] (str "#" (r/resolve router k params))))
|
([k params] (str "#" (r/resolve router k params))))
|
||||||
|
|
||||||
;; which routes need valid login credentials?
|
;; which routes need valid login credentials?
|
||||||
(def protected-routes #{::main ::artist-view ::album-view})
|
(def protected-routes #{::main ::artist-view ::album-view ::search})
|
||||||
|
|
||||||
;; which data should be requested for which route? can either be a vector or a function returning a vector
|
;; which data should be requested for which route? can either be a vector or a function returning a vector
|
||||||
|
|
||||||
|
|
@ -42,6 +43,11 @@
|
||||||
[route-id params query]
|
[route-id params query]
|
||||||
[:api/request "getAlbum" (select-keys params [:id])])
|
[:api/request "getAlbum" (select-keys params [:id])])
|
||||||
|
|
||||||
|
(defmethod -route-events ::search
|
||||||
|
[route-id params query]
|
||||||
|
[[:search/restore-term-from-param (:query query)]
|
||||||
|
[:api/request "search3" query]])
|
||||||
|
|
||||||
;; shouldn't need to change anything below
|
;; shouldn't need to change anything below
|
||||||
|
|
||||||
(defn- n-events?
|
(defn- n-events?
|
||||||
|
|
@ -91,8 +97,9 @@
|
||||||
credentials'(get-in context [:coeffects :db :credentials])]
|
credentials'(get-in context [:coeffects :db :credentials])]
|
||||||
(println "calling do-navigation with" route credentials')
|
(println "calling do-navigation with" route credentials')
|
||||||
(reset! credentials credentials')
|
(reset! credentials credentials')
|
||||||
|
(println "context" context)
|
||||||
(apply r/navigate! router route)
|
(apply r/navigate! router route)
|
||||||
context))))
|
(dissoc context :event)))))
|
||||||
|
|
||||||
(re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))
|
(re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,15 @@
|
||||||
[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]
|
||||||
|
[airsonic-ui.helpers :refer [add-classes]]
|
||||||
|
|
||||||
[airsonic-ui.views.notifications :refer [notification-list]]
|
[airsonic-ui.views.notifications :refer [notification-list]]
|
||||||
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
|
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
|
||||||
[airsonic-ui.views.audio-player :refer [audio-player]]
|
[airsonic-ui.views.audio-player :refer [audio-player]]
|
||||||
[airsonic-ui.views.login :refer [login-form]]
|
[airsonic-ui.views.login :refer [login-form]]
|
||||||
[airsonic-ui.views.album :as album]
|
[airsonic-ui.views.album :as album]
|
||||||
[airsonic-ui.views.song :as song]))
|
[airsonic-ui.views.song :as song]
|
||||||
|
[airsonic-ui.components.search.views :as search]))
|
||||||
|
|
||||||
;; TODO: Find better names and places for these.
|
;; TODO: Find better names and places for these.
|
||||||
|
|
||||||
|
|
@ -31,6 +33,7 @@
|
||||||
|
|
||||||
(defn sidebar [user]
|
(defn sidebar [user]
|
||||||
[:aside.menu.section
|
[:aside.menu.section
|
||||||
|
[search/form]
|
||||||
[:p.menu-label "Music"]
|
[:p.menu-label "Music"]
|
||||||
[:ul.menu-list
|
[:ul.menu-list
|
||||||
[:li [:a "By artist"]]
|
[:li [:a "By artist"]]
|
||||||
|
|
@ -56,20 +59,21 @@
|
||||||
[:main.columns
|
[:main.columns
|
||||||
[:div.column.is-2.sidebar
|
[:div.column.is-2.sidebar
|
||||||
[sidebar user]]
|
[sidebar user]]
|
||||||
[:div.column
|
[:div.column.is-10
|
||||||
[:section.section
|
[:section.section
|
||||||
[breadcrumbs content]
|
[breadcrumbs content]
|
||||||
(case route-id
|
(case route-id
|
||||||
::routes/main [most-recent content]
|
::routes/main [most-recent content]
|
||||||
::routes/artist-view [artist-detail content]
|
::routes/artist-view [artist-detail content]
|
||||||
::routes/album-view [album-detail content])]]]
|
::routes/album-view [album-detail content]
|
||||||
|
::routes/search [search/results content])]]]
|
||||||
[audio-player]]))
|
[audio-player]]))
|
||||||
|
|
||||||
(defn main-panel []
|
(defn main-panel []
|
||||||
(let [notifications @(subscribe [::subs/notifications])
|
(let [notifications @(subscribe [::subs/notifications])
|
||||||
is-booting? @(subscribe [::subs/is-booting?])
|
is-booting? @(subscribe [::subs/is-booting?])
|
||||||
[route-id params query] @(subscribe [:routes/current-route])]
|
[route-id params query] @(subscribe [:routes/current-route])]
|
||||||
[:div
|
[(add-classes :div route-id)
|
||||||
[notification-list notifications]
|
[notification-list notifications]
|
||||||
(if is-booting?
|
(if is-booting?
|
||||||
[:div.app-loading>div.loader]
|
[:div.app-loading>div.loader]
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
(ns airsonic-ui.views.album
|
(ns airsonic-ui.views.album
|
||||||
(:require [airsonic-ui.routes :as routes :refer [url-for]]
|
(:require [airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
[airsonic-ui.views.cover :refer [cover]]))
|
[airsonic-ui.views.cover :refer [cover card]]))
|
||||||
|
|
||||||
(defn preview [album]
|
(defn preview [album]
|
||||||
(let [{:keys [artist artistId name coverArt id]} album]
|
(let [{:keys [artist artistId name id]} album]
|
||||||
[:article.card.album-preview
|
[card album
|
||||||
[:div.card-image
|
:url-fn #(url-for ::routes/album-view {:id id})
|
||||||
[:a {:href (url-for ::routes/album-view {:id id})} [cover album 256]]]
|
:content [:div
|
||||||
[:div.card-content
|
;; link to album
|
||||||
;; link to album
|
[:div.title.is-5
|
||||||
[:div.title.is-5
|
[:a {:href (url-for ::routes/album-view {:id id})
|
||||||
[:a {:href (url-for ::routes/album-view {:id id})} name]]
|
:title name} name]]
|
||||||
;; link to artist page
|
;; link to artist page
|
||||||
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})} artist]]]]))
|
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})
|
||||||
|
:title artist} artist]]]]))
|
||||||
|
|
||||||
(defn listing [albums]
|
(defn listing [albums]
|
||||||
;; always show 5 in a row
|
;; always show 5 in a row
|
||||||
[:div
|
[:div.columns.is-multiline.is-mobile
|
||||||
[:div.columns.is-multiline.is-mobile
|
(for [[idx album] (map-indexed vector albums)]
|
||||||
(for [[idx album] (map-indexed vector albums)]
|
^{:key idx} [:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
|
||||||
^{:key idx} [:div.column.is-one-fifth-desktop.is-one-quarter-tablet.is-half-mobile
|
[preview album]])])
|
||||||
[preview album]])]])
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
(ns airsonic-ui.views.audio-player
|
(ns airsonic-ui.views.audio-player
|
||||||
(:require [re-frame.core :refer [subscribe]]
|
(:require [re-frame.core :refer [subscribe]]
|
||||||
[airsonic-ui.utils.helpers :refer [dispatch]]
|
[airsonic-ui.helpers :refer [add-classes dispatch]]
|
||||||
[airsonic-ui.events :as events]
|
[airsonic-ui.events :as events]
|
||||||
[airsonic-ui.views.cover :refer [cover]]
|
[airsonic-ui.views.cover :refer [cover]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]))
|
[airsonic-ui.views.icon :refer [icon]]))
|
||||||
|
|
@ -25,12 +25,6 @@
|
||||||
[icon icon-glyph]])
|
[icon icon-glyph]])
|
||||||
buttons))])
|
buttons))])
|
||||||
|
|
||||||
(defn- add-classes
|
|
||||||
"Adds one or more classes to a hiccup keyword"
|
|
||||||
[elem & classes]
|
|
||||||
(keyword (apply str (name elem) (->> (filter identity classes)
|
|
||||||
(map #(str "." (name %)))))))
|
|
||||||
|
|
||||||
(defn- toggle-shuffle [playback-mode]
|
(defn- toggle-shuffle [playback-mode]
|
||||||
(dispatch [::events/set-playback-mode (if (= playback-mode :shuffled)
|
(dispatch [::events/set-playback-mode (if (= playback-mode :shuffled)
|
||||||
:linear :shuffled)]))
|
:linear :shuffled)]))
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,13 @@
|
||||||
;; hierarchy no matter how you came to the url. They should allow easy
|
;; hierarchy no matter how you came to the url. They should allow easy
|
||||||
;; navigation upwards that hierarchy (e.g. album -> artist)
|
;; navigation upwards that hierarchy (e.g. album -> artist)
|
||||||
|
|
||||||
(defn content-type
|
(defn page-type
|
||||||
"Helper to see what kind of view we're currently dealing with"
|
"Helper to see what kind of view we're currently dealing with"
|
||||||
[content]
|
[content]
|
||||||
(case (set (keys content))
|
(case (set (keys content))
|
||||||
#{:artist :artist-info} :artist
|
#{:artist :artist-info} :artist
|
||||||
#{:album} :album
|
#{:album} :album
|
||||||
|
#{:search} :search
|
||||||
:other-content))
|
:other-content))
|
||||||
|
|
||||||
(defn- bulma-breadcrumbs [& items]
|
(defn- bulma-breadcrumbs [& items]
|
||||||
|
|
@ -20,7 +21,7 @@
|
||||||
[:li {:key idx} [:a {:href href} label]])
|
[:li {:key idx} [:a {:href href} label]])
|
||||||
[:li.is-active>a (last items)]]])
|
[:li.is-active>a (last items)]]])
|
||||||
|
|
||||||
(defmulti breadcrumbs content-type)
|
(defmulti breadcrumbs page-type)
|
||||||
|
|
||||||
(defmethod breadcrumbs :default [content]
|
(defmethod breadcrumbs :default [content]
|
||||||
[bulma-breadcrumbs "Start"])
|
[bulma-breadcrumbs "Start"])
|
||||||
|
|
@ -35,3 +36,8 @@
|
||||||
[(url-for ::routes/main) "Start"]
|
[(url-for ::routes/main) "Start"]
|
||||||
[(url-for ::routes/artist-view {:id (:artistId album)}) (:artist album)]
|
[(url-for ::routes/artist-view {:id (:artistId album)}) (:artist album)]
|
||||||
(:name album)])
|
(:name album)])
|
||||||
|
|
||||||
|
(defmethod breadcrumbs :search [_]
|
||||||
|
[bulma-breadcrumbs
|
||||||
|
[(url-for ::routes/main) "Start"]
|
||||||
|
"Search"])
|
||||||
|
|
|
||||||
|
|
@ -65,3 +65,8 @@
|
||||||
[:img {:src original
|
[:img {:src original
|
||||||
:srcSet (str original ", " retina " 2x")}]
|
:srcSet (str original ", " retina " 2x")}]
|
||||||
[missing-cover item size])]))
|
[missing-cover item size])]))
|
||||||
|
|
||||||
|
(defn card [item & {:keys [url-fn content size] :or {size 256}}]
|
||||||
|
[:article.card.preview-card
|
||||||
|
[:div.card-image [:a {:href (url-fn item)} [cover item size]]]
|
||||||
|
[:div.card-content content]])
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(ns airsonic-ui.views.icon)
|
(ns airsonic-ui.views.icon)
|
||||||
|
|
||||||
(defn icon [glyph]
|
(defn icon [glyph & extra]
|
||||||
[:span.icon [:span.oi {:data-glyph (name glyph)}]])
|
[:span.icon [:span.oi {:data-glyph (name glyph)}]])
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
(ns airsonic-ui.views.song
|
(ns airsonic-ui.views.song
|
||||||
(:require [airsonic-ui.utils.helpers :refer [dispatch]]
|
(:require [airsonic-ui.helpers :refer [dispatch]]
|
||||||
[airsonic-ui.events :as events]
|
[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]]))
|
[airsonic-ui.views.icon :refer [icon]]))
|
||||||
|
|
@ -7,9 +7,9 @@
|
||||||
(defn item [songs song idx]
|
(defn item [songs song idx]
|
||||||
(let [artist-id (:artistId song)]
|
(let [artist-id (:artistId song)]
|
||||||
[:div
|
[:div
|
||||||
[:a
|
(if artist-id
|
||||||
(when artist-id {:href (url-for ::routes/artist-view {:id artist-id})})
|
[:a {:href (url-for ::routes/artist-view {:id artist-id})} (:artist song)]
|
||||||
(:artist song)]
|
(:artist song))
|
||||||
" - "
|
" - "
|
||||||
[:a
|
[:a
|
||||||
{:href "#" :on-click (dispatch [::events/play-songs songs idx])}
|
{:href "#" :on-click (dispatch [::events/play-songs songs idx])}
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,9 @@
|
||||||
.missing-cover
|
.missing-cover
|
||||||
display: block
|
display: block
|
||||||
|
|
||||||
.album-preview
|
// preview card for album or artist listings
|
||||||
|
.preview-card
|
||||||
|
.card-content > div,
|
||||||
.title,
|
.title,
|
||||||
.subtitle
|
.subtitle
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
|
|
@ -79,6 +81,7 @@
|
||||||
height: auto
|
height: auto
|
||||||
max-width: 256px
|
max-width: 256px
|
||||||
max-height: 256px
|
max-height: 256px
|
||||||
|
margin: 0
|
||||||
|
|
||||||
// occurs in album detail view
|
// occurs in album detail view
|
||||||
.table
|
.table
|
||||||
|
|
@ -107,3 +110,12 @@
|
||||||
.loading-spinner
|
.loading-spinner
|
||||||
.icon
|
.icon
|
||||||
animation: 1s infinite you-spin-my-head-right-round
|
animation: 1s infinite you-spin-my-head-right-round
|
||||||
|
|
||||||
|
|
||||||
|
// route specific styling
|
||||||
|
.search
|
||||||
|
.content .section
|
||||||
|
padding: 1.5rem 0
|
||||||
|
|
||||||
|
.preview-card .card-content
|
||||||
|
padding: 0.375rem 0.75rem 0.75rem
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
(ns airsonic-ui.api.helpers-test
|
(ns airsonic-ui.api.helpers-test
|
||||||
(:require [cljs.test :refer [deftest testing is]]
|
(:require [cljs.test :refer [deftest testing is]]
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[airsonic-ui.fixtures :refer [responses]]
|
[airsonic-ui.fixtures :as fixtures :refer [responses]]
|
||||||
[airsonic-ui.api.helpers :as api]))
|
[airsonic-ui.api.helpers :as api]))
|
||||||
|
|
||||||
(defn- url
|
(defn- url
|
||||||
|
|
@ -20,6 +20,12 @@
|
||||||
(is (string? (re-find #"f=json" (fixtures :default-url))))
|
(is (string? (re-find #"f=json" (fixtures :default-url))))
|
||||||
(is (string? (re-find #"v=1\.15\.0" (fixtures :default-url))))))
|
(is (string? (re-find #"v=1\.15\.0" (fixtures :default-url))))))
|
||||||
|
|
||||||
|
(deftest parameter-encoding
|
||||||
|
(testing "Should escape url parameters"
|
||||||
|
(let [query "äöüß"
|
||||||
|
encoded-str (js/encodeURIComponent query)]
|
||||||
|
(is (str/includes? (api/url "http://localhost" "search3" {:query query}) encoded-str)))))
|
||||||
|
|
||||||
(deftest song-urls
|
(deftest song-urls
|
||||||
(testing "Should construct the url based on a song's id"
|
(testing "Should construct the url based on a song's id"
|
||||||
(let [song {:id 1234}]
|
(let [song {:id 1234}]
|
||||||
|
|
@ -46,7 +52,7 @@
|
||||||
(try
|
(try
|
||||||
(api/unwrap-response error-response)
|
(api/unwrap-response error-response)
|
||||||
(catch ExceptionInfo e
|
(catch ExceptionInfo e
|
||||||
(= (:error error-response) (ex-data e)))))))
|
(is (= (get-in error-response [:subsonic-response :error]) (ex-data e))))))))
|
||||||
|
|
||||||
(deftest error-recognition
|
(deftest error-recognition
|
||||||
(testing "Should detect error responses"
|
(testing "Should detect error responses"
|
||||||
|
|
@ -55,3 +61,13 @@
|
||||||
(testing "Should pass on good responses"
|
(testing "Should pass on good responses"
|
||||||
(is (false? (api/is-error? (:ok responses))))
|
(is (false? (api/is-error? (:ok responses))))
|
||||||
(is (false? (api/is-error? (:auth-success responses))))))
|
(is (false? (api/is-error? (:auth-success responses))))))
|
||||||
|
|
||||||
|
(deftest content-type
|
||||||
|
(testing "Should detect whether the data we look at represents a song"
|
||||||
|
(is (= :content-type/song (api/content-type fixtures/song))))
|
||||||
|
(testing "Should detect whether the data we look at represents an artist"
|
||||||
|
(is (= :content-type/artist (api/content-type fixtures/artist)))
|
||||||
|
(is (= :content-type/artist (api/content-type (dissoc fixtures/artist :coverArt)))))
|
||||||
|
(testing "Should detect whether the data we look at represents an album"
|
||||||
|
(is (= :content-type/album (api/content-type fixtures/album)))
|
||||||
|
(is (= :content-type/album (api/content-type (dissoc fixtures/album :coverArt))))))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,12 @@
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
||||||
|
(deftest single-response
|
||||||
|
(testing "Should return the response for a specified endpoint"
|
||||||
|
(let [db {:api/responses {["search2" {:query "query term"}] :result}}]
|
||||||
|
(is (= :result (sub/response-for db [:api/response-for "search2" {:query "query term"}])))
|
||||||
|
(is (nil? (sub/response-for db [:api/response-for "search2" {:query "another query term"}]))))))
|
||||||
|
|
||||||
(deftest endpoint-keywordification
|
(deftest endpoint-keywordification
|
||||||
(testing "Should strip prefixes"
|
(testing "Should strip prefixes"
|
||||||
(is (= :artist-info (sub/endpoint->kw "getArtistInfo")))
|
(is (= :artist-info (sub/endpoint->kw "getArtistInfo")))
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
(ns airsonic-ui.audio.playlist-test
|
(ns airsonic-ui.audio.playlist-test
|
||||||
(:require [cljs.test :refer [deftest testing is]]
|
(:require [cljs.test :refer [deftest testing is]]
|
||||||
[airsonic-ui.audio.playlist :as playlist]
|
[airsonic-ui.audio.playlist :as playlist]
|
||||||
[airsonic-ui.utils.helpers :refer [find-where]]
|
[airsonic-ui.helpers :refer [find-where]]
|
||||||
[airsonic-ui.fixtures :as fixtures]
|
[airsonic-ui.fixtures :as fixtures]
|
||||||
[airsonic-ui.test-helpers :as helpers]
|
[airsonic-ui.test-helpers :as helpers]
|
||||||
[debux.cs.core :refer-macros [dbg]]))
|
[debux.cs.core :refer-macros [dbg]]))
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,20 @@
|
||||||
:error {:code 40
|
:error {:code 40
|
||||||
:message "Wrong username or password."}}}})
|
:message "Wrong username or password."}}}})
|
||||||
|
|
||||||
|
(def artist
|
||||||
|
{:id "499", :name "Tomemitsu", :coverArt "ar-497", :albumCount 1})
|
||||||
|
|
||||||
|
(def album
|
||||||
|
{:artistId "258",
|
||||||
|
:name "Tocotronic",
|
||||||
|
:songCount 26,
|
||||||
|
:created "2017-12-31T08:18:45.000Z",
|
||||||
|
:duration 7383,
|
||||||
|
:artist "Tocotronic",
|
||||||
|
:year 2015,
|
||||||
|
:id "439",
|
||||||
|
:coverArt "al-439"})
|
||||||
|
|
||||||
(def song
|
(def song
|
||||||
{:artistId 42,
|
{:artistId 42,
|
||||||
:path "DJ Koze/DJ Koze - Reincarnations Part 2, The Remix Chapter 2009-2014/14. Apparat - Black Water (DJ Koze Remix).mp3",
|
:path "DJ Koze/DJ Koze - Reincarnations Part 2, The Remix Chapter 2009-2014/14. Apparat - Black Water (DJ Koze Remix).mp3",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
(ns airsonic-ui.utils.helpers-test
|
(ns airsonic-ui.helpers-test
|
||||||
(:require [cljs.test :refer [deftest testing is]]
|
(:require [cljs.test :refer [deftest testing is]]
|
||||||
[airsonic-ui.utils.helpers :as helpers]))
|
[airsonic-ui.helpers :as helpers]))
|
||||||
|
|
||||||
(deftest find-where
|
(deftest find-where
|
||||||
(testing "Finds the correct item and index"
|
(testing "Finds the correct item and index"
|
||||||
|
|
@ -12,3 +12,12 @@
|
||||||
:bar false})))))
|
:bar false})))))
|
||||||
(testing "Returns nil when nothing is found"
|
(testing "Returns nil when nothing is found"
|
||||||
(is (nil? (helpers/find-where (partial = 2) (range 2))))))
|
(is (nil? (helpers/find-where (partial = 2) (range 2))))))
|
||||||
|
|
||||||
|
(deftest add-classes
|
||||||
|
(testing "Should add classes to a simple hiccup keyword"
|
||||||
|
(is (= :div.foo (helpers/add-classes :div :foo)))
|
||||||
|
(is (= :div.bar.bar (helpers/add-classes :div.bar :bar)))
|
||||||
|
(is (= :div.foo.bar (helpers/add-classes :div.foo :bar))))
|
||||||
|
(testing "Should add classes to the innermost child of a nested hiccup element"
|
||||||
|
(is (= :p>input.input (helpers/add-classes :p>input :input)))
|
||||||
|
(is (= :div.field>p>input.input.has-background-red (helpers/add-classes :div.field>p>input.input :has-background-red)))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue