1
0
Fork 0
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:
Arne Schlüter 2018-08-28 16:07:45 +02:00
commit 7653af5dd1
22 changed files with 236 additions and 49 deletions

View file

@ -1,5 +1,6 @@
(ns airsonic-ui.api.helpers
(:require [clojure.string :as str]))
(:require [clojure.string :as str]
[clojure.set :as set]))
(def default-params {:f "json"
:c "airsonic-ui-cljs"
@ -41,11 +42,22 @@
"Retrieves the actual response body"
[response]
(if (is-error? response)
(let [error (:error response)]
(throw (->exception response)))
(throw (->exception response))
(unwrap-response* response)))
(defn error-msg
[exception-info]
(let [{:keys [code message]} (ex-data exception-info)]
(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)))

View file

@ -2,6 +2,13 @@
(:require [clojure.string :as str]
[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
"Given an endpoint like `getAlbumList2`, returns a cleaned keyword like
`:album-list``.

View file

@ -2,7 +2,7 @@
"Implements playlist queues that support different kinds of repetition and
song ordering."
(: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]
cljs.core/ICounted

View 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}]]}))

View 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])))

View 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))]]))

View file

@ -10,6 +10,8 @@
[airsonic-ui.audio.core]
[airsonic-ui.api.events]
[airsonic-ui.api.subs]
[airsonic-ui.components.search.events]
[airsonic-ui.components.search.subs]
[airsonic-ui.events :as events]
[airsonic-ui.views :as views]
[airsonic-ui.config :as config]))

View file

@ -1,4 +1,4 @@
(ns airsonic-ui.utils.helpers
(ns airsonic-ui.helpers
"Assorted helper functions"
(:require [re-frame.core :as rf]))
@ -16,3 +16,9 @@
(fn [e]
(.preventDefault e)
(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 %)))))))

View file

@ -5,11 +5,12 @@
(def default-route ::login)
(def router
(defonce router
(r/router [["/" ::login]
["/main" ::main]
["/artist/:id" ::artist-view]
["/album/:id" ::album-view]]))
["/album/:id" ::album-view]
["/search" ::search]]))
;; use this in views to construct a url
(defn url-for
@ -17,7 +18,7 @@
([k params] (str "#" (r/resolve router k params))))
;; 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
@ -42,6 +43,11 @@
[route-id params query]
[: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
(defn- n-events?
@ -91,8 +97,9 @@
credentials'(get-in context [:coeffects :db :credentials])]
(println "calling do-navigation with" route credentials')
(reset! credentials credentials')
(println "context" context)
(apply r/navigate! router route)
context))))
(dissoc context :event)))))
(re-frame/reg-event-fx :routes/do-navigation do-navigation (fn [& _] nil))

View file

@ -3,13 +3,15 @@
[airsonic-ui.routes :as routes :refer [url-for]]
[airsonic-ui.events :as events]
[airsonic-ui.subs :as subs]
[airsonic-ui.helpers :refer [add-classes]]
[airsonic-ui.views.notifications :refer [notification-list]]
[airsonic-ui.views.breadcrumbs :refer [breadcrumbs]]
[airsonic-ui.views.audio-player :refer [audio-player]]
[airsonic-ui.views.login :refer [login-form]]
[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.
@ -31,6 +33,7 @@
(defn sidebar [user]
[:aside.menu.section
[search/form]
[:p.menu-label "Music"]
[:ul.menu-list
[:li [:a "By artist"]]
@ -56,20 +59,21 @@
[:main.columns
[:div.column.is-2.sidebar
[sidebar user]]
[:div.column
[:div.column.is-10
[:section.section
[breadcrumbs content]
(case route-id
::routes/main [most-recent 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]]))
(defn main-panel []
(let [notifications @(subscribe [::subs/notifications])
is-booting? @(subscribe [::subs/is-booting?])
[route-id params query] @(subscribe [:routes/current-route])]
[:div
[(add-classes :div route-id)
[notification-list notifications]
(if is-booting?
[:div.app-loading>div.loader]

View file

@ -1,23 +1,23 @@
(ns airsonic-ui.views.album
(: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]
(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]]]]))
(let [{:keys [artist artistId name id]} album]
[card album
:url-fn #(url-for ::routes/album-view {:id id})
:content [:div
;; link to album
[:div.title.is-5
[:a {:href (url-for ::routes/album-view {:id id})
:title name} name]]
;; link to artist page
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist-view {:id artistId})
:title artist} artist]]]]))
(defn listing [albums]
;; always show 5 in a row
[:div
[: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-half-mobile
[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-half-mobile
[preview album]])])

View file

@ -1,6 +1,6 @@
(ns airsonic-ui.views.audio-player
(: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.views.cover :refer [cover]]
[airsonic-ui.views.icon :refer [icon]]))
@ -25,12 +25,6 @@
[icon icon-glyph]])
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]
(dispatch [::events/set-playback-mode (if (= playback-mode :shuffled)
:linear :shuffled)]))

View file

@ -5,12 +5,13 @@
;; hierarchy no matter how you came to the url. They should allow easy
;; 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"
[content]
(case (set (keys content))
#{:artist :artist-info} :artist
#{:album} :album
#{:search} :search
:other-content))
(defn- bulma-breadcrumbs [& items]
@ -20,7 +21,7 @@
[:li {:key idx} [:a {:href href} label]])
[:li.is-active>a (last items)]]])
(defmulti breadcrumbs content-type)
(defmulti breadcrumbs page-type)
(defmethod breadcrumbs :default [content]
[bulma-breadcrumbs "Start"])
@ -35,3 +36,8 @@
[(url-for ::routes/main) "Start"]
[(url-for ::routes/artist-view {:id (:artistId album)}) (:artist album)]
(:name album)])
(defmethod breadcrumbs :search [_]
[bulma-breadcrumbs
[(url-for ::routes/main) "Start"]
"Search"])

View file

@ -65,3 +65,8 @@
[:img {:src original
:srcSet (str original ", " retina " 2x")}]
[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]])

View file

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

View file

@ -1,5 +1,5 @@
(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.routes :as routes :refer [url-for]]
[airsonic-ui.views.icon :refer [icon]]))
@ -7,9 +7,9 @@
(defn item [songs song idx]
(let [artist-id (:artistId song)]
[:div
[:a
(when artist-id {:href (url-for ::routes/artist-view {:id artist-id})})
(:artist song)]
(if artist-id
[:a {:href (url-for ::routes/artist-view {:id artist-id})} (:artist song)]
(:artist song))
" - "
[:a
{:href "#" :on-click (dispatch [::events/play-songs songs idx])}