mirror of
https://github.com/heyarne/airsonic-ui.git
synced 2026-05-06 18:33:38 +02:00
Merge branch 'master' of github.com:heyarne/airsonic-ui
This commit is contained in:
commit
534bb9c5b5
29 changed files with 1773 additions and 869 deletions
|
|
@ -1,6 +1,12 @@
|
||||||
module.exports = function (config) {
|
module.exports = function (config) {
|
||||||
const configuration = {
|
const configuration = {
|
||||||
browsers: ['ChromeHeadless'],
|
browsers: ['ChromeHeadless'],
|
||||||
|
// The tests are sometimes run before the tests were completely written
|
||||||
|
// to disc; this is a known problem unfortunately. This is a hack to at
|
||||||
|
// least keep the browsers connected so the tests are compiled and run
|
||||||
|
// again even if a developer isn't aware of this
|
||||||
|
autoWatchBatchDelay: 100,
|
||||||
|
browserNoActivityTimeout: 60 * 1000 * 10,
|
||||||
// The directory where the output file lives
|
// The directory where the output file lives
|
||||||
basePath: 'public/test',
|
basePath: 'public/test',
|
||||||
// The file itself
|
// The file itself
|
||||||
|
|
|
||||||
1269
package-lock.json
generated
1269
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
|
@ -5,13 +5,13 @@
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build:cljs": "shadow-cljs release app",
|
"build:cljs": "shadow-cljs release app",
|
||||||
"build:sass": "node-sass --output-style compressed src/sass/app.sass public/app/style.css",
|
"build:sass": "node-sass --output-style compressed src/sass/app.sass | postcss -o public/app/style.css",
|
||||||
"build": "rm -r public/*; run-p copy:* build:*",
|
"build": "rm -r public/*; run-p copy:* build:*",
|
||||||
"copy:assets": "cp -R src/assets/* public/",
|
"copy:assets": "cp -R src/assets/* public/",
|
||||||
"copy:icons": "cp -R node_modules/open-iconic/font/fonts public",
|
"copy:icons": "cp -R node_modules/open-iconic/font/fonts public",
|
||||||
"deploy": "npm run build && gh-pages -d public -m \"Deploying $(git rev-parse --short HEAD)\"",
|
"deploy": "npm run build && gh-pages -d public -m \"Deploying $(git rev-parse --short HEAD)\"",
|
||||||
"dev:cljs": "shadow-cljs watch app test",
|
"dev:cljs": "shadow-cljs watch app test",
|
||||||
"dev:sass": "npm run build:sass; node-sass -w src/sass/app.sass public/app/style.css",
|
"dev:sass": "npm run build:sass; node-sass -w src/sass/app.sass | postcss -o public/app/style.css",
|
||||||
"dev:test": "karma start --reporters notify,progress --auto-watch",
|
"dev:test": "karma start --reporters notify,progress --auto-watch",
|
||||||
"dev": "rm -r public/*; npm-run-all copy:* test:compile -p dev:*",
|
"dev": "rm -r public/*; npm-run-all copy:* test:compile -p dev:*",
|
||||||
"test": "run-s test:compile test:run",
|
"test": "run-s test:compile test:run",
|
||||||
|
|
@ -26,15 +26,18 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hugojosefson/color-hash": "^2.0.3",
|
"@hugojosefson/color-hash": "^2.0.3",
|
||||||
|
"autoprefixer": "^9.4.10",
|
||||||
"bulma": "^0.7.3",
|
"bulma": "^0.7.3",
|
||||||
"create-react-class": "^15.6.3",
|
"create-react-class": "^15.6.3",
|
||||||
"open-iconic": "^1.1.1",
|
"open-iconic": "^1.1.1",
|
||||||
"react": "^16.8.1",
|
"postcss-cli": "^6.1.2",
|
||||||
"react-dom": "^16.8.1"
|
"react": "^16.8.4",
|
||||||
|
"react-dom": "^16.8.4",
|
||||||
|
"react-sortable-hoc": "^1.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"gh-pages": "^1.2.0",
|
"gh-pages": "^1.2.0",
|
||||||
"karma": "^3.1.4",
|
"karma": "^4.0.1",
|
||||||
"karma-chrome-launcher": "^2.2.0",
|
"karma-chrome-launcher": "^2.2.0",
|
||||||
"karma-cljs-test": "^0.1.0",
|
"karma-cljs-test": "^0.1.0",
|
||||||
"karma-notify-reporter": "^1.1.0",
|
"karma-notify-reporter": "^1.1.0",
|
||||||
|
|
@ -43,6 +46,6 @@
|
||||||
"react-flip-move": "^3.0.3",
|
"react-flip-move": "^3.0.3",
|
||||||
"react-highlight.js": "^1.0.7",
|
"react-highlight.js": "^1.0.7",
|
||||||
"sass": "^1.17.0",
|
"sass": "^1.17.0",
|
||||||
"shadow-cljs": "^2.7.30"
|
"shadow-cljs": "^2.8.14"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
5
postcss.config.js
Normal file
5
postcss.config.js
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require('autoprefixer')
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -11,11 +11,11 @@
|
||||||
[funcool/bide "1.6.0"]
|
[funcool/bide "1.6.0"]
|
||||||
[fipp "0.6.14"]
|
[fipp "0.6.14"]
|
||||||
;; debugging
|
;; debugging
|
||||||
[day8.re-frame/re-frame-10x "0.3.3-react16"]
|
[day8.re-frame/re-frame-10x "0.3.7-react16"]
|
||||||
[day8.re-frame/tracing "0.5.1"]
|
#_[day8.re-frame/tracing "0.5.1"]
|
||||||
[philoskim/debux "0.4.11"]
|
[philoskim/debux "0.5.6"]
|
||||||
;; for CIDER
|
;; for CIDER
|
||||||
[cider/cider-nrepl "0.18.0"]]
|
[cider/cider-nrepl "0.21.1"]]
|
||||||
|
|
||||||
:nrepl {:port 9000}
|
:nrepl {:port 9000}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@
|
||||||
[airsonic-ui.audio.playlist :as playlist]
|
[airsonic-ui.audio.playlist :as playlist]
|
||||||
[goog.functions :refer [throttle]]))
|
[goog.functions :refer [throttle]]))
|
||||||
|
|
||||||
;; TODO: Manage buffering
|
|
||||||
|
|
||||||
(defonce audio (atom nil))
|
(defonce audio (atom nil))
|
||||||
|
|
||||||
(defn normalize-time-ranges [time-ranges]
|
(defn normalize-time-ranges [time-ranges]
|
||||||
|
|
@ -28,7 +26,6 @@
|
||||||
|
|
||||||
; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics
|
; explanation of these events: https://developer.mozilla.org/en-US/Apps/Fundamentals/Audio_and_video_delivery/Cross-browser_audio_basics
|
||||||
|
|
||||||
|
|
||||||
(defn attach-listeners! [el]
|
(defn attach-listeners! [el]
|
||||||
(let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)]
|
(let [emit-audio-update (throttle #(rf/dispatch [:audio/update (->status el)]) 16)]
|
||||||
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]]
|
(doseq [event ["loadstart" "progress" "play" "timeupdate" "pause" "volumechange"]]
|
||||||
|
|
@ -56,7 +53,8 @@
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(when-let [audio @audio]
|
(when-let [audio @audio]
|
||||||
(.pause audio)
|
(.pause audio)
|
||||||
(set! (.-currentTime audio) 0))))
|
(set! (.-currentTime audio) 0)
|
||||||
|
(set! (.-src audio) ""))))
|
||||||
|
|
||||||
(rf/reg-fx
|
(rf/reg-fx
|
||||||
:audio/toggle-play-pause
|
:audio/toggle-play-pause
|
||||||
|
|
@ -102,25 +100,26 @@
|
||||||
|
|
||||||
(rf/reg-sub :audio/summary summary)
|
(rf/reg-sub :audio/summary summary)
|
||||||
|
|
||||||
(defn playlist
|
(defn current-playlist
|
||||||
"Lists the complete playlist"
|
"Lists the complete current-queue"
|
||||||
[summary _]
|
[summary _]
|
||||||
(:playlist summary))
|
(:current-playlist summary))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
:audio/playlist
|
:audio/current-playlist
|
||||||
:<- [:audio/summary]
|
:<- [:audio/summary]
|
||||||
playlist)
|
current-playlist)
|
||||||
|
|
||||||
(defn current-song
|
(defn current-song
|
||||||
"Gives us information about the currently played song as presented by
|
"Gives us information about the currently played song as presented by
|
||||||
the airsonic api"
|
the airsonic api"
|
||||||
[playlist _]
|
[playlist _]
|
||||||
(playlist/peek playlist))
|
(when-not (empty? playlist)
|
||||||
|
(playlist/current-song playlist)))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
:audio/current-song
|
:audio/current-song
|
||||||
:<- [:audio/playlist]
|
:<- [:audio/current-playlist]
|
||||||
current-song)
|
current-song)
|
||||||
|
|
||||||
(defn playback-status
|
(defn playback-status
|
||||||
|
|
|
||||||
|
|
@ -1,139 +1,196 @@
|
||||||
(ns airsonic-ui.audio.playlist
|
(ns airsonic-ui.audio.playlist
|
||||||
"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])
|
|
||||||
(:require [airsonic-ui.helpers :refer [find-where]]))
|
|
||||||
|
|
||||||
(defrecord Playlist [queue playback-mode repeat-mode]
|
;; Turns out we can nicely implement this by thinly wrapping a sequence of items
|
||||||
|
;; We re-use the core ClojureScript protocols internally but provide a nice and
|
||||||
|
;; explicit API to consume
|
||||||
|
|
||||||
|
(defprotocol IPlaylist
|
||||||
|
(current-song [this])
|
||||||
|
(next-song [this])
|
||||||
|
(previous-song [this])
|
||||||
|
|
||||||
|
(set-current-song [this song-idx]
|
||||||
|
"Advances the queue to the song given by song-idx")
|
||||||
|
(set-playback-mode [this playback-mode]
|
||||||
|
"Changes the playback mode of a playlist and re-shuffles it if necessary")
|
||||||
|
(set-repeat-mode [this repeat-mode]
|
||||||
|
"Allows you to change how the next and previous song are selected")
|
||||||
|
|
||||||
|
(enqueue-last
|
||||||
|
[this song source]
|
||||||
|
[this song]
|
||||||
|
"Registers a song to be played last, optionally remembering the source route")
|
||||||
|
(enqueue-next
|
||||||
|
[this song source]
|
||||||
|
[this song]
|
||||||
|
"Registers a song to be played next, optionally remembering the source route")
|
||||||
|
|
||||||
|
(move-song [this from-idx to-idx]
|
||||||
|
"Allows you to move a song in a playlist")
|
||||||
|
(remove-song [this song-idx]
|
||||||
|
"Removes a song from the playlist"))
|
||||||
|
|
||||||
|
;; helpers to manage creating playlists
|
||||||
|
|
||||||
|
(defn- mark-original-order
|
||||||
|
"This function is used if we switch from linear to shuffled; it allows us to
|
||||||
|
restore the order of the queue when it was created."
|
||||||
|
[items]
|
||||||
|
(->> (sort-by (comp :playlist/linear-order meta) items)
|
||||||
|
(map-indexed (fn [idx item]
|
||||||
|
(vary-meta item assoc :playlist/linear-order idx)))))
|
||||||
|
|
||||||
|
(defn- linear-queue
|
||||||
|
[items]
|
||||||
|
(->> (mark-original-order items)
|
||||||
|
(map-indexed vector)
|
||||||
|
(into (sorted-map))))
|
||||||
|
|
||||||
|
(defn- shuffled-queue
|
||||||
|
[items]
|
||||||
|
(let [shuffled-indices (shuffle (range (count items)))]
|
||||||
|
(->> (mark-original-order items)
|
||||||
|
(map vector shuffled-indices)
|
||||||
|
(into (sorted-map)))))
|
||||||
|
|
||||||
|
;; the exported interface:
|
||||||
|
|
||||||
|
(defrecord Playlist [items current-idx playback-mode repeat-mode]
|
||||||
cljs.core/ICounted
|
cljs.core/ICounted
|
||||||
(-count [this]
|
(-count [_]
|
||||||
(count (:queue this))))
|
(count items))
|
||||||
|
|
||||||
|
cljs.core/ISequential
|
||||||
|
cljs.core/ISeqable
|
||||||
|
(-seq [_] items)
|
||||||
|
|
||||||
|
IPlaylist
|
||||||
|
(current-song [_]
|
||||||
|
(get items current-idx))
|
||||||
|
|
||||||
|
(next-song [this]
|
||||||
|
(update this :current-idx
|
||||||
|
(fn [current-idx]
|
||||||
|
(cond
|
||||||
|
(= repeat-mode :repeat-single) current-idx
|
||||||
|
|
||||||
|
(or (= repeat-mode :repeat-all)
|
||||||
|
(< current-idx (dec (count this))))
|
||||||
|
(mod (inc current-idx) (count this))))))
|
||||||
|
|
||||||
|
(previous-song [this]
|
||||||
|
(update this :current-idx
|
||||||
|
(fn [current-idx]
|
||||||
|
(cond
|
||||||
|
(= repeat-mode :repeat-single) current-idx
|
||||||
|
|
||||||
|
(or (= repeat-mode :repeat-all)
|
||||||
|
(> current-idx 0))
|
||||||
|
(mod (dec current-idx) (count this))
|
||||||
|
|
||||||
|
:else nil))))
|
||||||
|
|
||||||
|
(set-current-song [playlist song-idx]
|
||||||
|
(assoc playlist :current-idx song-idx))
|
||||||
|
|
||||||
|
(set-playback-mode [playlist playback-mode]
|
||||||
|
(let [current-song (current-song playlist)
|
||||||
|
queue-fn (case playback-mode
|
||||||
|
:shuffled shuffled-queue
|
||||||
|
:linear linear-queue)
|
||||||
|
next-playlist (-> (assoc playlist :playback-mode playback-mode)
|
||||||
|
(update :items (comp queue-fn vals)))
|
||||||
|
next-idx (first (keep (fn [[idx song]]
|
||||||
|
(when (= song current-song)
|
||||||
|
idx))
|
||||||
|
(:items next-playlist)))]
|
||||||
|
;; we have to find out the index of the currently playing song after the
|
||||||
|
;; playlist was created because it might change when shuffling / unshuffling
|
||||||
|
(set-current-song next-playlist next-idx)))
|
||||||
|
|
||||||
|
(set-repeat-mode [playlist repeat-mode]
|
||||||
|
(assoc playlist :repeat-mode repeat-mode))
|
||||||
|
|
||||||
|
(enqueue-last [this song source]
|
||||||
|
(let [order (inc (key (last items)))]
|
||||||
|
;; Arguably this is a bit weird; but if you want to play something last in
|
||||||
|
;; a shuffled playlist, you want to play it last I guess.
|
||||||
|
(assoc-in this [:items order]
|
||||||
|
(vary-meta song assoc
|
||||||
|
:playlist/linear-order order
|
||||||
|
:playlist/source source))))
|
||||||
|
(enqueue-last [this song] (enqueue-last this song nil))
|
||||||
|
|
||||||
|
(enqueue-next [this song source]
|
||||||
|
;; we slice the songs up until the currently playing one and increase the
|
||||||
|
;; order for all the songs after
|
||||||
|
(let [songs (vec (vals items))
|
||||||
|
reordered (-> (subvec songs 0 (inc current-idx))
|
||||||
|
(conj (vary-meta song assoc
|
||||||
|
:playlist/linear-order (inc current-idx)
|
||||||
|
:playlist/source source))
|
||||||
|
(concat (subvec songs (inc current-idx))))]
|
||||||
|
(assoc this :items (->> (map-indexed vector reordered)
|
||||||
|
(into (sorted-map))))))
|
||||||
|
(enqueue-next [this song] (enqueue-next this song nil))
|
||||||
|
|
||||||
|
(move-song [this from-idx to-idx]
|
||||||
|
;; we have to decide whether we move all items in-between
|
||||||
|
;; one up or one down; this depends on whether we move our
|
||||||
|
;; item to the front or to the back
|
||||||
|
(let [shift-fn (cond
|
||||||
|
(< from-idx to-idx) inc
|
||||||
|
(> from-idx to-idx) dec)
|
||||||
|
start (min from-idx to-idx)
|
||||||
|
end (inc (max from-idx to-idx))
|
||||||
|
steps (range start end)
|
||||||
|
result (update this :items
|
||||||
|
(fn [items]
|
||||||
|
(-> (reduce (fn [result idx]
|
||||||
|
(assoc result idx (get items (shift-fn idx))))
|
||||||
|
items steps)
|
||||||
|
(assoc to-idx (get items from-idx)))))]
|
||||||
|
(cond
|
||||||
|
(= from-idx current-idx) (assoc result :current-idx to-idx)
|
||||||
|
(<= to-idx current-idx from-idx) (update result :current-idx inc)
|
||||||
|
(>= to-idx current-idx from-idx) (update result :current-idx dec)
|
||||||
|
:else result)))
|
||||||
|
|
||||||
|
(remove-song [this song-idx]
|
||||||
|
(cond-> (update this :items #(let [n-items (count %)]
|
||||||
|
(-> (reduce (fn [items idx]
|
||||||
|
(assoc items idx (get items (inc idx))))
|
||||||
|
% (range song-idx n-items))
|
||||||
|
(dissoc (dec n-items)))))
|
||||||
|
(= song-idx current-idx) (assoc :current-idx -1))))
|
||||||
|
|
||||||
|
;; constructor wrapper
|
||||||
|
|
||||||
|
(defn set-item-source
|
||||||
|
"Can be used to attach a source route to an item"
|
||||||
|
[item source]
|
||||||
|
(vary-meta item assoc :playlist/source source))
|
||||||
|
|
||||||
|
(defn item-source
|
||||||
|
"Retrieve the source of an item in the playlist"
|
||||||
|
[item]
|
||||||
|
(:playlist/source (meta item)))
|
||||||
|
|
||||||
(defmulti ->playlist
|
(defmulti ->playlist
|
||||||
"Creates a new playlist that behaves according to the given playback- and
|
"Creates a new playlist that behaves according to the given playback- and
|
||||||
repeat-mode parameters."
|
repeat-mode parameters."
|
||||||
(fn [queue & {:keys [playback-mode #_repeat-mode]}]
|
(fn [_ & {:keys [playback-mode]}] playback-mode))
|
||||||
playback-mode))
|
|
||||||
|
|
||||||
(defn- mark-first-song [queue]
|
|
||||||
(let [[first-idx _] (find-where #(= 0 (:playlist/order %)) queue)]
|
|
||||||
(assoc-in queue [first-idx :playlist/currently-playing?] true)))
|
|
||||||
|
|
||||||
(defmethod ->playlist :linear
|
(defmethod ->playlist :linear
|
||||||
[queue & {:keys [playback-mode repeat-mode]}]
|
[items & {:keys [playback-mode repeat-mode source]}]
|
||||||
(let [queue (-> (mapv (fn [order song] (assoc song :playlist/order order)) (range) queue)
|
(->Playlist (->> (map #(set-item-source % source) items)
|
||||||
(mark-first-song))]
|
(linear-queue))
|
||||||
(->Playlist queue playback-mode repeat-mode)))
|
0 playback-mode repeat-mode))
|
||||||
|
|
||||||
(defn- -shuffle-songs [queue]
|
|
||||||
(->> (shuffle (range (count queue)))
|
|
||||||
(mapv (fn [song order] (assoc song :playlist/order order)) queue)))
|
|
||||||
|
|
||||||
(defmethod ->playlist :shuffled
|
(defmethod ->playlist :shuffled
|
||||||
[queue & {:keys [playback-mode repeat-mode]}]
|
[items & {:keys [playback-mode repeat-mode source]}]
|
||||||
(let [queue (conj (mapv #(update % :playlist/order inc) (-shuffle-songs (rest queue)))
|
(->Playlist (->> (map #(set-item-source % source) items)
|
||||||
(assoc (first queue) :playlist/order 0 :playlist/currently-playing? true))]
|
(shuffled-queue))
|
||||||
(->Playlist queue playback-mode repeat-mode)))
|
0 playback-mode repeat-mode))
|
||||||
|
|
||||||
(defn set-current-song
|
|
||||||
"Marks a song in the queue as currently playing, given its ID"
|
|
||||||
[playlist next-idx]
|
|
||||||
(let [[current-idx _] (find-where :playlist/currently-playing? (:queue playlist))]
|
|
||||||
(-> (if current-idx
|
|
||||||
(update-in playlist [:queue current-idx] dissoc :playlist/currently-playing?)
|
|
||||||
playlist)
|
|
||||||
(assoc-in [:queue next-idx :playlist/currently-playing?] true))))
|
|
||||||
|
|
||||||
(defn set-playback-mode
|
|
||||||
"Changes the playback mode of a playlist and re-shuffles it if necessary"
|
|
||||||
[playlist playback-mode]
|
|
||||||
(if (= playback-mode :shuffled)
|
|
||||||
;; for shuffled playlists we reorder the songs make sure that the currently
|
|
||||||
;; playing song has order 0
|
|
||||||
(let [playlist (->playlist (:queue playlist) :playback-mode playback-mode :repeat-mode (:repeat-mode playlist))
|
|
||||||
[current-idx current-song] (find-where :playlist/currently-playing? (:queue playlist))
|
|
||||||
[swap-idx _] (find-where #(= 0 (:playlist/order %)) (:queue playlist))]
|
|
||||||
(-> (assoc-in playlist [:queue current-idx :playlist/order] 0)
|
|
||||||
(assoc-in [:queue swap-idx :playlist/order] (:playlist/order current-song))))
|
|
||||||
;; for linear songs we just make sure that the current does not change
|
|
||||||
(let [[current-idx _] (find-where :playlist/currently-playing? (:queue playlist))]
|
|
||||||
(-> (->playlist (:queue playlist) :playback-mode playback-mode :repeat-mode (:repeat-mode playlist))
|
|
||||||
(set-current-song current-idx)))))
|
|
||||||
|
|
||||||
(defn set-repeat-mode
|
|
||||||
"Allows to change the way the next and previous song of a playlist is selected"
|
|
||||||
[playlist repeat-mode]
|
|
||||||
(assoc playlist :repeat-mode repeat-mode))
|
|
||||||
|
|
||||||
(defn peek
|
|
||||||
"Returns the song in a playlist that is currently playing"
|
|
||||||
[playlist]
|
|
||||||
(->> (:queue playlist)
|
|
||||||
(filter :playlist/currently-playing?)
|
|
||||||
(first)))
|
|
||||||
|
|
||||||
(defmulti next-song "Advances the currently playing song" :repeat-mode)
|
|
||||||
|
|
||||||
(defmethod next-song :repeat-none
|
|
||||||
[playlist]
|
|
||||||
;; this is pretty easy; get the next song and stop playing at the at
|
|
||||||
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:queue playlist))
|
|
||||||
[next-idx _] (find-where #(= (:playlist/order %) (inc (:playlist/order current-song))) (:queue playlist))]
|
|
||||||
(update playlist :queue
|
|
||||||
(fn [queue]
|
|
||||||
(cond-> queue
|
|
||||||
current-idx (update current-idx dissoc :playlist/currently-playing?)
|
|
||||||
next-idx (assoc-in [next-idx :playlist/currently-playing?] true))))))
|
|
||||||
|
|
||||||
(defmethod next-song :repeat-single [playlist] playlist)
|
|
||||||
|
|
||||||
(defmethod next-song :repeat-all
|
|
||||||
[playlist]
|
|
||||||
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:queue playlist))
|
|
||||||
[next-idx _] (find-where #(= (:playlist/order %) (inc (:playlist/order current-song))) (:queue playlist))]
|
|
||||||
(-> (update-in playlist [:queue current-idx] dissoc :playlist/currently-playing?)
|
|
||||||
(update :queue
|
|
||||||
(fn [queue]
|
|
||||||
;; we need special treatment here if we're playing the last song and
|
|
||||||
;; have a shuffled playlist because we need to re-shuffle
|
|
||||||
(if next-idx
|
|
||||||
(assoc-in queue [next-idx :playlist/currently-playing?] true)
|
|
||||||
(case (:playback-mode playlist)
|
|
||||||
:linear (assoc-in queue [0 :playlist/currently-playing?] true)
|
|
||||||
:shuffled (let [queue' (-shuffle-songs queue)
|
|
||||||
[next-idx _] (find-where #(= (:playlist/order %) 0) queue')]
|
|
||||||
(assoc-in queue' [next-idx :playlist/currently-playing?] true)))))))))
|
|
||||||
|
|
||||||
(defmulti previous-song "Goes back along the playback queue" :repeat-mode)
|
|
||||||
|
|
||||||
(defmethod previous-song :repeat-single [playlist] playlist)
|
|
||||||
|
|
||||||
(defmethod previous-song :repeat-none [playlist]
|
|
||||||
(let [[current-idx current-song] (find-where :playlist/currently-playing? (:queue playlist))
|
|
||||||
[next-idx _] (find-where #(= (:playlist/order %) (dec (:playlist/order current-song))) (:queue playlist))]
|
|
||||||
(set-current-song playlist (or next-idx current-idx))))
|
|
||||||
|
|
||||||
(defmethod previous-song :repeat-all [playlist]
|
|
||||||
(let [[_ current-song] (find-where :playlist/currently-playing? (:queue playlist))
|
|
||||||
[next-idx _] (find-where #(= (:playlist/order %)
|
|
||||||
(rem (dec (:playlist/order current-song)) (count playlist)))
|
|
||||||
(:queue playlist))]
|
|
||||||
(if next-idx
|
|
||||||
(set-current-song playlist next-idx)
|
|
||||||
(if (= :shuffled (:playback-mode playlist))
|
|
||||||
(let [highest-order (dec (count playlist))
|
|
||||||
playlist (update playlist :queue -shuffle-songs)
|
|
||||||
[last-idx _] (find-where #(= (:playlist/order %) highest-order) (:queue playlist))]
|
|
||||||
(set-current-song playlist last-idx))
|
|
||||||
(set-current-song playlist (mod (dec (:playlist/order current-song)) (count playlist)))))))
|
|
||||||
|
|
||||||
(defn enqueue-last [playlist song]
|
|
||||||
(let [highest-order (last (sort (map :playlist/order (:queue playlist))))]
|
|
||||||
(update playlist :queue conj (assoc song :playlist/order (inc highest-order)))))
|
|
||||||
|
|
||||||
(defn enqueue-next [playlist song]
|
|
||||||
(let [[_ current-song] (find-where :playlist/currently-playing? (:queue playlist))]
|
|
||||||
(update playlist :queue
|
|
||||||
(fn [queue]
|
|
||||||
(-> (mapv #(if (> (:playlist/order %) (:playlist/order current-song)) (update % :playlist/order inc) %) queue)
|
|
||||||
(conj (assoc song :playlist/order (inc (:playlist/order current-song)))))))))
|
|
||||||
|
|
|
||||||
|
|
@ -3,56 +3,85 @@
|
||||||
[airsonic-ui.audio.playlist :as playlist]
|
[airsonic-ui.audio.playlist :as playlist]
|
||||||
[airsonic-ui.api.helpers :as api]))
|
[airsonic-ui.api.helpers :as api]))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
|
||||||
; sets up the db, starts to play a song and adds the rest to a playlist
|
; sets up the db, starts to play a song and adds the rest to a playlist
|
||||||
:audio-player/play-all
|
(defn play-all-songs [{:keys [db]
|
||||||
(fn [{:keys [db]} [_ songs start-idx]]
|
:routes/keys [current-route]} [_ songs start-idx]]
|
||||||
(let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all)
|
(let [playlist (-> (playlist/->playlist songs :playback-mode :linear :repeat-mode :repeat-all :source current-route)
|
||||||
(playlist/set-current-song start-idx))]
|
(playlist/set-current-song start-idx))]
|
||||||
{:audio/play (api/stream-url (:credentials db) (playlist/peek playlist))
|
{:audio/play (api/stream-url (:credentials db) (playlist/current-song playlist))
|
||||||
:db (assoc-in db [:audio :playlist] playlist)})))
|
:db (assoc-in db [:audio :current-playlist] playlist)}))
|
||||||
|
|
||||||
|
(rf/reg-event-fx
|
||||||
|
:audio-player/play-all
|
||||||
|
[(rf/inject-cofx :routes/current-route)]
|
||||||
|
play-all-songs)
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
:audio-player/set-playback-mode
|
:audio-player/set-playback-mode
|
||||||
(fn [db [_ playback-mode]]
|
(fn [db [_ playback-mode]]
|
||||||
(update-in db [:audio :playlist] #(playlist/set-playback-mode % playback-mode))))
|
(update-in db [:audio :current-playlist] #(playlist/set-playback-mode % playback-mode))))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
:audio-player/set-repeat-mode
|
:audio-player/set-repeat-mode
|
||||||
(fn [db [_ repeat-mode]]
|
(fn [db [_ repeat-mode]]
|
||||||
(update-in db [:audio :playlist] #(playlist/set-repeat-mode % repeat-mode))))
|
(update-in db [:audio :current-playlist] #(playlist/set-repeat-mode % repeat-mode))))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
:audio-player/next-song
|
:audio-player/next-song
|
||||||
(fn [{:keys [db]} _]
|
(fn [{:keys [db]} _]
|
||||||
(let [db (update-in db [:audio :playlist] playlist/next-song)
|
(let [db (update-in db [:audio :current-playlist] playlist/next-song)
|
||||||
next (playlist/peek (get-in db [:audio :playlist]))]
|
next (playlist/current-song (get-in db [:audio :current-playlist]))]
|
||||||
{:db db
|
{:db db
|
||||||
:audio/play (api/stream-url (:credentials db) next)})))
|
:audio/play (api/stream-url (:credentials db) next)})))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
:audio-player/previous-song
|
:audio-player/previous-song
|
||||||
(fn [{:keys [db]} _]
|
(fn [{:keys [db]} _]
|
||||||
(let [db (update-in db [:audio :playlist] playlist/previous-song)
|
(let [db (update-in db [:audio :current-playlist] playlist/previous-song)
|
||||||
prev (playlist/peek (get-in db [:audio :playlist]))]
|
song (playlist/current-song (get-in db [:audio :current-playlist]))]
|
||||||
{:db db
|
{:db db
|
||||||
:audio/play (api/stream-url (:credentials db) prev)})))
|
:audio/play (api/stream-url (:credentials db) song)})))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(defn set-current-song [{:keys [db]} [_ idx]]
|
||||||
|
(let [db (update-in db [:audio :current-playlist] playlist/set-current-song idx)
|
||||||
|
song (playlist/current-song (get-in db [:audio :current-playlist]))]
|
||||||
|
{:db db
|
||||||
|
:audio/play (api/stream-url (:credentials db) song)}))
|
||||||
|
|
||||||
|
(rf/reg-event-fx :audio-player/set-current-song set-current-song)
|
||||||
|
|
||||||
|
(rf/reg-event-fx
|
||||||
:audio-player/enqueue-next
|
:audio-player/enqueue-next
|
||||||
(fn [db [_ song]]
|
[(rf/inject-cofx :routes/current-route)]
|
||||||
(update-in db [:audio :playlist] #(playlist/enqueue-next % song))))
|
(fn [{:keys [db]
|
||||||
|
:routes/keys [current-route]} [_ song]]
|
||||||
|
{:db (update-in db [:audio :current-playlist] #(playlist/enqueue-next % song current-route))}))
|
||||||
|
|
||||||
|
(rf/reg-event-fx
|
||||||
|
:audio-player/enqueue-last
|
||||||
|
[(rf/inject-cofx :routes/current-route)]
|
||||||
|
(fn [{:keys [db]
|
||||||
|
:routes/keys [current-route]} [_ song]]
|
||||||
|
{:db (update-in db [:audio :current-playlist] #(playlist/enqueue-last % song current-route))}))
|
||||||
|
|
||||||
(rf/reg-event-db
|
(rf/reg-event-db
|
||||||
:audio-player/enqueue-last
|
:audio-player/move-song
|
||||||
(fn [db [_ song]]
|
(fn [db [_ from-idx to-idx]]
|
||||||
(update-in db [:audio :playlist] #(playlist/enqueue-last % song))))
|
(update-in db [:audio :current-playlist] #(playlist/move-song % from-idx to-idx))))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
:audio-player/toggle-play-pause
|
:audio-player/toggle-play-pause
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{:audio/toggle-play-pause nil}))
|
{:audio/toggle-play-pause nil}))
|
||||||
|
|
||||||
|
(defn remove-song [{:keys [db]} [_ song-idx]]
|
||||||
|
(let [song-removed (update-in db [:audio :current-playlist] #(playlist/remove-song % song-idx))]
|
||||||
|
(cond-> {:db song-removed}
|
||||||
|
(nil? (playlist/current-song (get-in song-removed [:audio :current-playlist])))
|
||||||
|
(assoc :audio/stop nil))))
|
||||||
|
|
||||||
|
(rf/reg-event-fx :audio-player/remove-song remove-song)
|
||||||
|
|
||||||
(defn audio-update
|
(defn audio-update
|
||||||
"Reacts to audio events fired by the HTML5 audio player and plays the next
|
"Reacts to audio events fired by the HTML5 audio player and plays the next
|
||||||
track if necessary."
|
track if necessary."
|
||||||
|
|
@ -65,7 +94,7 @@
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
:audio-player/seek
|
:audio-player/seek
|
||||||
(fn [{:keys [db]} [_ percentage]]
|
(fn [{:keys [db]} [_ percentage]]
|
||||||
(let [duration (:duration (playlist/peek (get-in db [:audio :playlist])))]
|
(let [duration (:duration (playlist/current-song (get-in db [:audio :current-playlist])))]
|
||||||
{:audio/seek [percentage duration]})))
|
{:audio/seek [percentage duration]})))
|
||||||
|
|
||||||
(rf/reg-event-fx
|
(rf/reg-event-fx
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
[airsonic-ui.routes :as routes]
|
[airsonic-ui.routes :as routes]
|
||||||
[airsonic-ui.helpers :as h]
|
[airsonic-ui.helpers :as h]
|
||||||
[airsonic-ui.views.cover :refer [cover]]
|
[airsonic-ui.views.cover :refer [cover]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]))
|
[bulma.icon :refer [icon]]))
|
||||||
|
|
||||||
;; currently playing / coming next / audio controls...
|
;; currently playing / coming next / audio controls...
|
||||||
|
|
||||||
|
|
@ -121,9 +121,8 @@
|
||||||
{:on-click toggle-volume-slider}
|
{:on-click toggle-volume-slider}
|
||||||
[icon volume-icon]]]))
|
[icon volume-icon]]]))
|
||||||
|
|
||||||
(defn playback-mode-controls [playlist]
|
(defn playback-mode-controls [{:keys [repeat-mode playback-mode]}]
|
||||||
(let [{:keys [repeat-mode playback-mode]} playlist
|
(let [button :p.control>button.button.is-light
|
||||||
button :p.control>button.button.is-light
|
|
||||||
shuffle-button (h/add-classes button (when (= playback-mode :shuffled) :is-primary))
|
shuffle-button (h/add-classes button (when (= playback-mode :shuffled) :is-primary))
|
||||||
repeat-button (h/add-classes button (case repeat-mode
|
repeat-button (h/add-classes button (case repeat-mode
|
||||||
:repeat-single :is-info
|
:repeat-single :is-info
|
||||||
|
|
@ -142,7 +141,7 @@
|
||||||
|
|
||||||
(defn audio-player []
|
(defn audio-player []
|
||||||
(let [current-song @(subscribe [:audio/current-song])
|
(let [current-song @(subscribe [:audio/current-song])
|
||||||
playlist @(subscribe [:audio/playlist])
|
current-playlist @(subscribe [:audio/current-playlist])
|
||||||
playback-status @(subscribe [:audio/playback-status])
|
playback-status @(subscribe [:audio/playback-status])
|
||||||
is-playing? @(subscribe [:audio/is-playing?])]
|
is-playing? @(subscribe [:audio/is-playing?])]
|
||||||
[:nav.audio-player
|
[:nav.audio-player
|
||||||
|
|
@ -153,6 +152,6 @@
|
||||||
[progress-indicators current-song playback-status]
|
[progress-indicators current-song playback-status]
|
||||||
[playback-controls is-playing?]
|
[playback-controls is-playing?]
|
||||||
[volume-controls playback-status]
|
[volume-controls playback-status]
|
||||||
[playback-mode-controls playlist]]
|
[playback-mode-controls current-playlist]]
|
||||||
;; not playing anything
|
;; not playing anything
|
||||||
[:p.navbar-item.idle-notification "No audio playing"])]))
|
[:p.navbar-item.idle-notification "No audio playing"])]))
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,36 @@
|
||||||
(ns airsonic-ui.components.collection.views
|
(ns airsonic-ui.components.collection.views
|
||||||
"A collection is a list of audio files that belong together (e.g. an album or
|
"A collection is a list of audio files that belong together (e.g. an album or
|
||||||
a podcast's overview)"
|
a podcast's overview)"
|
||||||
(:require [airsonic-ui.helpers :refer [format-duration]]
|
(:require [re-frame.core :refer [subscribe]]
|
||||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
[bulma.icon :refer [icon]]
|
||||||
[airsonic-ui.views.cover :refer [cover card]]
|
[bulma.dropdown.views :refer [dropdown]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]
|
[airsonic-ui.helpers :as h]
|
||||||
[airsonic-ui.views.song :as song]))
|
[airsonic-ui.routes :as routes]
|
||||||
|
[airsonic-ui.views.cover :refer [cover card]]))
|
||||||
|
|
||||||
(defn collection-info [{:keys [songCount duration year]}]
|
(defn collection-info [{:keys [songCount duration year]}]
|
||||||
(vec (cond-> [:ul.is-smaller.collection-info
|
(vec (cond-> [:ul.is-smaller.collection-info
|
||||||
[:li [icon :audio-spectrum] (str songCount (if (= 1 songCount)
|
[:li [icon :audio-spectrum] (str songCount (if (= 1 songCount)
|
||||||
" track" " tracks"))]
|
" track" " tracks"))]
|
||||||
[:li [icon :clock] (format-duration duration)]]
|
[:li [icon :clock] (h/format-duration duration)]]
|
||||||
year (conj [:li [icon :calendar] (str "Released in " year)]))))
|
year (conj [:li [icon :calendar] (str "Released in " year)]))))
|
||||||
|
|
||||||
(defn album-card [album]
|
;; TODO: Maybe this view belongs somewhere else?
|
||||||
(let [{:keys [artist artistId name id]} album]
|
;; Something like a collection-grid component?
|
||||||
|
|
||||||
|
(defn album-card
|
||||||
|
"A single element in a grid of albums. Shows the cover, artist and album name."
|
||||||
|
[{:keys [artist artistId name id] :as album}]
|
||||||
[card album
|
[card album
|
||||||
:url-fn #(url-for ::routes/album.detail {:id id})
|
:url-fn #(routes/url-for ::routes/album.detail {:id id})
|
||||||
:content [:div
|
:content [:div
|
||||||
;; link to album
|
;; link to album
|
||||||
[:div.title.is-5
|
[:div.title.is-5
|
||||||
[:a {:href (url-for ::routes/album.detail {:id id})
|
[:a {:href (routes/url-for ::routes/album.detail {:id id})
|
||||||
:title name} name]]
|
:title name} name]]
|
||||||
;; link to artist page
|
;; link to artist page
|
||||||
[:div.subtitle.is-6 [:a {:href (url-for ::routes/artist.detail {:id artistId})
|
[:div.subtitle.is-6 [:a {:href (routes/url-for ::routes/artist.detail {:id artistId})
|
||||||
:title artist} artist]]]]))
|
:title artist} artist]]]])
|
||||||
|
|
||||||
(defn listing [albums]
|
(defn listing [albums]
|
||||||
;; always show 5 in a row
|
;; always show 5 in a row
|
||||||
|
|
@ -34,8 +39,54 @@
|
||||||
^{: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
|
||||||
[album-card album]])])
|
[album-card album]])])
|
||||||
|
|
||||||
|
;; TODO: Avoid duplication
|
||||||
|
(defn artist-link [{id :artistId, artist :artist}]
|
||||||
|
(if id
|
||||||
|
[:a {:href (routes/url-for ::routes/artist.detail {:id id})} artist]
|
||||||
|
artist))
|
||||||
|
|
||||||
|
(defn song-link [{:keys [songs song idx]}]
|
||||||
|
[:a
|
||||||
|
{:href "#" :on-click (h/muted-dispatch [:audio-player/play-all songs idx] :sync? true)}
|
||||||
|
(:title song)])
|
||||||
|
|
||||||
|
(defn song-actions [song]
|
||||||
|
[dropdown {:items [{:label "Play next" :event [:audio-player/enqueue-next song]}
|
||||||
|
{:label "Play last" :event [:audio-player/enqueue-last song]}]}])
|
||||||
|
|
||||||
|
(defn default-thead []
|
||||||
|
[:thead>tr
|
||||||
|
[:td.is-narrow]
|
||||||
|
[:td.song-artist "Artist"]
|
||||||
|
[:td.song-title "Title"]
|
||||||
|
[:td.song-duration "Duration"]
|
||||||
|
[:td.is-narrow]])
|
||||||
|
|
||||||
|
(defn default-tbody [{:keys [songs current-song]}]
|
||||||
|
[:tbody
|
||||||
|
(for [[idx song] (map-indexed vector songs)]
|
||||||
|
^{:key idx}
|
||||||
|
[(if (= (:id song) (:id current-song)) :tr.is-playing :tr)
|
||||||
|
[:td.song-tracknr.is-narrow (:track song)]
|
||||||
|
[:td.song-artist [artist-link song]]
|
||||||
|
[:td.song-title [song-link {:songs songs
|
||||||
|
:song song
|
||||||
|
:idx idx}]]
|
||||||
|
[:td.song-duration (h/format-duration (:duration song) :brief? true)]
|
||||||
|
[:td.song-actions.is-narrow [song-actions song]]])])
|
||||||
|
|
||||||
|
(defn song-table [{:keys [songs thead tbody]
|
||||||
|
:or {thead default-thead, tbody default-tbody}}]
|
||||||
|
;; we subscribe here instead of one level higher up to make this a more
|
||||||
|
;; reusable component; this way we can for example get a list of all songs
|
||||||
|
;; in a search result and easily highlight the currently playing track
|
||||||
|
(let [current-song @(subscribe [:audio/current-song])]
|
||||||
|
[:table.song-listing-table.table.is-fullwidth
|
||||||
|
[thead]
|
||||||
|
[tbody {:songs songs, :current-song current-song}]]))
|
||||||
|
|
||||||
(defn detail
|
(defn detail
|
||||||
"Lists all songs in an album"
|
"Shows a detail view of a single album, listing all "
|
||||||
[{:keys [album]}]
|
[{:keys [album]}]
|
||||||
[:div
|
[:div
|
||||||
[:section.hero.is-small>div.hero-body
|
[:section.hero.is-small>div.hero-body
|
||||||
|
|
@ -46,4 +97,5 @@
|
||||||
[:h2.title (:name album)]
|
[:h2.title (:name album)]
|
||||||
[:h3.subtitle (:artist album)]
|
[:h3.subtitle (:artist album)]
|
||||||
[collection-info album]]]]]
|
[collection-info album]]]]]
|
||||||
[:section.section>div.container [song/listing (:song album)]]])
|
[:section.section>div.container
|
||||||
|
[song-table {:songs (:song album)}]]])
|
||||||
|
|
|
||||||
15
src/cljs/airsonic_ui/components/current_queue/subs.cljs
Normal file
15
src/cljs/airsonic_ui/components/current_queue/subs.cljs
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
(ns airsonic-ui.components.current-queue.subs
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
(defn queue-info [playlist]
|
||||||
|
{:count (count playlist)
|
||||||
|
:duration
|
||||||
|
(reduce (fn [acc [_ item]]
|
||||||
|
(+ acc (:duration item))) 0 (:items playlist))})
|
||||||
|
|
||||||
|
(println "registering the sub")
|
||||||
|
|
||||||
|
(rf/reg-sub
|
||||||
|
:current-queue/info
|
||||||
|
:<- [:audio/current-playlist]
|
||||||
|
queue-info)
|
||||||
|
|
@ -1,12 +1,107 @@
|
||||||
(ns airsonic-ui.components.current-queue.views
|
(ns airsonic-ui.components.current-queue.views
|
||||||
(:require [re-frame.core :refer [subscribe]]
|
(:require [re-frame.core :refer [subscribe dispatch-sync]]
|
||||||
[airsonic-ui.views.song :as song]
|
[reagent.core :as r]
|
||||||
[airsonic-ui.routes :as r]))
|
["react-sortable-hoc" :refer [SortableHandle]]
|
||||||
|
[bulma.icon :refer [icon]]
|
||||||
|
[bulma.dropdown.views :refer [dropdown]]
|
||||||
|
[airsonic-ui.helpers :as helpers]
|
||||||
|
[airsonic-ui.audio.playlist :as playlist]
|
||||||
|
[airsonic-ui.components.collection.views :as collection]
|
||||||
|
[airsonic-ui.components.sortable.views :as sortable]
|
||||||
|
[airsonic-ui.routes :as routes]
|
||||||
|
|
||||||
|
;; ↓ registers subscription handlers ↓
|
||||||
|
[airsonic-ui.components.current-queue.subs]))
|
||||||
|
|
||||||
|
(def SortHandle
|
||||||
|
(SortableHandle.
|
||||||
|
;; Alternative to r/reactify-component, which doens't convert props and hiccup,
|
||||||
|
;; is to just provide fn as component and use as-element or create-element
|
||||||
|
;; to return React elements from the component.
|
||||||
|
(fn []
|
||||||
|
(r/as-element [:span.is-size-7.has-text-grey-lighter
|
||||||
|
[icon :elevator]]))))
|
||||||
|
|
||||||
|
(defn song-actions [{:keys [song idx]}]
|
||||||
|
[dropdown {:items [{:label "Remove from queue"
|
||||||
|
:event [:audio-player/remove-song idx]}
|
||||||
|
{:label "Go to source"
|
||||||
|
:event [:routes/do-navigation (playlist/item-source song)]}]}])
|
||||||
|
|
||||||
|
(defn artist-link [{id :artistId, artist :artist}]
|
||||||
|
(if id
|
||||||
|
[:a {:href (routes/url-for ::routes/artist.detail {:id id})} artist]
|
||||||
|
artist))
|
||||||
|
|
||||||
|
(defn song-link [song idx]
|
||||||
|
[:a
|
||||||
|
{:href "#"
|
||||||
|
:on-click (helpers/muted-dispatch [:audio-player/set-current-song idx])}
|
||||||
|
(:title song)])
|
||||||
|
|
||||||
|
(defn song-table-head []
|
||||||
|
[:thead>tr
|
||||||
|
[:td.is-narrow]
|
||||||
|
[:td.song-artist "Artist"]
|
||||||
|
[:td.song-title "Title"]
|
||||||
|
[:td.song-duration "Duration"]
|
||||||
|
[:td.song-actions.is-narrow]])
|
||||||
|
|
||||||
|
(defn song-table-sortable-tbdoy [{:keys [songs current-song-idx]}]
|
||||||
|
;; we need this closure to pass in custom arguments (current-song-idx)
|
||||||
|
(fn []
|
||||||
|
[sortable/sortable-component
|
||||||
|
{:items songs
|
||||||
|
:container [:tbody]
|
||||||
|
:helper-class "sortable-is-moving"
|
||||||
|
|
||||||
|
:render-item
|
||||||
|
(fn [{[idx song] :value}]
|
||||||
|
[(if (= idx current-song-idx) :tr.is-playing :tr)
|
||||||
|
[:td.sortable-handle.is-narrow [:> SortHandle]]
|
||||||
|
[:td.song-artist [artist-link song]]
|
||||||
|
[:td.song-title [song-link song idx]]
|
||||||
|
[:td.song-duration (helpers/format-duration (:duration song) :brief? true)]
|
||||||
|
[:td.song-actions.is-narrow [song-actions {:song song
|
||||||
|
:idx idx}]]])
|
||||||
|
|
||||||
|
:on-sort-end
|
||||||
|
(fn [{:keys [old-idx new-idx]}]
|
||||||
|
;; if we don't dispatch-sync, the UI sometimes places the row back and
|
||||||
|
;; resorts it a litle later
|
||||||
|
(dispatch-sync [:audio-player/move-song old-idx new-idx]))}]))
|
||||||
|
|
||||||
|
(defn song-table [{:keys [songs current-song-idx]}]
|
||||||
|
[collection/song-table
|
||||||
|
{:songs songs
|
||||||
|
:thead song-table-head
|
||||||
|
:tbody (song-table-sortable-tbdoy {:songs songs
|
||||||
|
:current-song-idx current-song-idx})}])
|
||||||
|
|
||||||
|
(defn queue-info [{:keys [playlist-info]}]
|
||||||
|
[:ul.is-smaller.collection-info
|
||||||
|
[:li [icon :audio-spectrum] (str (:count playlist-info)
|
||||||
|
(if (pos? (:count playlist-info))
|
||||||
|
" tracks"
|
||||||
|
" track"))]
|
||||||
|
[:li [icon :clock] (helpers/format-duration (:duration playlist-info))]])
|
||||||
|
|
||||||
|
(defn playlist [props]
|
||||||
|
[:div
|
||||||
|
[queue-info props]
|
||||||
|
[song-table {:songs (get-in props [:current-playlist :items])
|
||||||
|
:current-song-idx (get-in props [:current-playlist :current-idx])}]])
|
||||||
|
|
||||||
|
(defn empty-playlist []
|
||||||
|
[:p "You are currently not playing anything. Use the search or go to your "
|
||||||
|
[:a {:href (routes/url-for ::routes/library)} "Library"] " to start playing some music."])
|
||||||
|
|
||||||
(defn current-queue []
|
(defn current-queue []
|
||||||
|
(let [current-playlist @(subscribe [:audio/current-playlist])
|
||||||
|
playlist-info @(subscribe [:current-queue/info])]
|
||||||
[:section.section>div.container
|
[:section.section>div.container
|
||||||
[:h1.title "Current Queue"]
|
[:h1.title "Current Queue"]
|
||||||
(if-let [playlist @(subscribe [:audio/playlist])]
|
(if (empty? current-playlist)
|
||||||
[song/listing (:queue playlist)]
|
[empty-playlist]
|
||||||
[:p "You are currently not playing anything. Use the search or go to your "
|
[playlist {:current-playlist current-playlist
|
||||||
[:a {:href (r/url-for ::r/library)} "Library"] " to start playing some music."])])
|
:playlist-info playlist-info}])]))
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
[airsonic-ui.components.podcast.subs :as subs]
|
[airsonic-ui.components.podcast.subs :as subs]
|
||||||
[airsonic-ui.views.cover :refer [cover card]]
|
[airsonic-ui.views.cover :refer [cover card]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]
|
[bulma.icon :refer [icon]]
|
||||||
[airsonic-ui.components.debug.views :refer [debug]]))
|
[airsonic-ui.components.debug.views :refer [debug]]))
|
||||||
|
|
||||||
;; TODO: Implement detail pages for podcasts
|
;; TODO: Implement detail pages for podcasts
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,15 @@
|
||||||
(:require [re-frame.core :refer [dispatch subscribe]]
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
[goog.functions :refer [debounce]]
|
[goog.functions :refer [debounce]]
|
||||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
[airsonic-ui.routes :as routes :refer [url-for]]
|
||||||
[airsonic-ui.views.song :as song]
|
[airsonic-ui.helpers :as h]
|
||||||
|
[airsonic-ui.components.collection.views :as collection]
|
||||||
[airsonic-ui.views.cover :refer [card]]))
|
[airsonic-ui.views.cover :refer [card]]))
|
||||||
|
|
||||||
|
(def search
|
||||||
|
(debounce #(dispatch [:search/do-search (.. % -target -value)]) 100))
|
||||||
|
|
||||||
(defn form []
|
(defn form []
|
||||||
(let [search-term @(subscribe [:search/current-term])
|
(let [search-term @(subscribe [:search/current-term])]
|
||||||
throttled-search (debounce #(dispatch [:search/do-search (.. % -target -value)]) 100)]
|
|
||||||
(fn []
|
(fn []
|
||||||
[:form {:on-submit #(.preventDefault %)}
|
[:form {:on-submit #(.preventDefault %)}
|
||||||
[:div.feld>p.control
|
[:div.feld>p.control
|
||||||
|
|
@ -15,7 +18,7 @@
|
||||||
;; the event might be gone when we the dispatched
|
;; the event might be gone when we the dispatched
|
||||||
;; function is fired, we need to persist it
|
;; function is fired, we need to persist it
|
||||||
(.persist e)
|
(.persist e)
|
||||||
(throttled-search e))
|
(search e))
|
||||||
:default-value search-term
|
:default-value search-term
|
||||||
:placeholder "Search"}]]])))
|
:placeholder "Search"}]]])))
|
||||||
|
|
||||||
|
|
@ -41,8 +44,34 @@
|
||||||
(defn album-results [{:keys [album]}]
|
(defn album-results [{:keys [album]}]
|
||||||
[result-cards (map (juxt album-url identity) album)])
|
[result-cards (map (juxt album-url identity) album)])
|
||||||
|
|
||||||
(defn song-results [{:keys [song]}]
|
(defn song-table-thead []
|
||||||
[song/listing song])
|
[:thead
|
||||||
|
[:td.song-artist "Artist"]
|
||||||
|
[:td.song-album "Album"]
|
||||||
|
[:td.song-title "Title"]
|
||||||
|
[:td.song-duration "Duration"]
|
||||||
|
[:td.song-actions.is-narrow]])
|
||||||
|
|
||||||
|
(defn album-link [{id :albumId :as song}]
|
||||||
|
[:a {:href (routes/url-for ::routes/album.detail {:id id})} (:album song)])
|
||||||
|
|
||||||
|
(defn song-table-tbody [{:keys [songs current-song]}]
|
||||||
|
[:tbody
|
||||||
|
(for [[idx song] (map-indexed vector songs)]
|
||||||
|
^{:key idx}
|
||||||
|
[(if (= (:id song) (:id current-song)) :tr.is-playing :tr)
|
||||||
|
[:td.song-artist [collection/artist-link song]]
|
||||||
|
[:td.song-album [album-link song]]
|
||||||
|
[:td.song-title [collection/song-link {:songs songs
|
||||||
|
:song song
|
||||||
|
:idx idx}]]
|
||||||
|
[:td.song-duration (h/format-duration (:duration song) :brief? true)]
|
||||||
|
[:td.song-actions.is-narrow [collection/song-actions song]]])])
|
||||||
|
|
||||||
|
(defn song-results [{songs :song}]
|
||||||
|
[collection/song-table {:songs songs
|
||||||
|
:thead song-table-thead
|
||||||
|
:tbody song-table-tbody}])
|
||||||
|
|
||||||
(defn results [{:keys [search]}]
|
(defn results [{:keys [search]}]
|
||||||
(let [term @(subscribe [:search/current-term])]
|
(let [term @(subscribe [:search/current-term])]
|
||||||
|
|
|
||||||
98
src/cljs/airsonic_ui/components/sortable/views.cljs
Normal file
98
src/cljs/airsonic_ui/components/sortable/views.cljs
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
(ns airsonic-ui.components.sortable.views
|
||||||
|
(:require [reagent.core :as r]
|
||||||
|
[clojure.string :as str]
|
||||||
|
["react-sortable-hoc" :refer [SortableHandle SortableElement
|
||||||
|
SortableContainer]]))
|
||||||
|
;; this code is taken and adapted from https://github.com/reagent-project/reagent/blob/72c95257c13e5de1531e16d1a06da7686041d3f4/examples/react-sortable-hoc/src/example/core.cljs
|
||||||
|
|
||||||
|
(defn make-wrapper [{:keys [container render-item]}]
|
||||||
|
(let [SortableItem (SortableElement.
|
||||||
|
(r/reactify-component render-item))]
|
||||||
|
(SortableContainer.
|
||||||
|
(r/reactify-component
|
||||||
|
(fn [{:keys [items]}]
|
||||||
|
(into container
|
||||||
|
(for [[idx value] (map-indexed vector items)]
|
||||||
|
(r/create-element
|
||||||
|
SortableItem
|
||||||
|
#js {:key (str "item-" idx)
|
||||||
|
:index idx
|
||||||
|
:value value}))))))))
|
||||||
|
|
||||||
|
(defn style-map
|
||||||
|
"Returns a map representing all currently set css styles; this makes sense
|
||||||
|
so we can save a non-updating version of it."
|
||||||
|
[node]
|
||||||
|
(let [style (js/window.getComputedStyle node)]
|
||||||
|
(into {} (keep (fn [idx]
|
||||||
|
(let [property (.item style idx)]
|
||||||
|
[property (.getPropertyValue style property)]))
|
||||||
|
(range (.-length style))))))
|
||||||
|
|
||||||
|
(defn node-seq
|
||||||
|
"Returns a seq of all of a node's children"
|
||||||
|
[node]
|
||||||
|
(loop [waiting [node]
|
||||||
|
nodes []]
|
||||||
|
(if-let [node (first waiting)]
|
||||||
|
(if-let [children (array-seq (.-children node))]
|
||||||
|
(recur (concat (rest waiting) children) (conj nodes node))
|
||||||
|
(recur (rest waiting) (conj nodes node)))
|
||||||
|
(rest nodes))))
|
||||||
|
|
||||||
|
(defn style-snapshot
|
||||||
|
"Recursively grabs the of all of a node's children"
|
||||||
|
[node]
|
||||||
|
(into [] (map style-map (node-seq node))))
|
||||||
|
|
||||||
|
(defn style-from-map!
|
||||||
|
"Restores the styling saved in a stylemap"
|
||||||
|
[style-map node]
|
||||||
|
(let [style (str/join ";" (map (fn [[k v]] (str k ": " v)) style-map))]
|
||||||
|
(.setAttribute node "style" style)))
|
||||||
|
|
||||||
|
(defn restore-snapshot
|
||||||
|
"Recursively restores the styling of all of a nodes children"
|
||||||
|
[style-snapshot node]
|
||||||
|
(let [nodes (vec (node-seq node))]
|
||||||
|
(dotimes [i (count nodes)]
|
||||||
|
(style-from-map! (nth style-snapshot i) (nth nodes i)))))
|
||||||
|
|
||||||
|
(defonce saved-snapshot (atom nil))
|
||||||
|
|
||||||
|
(defn sortable-component
|
||||||
|
"This function allows us to generate sortable components in a reusable way.
|
||||||
|
It takes a prop-map with several keys:
|
||||||
|
|
||||||
|
- :container A hiccup-vector that will be used as the container
|
||||||
|
- :items A seq containing the values we want to render and sort
|
||||||
|
- :render-item Decides how we render each child; will be passed {:value value}
|
||||||
|
- :on-sort-end Will be called with a map containing :old-idx & :new-idx
|
||||||
|
- :helper-class Will be appended to the element that's sorted when it's
|
||||||
|
appended to the body"
|
||||||
|
[{:keys [container items render-item on-sort-end helper-class]}]
|
||||||
|
(let [Wrapper (make-wrapper {:container container
|
||||||
|
:render-item render-item})]
|
||||||
|
(r/create-element
|
||||||
|
Wrapper
|
||||||
|
#js {:items items
|
||||||
|
:helperClass helper-class
|
||||||
|
:axis "y"
|
||||||
|
:lockAxis "y"
|
||||||
|
|
||||||
|
;; save the style of all of the rows children
|
||||||
|
:updateBeforeSortStart
|
||||||
|
(fn [event]
|
||||||
|
(reset! saved-snapshot (style-snapshot (.-node event))))
|
||||||
|
:onSortStart
|
||||||
|
(fn [_]
|
||||||
|
;; the node we get passed as parameter is the original node unfortunately
|
||||||
|
(restore-snapshot @saved-snapshot (js/document.querySelector "body > :last-child")))
|
||||||
|
|
||||||
|
;; update the state to reflect the new order
|
||||||
|
:onSortEnd
|
||||||
|
(fn [event]
|
||||||
|
(on-sort-end {:old-idx (.-oldIndex event)
|
||||||
|
:new-idx (.-newIndex event)}))
|
||||||
|
|
||||||
|
:useDragHandle true})))
|
||||||
|
|
@ -4,14 +4,6 @@
|
||||||
[clojure.string :as str])
|
[clojure.string :as str])
|
||||||
(:import [goog.string format]))
|
(:import [goog.string format]))
|
||||||
|
|
||||||
(defn find-where
|
|
||||||
"Returns the the first item in `coll` with its index for which `(p song)`
|
|
||||||
is truthy"
|
|
||||||
[p coll]
|
|
||||||
(->> (map-indexed vector coll)
|
|
||||||
(reduce (fn [_ [idx song]]
|
|
||||||
(when (p song) (reduced [idx song]))) nil)))
|
|
||||||
|
|
||||||
(defn muted-dispatch
|
(defn muted-dispatch
|
||||||
"Dispatches a re-frame event while canceling default DOM behavior; to be
|
"Dispatches a re-frame event while canceling default DOM behavior; to be
|
||||||
called for example in `:on-click`."
|
called for example in `:on-click`."
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
[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.login :refer [login-form]]
|
[airsonic-ui.views.login :refer [login-form]]
|
||||||
[airsonic-ui.views.icon :refer [icon]]
|
[bulma.icon :refer [icon]]
|
||||||
|
|
||||||
[airsonic-ui.components.about.views :refer [about]]
|
[airsonic-ui.components.about.views :refer [about]]
|
||||||
[airsonic-ui.components.artist.views :as artist]
|
[airsonic-ui.components.artist.views :as artist]
|
||||||
|
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
(ns airsonic-ui.views.song
|
|
||||||
(:require [re-frame.core :refer [subscribe]]
|
|
||||||
[airsonic-ui.helpers :refer [muted-dispatch format-duration]]
|
|
||||||
[airsonic-ui.routes :as routes :refer [url-for]]
|
|
||||||
[airsonic-ui.views.icon :refer [icon]]))
|
|
||||||
|
|
||||||
(defn item [songs song idx]
|
|
||||||
(let [artist-id (:artistId song)
|
|
||||||
duration (:duration song)]
|
|
||||||
[:div
|
|
||||||
(if artist-id
|
|
||||||
[:a {:href (url-for ::routes/artist.detail {:id artist-id})} (:artist song)]
|
|
||||||
(:artist song))
|
|
||||||
" - "
|
|
||||||
[:a
|
|
||||||
{:href "#" :on-click (muted-dispatch [:audio-player/play-all songs idx] :sync? true)}
|
|
||||||
(:title song)]
|
|
||||||
[:span.duration (format-duration duration)]]))
|
|
||||||
|
|
||||||
(defn listing [songs]
|
|
||||||
(let [current-song @(subscribe [:audio/current-song])]
|
|
||||||
[:table.table.is-striped.is-hoverable.is-fullwidth.song-list>tbody
|
|
||||||
(for [[idx song] (map-indexed vector songs)]
|
|
||||||
(let [tag (if (= (:id song) (:id current-song)) :tr.song.is-playing :tr.song)]
|
|
||||||
^{:key idx} [tag
|
|
||||||
[:td.grow [item songs song idx]]
|
|
||||||
[:td>a {:title "Play next"
|
|
||||||
:href "#"
|
|
||||||
:on-click (muted-dispatch [:audio-player/enqueue-next song])}
|
|
||||||
[icon :plus]]
|
|
||||||
[:td>a {:title "Play last"
|
|
||||||
:href "#"
|
|
||||||
:on-click (muted-dispatch [:audio-player/enqueue-last song])}
|
|
||||||
[icon :caret-right]]]))]))
|
|
||||||
20
src/cljs/bulma/dropdown/events.cljs
Normal file
20
src/cljs/bulma/dropdown/events.cljs
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
(ns bulma.dropdown.events
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
(defn show-dropdown [db [_ dropdown-id]]
|
||||||
|
(assoc-in db [:bulma :visible-dropdown] dropdown-id))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::show show-dropdown)
|
||||||
|
|
||||||
|
(defn hide-dropdown [db _]
|
||||||
|
(update db :bulma dissoc :visible-dropdown))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::hide hide-dropdown)
|
||||||
|
|
||||||
|
(defn toggle-dropdown [db [_ dropdown-id]]
|
||||||
|
(let [visible-dropdown (get-in db [:bulma :visible-dropdown])]
|
||||||
|
(if (= visible-dropdown dropdown-id)
|
||||||
|
(hide-dropdown db [::hide])
|
||||||
|
(show-dropdown db [::show dropdown-id]))))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::toggle toggle-dropdown)
|
||||||
22
src/cljs/bulma/dropdown/subs.cljs
Normal file
22
src/cljs/bulma/dropdown/subs.cljs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
(ns bulma.dropdown.subs
|
||||||
|
(:require [re-frame.core :as rf]))
|
||||||
|
|
||||||
|
;; NOTE: This is almost the same as bulma.modal.subs
|
||||||
|
;; Maybe we can provide some abstraction that covers both, but maybe we shouldn't
|
||||||
|
|
||||||
|
(defn visible-dropdown
|
||||||
|
"Gives us the ID of the currently visible dropdown"
|
||||||
|
[db _]
|
||||||
|
(get-in db [:bulma :visible-dropdown]))
|
||||||
|
|
||||||
|
(rf/reg-sub ::visible-dropdown visible-dropdown)
|
||||||
|
|
||||||
|
(defn visible?
|
||||||
|
"Predicate to check the visibility of a single modal"
|
||||||
|
[visible-dropdown [_ dropdown-id]]
|
||||||
|
(= visible-dropdown dropdown-id))
|
||||||
|
|
||||||
|
(rf/reg-sub
|
||||||
|
::visible?
|
||||||
|
:<- [::visible-dropdown]
|
||||||
|
visible?)
|
||||||
43
src/cljs/bulma/dropdown/views.cljs
Normal file
43
src/cljs/bulma/dropdown/views.cljs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
(ns bulma.dropdown.views
|
||||||
|
(:require [re-frame.core :refer [dispatch subscribe]]
|
||||||
|
[reagent.core :as r]
|
||||||
|
[bulma.icon :refer [icon]]
|
||||||
|
[bulma.dropdown.events :as ev]
|
||||||
|
[bulma.dropdown.subs :as sub]))
|
||||||
|
|
||||||
|
(defn choose-action [event-vector]
|
||||||
|
(fn [e]
|
||||||
|
(.preventDefault e)
|
||||||
|
(dispatch [::ev/hide])
|
||||||
|
(dispatch event-vector)))
|
||||||
|
|
||||||
|
(defn generate-id []
|
||||||
|
(str "bulma-dropdown-" (random-uuid)))
|
||||||
|
|
||||||
|
(defn click-overlay
|
||||||
|
[]
|
||||||
|
[:div {:style {:position "fixed"
|
||||||
|
:z-index 19 ;; <- 20 is the z-index of .dropdown-menu
|
||||||
|
:top 0
|
||||||
|
:left 0
|
||||||
|
:bottom 0
|
||||||
|
:right 0}
|
||||||
|
:on-click #(dispatch [::ev/hide])}])
|
||||||
|
|
||||||
|
(defn dropdown [{:keys [items]}]
|
||||||
|
(let [dropdown-id (generate-id)]
|
||||||
|
(fn []
|
||||||
|
(let [visible? @(subscribe [::sub/visible? dropdown-id])]
|
||||||
|
[(if visible? :div.dropdown.is-right.is-active :div.dropdown.is-right)
|
||||||
|
(when visible? [click-overlay])
|
||||||
|
[:div.dropdown-trigger
|
||||||
|
[:span.is-small.button {:aria-haspopup "true"
|
||||||
|
:aria-controls dropdown-id
|
||||||
|
:on-click #(dispatch [::ev/toggle dropdown-id])}
|
||||||
|
[icon :ellipses]]]
|
||||||
|
[:div.dropdown-menu {:id dropdown-id, :role "menu"}
|
||||||
|
[:div.dropdown-content
|
||||||
|
(for [[idx {:keys [label event]}] (map-indexed vector items)]
|
||||||
|
^{:key (str dropdown-id "-" idx)}
|
||||||
|
[:a.dropdown-item {:href "#"
|
||||||
|
:on-click (choose-action event)} label])]]]))))
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
(ns airsonic-ui.views.icon)
|
(ns bulma.icon)
|
||||||
|
|
||||||
(defn icon [glyph & extra]
|
(defn icon [glyph]
|
||||||
[:span.icon [:span.oi {:data-glyph (name glyph)}]])
|
[:span.icon [:span.oi {:data-glyph (name glyph)}]])
|
||||||
|
|
@ -218,18 +218,6 @@
|
||||||
.grow
|
.grow
|
||||||
width: 100%
|
width: 100%
|
||||||
|
|
||||||
// duh
|
|
||||||
.song-list
|
|
||||||
.song
|
|
||||||
.duration
|
|
||||||
padding-left: .5rem
|
|
||||||
color: $grey-light
|
|
||||||
font-weight: normal
|
|
||||||
|
|
||||||
&.is-playing
|
|
||||||
background-color: $light !important
|
|
||||||
font-weight: bold
|
|
||||||
|
|
||||||
// useful in general to pull elements closer together; bulma es very generous
|
// useful in general to pull elements closer together; bulma es very generous
|
||||||
// with whitespace
|
// with whitespace
|
||||||
.section
|
.section
|
||||||
|
|
@ -302,17 +290,6 @@
|
||||||
margin-right: 1rem
|
margin-right: 1rem
|
||||||
margin-bottom: 0
|
margin-bottom: 0
|
||||||
|
|
||||||
.collection-info
|
|
||||||
list-style: none
|
|
||||||
|
|
||||||
li
|
|
||||||
display: inline-block
|
|
||||||
margin-left: 0.75rem
|
|
||||||
|
|
||||||
&:first-child
|
|
||||||
margin-left: 0
|
|
||||||
|
|
||||||
|
|
||||||
.song-list
|
.song-list
|
||||||
counter-reset: track
|
counter-reset: track
|
||||||
|
|
||||||
|
|
@ -326,3 +303,54 @@
|
||||||
font-weight: normal
|
font-weight: normal
|
||||||
display: inline
|
display: inline
|
||||||
padding-right: 0.375rem
|
padding-right: 0.375rem
|
||||||
|
|
||||||
|
.collection-info
|
||||||
|
list-style: none
|
||||||
|
|
||||||
|
li
|
||||||
|
display: inline-block
|
||||||
|
margin-left: 0.75rem
|
||||||
|
|
||||||
|
&:first-child
|
||||||
|
margin-left: 0
|
||||||
|
|
||||||
|
.song-listing-table
|
||||||
|
tr.is-playing
|
||||||
|
background-color: $table-row-active-background-color
|
||||||
|
color: $table-row-active-color
|
||||||
|
|
||||||
|
a, strong, td.song-duration, td.sort-handle span
|
||||||
|
color: currentColor
|
||||||
|
|
||||||
|
span.button, div.dropdown
|
||||||
|
color: $table-color
|
||||||
|
|
||||||
|
td
|
||||||
|
&.is-narrow
|
||||||
|
white-space: nowrap
|
||||||
|
|
||||||
|
&.song-duration
|
||||||
|
text-align: right
|
||||||
|
|
||||||
|
&.sortable-handle
|
||||||
|
-webkit-touch-callout: none
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
tbody .song-duration
|
||||||
|
color: $grey-light
|
||||||
|
|
||||||
|
tr:hover
|
||||||
|
.button
|
||||||
|
|
||||||
|
// drag'n'drop
|
||||||
|
.sortable-handle
|
||||||
|
span
|
||||||
|
cursor: grabbing
|
||||||
|
user-select: none
|
||||||
|
|
||||||
|
tr.sortable-is-moving.is-playing
|
||||||
|
background-color: $table-row-active-background-color
|
||||||
|
color: $table-row-active-color
|
||||||
|
|
||||||
|
a, strong, td.song-duration, td.sort-handle span
|
||||||
|
color: currentColor
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
(ns airsonic-ui.audio.core-test
|
(ns airsonic-ui.audio.core-test
|
||||||
(:require [airsonic-ui.audio.core :as audio]
|
(:require [airsonic-ui.audio.core :as audio]
|
||||||
[airsonic-ui.audio.playlist-test :as p]
|
#_[airsonic-ui.audio.playlist-test :as p]
|
||||||
[airsonic-ui.fixtures :as fixtures]
|
#_[airsonic-ui.fixtures :as fixtures]
|
||||||
[cljs.test :refer [deftest testing is]]))
|
[cljs.test :refer [deftest testing is]]))
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
||||||
(deftest current-song-subscription
|
(deftest current-song-subscription
|
||||||
|
;; NOTE: Should the subscription be moved to the playlist.cljs?
|
||||||
|
#_(testing "Should provide information about the song"
|
||||||
(letfn [(current-song [db]
|
(letfn [(current-song [db]
|
||||||
(-> (audio/summary db [:audio/summary])
|
(-> (audio/summary db [:audio/summary])
|
||||||
(audio/current-song [:audio/current-song])))]
|
(audio/current-song [:audio/current-song])))]
|
||||||
(testing "Should provide information about the song"
|
(= fixtures/song (current-song p/fixture))))
|
||||||
(= fixtures/song (current-song p/fixture)))))
|
(testing "Should work fine when no song is playing"
|
||||||
|
(is (nil? (audio/current-song nil [:audio/current-song])))))
|
||||||
|
|
||||||
(deftest playback-status-subscription
|
(deftest playback-status-subscription
|
||||||
(letfn [(is-playing? [playback-status]
|
(letfn [(is-playing? [playback-status]
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,12 @@
|
||||||
(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.helpers :refer [find-where]]
|
|
||||||
[airsonic-ui.fixtures :as fixtures]
|
[airsonic-ui.fixtures :as fixtures]
|
||||||
[airsonic-ui.test-helpers :as helpers]
|
[airsonic-ui.test-helpers :refer [song song-queue]]
|
||||||
[debux.cs.core :refer-macros [dbg]]))
|
#_[debux.cs.core :refer-macros [dbg]]))
|
||||||
|
|
||||||
(enable-console-print!)
|
(enable-console-print!)
|
||||||
|
|
||||||
(defn- song []
|
|
||||||
(hash-map :id (rand-int 9999)
|
|
||||||
:coverArt (rand-int 9999)
|
|
||||||
:year (+ 1900 (rand-int 118))
|
|
||||||
:artist (helpers/rand-str)
|
|
||||||
:artistId (rand-int 100000)
|
|
||||||
:title (helpers/rand-str)
|
|
||||||
:album (helpers/rand-str)))
|
|
||||||
|
|
||||||
(defn- song-queue
|
|
||||||
"Generates a seq of n different songs"
|
|
||||||
[n]
|
|
||||||
(let [r-int (atom 0)]
|
|
||||||
(with-redefs [rand-int #(mod (swap! r-int inc) %1)]
|
|
||||||
(repeatedly n song))))
|
|
||||||
|
|
||||||
(def fixture
|
(def fixture
|
||||||
{:audio {:current-song fixtures/song
|
{:audio {:current-song fixtures/song
|
||||||
:playlist (song-queue 20)
|
:playlist (song-queue 20)
|
||||||
|
|
@ -33,14 +16,22 @@
|
||||||
|
|
||||||
(deftest playlist-creation
|
(deftest playlist-creation
|
||||||
(testing "Playlist creation"
|
(testing "Playlist creation"
|
||||||
(testing "should give us the correct current song"
|
(testing "should give us the correct current song for linear playback-mode"
|
||||||
(let [queue (song-queue 10)]
|
(let [queue (song-queue 10)]
|
||||||
(doseq [playback-mode [:linear :shuffled]
|
(doseq [repeat-mode [:repeat-none :repeat-single :repeat-all]]
|
||||||
repeat-mode [:repeat-none :repeat-single :repeat-all]]
|
|
||||||
(is (same-song? (first queue)
|
(is (same-song? (first queue)
|
||||||
(-> (playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode)
|
(-> (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)
|
||||||
(playlist/peek)))
|
(playlist/current-song)))
|
||||||
(str playback-mode ", " repeat-mode)))))
|
(str "repeat-mode: " repeat-mode)))))
|
||||||
|
|
||||||
|
(testing "any current song for shuffled playback mode"
|
||||||
|
(let [queue (song-queue 10)]
|
||||||
|
(doseq [repeat-mode [:repeat-none :repeat-single :repeat-all]]
|
||||||
|
(is (some? ((set queue)
|
||||||
|
(-> (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)
|
||||||
|
(playlist/current-song))))
|
||||||
|
(str "repeat-mode: " repeat-mode)))))
|
||||||
|
|
||||||
(testing "should give us a playlist with the correct number of tracks"
|
(testing "should give us a playlist with the correct number of tracks"
|
||||||
(let [queue (song-queue 100)]
|
(let [queue (song-queue 100)]
|
||||||
(doseq [playback-mode [:linear :shuffled]
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
|
@ -55,21 +46,31 @@
|
||||||
(let [queue (song-queue 10)
|
(let [queue (song-queue 10)
|
||||||
linear (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)
|
linear (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)
|
||||||
shuffled (playlist/set-playback-mode linear :shuffled)]
|
shuffled (playlist/set-playback-mode linear :shuffled)]
|
||||||
|
(testing "should indicate the new playback mode"
|
||||||
|
(is (= :linear (:playback-mode linear)))
|
||||||
|
(is (= :shuffled (:playback-mode shuffled))))
|
||||||
(testing "should re-order the tracks"
|
(testing "should re-order the tracks"
|
||||||
(is (not= (map :playlist/order (:queue shuffled)) (map :playlist/order (:queue linear)))))
|
(is (not= (:items shuffled) (:items linear))))
|
||||||
(testing "should not change the currently playing track"
|
(testing "should not change the currently playing track"
|
||||||
(is (same-song? (playlist/peek linear) (playlist/peek shuffled))))
|
(is (same-song? (playlist/current-song linear) (playlist/current-song shuffled))))
|
||||||
(testing "should not change the repeat mode"
|
(testing "should not change the repeat mode"
|
||||||
(is (= (:repeat-mode shuffled) (:repeat-mode linear))))))
|
(is (= (:repeat-mode shuffled) (:repeat-mode linear))))))
|
||||||
(testing "from shuffled to linear"
|
(testing "from shuffled to linear"
|
||||||
(let [queue (song-queue 10)
|
(let [queue (song-queue 10)
|
||||||
shuffled (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-none)
|
shuffled (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-none)
|
||||||
linear (playlist/set-playback-mode shuffled :linear)]
|
linear (playlist/set-playback-mode shuffled :linear)]
|
||||||
|
(testing "should indicate the new playback mode"
|
||||||
|
(is (= :linear (:playback-mode linear)))
|
||||||
|
(is (= :shuffled (:playback-mode shuffled))))
|
||||||
(testing "should set the correct order for tracks"
|
(testing "should set the correct order for tracks"
|
||||||
(is (every? #(apply same-song? %) (interleave queue (:queue linear))))
|
(let [linear-order (comp :playlist/linear-order meta)]
|
||||||
(is (< (:playlist/order (first (:queue linear))) (:playlist/order (last (:queue linear))))))
|
(is (every? #(apply same-song? %) (interleave queue (vals (:items linear)))))
|
||||||
|
;; every song should have a smaller order than its successor
|
||||||
|
(is (->> (map linear-order (vals (:items linear)))
|
||||||
|
(partition 2 1)
|
||||||
|
(every? (fn [[a b]] (< a b)))))))
|
||||||
(testing "should not change the currently playing track"
|
(testing "should not change the currently playing track"
|
||||||
(is (same-song? (playlist/peek linear) (playlist/peek shuffled))))
|
(is (same-song? (playlist/current-song linear) (playlist/current-song shuffled))))
|
||||||
(testing "should not change the repeat mode"
|
(testing "should not change the repeat mode"
|
||||||
(is (= (:repeat-mode shuffled) (:repeat-mode linear))))))))
|
(is (= (:repeat-mode shuffled) (:repeat-mode linear))))))))
|
||||||
|
|
||||||
|
|
@ -91,17 +92,18 @@
|
||||||
(let [queue (song-queue 5)
|
(let [queue (song-queue 5)
|
||||||
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)]
|
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode repeat-mode)]
|
||||||
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
|
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
|
||||||
(playlist/peek)))
|
(playlist/current-song)))
|
||||||
(str repeat-mode ", skipped once"))
|
(str repeat-mode ", skipped once"))
|
||||||
(is (same-song? (nth queue 2) (-> (playlist/next-song playlist)
|
(is (same-song? (nth queue 2) (-> (playlist/next-song playlist)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/peek)))
|
(playlist/current-song)))
|
||||||
(str repeat-mode ", skipped twice")))))
|
(str repeat-mode ", skipped twice")))))
|
||||||
|
;; TODO: Write this test
|
||||||
(testing "Should go back to the first song when repeat-mode is all and we played the last song")
|
(testing "Should go back to the first song when repeat-mode is all and we played the last song")
|
||||||
(testing "Should always give the same track when repeat-mode is single"
|
(testing "Should always give the same track when repeat-mode is single"
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
|
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
|
||||||
played-back (map playlist/peek (iterate playlist/next-song playlist))]
|
played-back (map playlist/current-song (iterate playlist/next-song playlist))]
|
||||||
(is (same-song? (first queue) (nth played-back 0)))
|
(is (same-song? (first queue) (nth played-back 0)))
|
||||||
(is (same-song? (first queue) (nth played-back 1)))
|
(is (same-song? (first queue) (nth played-back 1)))
|
||||||
(is (same-song? (first queue) (nth played-back 2)))
|
(is (same-song? (first queue) (nth played-back 2)))
|
||||||
|
|
@ -110,7 +112,7 @@
|
||||||
(is (nil? (-> (song-queue 1)
|
(is (nil? (-> (song-queue 1)
|
||||||
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
|
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/peek))))))
|
(playlist/current-song))))))
|
||||||
|
|
||||||
(deftest shuffled-next-song
|
(deftest shuffled-next-song
|
||||||
(testing "Should play every track once when called for the entire queue"
|
(testing "Should play every track once when called for the entire queue"
|
||||||
|
|
@ -118,35 +120,34 @@
|
||||||
(let [length 10
|
(let [length 10
|
||||||
playlist (playlist/->playlist (song-queue length) :playback-mode :shuffled :repeat-mode repeat-mode)
|
playlist (playlist/->playlist (song-queue length) :playback-mode :shuffled :repeat-mode repeat-mode)
|
||||||
played-tracks (->> (iterate playlist/next-song playlist)
|
played-tracks (->> (iterate playlist/next-song playlist)
|
||||||
(map playlist/peek)
|
(map playlist/current-song)
|
||||||
(take length))]
|
(take length))]
|
||||||
(is (= (count played-tracks) (count (set played-tracks)))
|
(is (= (count played-tracks) (count (set played-tracks)))
|
||||||
(str repeat-mode)))))
|
(str repeat-mode)))))
|
||||||
(testing "Should re-shuffle the playlist when wrapping around and repeat-mode is all"
|
(testing "Should keep the song order when wrapping around and repeat-mode is all"
|
||||||
(let [playlist (playlist/->playlist (song-queue 100) :playback-mode :shuffled :repeat-mode :repeat-all)
|
(let [playlist (playlist/->playlist (song-queue 100) :playback-mode :shuffled :repeat-mode :repeat-all)
|
||||||
[last-idx _] (find-where #(= (:playlist/order %) 99) (:queue playlist))]
|
next-playlist (-> (playlist/set-current-song playlist 99)
|
||||||
(is (not= (map :playlist/order (:queue playlist))
|
(playlist/next-song))]
|
||||||
(map :playlist/order (:queue (-> (playlist/set-current-song playlist last-idx)
|
(= (playlist/current-song playlist)
|
||||||
(playlist/next-song))))))))
|
(playlist/current-song next-playlist))))
|
||||||
|
|
||||||
(testing "Should always give the same track when repeat-mode is single"
|
(testing "Should always give the same track when repeat-mode is single"
|
||||||
(let [queue (song-queue 3)
|
(let [playlist (playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-single)
|
||||||
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single)
|
played-back (map playlist/current-song (iterate playlist/next-song playlist))]
|
||||||
played-back (map playlist/peek (iterate playlist/next-song playlist))]
|
(dotimes [i 3]
|
||||||
(is (same-song? (first queue) (nth played-back 0)))
|
(is (same-song? (nth played-back i) (nth played-back (inc i)))))))
|
||||||
(is (same-song? (first queue) (nth played-back 1)))
|
|
||||||
(is (same-song? (first queue) (nth played-back 2)))
|
|
||||||
(is (same-song? (first queue) (nth played-back 3)) "wrapping around")))
|
|
||||||
(testing "Should stop playing at the end of the queue when repeat-mode is none"
|
(testing "Should stop playing at the end of the queue when repeat-mode is none"
|
||||||
(is (nil? (-> (song-queue 1)
|
(is (nil? (-> (song-queue 1)
|
||||||
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
|
(playlist/->playlist :playback-mode :linear :repeat-mode :repeat-none)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/peek))))))
|
(playlist/current-song))))))
|
||||||
|
|
||||||
(deftest linear-previous-song
|
(deftest linear-previous-song
|
||||||
(testing "Should always give the same track when repeat-mode is single"
|
(testing "Should always give the same track when repeat-mode is single"
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
|
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-single)
|
||||||
played-back (map playlist/peek (iterate playlist/next-song playlist))]
|
played-back (map playlist/current-song (iterate playlist/next-song playlist))]
|
||||||
(is (same-song? (first queue) (nth played-back 0)))
|
(is (same-song? (first queue) (nth played-back 0)))
|
||||||
(is (same-song? (first queue) (nth played-back 1)))
|
(is (same-song? (first queue) (nth played-back 1)))
|
||||||
(is (same-song? (first queue) (nth played-back 2)))
|
(is (same-song? (first queue) (nth played-back 2)))
|
||||||
|
|
@ -158,61 +159,61 @@
|
||||||
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
|
(is (same-song? (nth queue 1) (-> (playlist/next-song playlist)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/previous-song)
|
(playlist/previous-song)
|
||||||
(playlist/peek)))))))
|
(playlist/current-song)))))))
|
||||||
(testing "Should repeatedly give the first song when repeat-mode is none"
|
;; TODO: Should it?
|
||||||
|
#_(testing "Should repeatedly give the first song when repeat-mode is none"
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)]
|
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-none)]
|
||||||
(is (same-song? (first queue) (-> (playlist/previous-song playlist)
|
(is (same-song? (first queue) (-> (playlist/previous-song playlist)
|
||||||
(playlist/peek))))))
|
(playlist/current-song))))))
|
||||||
(testing "Should wrap around to last song when repeat-mode is all"
|
(testing "Should wrap around to last song when repeat-mode is all"
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-all)]
|
playlist (playlist/->playlist queue :playback-mode :linear :repeat-mode :repeat-all)]
|
||||||
(is (same-song? (last queue) (-> (playlist/previous-song playlist)
|
(is (same-song? (last queue) (-> (playlist/previous-song playlist)
|
||||||
(playlist/peek)))))))
|
(playlist/current-song)))))))
|
||||||
|
|
||||||
(deftest shuffled-previous-song
|
(deftest shuffled-previous-song
|
||||||
(with-redefs [shuffle reverse]
|
(with-redefs [shuffle reverse]
|
||||||
(testing "Should always give the same track when repeat-mode is single"
|
(testing "Should always give the same track when repeat-mode is single"
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single)
|
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single)
|
||||||
played-back (map playlist/peek (iterate playlist/next-song playlist))]
|
played-back (map playlist/current-song (iterate playlist/next-song playlist))]
|
||||||
(is (same-song? (first queue) (nth played-back 0)))
|
(dotimes [i 3]
|
||||||
(is (same-song? (first queue) (nth played-back 1)))
|
(is (same-song? (nth played-back i) (nth played-back (inc i)))))))
|
||||||
(is (same-song? (first queue) (nth played-back 2)))
|
|
||||||
(is (same-song? (first queue) (nth played-back 3)) "wrapping around")))
|
|
||||||
(testing "Should keep the playing order when repeat-mode is not single"
|
(testing "Should keep the playing order when repeat-mode is not single"
|
||||||
(doseq [repeat-mode '(:repeat-none :repeat-all)]
|
(doseq [repeat-mode '(:repeat-none :repeat-all)]
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)]
|
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)]
|
||||||
(is (same-song? (playlist/peek playlist)
|
(is (same-song? (playlist/current-song playlist)
|
||||||
(-> playlist
|
(-> playlist
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/previous-song)
|
(playlist/previous-song)
|
||||||
(playlist/peek)))
|
(playlist/current-song)))
|
||||||
(str "for repeat mode " repeat-mode))
|
(str "for repeat mode " repeat-mode))
|
||||||
(is (same-song? (-> (playlist/next-song playlist)
|
(is (same-song? (-> (playlist/next-song playlist)
|
||||||
(playlist/peek))
|
(playlist/current-song))
|
||||||
(-> (playlist/next-song playlist)
|
(-> (playlist/next-song playlist)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/previous-song)
|
(playlist/previous-song)
|
||||||
(playlist/peek)))
|
(playlist/current-song)))
|
||||||
(str "for repeat mode " repeat-mode)))))
|
(str "for repeat mode " repeat-mode)))))
|
||||||
(testing "Should re-shuffle when repeat-mode is all and we go back to before the first track"
|
(testing "Should keep the song order when repeat-mode is all and we go back to before the first track"
|
||||||
(let [playlist (with-redefs [shuffle identity]
|
(let [playlist (playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-all)
|
||||||
(playlist/->playlist (song-queue 10) :playback-mode :shuffled :repeat-mode :repeat-all))
|
next-playlist (-> (playlist/previous-song playlist)
|
||||||
playlist' (with-redefs [shuffle reverse]
|
(playlist/set-current-song 0))]
|
||||||
(playlist/previous-song playlist))]
|
(is (= (playlist/current-song playlist)
|
||||||
(is (not= (map :playlist/order (:queue playlist)) (map :playlist/order (:queue playlist'))))))))
|
(playlist/current-song next-playlist)))))))
|
||||||
|
|
||||||
(deftest set-current-song
|
(deftest set-current-song
|
||||||
(testing "Should correctly set the new song"
|
(testing "Should correctly set the new song"
|
||||||
|
(doseq [repeat-mode [:repeat-all :repeat-none]]
|
||||||
(let [queue (song-queue 3)
|
(let [queue (song-queue 3)
|
||||||
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode :repeat-single)
|
playlist (playlist/->playlist queue :playback-mode :shuffled :repeat-mode repeat-mode)
|
||||||
current-track (first queue)
|
|
||||||
next-track (-> (playlist/set-current-song playlist 1)
|
next-track (-> (playlist/set-current-song playlist 1)
|
||||||
(playlist/peek))]
|
(playlist/current-song))]
|
||||||
(is (not (nil? next-track)))
|
(is (not (nil? next-track)))
|
||||||
(is (not (same-song? current-track next-track))))))
|
(is (not (same-song? (playlist/current-song playlist)
|
||||||
|
next-track)))))))
|
||||||
|
|
||||||
(deftest enqueue-last
|
(deftest enqueue-last
|
||||||
(testing "Should make sure the song is played last"
|
(testing "Should make sure the song is played last"
|
||||||
|
|
@ -223,12 +224,12 @@
|
||||||
(playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode))
|
(playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode))
|
||||||
played-back (->> (iterate playlist/next-song playlist)
|
played-back (->> (iterate playlist/next-song playlist)
|
||||||
(take (dec length))
|
(take (dec length))
|
||||||
(map #(:id (playlist/peek %)))
|
(map #(:id (playlist/current-song %)))
|
||||||
(set))
|
(set))
|
||||||
to-enqueue (song)
|
to-enqueue (song)
|
||||||
playlist' (playlist/enqueue-last playlist to-enqueue)]
|
playlist' (playlist/enqueue-last playlist to-enqueue)]
|
||||||
(is (nil? (played-back (-> (->> (iterate playlist/next-song playlist')
|
(is (nil? (played-back (-> (->> (iterate playlist/next-song playlist')
|
||||||
(map playlist/peek))
|
(map playlist/current-song))
|
||||||
(nth length)
|
(nth length)
|
||||||
(:id))))
|
(:id))))
|
||||||
(str "for " playback-mode ", " repeat-mode)))))
|
(str "for " playback-mode ", " repeat-mode)))))
|
||||||
|
|
@ -240,7 +241,7 @@
|
||||||
played-back-songs (fn played-back-songs [playlist]
|
played-back-songs (fn played-back-songs [playlist]
|
||||||
(->> (iterate playlist/next-song playlist)
|
(->> (iterate playlist/next-song playlist)
|
||||||
(take length)
|
(take length)
|
||||||
(map playlist/peek)
|
(map playlist/current-song)
|
||||||
(map :playlist/order)))
|
(map :playlist/order)))
|
||||||
played-back (played-back-songs playlist)
|
played-back (played-back-songs playlist)
|
||||||
played-back' (played-back-songs (playlist/enqueue-last playlist (song)))]
|
played-back' (played-back-songs (playlist/enqueue-last playlist (song)))]
|
||||||
|
|
@ -249,11 +250,92 @@
|
||||||
|
|
||||||
(deftest enqueue-next
|
(deftest enqueue-next
|
||||||
(testing "Should play the song after the currently playing song"
|
(testing "Should play the song after the currently playing song"
|
||||||
(doseq [playback-mode '(:linear :shuffled)
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
repeat-mode '(:repeat-none :repeat-all)]
|
repeat-mode [:repeat-none :repeat-all]]
|
||||||
(let [length 5, queue (song-queue length)
|
(let [length 5, queue (song-queue length)
|
||||||
playlist (playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode)
|
playlist (playlist/->playlist queue :playback-mode playback-mode :repeat-mode repeat-mode)
|
||||||
next-song (song)]
|
next-song (song)]
|
||||||
(is (same-song? next-song (-> (playlist/enqueue-next playlist next-song)
|
(is (same-song? next-song (-> (playlist/enqueue-next playlist next-song)
|
||||||
(playlist/next-song)
|
(playlist/next-song)
|
||||||
(playlist/peek))))))))
|
(playlist/current-song))))))))
|
||||||
|
|
||||||
|
(deftest move-track
|
||||||
|
(testing "Should correctly set the new order"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)]
|
||||||
|
(is (same-song? (-> (playlist/next-song playlist)
|
||||||
|
(playlist/next-song)
|
||||||
|
(playlist/current-song))
|
||||||
|
(-> (playlist/move-song playlist 2 1)
|
||||||
|
(playlist/next-song)
|
||||||
|
(playlist/current-song)))))))
|
||||||
|
(testing "Should update the currently playing track's index"
|
||||||
|
(testing "when inserting a track before"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)]
|
||||||
|
(is (= 4 (-> (playlist/set-current-song playlist 3)
|
||||||
|
(playlist/move-song 5 3)
|
||||||
|
:current-idx))))))
|
||||||
|
(testing "when moving a track behind it"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)]
|
||||||
|
(is (= 2 (-> (playlist/set-current-song playlist 3)
|
||||||
|
(playlist/move-song 2 5)
|
||||||
|
:current-idx))))))
|
||||||
|
(testing "when moving it"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)]
|
||||||
|
(is (= 7 (-> (playlist/set-current-song playlist 3)
|
||||||
|
(playlist/move-song 3 7)
|
||||||
|
:current-idx))))))
|
||||||
|
(testing "when the current track is outside of the modified range"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)]
|
||||||
|
(is (= 3 (-> (playlist/set-current-song playlist 3)
|
||||||
|
(playlist/move-song 4 7)
|
||||||
|
:current-idx))))))))
|
||||||
|
|
||||||
|
(deftest remove-song
|
||||||
|
(with-redefs [shuffle identity]
|
||||||
|
(testing "Should remove a single song from the playlist"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)
|
||||||
|
playlist (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)
|
||||||
|
first-removed (playlist/remove-song playlist 0)
|
||||||
|
middle-removed (playlist/remove-song playlist 5)
|
||||||
|
last-removed (playlist/remove-song playlist 9)
|
||||||
|
song-not-in-list? (fn [song playlist]
|
||||||
|
(every? #(not (same-song? % song))
|
||||||
|
(vals (:items playlist))))]
|
||||||
|
(is (= 9 (count first-removed) (count middle-removed) (count last-removed)))
|
||||||
|
(is (song-not-in-list? (first queue) first-removed))
|
||||||
|
(is (same-song? (second queue) (get (:items first-removed) 0)))
|
||||||
|
(is (song-not-in-list? (nth queue 5) middle-removed))
|
||||||
|
(is (same-song? (nth queue 6) (get (:items middle-removed) 5)))
|
||||||
|
(is (song-not-in-list? (last queue) last-removed)))))
|
||||||
|
(testing "Should pause if the currently playing song is removed"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-all :repeat-single]]
|
||||||
|
(let [n-songs 10
|
||||||
|
queue (song-queue n-songs)]
|
||||||
|
(is (nil? (-> (playlist/->playlist queue :repeat-mode repeat-mode :playback-mode playback-mode)
|
||||||
|
(playlist/set-current-song 5)
|
||||||
|
(playlist/remove-song 5)
|
||||||
|
(playlist/current-song)))))))))
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,39 @@
|
||||||
(ns airsonic-ui.components.audio-player.events-test
|
(ns airsonic-ui.components.audio-player.events-test
|
||||||
(:require [cljs.test :refer-macros [deftest testing is]]
|
(:require [cljs.test :refer-macros [deftest testing is]]
|
||||||
[airsonic-ui.test-helpers :refer [dispatches?]]
|
[airsonic-ui.audio.core :as audio]
|
||||||
|
[airsonic-ui.audio.playlist :as playlist]
|
||||||
|
[airsonic-ui.fixtures :as fixtures]
|
||||||
|
[airsonic-ui.test-helpers :refer [dispatches? song-queue]]
|
||||||
[airsonic-ui.components.audio-player.events :as events]))
|
[airsonic-ui.components.audio-player.events :as events]))
|
||||||
|
|
||||||
|
|
||||||
(deftest song-has-ended
|
(deftest song-has-ended
|
||||||
(testing "Should play the next song when current song has ended"
|
(testing "Should play the next song when current song has ended"
|
||||||
(is (not (dispatches? (events/audio-update {} [:audio/update {:ended? false}]) :audio-player/next-song)))
|
(is (not (dispatches? (events/audio-update {} [:audio/update {:ended? false}]) :audio-player/next-song)))
|
||||||
(is (dispatches? (events/audio-update {} [:audio/update {:ended? true}]) :audio-player/next-song))))
|
(is (dispatches? (events/audio-update {} [:audio/update {:ended? true}]) :audio-player/next-song))))
|
||||||
|
|
||||||
|
(deftest changing-current-song
|
||||||
|
(testing "Should correctly set the current song index"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-single :repeat-all]]
|
||||||
|
(let [n-songs 100
|
||||||
|
next-idx (rand-int n-songs)
|
||||||
|
fixture {:db {:credentials fixtures/credentials
|
||||||
|
:audio {:current-playlist (playlist/->playlist (song-queue n-songs) :playback-mode playback-mode :repeat-mode repeat-mode)}}}
|
||||||
|
effects (events/set-current-song fixture [:audio/set-current-song next-idx])]
|
||||||
|
(is (= next-idx
|
||||||
|
(-> (:db effects)
|
||||||
|
(audio/summary [:audio/summary])
|
||||||
|
(audio/current-playlist [:audio/current-playlist])
|
||||||
|
(:current-idx)))
|
||||||
|
(str "for playback-mode " playback-mode " and repeat-mode " repeat-mode))
|
||||||
|
(is (contains? effects :audio/play))))))
|
||||||
|
|
||||||
|
(deftest removing-currently-playing-song
|
||||||
|
(testing "Should stop all audio when removing the currently playing song"
|
||||||
|
(doseq [playback-mode [:linear :shuffled]
|
||||||
|
repeat-mode [:repeat-none :repeat-single :repeat-all]]
|
||||||
|
(let [n-songs 100
|
||||||
|
fixture {:db {:credentials fixtures/credentials
|
||||||
|
:audio {:current-playlist (playlist/->playlist (song-queue n-songs) :playback-mode playback-mode :repeat-mode repeat-mode)}}}]
|
||||||
|
(is (contains? (events/remove-song fixture [:audio/remove-song 0]) :audio/stop))
|
||||||
|
(is (not (contains? (events/remove-song fixture [:audio/remove-song 99]) :audio/stop)))))))
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,6 @@
|
||||||
(:require [cljs.test :refer [deftest testing is]]
|
(:require [cljs.test :refer [deftest testing is]]
|
||||||
[airsonic-ui.helpers :as helpers]))
|
[airsonic-ui.helpers :as helpers]))
|
||||||
|
|
||||||
(deftest find-where
|
|
||||||
(testing "Finds the correct item and index"
|
|
||||||
(is (= [0 1] (helpers/find-where (partial = 1) (range 1 10))))
|
|
||||||
(is (= [2 {:foo true, :bar false}] (helpers/find-where :foo '({}
|
|
||||||
{:foo false
|
|
||||||
:bar true}
|
|
||||||
{:foo true
|
|
||||||
:bar false})))))
|
|
||||||
(testing "Returns nil when nothing is found"
|
|
||||||
(is (nil? (helpers/find-where (partial = 2) (range 2))))))
|
|
||||||
|
|
||||||
(deftest add-classes
|
(deftest add-classes
|
||||||
(testing "Should add classes to a simple hiccup keyword"
|
(testing "Should add classes to a simple hiccup keyword"
|
||||||
(is (= :div.foo (helpers/add-classes :div :foo)))
|
(is (= :div.foo (helpers/add-classes :div :foo)))
|
||||||
|
|
|
||||||
|
|
@ -17,3 +17,19 @@
|
||||||
(from arr #(-> (str 0 (.toString % 16))
|
(from arr #(-> (str 0 (.toString % 16))
|
||||||
(.substr -2)))
|
(.substr -2)))
|
||||||
(join "")))))
|
(join "")))))
|
||||||
|
|
||||||
|
(defn song []
|
||||||
|
(hash-map :id (rand-int 9999)
|
||||||
|
:coverArt (rand-int 9999)
|
||||||
|
:year (+ 1900 (rand-int 118))
|
||||||
|
:artist (rand-str)
|
||||||
|
:artistId (rand-int 100000)
|
||||||
|
:title (rand-str)
|
||||||
|
:album (rand-str)))
|
||||||
|
|
||||||
|
(defn song-queue
|
||||||
|
"Generates a seq of n different songs"
|
||||||
|
[n]
|
||||||
|
(let [r-int (atom 0)]
|
||||||
|
(with-redefs [rand-int #(mod (swap! r-int inc) %1)]
|
||||||
|
(repeatedly n song))))
|
||||||
|
|
|
||||||
40
test/cljs/bulma/dropdown_test.cljs
Normal file
40
test/cljs/bulma/dropdown_test.cljs
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
(ns bulma.dropdown-test
|
||||||
|
(:require [cljs.test :refer-macros [deftest testing is]]
|
||||||
|
[bulma.dropdown.subs :as sub]
|
||||||
|
[bulma.dropdown.events :as ev]))
|
||||||
|
|
||||||
|
;; NOTE: Here as well; this code is very much like the modal code
|
||||||
|
;; Not sure whether to explicitly duplicate it or provide some smarter
|
||||||
|
;; abstraction that's harder to understand at first sight
|
||||||
|
|
||||||
|
(enable-console-print!)
|
||||||
|
|
||||||
|
(deftest bulma-dropdowns
|
||||||
|
(testing "Should create a collection of dropdowns if there is none"
|
||||||
|
(let [new-db (ev/show-dropdown {} [::ev/show :some-dropdown-id])]
|
||||||
|
(is (= :some-dropdown-id (sub/visible-dropdown new-db [::sub/visible-dropdown])))))
|
||||||
|
(testing "Should hide other dropdowns when displaying a new one"
|
||||||
|
(let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3]
|
||||||
|
new-db (reduce (fn [db dropdown-id]
|
||||||
|
(ev/show-dropdown db [::ev/show dropdown-id]))
|
||||||
|
{} dropdown-ids)]
|
||||||
|
(is (= :some-id-3 (sub/visible-dropdown new-db [::sub/visible-dropdown])))))
|
||||||
|
(testing "Should remove a dropdown from the collection when we hide it"
|
||||||
|
(let [dropdown-ids [:some-id-1 :some-id-2 :some-id-3]
|
||||||
|
new-db (-> (reduce (fn [db dropdown-id]
|
||||||
|
(ev/show-dropdown db [::ev/show dropdown-id]))
|
||||||
|
{} dropdown-ids)
|
||||||
|
(ev/hide-dropdown [::ev/hide]))]
|
||||||
|
(is (not (some? (sub/visible-dropdown new-db [::sub/visible-dropdown]))))))
|
||||||
|
(testing "Should tell us about the visibility of a dropdown with a predicate"
|
||||||
|
(is (true? (-> (ev/show-dropdown {} [::ev/show :getting-repetitive])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :getting-repetitive])))))
|
||||||
|
(testing "Dropdown toggling"
|
||||||
|
(is (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :some-generic-dropdown]))))
|
||||||
|
(is (not (true? (-> (ev/toggle-dropdown {} [::ev/toggle :some-generic-dropdown])
|
||||||
|
(ev/toggle-dropdown [::ev/toggle :some-generic-dropdown])
|
||||||
|
(sub/visible-dropdown [::sub/visible-dropdown])
|
||||||
|
(sub/visible? [::sub/visible? :some-generic-dropdown])))))))
|
||||||
Loading…
Add table
Add a link
Reference in a new issue