diff --git a/src/cljs/airsonic_ui/components/about/views.cljs b/src/cljs/airsonic_ui/components/about/views.cljs new file mode 100644 index 0000000..16bdec8 --- /dev/null +++ b/src/cljs/airsonic_ui/components/about/views.cljs @@ -0,0 +1,16 @@ +(ns airsonic-ui.components.about.views) + +(defn about [] + [:section.section>div.container.content + [:h1 "About"] + [:p "This is a frontend for " [:a {:href "https://airsonic.github.io/" + :target "_blank"} "airsonic"] ", a free and open source media server. You can think of airsonic as a Spotify that you can run out of a shoebox in your bedroom, enabling you to listen to your own music wherever you are."] + [:h2 "Motivation"] + [:p "The current frontend of airsonic has been written quite a long time ago - eons on a web-development timescale, where the clocks tick a bit different. While it has many features it has unfortunately aged noticeably. It does not work well on mobile and some features, such as sharing parts of your music library, require Adobe Flash, leaving them practically unusable and insecure."] + [:p "This fronted aims to provide a focused subset. Its focus for now is on playing and sharing music. Setting up the airsonic instance has to be done via the old interface, as does podcast management."] + [:h2 "Contact"] + [:p "The airsonic community can be found on " [:a {:href "https://riot.im/app/#/room/#airsonic:matrix.org" + :target "_blank"} "Matrix"] + " and IRC (#airsonic on freenode). There is also a " [:a {:href "https://www.reddit.com/r/airsonic/" + :target "_blank"} "dedicated Subreddit"] ". If you think you found bugs in the frontend, it's probably a good idea to " [:a {:href "https://github.com/heyarne/airsonic-ui/issues" + :target "_blank"} "report them on github"] ". I hope you have fun with the software! If you want to say thanks or have a use case that you feel could be covered, feel free to get in touch. Just know that everybody involved does this in their free time."]]) diff --git a/src/cljs/airsonic_ui/components/audio_player/views.cljs b/src/cljs/airsonic_ui/components/audio_player/views.cljs index c18b897..8a12dbe 100644 --- a/src/cljs/airsonic_ui/components/audio_player/views.cljs +++ b/src/cljs/airsonic_ui/components/audio_player/views.cljs @@ -1,5 +1,6 @@ (ns airsonic-ui.components.audio-player.views (:require [re-frame.core :refer [subscribe]] + [airsonic-ui.routes :as routes] [airsonic-ui.helpers :refer [add-classes muted-dispatch]] [airsonic-ui.views.cover :refer [cover]] [airsonic-ui.views.icon :refer [icon]])) @@ -7,8 +8,8 @@ ;; currently playing / coming next / audio controls... (defn current-song-info [song status] - [:article - [:div (:artist song) " - " (:title song)] + [:article.current-song-info + [:span (:artist song) " - " (:title song)] ;; FIXME: Sometimes items don't have a duration [:progress.progress.is-tiny {:value (:current-time status) :max (:duration song)}]]) @@ -17,10 +18,15 @@ [:div.field.has-addons (let [buttons [[:media-step-backward :audio-player/previous-song] [(if is-playing? :media-pause :media-play) :audio-player/toggle-play-pause] - [:media-step-forward :audio-player/next-song]]] + [:media-step-forward :audio-player/next-song]] + title {:media-step-backward "Previous" + :media-play "Play" + :media-pause "Pause" + :media-step-forward "Next"}] (map (fn [[icon-glyph event]] ^{:key icon-glyph} [:p.control>button.button.is-light - {:on-click (muted-dispatch [event])} + {:on-click (muted-dispatch [event]) + :title (title icon-glyph)} [icon icon-glyph]]) buttons))]) @@ -41,10 +47,16 @@ repeat-button (add-classes button (case repeat-mode :repeat-single :is-info :repeat-all :is-primary - nil))] + nil)) + repeat-title (case repeat-mode + :repeat-all "Click to repeat current track" + :repeat-single "Click to repeat all" + "Click to repeat current track")] [:div.field.has-addons - ^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode)} [icon :random]] - ^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode)} [icon :loop]]])) + ^{:key :shuffle-button} [shuffle-button {:on-click (toggle-shuffle playback-mode) + :title "Shuffle"} [icon :random]] + ^{:key :repeat-button} [repeat-button {:on-click (toggle-repeat-mode repeat-mode) + :title repeat-title} [icon :loop]]])) (defn audio-player [] (let [current-song @(subscribe [:audio/current-song]) @@ -60,7 +72,8 @@ [:div.media-left [cover current-song 48]] [:div.media-content [current-song-info current-song playback-status]]] [:div.level-right - [:div.buttons-start [song-controls is-playing?]] - [:div.buttons-end [playback-mode-controls playlist]]]] + [:div.button-group [:p.control>a.button.is-light {:href (routes/url-for ::routes/current-queue) :title "Go to current queue"} [icon :menu]]] + [:div.button-group [song-controls is-playing?]] + [:div.button-group [playback-mode-controls playlist]]]] ;; not playing anything [:p.navbar-item.idle-notification "No audio playing"])]])) diff --git a/src/cljs/airsonic_ui/components/bangpow/views.cljs b/src/cljs/airsonic_ui/components/bangpow/views.cljs new file mode 100644 index 0000000..374e302 --- /dev/null +++ b/src/cljs/airsonic_ui/components/bangpow/views.cljs @@ -0,0 +1,10 @@ +(ns airsonic-ui.components.bangpow.views) + +(defn not-found [] + [:section.section>div.container.content + [:h1 "Oooops..."] + [:p "That should not have happened. There are multiple things that might have gone wrong:"] + [:ul + [:li "You clicked a wrong link. Maybe you copy and pasted it and missed something."] + [:li "It's a bug in the user interface. In that case: sorry! You can report it " [:a {:href "https://github.com/heyarne/airsonic-ui/issues" + :target "_blank"} "on github"]"."]]]) diff --git a/src/cljs/airsonic_ui/components/current_queue/views.cljs b/src/cljs/airsonic_ui/components/current_queue/views.cljs new file mode 100644 index 0000000..1124f33 --- /dev/null +++ b/src/cljs/airsonic_ui/components/current_queue/views.cljs @@ -0,0 +1,12 @@ +(ns airsonic-ui.components.current-queue.views + (:require [re-frame.core :refer [subscribe]] + [airsonic-ui.views.song :as song] + [airsonic-ui.routes :as r])) + +(defn current-queue [] + [:section.section>div.container + [:h1.title "Current Queue"] + (if-let [playlist @(subscribe [:audio/playlist])] + [song/listing (:queue playlist)] + [:p "You are currently not playing anything. Use the search or go to your " + [:a {:href (r/url-for ::r/library)} "Library"] " to start playing some music."])]) diff --git a/src/cljs/airsonic_ui/events.cljs b/src/cljs/airsonic_ui/events.cljs index b1d17c4..30ef266 100644 --- a/src/cljs/airsonic_ui/events.cljs +++ b/src/cljs/airsonic_ui/events.cljs @@ -3,8 +3,7 @@ [ajax.core :as ajax] [airsonic-ui.routes :as routes] [airsonic-ui.db :as db] - [airsonic-ui.api.helpers :as api] - [airsonic-ui.audio.playlist :as playlist])) + [airsonic-ui.api.helpers :as api])) (re-frame/reg-fx ;; a simple effect to keep println statements out of our event handlers diff --git a/src/cljs/airsonic_ui/routes.cljs b/src/cljs/airsonic_ui/routes.cljs index 811e496..53938d5 100644 --- a/src/cljs/airsonic_ui/routes.cljs +++ b/src/cljs/airsonic_ui/routes.cljs @@ -13,7 +13,9 @@ ["/album/:id" ::album.detail] ["/search" ::search] ["/podcast" ::podcast.overview] - ["/podcast/:id" ::podcast.detail]])) + ["/podcast/:id" ::podcast.detail] + ["/current-queue" ::current-queue] + ["/about" ::about]])) ;; use this in views to construct a url (defn url-for diff --git a/src/cljs/airsonic_ui/views.cljs b/src/cljs/airsonic_ui/views.cljs index b9b31c3..d4a79f2 100644 --- a/src/cljs/airsonic_ui/views.cljs +++ b/src/cljs/airsonic_ui/views.cljs @@ -11,12 +11,17 @@ [airsonic-ui.views.notifications :refer [notification-list]] [airsonic-ui.views.breadcrumbs :refer [breadcrumbs]] [airsonic-ui.views.login :refer [login-form]] - [airsonic-ui.components.audio-player.views :refer [audio-player]] - [airsonic-ui.components.search.views :as search] - [airsonic-ui.components.library.views :as library] + [airsonic-ui.views.icon :refer [icon]] + + [airsonic-ui.components.about.views :refer [about]] [airsonic-ui.components.artist.views :as artist] + [airsonic-ui.components.audio-player.views :refer [audio-player]] + [airsonic-ui.components.bangpow.views :refer [not-found]] [airsonic-ui.components.collection.views :as collection] - [airsonic-ui.components.podcast.views :as podcast])) + [airsonic-ui.components.current-queue.views :refer [current-queue]] + [airsonic-ui.components.library.views :as library] + [airsonic-ui.components.podcast.views :as podcast] + [airsonic-ui.components.search.views :as search])) (def logo-url "./img/airsonic-light-350x100.png") @@ -62,24 +67,27 @@ [:div.navbar-start [:div.navbar-item [search/form]]] [:div.navbar-end + [:a.navbar-item {:href (url-for ::routes/current-queue) + :title "Current queue"} [icon :audio]] (when stream-role [navbar-dropdown "Library" [[{:href (url-for ::routes/library {:criteria "recent"})} "Recently played"] [{:href (url-for ::routes/library {:criteria "newest"})} "Newest additions"] [{:href (url-for ::routes/library {:criteria "starred"})} "Starred"]]]) (when podcast-role - (let [podcast-url (url-for ::routes/podcast.overview)] + #_(let [podcast-url (url-for ::routes/podcast.overview)] [navbar-dropdown "Podcast" {:href podcast-url} [[{:href podcast-url} "Overview"]]])) (when playlist-role - [navbar-item {} "Playlists"]) + #_[navbar-item {} "Playlists"]) (when share-role - [navbar-item {} "Shares"]) + #_[navbar-item {} "Shares"]) [:div.navbar-item.has-dropdown.is-hoverable [:div.navbar-link "More"] [:div.navbar-dropdown.is-right (when settings-role - [navbar-item {} "Settings"]) + #_[navbar-item {} "Settings"]) + [:a.navbar-item {:href (url-for ::routes/about)} "About"] [:a.navbar-item {:on-click (fn [_] (toggle-navbar-active!) @@ -107,7 +115,10 @@ ::routes/album.detail [collection/detail content] ::routes/search [search/results content] ::routes/podcast.overview [podcast/overview content] - ::routes/podcast.detail [podcast/detail content])] + ::routes/podcast.detail [podcast/detail content] + ::routes/current-queue [current-queue] + ::routes/about [about] + [not-found])] [audio-player]])) (defn main-panel diff --git a/src/cljs/airsonic_ui/views/song.cljs b/src/cljs/airsonic_ui/views/song.cljs index cae865c..b134fee 100644 --- a/src/cljs/airsonic_ui/views/song.cljs +++ b/src/cljs/airsonic_ui/views/song.cljs @@ -1,5 +1,6 @@ (ns airsonic-ui.views.song - (:require [airsonic-ui.helpers :refer [muted-dispatch]] + (:require [re-frame.core :refer [subscribe]] + [airsonic-ui.helpers :refer [muted-dispatch]] [airsonic-ui.routes :as routes :refer [url-for]] [airsonic-ui.views.icon :refer [icon]])) @@ -15,15 +16,17 @@ (:title song)]])) (defn listing [songs] - [:table.table.is-striped.is-hoverable.is-fullwidth.song-list>tbody - (for [[idx song] (map-indexed vector songs)] - ^{:key idx} [:tr - [: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]]])]) + (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]]]))])) diff --git a/src/sass/app.sass b/src/sass/app.sass index 757a7aa..6f7a2f0 100644 --- a/src/sass/app.sass +++ b/src/sass/app.sass @@ -48,25 +48,25 @@ .level-right display: flex - .buttons-start, - .buttons-end + .button-group margin: 0 .5rem + + .button-group + margin-left: 0 + =tablet flex-grow: 0 flex-shrink: 1 padding-left: .5rem padding-right: .5rem - .buttons-start - margin-left: .75rem - .media flex-grow: 1 align-items: center - progress - width: 100% + .current-song-info + progress + width: 100% .progress.is-tiny height: .25rem @@ -100,6 +100,11 @@ .grow width: 100% +// duh +.song-list + .song.is-playing + background-color: $light !important + // floating notifications .notifications:not(:empty) @extend .container