Browse Source

chore: apply formatting and linting

Miguel Ángel Moreno 5 months ago
parent
commit
01c3e19c6b
46 changed files with 1149 additions and 804 deletions
  1. 14 11
      build.clj
  2. 26 26
      deps.edn
  3. 2 6
      src/backend/tubo/core.clj
  4. 39 31
      src/backend/tubo/downloader_impl.clj
  5. 31 26
      src/backend/tubo/handler.clj
  6. 2 7
      src/backend/tubo/http.clj
  7. 16 16
      src/backend/tubo/routes.clj
  8. 6 6
      src/frontend/tubo/api.cljs
  9. 101 67
      src/frontend/tubo/bookmarks/events.cljs
  10. 7 4
      src/frontend/tubo/bookmarks/modals.cljs
  11. 20 12
      src/frontend/tubo/bookmarks/views.cljs
  12. 17 10
      src/frontend/tubo/channel/events.cljs
  13. 12 9
      src/frontend/tubo/channel/views.cljs
  14. 17 10
      src/frontend/tubo/comments/events.cljs
  15. 32 15
      src/frontend/tubo/comments/views.cljs
  16. 53 35
      src/frontend/tubo/components/items.cljs
  17. 46 29
      src/frontend/tubo/components/layout.cljs
  18. 19 12
      src/frontend/tubo/components/navigation.cljs
  19. 90 59
      src/frontend/tubo/components/player.cljs
  20. 1 4
      src/frontend/tubo/core.cljs
  21. 46 32
      src/frontend/tubo/events.cljs
  22. 53 32
      src/frontend/tubo/kiosks/events.cljs
  23. 13 11
      src/frontend/tubo/kiosks/views.cljs
  24. 14 7
      src/frontend/tubo/modals/events.cljs
  25. 1 1
      src/frontend/tubo/modals/views.cljs
  26. 14 8
      src/frontend/tubo/notifications/events.cljs
  27. 4 2
      src/frontend/tubo/notifications/views.cljs
  28. 95 80
      src/frontend/tubo/player/events.cljs
  29. 9 9
      src/frontend/tubo/player/subs.cljs
  30. 62 44
      src/frontend/tubo/player/views.cljs
  31. 16 10
      src/frontend/tubo/playlist/events.cljs
  32. 8 6
      src/frontend/tubo/playlist/views.cljs
  33. 29 19
      src/frontend/tubo/queue/events.cljs
  34. 40 29
      src/frontend/tubo/queue/views.cljs
  35. 51 40
      src/frontend/tubo/routes.cljs
  36. 26 14
      src/frontend/tubo/search/events.cljs
  37. 7 9
      src/frontend/tubo/search/views.cljs
  38. 4 3
      src/frontend/tubo/services/events.cljs
  39. 8 4
      src/frontend/tubo/services/views.cljs
  40. 34 15
      src/frontend/tubo/settings/events.cljs
  41. 10 7
      src/frontend/tubo/settings/views.cljs
  42. 19 9
      src/frontend/tubo/stream/events.cljs
  43. 20 11
      src/frontend/tubo/stream/views.cljs
  44. 0 2
      src/frontend/tubo/subs.cljs
  45. 12 4
      src/frontend/tubo/utils.cljs
  46. 3 1
      src/frontend/tubo/views.cljs

+ 14 - 11
build.clj

@@ -9,28 +9,31 @@
 (def basis (b/create-basis {:project "deps.edn"}))
 (def uber-file (format "target/%s-%s.jar" (name lib) version))
 
-(defn clean [_]
+(defn clean
+  [_]
   (b/delete {:path "target"}))
 
-(defn aot-compile [_]
+(defn aot-compile
+  [_]
   (println "Compiling AOT namespaces...")
-  (b/compile-clj {:basis basis
-                  :src-dir ["src"]
-                  :class-dir class-dir
+  (b/compile-clj {:basis      basis
+                  :src-dir    ["src"]
+                  :class-dir  class-dir
                   :ns-compile ['tubo.downloader-impl]})
   (println "Compiled AOT namespaces"))
 
-(defn uberjar [_]
+(defn uberjar
+  [_]
   (clean nil)
   (aot-compile nil)
-  (b/copy-dir {:src-dirs ["src/clj" "resources"]
+  (b/copy-dir {:src-dirs   ["src/clj" "resources"]
                :target-dir class-dir})
-  (b/compile-clj {:basis basis
-                  :src-dir ["src"]
+  (b/compile-clj {:basis     basis
+                  :src-dir   ["src"]
                   :class-dir class-dir})
   (shadow/release :tubo)
   (b/uber {:class-dir class-dir
            :uber-file uber-file
-           :basis basis
-           :main 'tubo.core})
+           :basis     basis
+           :main      'tubo.core})
   (println "Uberjar: " uber-file))

+ 26 - 26
deps.edn

@@ -1,34 +1,34 @@
 {:deps {com.github.TeamNewPipe/NewpipeExtractor {:mvn/version "0.22.7"}
-        com.squareup.okhttp3/okhttp {:mvn/version "4.10.0"}
-        http-kit/http-kit {:mvn/version "2.7.0-alpha1"}
-        metosin/reitit-core {:mvn/version "0.5.18"}
-        metosin/reitit-ring {:mvn/version "0.5.18"}
-        metosin/reitit-middleware {:mvn/version "0.5.18"}
-        metosin/reitit-malli {:mvn/version "0.5.18"}
-        ring/ring {:mvn/version "1.9.5"}
-        ring/ring-json {:mvn/version "0.5.1"}
-        org.clojure/java.data {:mvn/version "1.0.95"}
-        hiccup/hiccup {:mvn/version "1.0.5"}
-        ring-cors/ring-cors {:mvn/version "0.1.13"}}
+        com.squareup.okhttp3/okhttp             {:mvn/version "4.10.0"}
+        http-kit/http-kit                       {:mvn/version "2.7.0-alpha1"}
+        metosin/reitit-core                     {:mvn/version "0.5.18"}
+        metosin/reitit-ring                     {:mvn/version "0.5.18"}
+        metosin/reitit-middleware               {:mvn/version "0.5.18"}
+        metosin/reitit-malli                    {:mvn/version "0.5.18"}
+        ring/ring                               {:mvn/version "1.9.5"}
+        ring/ring-json                          {:mvn/version "0.5.1"}
+        org.clojure/java.data                   {:mvn/version "1.0.95"}
+        hiccup/hiccup                           {:mvn/version "1.0.5"}
+        ring-cors/ring-cors                     {:mvn/version "0.1.13"}}
  :paths ["src/backend" "resources" "classes"]
  :mvn/repos {"jitpack" {:url "https://jitpack.io"}}
  :aliases
- {:build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.4"}}
+ {:build {:deps       {io.github.clojure/tools.build {:mvn/version "0.9.4"}}
           :ns-default build}
   :frontend
   {:extra-paths ["src/frontend"]
-   :extra-deps {thheller/shadow-cljs {:mvn/version "2.28.8"}
-                cider/cider-nrepl {:mvn/version "0.28.4"}
-                metosin/reitit-frontend {:mvn/version "0.5.18"}
-                reagent/reagent {:mvn/version "1.1.1"}
-                re-frame/re-frame {:mvn/version "1.3.0"}
-                day8.re-frame/http-fx {:mvn/version "0.2.4"}
-                cljs-ajax/cljs-ajax {:mvn/version "0.8.4"}
-                akiroz.re-frame/storage {:mvn/version "0.1.4"}
-                re-frame-utils/re-frame-utils {:mvn/version "0.1.0"}
-                nano-id/nano-id {:mvn/version "1.1.0"}
-                com.github.scopews/svgreq {:mvn/version "1.1.0"}
-                funcool/promesa {:mvn/version "11.0.678"}
-                re-promise/re-promise {:mvn/version "0.1.1"}}
-   :main-opts ["-m" "shadow.cljs.devtools.cli"]}
+   :extra-deps  {thheller/shadow-cljs          {:mvn/version "2.28.8"}
+                 cider/cider-nrepl             {:mvn/version "0.28.4"}
+                 metosin/reitit-frontend       {:mvn/version "0.5.18"}
+                 reagent/reagent               {:mvn/version "1.1.1"}
+                 re-frame/re-frame             {:mvn/version "1.3.0"}
+                 day8.re-frame/http-fx         {:mvn/version "0.2.4"}
+                 cljs-ajax/cljs-ajax           {:mvn/version "0.8.4"}
+                 akiroz.re-frame/storage       {:mvn/version "0.1.4"}
+                 re-frame-utils/re-frame-utils {:mvn/version "0.1.0"}
+                 nano-id/nano-id               {:mvn/version "1.1.0"}
+                 com.github.scopews/svgreq     {:mvn/version "1.1.0"}
+                 funcool/promesa               {:mvn/version "11.0.678"}
+                 re-promise/re-promise         {:mvn/version "0.1.1"}}
+   :main-opts   ["-m" "shadow.cljs.devtools.cli"]}
   :run {:main-opts ["-m" "tubo.core"]}}}

+ 2 - 6
src/backend/tubo/core.clj

@@ -3,10 +3,6 @@
   (:require
    [tubo.http :as http]))
 
-(defn -main
-  [& _]
-  (http/start-server!))
+(defn -main [& _] (http/start-server!))
 
-(defn reset
-  []
-  (http/stop-server!))
+(defn reset [] (http/stop-server!))

+ 39 - 31
src/backend/tubo/downloader_impl.clj

@@ -1,33 +1,36 @@
 (ns tubo.downloader-impl
   (:import
-   [org.schabi.newpipe.extractor.downloader Response Request]
+   [org.schabi.newpipe.extractor.downloader Response]
    [okhttp3 Request$Builder OkHttpClient$Builder RequestBody]))
 
 (gen-class
- :name tubo.DownloaderImpl
+ :name         tubo.DownloaderImpl
  :constructors {[okhttp3.OkHttpClient$Builder] []}
- :extends org.schabi.newpipe.extractor.downloader.Downloader
- :init downloader-impl)
+ :extends      org.schabi.newpipe.extractor.downloader.Downloader
+ :init         downloader-impl)
 
 (gen-class
- :name tubo.DownloaderImpl
+ :name         tubo.DownloaderImpl
  :constructors {[okhttp3.OkHttpClient$Builder] []}
- :extends org.schabi.newpipe.extractor.downloader.Downloader
- :prefix "-"
- :main false
- :state state
- :init downloader-impl
- :methods [#^{:static true} [init [] tubo.DownloaderImpl]
-           #^{:static true} [getInstance [] tubo.DownloaderImpl]])
+ :extends      org.schabi.newpipe.extractor.downloader.Downloader
+ :prefix       "-"
+ :main         false
+ :state        state
+ :init         downloader-impl
+ :methods      [#^{:static true} [init [] tubo.DownloaderImpl]
+                #^{:static true} [getInstance [] tubo.DownloaderImpl]])
 
-(def user-agent "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
+(def user-agent
+  "Mozilla/5.0 (Windows NT 10.0; rv:91.0) Gecko/20100101 Firefox/91.0")
 (def instance (atom nil))
 
-(defn -downloader-impl [builder]
-  [[] (atom {:client
-             (.. builder
-                 (readTimeout 30 (java.util.concurrent.TimeUnit/SECONDS))
-                 (build))})])
+(defn -downloader-impl
+  [builder]
+  [[]
+   (atom {:client
+          (.. builder
+              (readTimeout 30 (java.util.concurrent.TimeUnit/SECONDS))
+              (build))})])
 
 (defn -init
   ([]
@@ -35,33 +38,37 @@
   ([builder]
    (reset! instance (tubo.DownloaderImpl. builder))))
 
-(defn -getInstance []
+(defn -getInstance
+  []
   (or @instance (-init)))
 
-(defn -execute [this request]
-  (let [http-method (.httpMethod request)
-        url (.url request)
-        headers (.headers request)
-        data-to-send (.dataToSend request)
-        request-body (and data-to-send (RequestBody/create nil data-to-send))
+(defn -execute
+  [this request]
+  (let [http-method     (.httpMethod request)
+        url             (.url request)
+        headers         (.headers request)
+        data-to-send    (.dataToSend request)
+        request-body    (and data-to-send (RequestBody/create nil data-to-send))
         request-builder (.. (Request$Builder.)
                             (method http-method request-body)
                             (url url)
                             (addHeader "User-Agent" user-agent))]
     (doseq [pair (.entrySet headers)]
-      (let [header-name (.getKey pair)
+      (let [header-name       (.getKey pair)
             header-value-list (.getValue pair)]
         (if (> (.size header-value-list) 1)
           (do
             (.removeHeader request-builder header-name)
             (doseq [header-value header-value-list]
               (.addHeader request-builder header-name header-value)))
-          (if (= (.size header-value-list) 1)
+          (when (= (.size header-value-list) 1)
             (.header request-builder header-name (.get header-value-list 0))))))
-    (let [response (.. (@(.state this) :client) (newCall (.build request-builder)) (execute))
-          body (.body response)
+    (let [response                (.. (@(.state this) :client)
+                                      (newCall (.build request-builder))
+                                      (execute))
+          body                    (.body response)
           response-body-to-return (and body (.string body))
-          latest-url (.. response (request) (url) (toString))]
+          latest-url              (.. response (request) (url) (toString))]
       (when (= (.code response) 429)
         (.close response))
       (Response. (.code response)
@@ -70,4 +77,5 @@
                  response-body-to-return
                  latest-url))))
 
-(comment (compile 'tubo.downloader-impl))
+(comment
+  (compile 'tubo.downloader-impl))

+ 31 - 26
src/backend/tubo/handler.clj

@@ -3,11 +3,7 @@
    [clojure.string :as str]
    [hiccup.page :as hiccup]
    [ring.util.response :refer [response]]
-   [tubo.api.streams :as streams]
-   [tubo.api.channels :as channels]
-   [tubo.api.playlists :as playlists]
-   [tubo.api.comments :as comments]
-   [tubo.api.services :as services]))
+   [tubo.api :as api]))
 
 (defn index
   [_]
@@ -17,9 +13,10 @@
      [:meta {:charset "UTF-8"}]
      [:meta
       {:name "viewport"
-       :content "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"}]
+       :content
+       "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"}]
      [:title "Tubo"]
-     [:link {:rel "icon" :type "image/png" :href "/icons/tubo.svg"}]
+     [:link {:rel "icon" :type "image/svg+xml" :href "/icons/tubo.svg"}]
      (hiccup/include-css "/styles/index.css")]
     [:body
      [:div#app]
@@ -28,46 +25,54 @@
 
 (defn search
   [{:keys [parameters] :as req}]
-  (let [{:keys [service-id]} (:path parameters)
-        {:keys [q]} (:query parameters)
+  (let [{:keys [service-id]}                         (:path parameters)
+        {:keys [q]}                                  (:query parameters)
         {:strs [contentFilters sortFilter nextPage]} (:query-params req)
-        content-filters (and contentFilters (str/split contentFilters #","))]
+        content-filters                              (and contentFilters
+                                                          (str/split
+                                                           contentFilters
+                                                           #","))]
     (response (if nextPage
-                (services/search service-id q contentFilters sortFilter nextPage)
-                (services/search service-id q contentFilters sortFilter)))))
+                (api/get-search service-id q contentFilters sortFilter nextPage)
+                (api/get-search service-id q contentFilters sortFilter)))))
 
 (defn channel
   [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}]
   (response (if nextPage
-              (channels/get-channel url nextPage)
-              (channels/get-channel url))))
+              (api/get-channel url nextPage)
+              (api/get-channel url))))
 
 (defn playlist
   [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}]
   (response (if nextPage
-              (playlists/get-playlist url nextPage)
-              (playlists/get-playlist url))))
+              (api/get-playlist url nextPage)
+              (api/get-playlist url))))
 
 (defn comments
   [{{:keys [url]} :path-params {:strs [nextPage]} :query-params}]
   (response (if nextPage
-              (comments/get-comments url nextPage)
-              (comments/get-comments url))))
+              (api/get-comments url nextPage)
+              (api/get-comments url))))
 
 (defn services
   [_]
-  (response (services/get-services)))
+  (response (api/get-services)))
 
 (defn kiosks
   [{{{:keys [service-id]} :path} :parameters}]
-  (response (services/get-kiosks service-id)))
+  (response (api/get-kiosks service-id)))
 
 (defn kiosk
-  [{{{:keys [kiosk-id service-id]} :path} :parameters {:strs [nextPage]} :query-params}]
+  [{{{:keys [kiosk-id service-id]} :path} :parameters
+    {:strs [nextPage]}                    :query-params}]
   (response (cond
-              (and kiosk-id service-id nextPage) (services/get-kiosk kiosk-id service-id nextPage)
-              (and kiosk-id service-id) (services/get-kiosk kiosk-id service-id)
-              :else (services/get-kiosk service-id))))
+              (and kiosk-id service-id nextPage) (api/get-kiosk kiosk-id
+                                                                service-id
+                                                                nextPage)
+              (and kiosk-id service-id)          (api/get-kiosk kiosk-id
+                                                                service-id)
+              :else                              (api/get-kiosk service-id))))
 
-(defn stream [{{:keys [url]} :path-params}]
-  (response (streams/get-stream url)))
+(defn stream
+  [{{:keys [url]} :path-params}]
+  (response (api/get-stream url)))

+ 2 - 7
src/backend/tubo/http.clj

@@ -10,15 +10,10 @@
 (defonce server (atom nil))
 
 (defn start-server!
-  ([]
-   (start-server! 3000))
+  ([] (start-server! 3000))
   ([port]
    (NewPipe/init (DownloaderImpl/init) (Localization. "en" "US"))
    (reset! server (run-server #'routes/app {:port port}))
    (println "Server running in port" port)))
 
-(defn stop-server!
-  []
-  (when @server
-    (@server :timeout 100)
-    (reset! server nil)))
+(defn stop-server! [] (when @server (@server :timeout 100) (reset! server nil)))

+ 16 - 16
src/backend/tubo/routes.clj

@@ -1,14 +1,11 @@
 (ns tubo.routes
   (:require
-   [malli.experimental.lite :as l]
    [reitit.ring :as ring]
-   [reitit.coercion :as coercion]
    [reitit.ring.coercion :as rrc]
    [reitit.coercion.malli]
    [ring.middleware.reload :refer [wrap-reload]]
    [ring.middleware.params :refer [wrap-params]]
    [ring.middleware.json :refer [wrap-json-response]]
-   [ring.middleware.cors :refer [wrap-cors]]
    [tubo.handler :as handler]))
 
 (def router
@@ -26,21 +23,24 @@
      ["/services"
       ["" {:get handler/services}]
       ["/:service-id/search"
-       {:get {:coercion reitit.coercion.malli/coercion
-              :parameters {:path {:service-id int?}
+       {:get {:coercion   reitit.coercion.malli/coercion
+              :parameters {:path  {:service-id int?}
                            :query {:q string?}}
-              :handler handler/search}}]
+              :handler    handler/search}}]
       ["/:service-id"
-       ["/default-kiosk" {:get {:coercion reitit.coercion.malli/coercion
-                                :parameters {:path {:service-id int?}}
-                                :handler handler/kiosk}}]
+       ["/default-kiosk"
+        {:get {:coercion   reitit.coercion.malli/coercion
+               :parameters {:path {:service-id int?}}
+               :handler    handler/kiosk}}]
        ["/kiosks"
-        ["" {:get {:coercion reitit.coercion.malli/coercion
-                   :parameters {:path {:service-id int?}}
-                   :handler handler/kiosks}}]
-        ["/:kiosk-id" {:get {:coercion reitit.coercion.malli/coercion
-                             :parameters {:path {:service-id int? :kiosk-id string?}}
-                             :handler handler/kiosk}}]]]]
+        [""
+         {:get {:coercion   reitit.coercion.malli/coercion
+                :parameters {:path {:service-id int?}}
+                :handler    handler/kiosks}}]
+        ["/:kiosk-id"
+         {:get {:coercion   reitit.coercion.malli/coercion
+                :parameters {:path {:service-id int? :kiosk-id string?}}
+                :handler    handler/kiosk}}]]]]
      ["/streams/:url" {:get handler/stream}]
      ["/channels/:url" {:get handler/channel}]
      ["/playlists/:url" {:get handler/playlist}]
@@ -55,7 +55,7 @@
    (ring/routes
     (ring/create-resource-handler {:path "/"})
     (ring/create-default-handler
-     {:not-found (constantly {:status 404, :body "Not found"})}))
+     {:not-found (constantly {:status 404 :body "Not found"})}))
    {:middleware [wrap-params
                  [wrap-json-response {:pretty true}]
                  wrap-reload]}))

+ 6 - 6
src/frontend/tubo/api.cljs

@@ -6,10 +6,10 @@
   ([uri on-success on-failure]
    (get-request uri on-success on-failure {}))
   ([uri on-success on-failure params]
-   {:http-xhrio {:method :get
-                 :uri (str "/api/v1" uri)
-                 :params params
-                 :format (ajax/json-request-format)
+   {:http-xhrio {:method          :get
+                 :uri             (str "/api/v1" uri)
+                 :params          params
+                 :format          (ajax/json-request-format)
                  :response-format (ajax/json-response-format {:keywords? true})
-                 :on-success on-success
-                 :on-failure on-failure}}))
+                 :on-success      on-success
+                 :on-failure      on-failure}}))

+ 101 - 67
src/frontend/tubo/bookmarks/events.cljs

@@ -2,14 +2,15 @@
   (:require
    [nano-id.core :refer [nano-id]]
    [promesa.core :as p]
-   [re-frame.core :as rf]
-   [tubo.bookmarks.modals :as modals]))
+   [re-frame.core :as rf]))
 
 (rf/reg-event-fx
  :bookmarks/add
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ bookmark notify?]]
-   (let [updated-db (update db :bookmarks conj
+   (let [updated-db (update db
+                            :bookmarks
+                            conj
                             (if (:id bookmark)
                               bookmark
                               (assoc bookmark :id (nano-id))))]
@@ -17,26 +18,31 @@
       :store (assoc store :bookmarks (:bookmarks updated-db))
       :fx    [[:dispatch [:modals/close]]
               (when notify?
-                [:dispatch [:notifications/add
-                            {:status-text
-                             (str "Added playlist \"" (:name bookmark) "\"")
-                             :failure :success}]])]})))
+                [:dispatch
+                 [:notifications/add
+                  {:status-text
+                   (str "Added playlist \"" (:name bookmark) "\"")
+                   :failure :success}]])]})))
 
 (rf/reg-event-fx
  :bookmarks/remove
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ id notify?]]
    (let [bookmark   (first (filter #(= (:id %) id) (:bookmarks db)))
-         updated-db (update db :bookmarks
-                            #(into [] (remove (fn [bookmark]
-                                                (= (:id bookmark) id)) %)))]
+         updated-db (update db
+                            :bookmarks
+                            #(into []
+                                   (remove (fn [bookmark]
+                                             (= (:id bookmark) id))
+                                           %)))]
      {:db    updated-db
       :store (assoc store :bookmarks (:bookmarks updated-db))
       :fx    (if notify?
-               [[:dispatch [:notifications/add
-                            {:status-text
-                             (str "Removed playlist \"" (:name bookmark) "\"")
-                             :failure :success}]]]
+               [[:dispatch
+                 [:notifications/add
+                  {:status-text
+                   (str "Removed playlist \"" (:name bookmark) "\"")
+                   :failure :success}]]]
                [])})))
 
 (rf/reg-event-fx
@@ -49,63 +55,82 @@
                (map (fn [item]
                       [:dispatch [:likes/remove item]])
                     (:items (first (:bookmarks db)))))
-              [:dispatch [:notifications/add
-                          {:status-text "Cleared all playlists"
-                           :failure     :success}]])}))
+              [:dispatch
+               [:notifications/add
+                {:status-text "Cleared all playlists"
+                 :failure     :success}]])}))
 
 (rf/reg-event-fx
  :likes/add-n
  (fn [_ [_ items]]
    {:fx (conj (map (fn [item]
-                     [:dispatch [:likes/add item]]) items)
-              [:dispatch [:notifications/add
-                          {:status-text (str "Added " (count items)
-                                             " items to likes")
-                           :failure     :success}]])}))
+                     [:dispatch [:likes/add item]])
+                   items)
+              [:dispatch
+               [:notifications/add
+                {:status-text (str "Added "
+                                   (count items)
+                                   " items to likes")
+                 :failure     :success}]])}))
 
 (rf/reg-event-fx
  :likes/add
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ item notify?]]
    (when-not (some #(= (:url %) (:url item))
-                   (-> db :bookmarks first :items))
-     (let [updated-db (update-in db [:bookmarks 0 :items]
+                   (-> db
+                       :bookmarks
+                       first
+                       :items))
+     (let [updated-db (update-in db
+                                 [:bookmarks 0 :items]
                                  #(into [] (conj (into [] %1) %2))
-                                 (assoc item :bookmark-id
-                                        (-> db :bookmarks first :id)))]
+                                 (assoc item
+                                        :bookmark-id
+                                        (-> db
+                                            :bookmarks
+                                            first
+                                            :id)))]
        {:db    updated-db
         :store (assoc store :bookmarks (:bookmarks updated-db))
         :fx    (if notify?
-                 [[:dispatch [:notifications/add
-                              {:status-text "Added to favorites"
-                               :failure     :success}]]]
+                 [[:dispatch
+                   [:notifications/add
+                    {:status-text "Added to favorites"
+                     :failure     :success}]]]
                  [])}))))
 
 (rf/reg-event-fx
  :likes/remove
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ item notify?]]
-   (let [updated-db (update-in db [:bookmarks 0 :items]
+   (let [updated-db (update-in db
+                               [:bookmarks 0 :items]
                                (fn [items]
                                  (remove #(= (:url %) (:url item)) items)))]
      {:db    updated-db
       :store (assoc store :bookmarks (:bookmarks updated-db))
       :fx    (if notify?
-               [[:dispatch [:notifications/add
-                            {:status-text "Removed from favorites"
-                             :failure     :success}]]]
+               [[:dispatch
+                 [:notifications/add
+                  {:status-text "Removed from favorites"
+                   :failure     :success}]]]
                [])})))
 
 (rf/reg-event-fx
  :bookmark/add-n
  (fn [_ [_ bookmark items]]
    {:fx (conj (map (fn [item]
-                     [:dispatch [:bookmark/add bookmark item]]) items)
-              [:dispatch [:notifications/add
-                          {:status-text (str "Added " (count items)
-                                             " items to playlist \""
-                                             (:name bookmark) "\"")
-                           :failure     :success}]])}))
+                     [:dispatch [:bookmark/add bookmark item]])
+                   items)
+              [:dispatch
+               [:notifications/add
+                {:status-text (str "Added "
+                                   (count items)
+                                   " items to playlist \""
+                                   (:name bookmark)
+                                   "\"")
+                 :failure     :success}]])}))
 
 (rf/reg-event-fx
  :bookmark/add
@@ -115,17 +140,20 @@
          pos        (.indexOf (:bookmarks db) selected)
          updated-db (if (some #(= (:url %) (:url item)) (:items selected))
                       db
-                      (update-in db [:bookmarks pos :items]
+                      (update-in db
+                                 [:bookmarks pos :items]
                                  #(into [] (conj (into [] %1) %2))
                                  (assoc item :bookmark-id (:id bookmark))))]
      {:db    updated-db
       :store (assoc store :bookmarks (:bookmarks updated-db))
       :fx    [[:dispatch [:modals/close]]
               (when notify?
-                [:dispatch [:notifications/add
-                            {:status-text (str "Added to playlist \""
-                                               (:name selected) "\"")
-                             :failure     :success}]])]})))
+                [:dispatch
+                 [:notifications/add
+                  {:status-text (str "Added to playlist \""
+                                     (:name selected)
+                                     "\"")
+                   :failure     :success}]])]})))
 
 (rf/reg-event-fx
  :bookmark/remove
@@ -134,28 +162,32 @@
    (let [selected   (first (filter #(= (:id %) (:bookmark-id bookmark))
                                    (:bookmarks db)))
          pos        (.indexOf (:bookmarks db) selected)
-         updated-db (update-in db [:bookmarks pos :items]
+         updated-db (update-in db
+                               [:bookmarks pos :items]
                                #(remove (fn [item]
                                           (= (:url item) (:url bookmark)))
                                         %))]
      {:db    updated-db
       :store (assoc store :bookmarks (:bookmarks updated-db))
-      :fx    [[:dispatch [:notifications/add
-                          {:status-text (str "Removed from playlist \""
-                                             (:name selected) "\"")
-                           :failure     :success}]]]})))
+      :fx    [[:dispatch
+               [:notifications/add
+                {:status-text (str "Removed from playlist \""
+                                   (:name selected)
+                                   "\"")
+                 :failure     :success}]]]})))
 
 (rf/reg-event-fx
  :bookmarks/add-imported
- (fn [{:keys [db]} [_ bookmarks]]
+ (fn [_ [_ bookmarks]]
    {:fx (conj (map-indexed (fn [i bookmark]
                              (if (= i 0)
                                [:dispatch [:likes/add-n (:items bookmark)]]
                                [:dispatch [:bookmarks/add bookmark]]))
                            bookmarks)
-              [:dispatch [:notifications/add
-                          {:status-text "Imported playlists successfully"
-                           :failure     :success}]])}))
+              [:dispatch
+               [:notifications/add
+                {:status-text "Imported playlists successfully"
+                 :failure     :success}]])}))
 
 (defn fetch-imported-bookmarks-items
   [bookmarks]
@@ -172,17 +204,18 @@
 
 (rf/reg-event-fx
  :bookmarks/process-import
- (fn [{:keys [db]} [_ bookmarks]]
+ (fn [_ [_ bookmarks]]
    {:promise
     {:call         #(-> (fetch-imported-bookmarks-items bookmarks)
                         (p/then (fn [res]
                                   (js->clj res :keywordize-keys true))))
      :on-success-n [[:notifications/clear]
                     [:bookmarks/add-imported]]}
-    :fx [[:dispatch [:notifications/add
-                     {:status-text "Importing playlists..."
-                      :failure     :success}
-                     false]]]}))
+    :fx [[:dispatch
+          [:notifications/add
+           {:status-text "Importing playlists..."
+            :failure     :success}
+           false]]]}))
 
 (rf/reg-fx
  :bookmarks/import!
@@ -194,14 +227,14 @@
              (rf/dispatch [:bookmarks/process-import (:playlists res)])
              (throw (js/Error. "Format not supported")))))
        (p/catch js/Error
-           (fn [error]
-             (rf/dispatch [:notifications/add
-                           {:status-text (.-message error)
-                            :failure     :error}]))))))
+         (fn [error]
+           (rf/dispatch [:notifications/add
+                         {:status-text (.-message error)
+                          :failure     :error}]))))))
 
 (rf/reg-event-fx
  :bookmarks/import
- (fn [{:keys [db]} [_ files]]
+ (fn [_ [_ files]]
    {:fx (map (fn [file] [:bookmarks/import! file]) files)}))
 
 (rf/reg-event-fx
@@ -212,16 +245,17 @@
      :mime-type "application/json"
      :data      (.stringify
                  js/JSON
-                 (clj->js {:format  "Tubo"
+                 (clj->js {:format "Tubo"
                            :version 1
                            :playlists
                            (map (fn [bookmark]
                                   {:name  (:name bookmark)
                                    :items (map :url (:items bookmark))})
                                 (:bookmarks db))}))}
-    :fx [[:dispatch [:notifications/add
-                     {:status-text "Exported playlists"
-                      :failure     :success}]]]}))
+    :fx [[:dispatch
+          [:notifications/add
+           {:status-text "Exported playlists"
+            :failure     :success}]]]}))
 
 (rf/reg-event-fx
  :bookmarks/fetch-page

+ 7 - 4
src/frontend/tubo/bookmarks/modals.cljs

@@ -2,16 +2,19 @@
   (:require
    [reagent.core :as r]
    [re-frame.core :as rf]
-   [reitit.frontend.easy :as rfe]
    [tubo.components.layout :as layout]
    [tubo.modals.views :as modals]))
 
 (defn bookmark-item
-  [{:keys [items id name] :as bookmark} item]
+  [{:keys [items name] :as bookmark} item]
   [:div.flex.w-full.h-24.rounded.px-2.cursor-pointer.hover:bg-gray-100.dark:hover:bg-stone-800
-   {:on-click #(rf/dispatch [(if (vector? item) :bookmark/add-n :bookmark/add) bookmark item])}
+   {:on-click #(rf/dispatch [(if (vector? item) :bookmark/add-n :bookmark/add)
+                             bookmark item])}
    [:div.w-24
-    [layout/thumbnail (-> items first :thumbnail-url) nil name nil
+    [layout/thumbnail
+     (-> items
+         first
+         :thumbnail-url) nil name nil
      :classes [:h-24 :py-2] :rounded? true]]
    [:div.flex.flex-col.py-4.px-4
     [:h1.line-clamp-1.font-bold {:title name} name]

+ 20 - 12
src/frontend/tubo/bookmarks/views.cljs

@@ -11,14 +11,18 @@
   []
   (let [!menu-active? (r/atom nil)]
     (fn []
-      (let [color     @(rf/subscribe [:service-color])
-            bookmarks @(rf/subscribe [:bookmarks])
+      (let [bookmarks @(rf/subscribe [:bookmarks])
             items     (map
                        #(assoc %
-                               :url (rfe/href :bookmark-page nil {:id (:id %)})
-                               :thumbnail-url (-> % :items first :thumbnail-url)
-                               :stream-count (count (:items %))
-                               :bookmark-id (:id %))
+                               :stream-count  (count (:items %))
+                               :bookmark-id   (:id %)
+                               :url           (rfe/href :bookmark-page
+                                                        nil
+                                                        {:id (:id %)})
+                               :thumbnail-url (-> %
+                                                  :items
+                                                  first
+                                                  :thumbnail-url))
                        bookmarks)]
         [layout/content-container
          [layout/content-header "Bookmarked Playlists"
@@ -32,10 +36,12 @@
                :type      "file"
                :multiple  "multiple"
                :on-click  #(reset! !menu-active? true)
-               :on-change #(rf/dispatch [:bookmarks/import (.. % -target -files)])}]
+               :on-change #(rf/dispatch [:bookmarks/import
+                                         (.. % -target -files)])}]
              [:label.whitespace-nowrap.cursor-pointer.w-full.h-full.absolute.right-0.top-0
               {:for "file-selector"}]
-             [:span.text-xs.w-10.min-w-4.w-4.flex.items-center [:i.fa-solid.fa-file-import]]
+             [:span.text-xs.w-10.min-w-4.w-4.flex.items-center
+              [:i.fa-solid.fa-file-import]]
              [:span "Import"]]
             {:label    "Export"
              :icon     [:i.fa-solid.fa-file-export]
@@ -50,9 +56,9 @@
   (let [!menu-active? (r/atom nil)]
     (fn []
       (let [bookmarks                    @(rf/subscribe [:bookmarks])
-            service-color                @(rf/subscribe [:service-color])
             {{:keys [id]} :query-params} @(rf/subscribe [:current-match])
-            {:keys [items name]}         (first (filter #(= (:id %) id) bookmarks))]
+            {:keys [items name]}         (first (filter #(= (:id %) id)
+                                                        bookmarks))]
         [layout/content-container
          [layout/content-header name
           (when-not (empty? items)
@@ -62,5 +68,7 @@
                :on-click #(rf/dispatch [:queue/add-n items true])}
               {:label    "Add to playlist"
                :icon     [:i.fa-solid.fa-plus]
-               :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark items]])}]])]
-         [items/related-streams (map #(assoc % :type "stream" :bookmark-id id) items)]]))))
+               :on-click #(rf/dispatch [:modals/open
+                                        [modals/add-to-bookmark items]])}]])]
+         [items/related-streams
+          (map #(assoc % :type "stream" :bookmark-id id) items)]]))))

+ 17 - 10
src/frontend/tubo/channel/events.cljs

@@ -7,16 +7,18 @@
 
 (rf/reg-event-fx
  :channel/fetch
- (fn [{:keys [db]} [_ uri on-success on-error]]
+ (fn [_ [_ uri on-success on-error]]
    (api/get-request
     (str "/channels/" (js/encodeURIComponent uri))
-    on-success on-error)))
+    on-success
+    on-error)))
 
 (rf/reg-event-fx
  :channel/load-page
  (fn [{:keys [db]} [_ res]]
    (let [channel-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db :channel channel-res
+     {:db (assoc db
+                 :channel           channel-res
                  :show-page-loading false)
       :fx [[:dispatch [:services/fetch channel-res]]
            [:document-title (:name channel-res)]]})))
@@ -24,15 +26,17 @@
 (rf/reg-event-fx
  :channel/bad-page-response
  (fn [{:keys [db]} [_ uri res]]
-   {:fx [[:dispatch [:change-view #(layout/error res [:channel/fetch-page uri])]]]
+   {:fx [[:dispatch
+          [:change-view #(layout/error res [:channel/fetch-page uri])]]]
     :db (assoc db :show-page-loading false)}))
 
 (rf/reg-event-fx
  :channel/fetch-page
  (fn [{:keys [db]} [_ uri]]
    {:fx [[:dispatch [:change-view channel/channel]]
-         [:dispatch [:channel/fetch uri [:channel/load-page]
-                     [:channel/bad-page-response uri]]]]
+         [:dispatch
+          [:channel/fetch uri [:channel/load-page]
+           [:channel/bad-page-response uri]]]]
     :db (assoc db :show-page-loading true)}))
 
 (rf/reg-event-db
@@ -44,7 +48,8 @@
            (assoc-in [:channel :next-page] nil)
            (assoc :show-pagination-loading false))
        (-> db
-           (update-in [:channel :related-streams] #(apply conj %1 %2)
+           (update-in [:channel :related-streams]
+                      #(apply conj %1 %2)
                       (:related-streams channel-res))
            (assoc-in [:channel :next-page] (:next-page channel-res))
            (assoc :show-pagination-loading false))))))
@@ -62,7 +67,9 @@
      {:db (assoc db :show-pagination-loading false)}
      (assoc
       (api/get-request
-       (str "/channels/" (js/encodeURIComponent uri) )
-       [:channel/load-paginated] [:channel/bad-paginated-response]
+       (str "/channels/" (js/encodeURIComponent uri))
+       [:channel/load-paginated]
+       [:channel/bad-paginated-response]
        {:nextPage (js/encodeURIComponent next-page-url)})
-      :db (assoc db :show-pagination-loading true)))))
+      :db
+      (assoc db :show-pagination-loading true)))))

+ 12 - 9
src/frontend/tubo/channel/views.cljs

@@ -7,16 +7,16 @@
    [tubo.components.layout :as layout]))
 
 (defn channel
-  [query-params]
-  (let [!menu-active? (r/atom nil)
+  [_]
+  (let [!menu-active?      (r/atom nil)
         !show-description? (r/atom false)]
     (fn [{{:keys [url]} :query-params}]
       (let [{:keys [banner avatar name description subscriber-count next-page
-                    related-streams]} @(rf/subscribe [:channel])
-            next-page-url             (:url next-page)
-            service-color             @(rf/subscribe [:service-color])
-            scrolled-to-bottom?       @(rf/subscribe [:scrolled-to-bottom])
-            page-loading?             @(rf/subscribe [:show-page-loading])]
+                    related-streams]}
+            @(rf/subscribe [:channel])
+            next-page-url (:url next-page)
+            scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])
+            page-loading? @(rf/subscribe [:show-page-loading])]
         (when (and next-page-url scrolled-to-bottom?)
           (rf/dispatch [:channel/fetch-paginated url next-page-url]))
         [:<>
@@ -27,7 +27,8 @@
          [layout/content-container
           [:div.flex.items-center.justify-between
            [:div.flex.items-center.my-4.mx-2
-            [layout/uploader-avatar {:uploader-avatar avatar :uploader-name name}]
+            [layout/uploader-avatar
+             {:uploader-avatar avatar :uploader-name name}]
             [:div.m-4
              [:h1.text-2xl.line-clamp-1.font-semibold {:title name} name]
              (when subscriber-count
@@ -41,7 +42,9 @@
                 :on-click #(rf/dispatch [:queue/add-n related-streams true])}
                {:label    "Add to playlist"
                 :icon     [:i.fa-solid.fa-plus]
-                :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark related-streams]])}]])]
+                :on-click #(rf/dispatch [:modals/open
+                                         [modals/add-to-bookmark
+                                          related-streams]])}]])]
           (when-not (empty? description)
             [layout/show-more-container @!show-description? description
              #(reset! !show-description? (not @!show-description?))])

+ 17 - 10
src/frontend/tubo/comments/events.cljs

@@ -5,9 +5,12 @@
 
 (rf/reg-event-fx
  :comments/fetch
- (fn [{:keys [db]} [_ url on-success on-error params]]
-   (api/get-request (str "/comments/" (js/encodeURIComponent url))
-                    on-success on-error params)))
+ (fn [_ [_ url on-success on-error params]]
+   (api/get-request (str "/comments/"
+                         (js/encodeURIComponent url))
+                    on-success
+                    on-error
+                    params)))
 
 (rf/reg-event-db
  :comments/load-page
@@ -19,8 +22,9 @@
 (rf/reg-event-fx
  :comments/fetch-page
  (fn [{:keys [db]} [_ url]]
-   {:fx [[:dispatch [:comments/fetch url
-                     [:comments/load-page] [:bad-response]]]]
+   {:fx [[:dispatch
+          [:comments/fetch url
+           [:comments/load-page] [:bad-response]]]]
     :db (-> db
             (assoc-in [:stream :show-comments-loading] true)
             (assoc-in [:stream :show-comments] true))}))
@@ -28,7 +32,8 @@
 (rf/reg-event-db
  :comments/toggle-replies
  (fn [db [_ comment-id]]
-   (update-in db [:stream :comments-page :comments]
+   (update-in db
+              [:stream :comments-page :comments]
               (fn [comments]
                 (map #(if (= (:id %) comment-id)
                         (assoc % :show-replies (not (:show-replies %)))
@@ -39,7 +44,8 @@
  :comments/load-paginated
  (fn [db [_ res]]
    (-> db
-       (update-in [:stream :comments-page :comments] #(apply conj %1 %2)
+       (update-in [:stream :comments-page :comments]
+                  #(apply conj %1 %2)
                   (:comments (js->clj res :keywordize-keys true)))
        (assoc-in [:stream :comments-page :next-page]
                  (:next-page (js->clj res :keywordize-keys true)))
@@ -51,6 +57,7 @@
    (if (empty? next-page-url)
      {:db (assoc db :show-pagination-loading false)}
      {:db (assoc db :show-pagination-loading true)
-      :fx [[:dispatch [:comments/fetch url
-                       [:comments/load-paginated] [:bad-response]
-                       {:nextPage (js/encodeURIComponent next-page-url)}]]]})))
+      :fx [[:dispatch
+            [:comments/fetch url
+             [:comments/load-paginated] [:bad-response]
+             {:nextPage (js/encodeURIComponent next-page-url)}]]]})))

+ 32 - 15
src/frontend/tubo/comments/views.cljs

@@ -6,23 +6,28 @@
    [tubo.utils :as utils]))
 
 (defn comment-top-metadata
-  [{:keys [pinned? uploader-name uploader-url uploader-verified? stream-position]}]
+  [{:keys [pinned? uploader-name uploader-url uploader-verified?
+           stream-position]}]
   [:div.flex.items-center
    (when pinned?
      [:i.fa-solid.fa-thumbtack.mr-2.text-xs])
    (when uploader-name
      [:div.flex.items-stretch
-      [:a {:href  (rfe/href :channel-page nil {:url uploader-url})
-           :title uploader-name}
-       [:h1.text-neutral-800.dark:text-gray-300.font-bold.line-clamp-1 uploader-name]]
+      [:a
+       {:href  (rfe/href :channel-page nil {:url uploader-url})
+        :title uploader-name}
+       [:h1.text-neutral-800.dark:text-gray-300.font-bold.line-clamp-1
+        uploader-name]]
       (when stream-position
         [:div.text-neutral-600.dark:text-neutral-300
-         [:span.mx-2.text-xs.whitespace-nowrap (utils/format-duration stream-position)]])])
+         [:span.mx-2.text-xs.whitespace-nowrap
+          (utils/format-duration stream-position)]])])
    (when uploader-verified?
      [:i.fa-solid.fa-circle-check.ml-2])])
 
 (defn comment-bottom-metadata
-  [{:keys [upload-date like-count hearted-by-uploader? author-avatar author-name]}]
+  [{:keys [upload-date like-count hearted-by-uploader? author-avatar
+           author-name]}]
   [:div.flex.items-center.my-2
    [:div.mr-4
     [:p (utils/format-date-ago upload-date)]]
@@ -34,8 +39,8 @@
      [:div.relative.w-4.h-4.mx-2
       [:i.fa-solid.fa-heart.absolute.-bottom-1.-right-1.text-xs.text-red-500]
       [:img.rounded-full.object-covermax-w-full.min-h-full
-       {:src author-avatar :title (str author-name " hearted this comment")}]])])
-
+       {:src   author-avatar
+        :title (str author-name " hearted this comment")}]])])
 
 (defn comment-item
   [{:keys [id text replies reply-count show-replies] :as comment}]
@@ -44,7 +49,9 @@
    [:div
     [comment-top-metadata comment]
     [:div.my-2
-     [:p {:dangerouslySetInnerHTML {:__html text} :class "[overflow-wrap:anywhere]"}]]
+     [:p
+      {:dangerouslySetInnerHTML {:__html text}
+       :class                   "[overflow-wrap:anywhere]"}]]
     [comment-bottom-metadata comment]
     [:div.flex.items-center.cursor-pointer
      {:on-click #(rf/dispatch [:comments/toggle-replies id])}
@@ -54,23 +61,33 @@
           [:p.font-bold "Hide replies"]
           [:i.fa-solid.fa-turn-up.mx-2.text-xs]]
          [:<>
-          [:p.font-bold (str reply-count (if (= reply-count 1) " reply" " replies"))]
+          [:p.font-bold
+           (str reply-count (if (= reply-count 1) " reply" " replies"))]
           [:i.fa-solid.fa-turn-down.mx-2.text-xs]]))]]])
 
 (defn comments
-  [{:keys [comments next-page disabled?]} {:keys [uploader-name uploader-avatar url]}]
+  [{:keys [comments next-page]}
+   {:keys [uploader-name uploader-avatar url]}]
   (let [pagination-loading? @(rf/subscribe [:show-pagination-loading])
-        service-color @(rf/subscribe [:service-color])]
+        service-color       @(rf/subscribe [:service-color])]
     [:div.flex.flex-col
      [:div
-      (for [[i {:keys [replies show-replies] :as comment}] (map-indexed vector comments)]
+      (for [[i {:keys [replies show-replies] :as comment}]
+            (map-indexed vector comments)]
         [:div.flex.flex-col {:key i}
          [:div.flex
-          [comment-item (assoc comment :author-name uploader-name :author-avatar uploader-avatar)]]
+          [comment-item
+           (assoc comment
+                  :author-name   uploader-name
+                  :author-avatar uploader-avatar)]]
          (when (and replies show-replies)
            [:div {:style {:marginLeft "32px"}}
             (for [[i reply] (map-indexed vector (:items replies))]
-              ^{:key i} [comment-item (assoc reply :author-name uploader-name :author-avatar uploader-avatar)])])])]
+              ^{:key i}
+              [comment-item
+               (assoc reply
+                      :author-name   uploader-name
+                      :author-avatar uploader-avatar)])])])]
      (when (:url next-page)
        (if pagination-loading?
          [layout/loading-icon service-color]

+ 53 - 35
src/frontend/tubo/components/items.cljs

@@ -11,41 +11,58 @@
 (defn item-popover
   [_ _]
   (let [!menu-active? (r/atom nil)]
-    (fn [{:keys [service-id audio-streams video-streams type url bookmark-id uploader-url] :as item} bookmarks]
-      (let [liked?  (some #(= (:url %) url) (-> bookmarks first :items))
-            items (if (or (= type "stream") audio-streams video-streams)
-                    [{:label    "Add to queue"
-                      :icon     [:i.fa-solid.fa-headphones]
-                      :on-click #(rf/dispatch [:player/switch-to-background item true])}
-                     {:label    "Play radio"
-                      :icon     [:i.fa-solid.fa-tower-cell]
-                      :on-click #(rf/dispatch [:player/start-radio item])}
-                     {:label    (if liked? "Remove favorite" "Favorite")
-                      :icon     [:i.fa-solid.fa-heart (when (and liked? service-id)
-                                                        {:style {:color (utils/get-service-color service-id)}})]
-                      :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) item true])}
-                     {:label    "Add to playlist"
-                      :icon     [:i.fa-solid.fa-plus]
-                      :on-click #(rf/dispatch [:modals/open [bookmarks/add-to-bookmark item]])}
-                     (when (some #(= (:url %) url) (:items (first (filter #(= (:id %) bookmark-id) bookmarks))))
-                       {:label    "Remove from playlist"
-                        :icon     [:i.fa-solid.fa-trash]
-                        :on-click #(rf/dispatch [:bookmark/remove item])})
-                     {:label    "Show channel details"
-                      :icon     [:i.fa-solid.fa-user]
-                      :on-click #(rf/dispatch [:navigate
-                                               {:name   :channel-page
-                                                :params {}
-                                                :query  {:url uploader-url}}])}]
-                    [(when (and bookmarks (some #(= (:id %) bookmark-id) (rest bookmarks)))
-                       {:label    "Remove playlist"
-                        :icon     [:i.fa-solid.fa-trash]
-                        :on-click #(rf/dispatch [:bookmarks/remove bookmark-id true])})])]
+    (fn [{:keys [service-id audio-streams video-streams type url bookmark-id
+                 uploader-url]
+          :as   item} bookmarks]
+      (let [liked? (some #(= (:url %) url)
+                         (-> bookmarks
+                             first
+                             :items))
+            items
+            (if (or (= type "stream") audio-streams video-streams)
+              [{:label    "Add to queue"
+                :icon     [:i.fa-solid.fa-headphones]
+                :on-click #(rf/dispatch [:player/switch-to-background item
+                                         true])}
+               {:label    "Play radio"
+                :icon     [:i.fa-solid.fa-tower-cell]
+                :on-click #(rf/dispatch [:player/start-radio item])}
+               {:label    (if liked? "Remove favorite" "Favorite")
+                :icon     [:i.fa-solid.fa-heart
+                           (when (and liked? service-id)
+                             {:style {:color (utils/get-service-color
+                                              service-id)}})]
+                :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add)
+                                         item true])}
+               {:label    "Add to playlist"
+                :icon     [:i.fa-solid.fa-plus]
+                :on-click #(rf/dispatch [:modals/open
+                                         [bookmarks/add-to-bookmark item]])}
+               (when (some #(= (:url %) url)
+                           (:items (first (filter #(= (:id %) bookmark-id)
+                                                  bookmarks))))
+                 {:label    "Remove from playlist"
+                  :icon     [:i.fa-solid.fa-trash]
+                  :on-click #(rf/dispatch [:bookmark/remove item])})
+               {:label    "Show channel details"
+                :icon     [:i.fa-solid.fa-user]
+                :on-click #(rf/dispatch [:navigate
+                                         {:name   :channel-page
+                                          :params {}
+                                          :query  {:url uploader-url}}])}]
+              [(when (and bookmarks
+                          (some #(= (:id %) bookmark-id) (rest bookmarks)))
+                 {:label    "Remove playlist"
+                  :icon     [:i.fa-solid.fa-trash]
+                  :on-click #(rf/dispatch [:bookmarks/remove bookmark-id
+                                           true])})])]
         (when (not-empty (remove nil? items))
           [layout/popover-menu !menu-active? items])))))
 
 (defn item-content
-  [{:keys [url name uploader-url uploader-name subscriber-count view-count stream-count verified?] :as item} route bookmarks]
+  [{:keys [url name uploader-url uploader-name subscriber-count view-count
+           stream-count verified?]
+    :as   item} route bookmarks]
   [:div
    (when name
      [:div.flex.items-center.my-2
@@ -57,9 +74,10 @@
     [:div.flex.items-center.my-2
      (conj
       (when uploader-url
-        [:a {:href  (rfe/href :channel-page nil {:url uploader-url})
-             :title uploader-name
-             :key url}])
+        [:a
+         {:href  (rfe/href :channel-page nil {:url uploader-url})
+          :title uploader-name
+          :key   url}])
       [:h1.text-neutral-800.dark:text-gray-300.font-semibold.pr-2.line-clamp-1.break-all
        {:class "[overflow-wrap:anywhere]" :title uploader-name :key url}
        uploader-name])
@@ -109,5 +127,5 @@
         {:class "xs:grid-cols-[repeat(auto-fill,_minmax(165px,_1fr))]"}
         (for [[i item] (map-indexed vector related-streams)]
           ^{:key i} [generic-item item bookmarks])])
-     (when (and pagination-loading? (not (empty? next-page-url)))
+     (when (and pagination-loading? (seq next-page-url))
        [layout/loading-icon service-color :text-md])]))

+ 46 - 29
src/frontend/tubo/components/layout.cljs

@@ -1,5 +1,6 @@
 (ns tubo.components.layout
   (:require
+   [clojure.string :as str]
    [re-frame.core :as rf]
    [reitit.frontend.easy :as rfe]
    [reagent.core :as r]
@@ -12,11 +13,13 @@
    [:div.relative.min-w-full
     [:a.absolute.min-w-full.min-h-full.z-10 {:href route :title name}]
     (if thumbnail-url
-      [:img.object-cover.min-h-full.max-h-full.min-w-full {:src thumbnail-url :class (when rounded? :rounded)}]
+      [:img.object-cover.min-h-full.max-h-full.min-w-full
+       {:src thumbnail-url :class (when rounded? :rounded)}]
       [:div.bg-gray-300.flex.min-h-full.min-w-full.justify-center.items-center.rounded
        [:i.fa-solid.fa-image.text-3xl.text-white]])
     (when duration
-      [:div.rounded.p-2.absolute {:style {:bottom 5 :right 5 :background "rgba(0,0,0,.7)" :zIndex "0"}}
+      [:div.rounded.p-2.absolute
+       {:style {:bottom 5 :right 5 :background "rgba(0,0,0,.7)" :zIndex "0"}}
        [:p.text-white.text-md
         (if (= duration 0)
           "LIVE"
@@ -66,14 +69,15 @@
      (conj
       (when uploader-url
         [:a.flex-auto.flex.min-h-full.min-w-full.max-h-full.max-w-full
-         {:href (rfe/href :channel-page nil {:url uploader-url})
+         {:href  (rfe/href :channel-page nil {:url uploader-url})
           :title uploader-name
-          :key uploader-url}])
+          :key   uploader-url}])
       [:img.flex-auto.rounded-full.object-cover.max-w-full.min-h-full
        {:src uploader-avatar :alt uploader-name :key uploader-name}])]))
 
 (defn button
-  [label on-click left-icon right-icon & {:keys [button-classes label-classes icon-classes]}]
+  [label on-click left-icon right-icon &
+   {:keys [button-classes label-classes icon-classes]}]
   [:button.px-4.rounded-3xl.py-1.outline-none.focus:ring-transparent.whitespace-nowrap
    {:on-click on-click :class button-classes}
    (when left-icon
@@ -86,21 +90,24 @@
   [label on-click left-icon right-icon]
   [button label on-click left-icon right-icon
    :button-classes ["bg-stone-800" "dark:bg-white"]
-   :label-classes  ["text-neutral-300" "dark:text-neutral-900"]])
+   :label-classes ["text-neutral-300" "dark:text-neutral-900"]])
 
 (defn secondary-button
   [label on-click left-icon right-icon]
   [button label on-click left-icon right-icon
-   :button-classes ["bg-neutral-100" "dark:bg-transparent" "border" "border-neutral-300" "dark:border-stone-700"]
+   :button-classes
+   ["bg-neutral-100" "dark:bg-transparent" "border" "border-neutral-300"
+    "dark:border-stone-700"]
    :label-classes ["text-neutral-500" "dark:text-white"]])
 
-(defn generic-input [label & children]
+(defn generic-input
+  [label & children]
   [:div.w-full.flex.justify-between.items-center.py-2.gap-x-4
    [:label label]
    (map-indexed #(with-meta %2 {:key %1}) children)])
 
 (defn text-input
-  [label key value on-change placeholder]
+  [label _key value on-change placeholder]
   [generic-input label
    [:input.text-black
     {:type          "text"
@@ -109,7 +116,7 @@
      :placeholder   placeholder}]])
 
 (defn boolean-input
-  [label key value on-change]
+  [label _key value on-change]
   [generic-input label
    [:input
     {:type      "checkbox"
@@ -118,27 +125,34 @@
      :on-change on-change}]])
 
 (defn select-input
-  [label key value options on-change]
+  [label _key value options on-change]
   [generic-input label
    [:select.focus:ring-transparent.bg-transparent.font-bold
     {:value     value
      :on-change on-change}
     (for [[i option] (map-indexed vector options)]
-      ^{:key i} [:option.dark:bg-neutral-900.border-none {:value option :key i} option])]])
+      ^{:key i}
+      [:option.dark:bg-neutral-900.border-none {:value option :key i}
+       option])]])
 
 (defn menu-item
   [{:keys [label icon on-click link] :as item}]
   (let [content [:<>
-                 [:span.text-xs.min-w-4.w-4.flex.justify-center.items-center icon]
+                 [:span.text-xs.min-w-4.w-4.flex.justify-center.items-center
+                  icon]
                  [:span.whitespace-nowrap label]]
-        classes ["relative" "flex" "items-center" "gap-x-3" "hover:bg-neutral-200"
+        classes ["relative" "flex" "items-center" "gap-x-3"
+                 "hover:bg-neutral-200"
                  "dark:hover:bg-stone-800" "py-2" "px-3" "rounded"]]
     (if link
-      [:a {:href  (:route link) :target (when (:external? link) "_blank")
-           :class (clojure.string/join " " classes)}
+      [:a
+       {:href   (:route link)
+        :target (when (:external? link) "_blank")
+        :class  (str/join " " classes)}
        content]
-      [:li {:on-click on-click
-            :class    (clojure.string/join " " classes)}
+      [:li
+       {:on-click on-click
+        :class    (str/join " " classes)}
        (if (vector? item) item content)])))
 
 (defn menu
@@ -176,7 +190,7 @@
      (map-indexed #(with-meta %2 {:key %1}) content))])
 
 (defn show-more-container
-  [open? text on-open]
+  [_open? _text _on-open]
   (let [!text-container  (atom nil)
         !resize-observer (atom nil)
         text-clamped?    (r/atom nil)]
@@ -186,11 +200,11 @@
         (when @!text-container
           (.observe
            (reset! !resize-observer
-                   (js/ResizeObserver.
-                    #(let [target (.-target (first %))]
-                       (reset! text-clamped?
-                               (> (.-scrollHeight target)
-                                  (.-clientHeight target))))))
+             (js/ResizeObserver.
+              #(let [target (.-target (first %))]
+                 (reset! text-clamped?
+                   (> (.-scrollHeight target)
+                      (.-clientHeight target))))))
            @!text-container)))
       :component-will-unmount
       #(when (and @!resize-observer @!text-container)
@@ -200,17 +214,20 @@
         [:div.py-3.min-w-full
          [:span.text-clip.pr-2
           {:dangerouslySetInnerHTML {:__html text}
-           :class (when-not open? "line-clamp-2")
-           :ref   #(reset! !text-container %)}]
+           :class                   (when-not open? "line-clamp-2")
+           :ref                     #(reset! !text-container %)}]
          (when (or @text-clamped? open?)
-           [:button.font-bold {:on-click on-open} (str "show " (if open? "less" "more"))])])})))
+           [:button.font-bold {:on-click on-open}
+            (str "show " (if open? "less" "more"))])])})))
 
-(defn error [{:keys [failure parse-error status status-text]} cb]
+(defn error
+  [{:keys [_failure parse-error status status-text]} cb]
   [:div.flex.flex-auto.h-full.items-center.justify-center.p-5
    [:div.flex.flex-col.gap-y-6.border-border-neutral-300.rounded.dark:border-stone-700.bg-neutral-300.dark:bg-neutral-800.p-5
     [:div.flex.items-center.gap-2.text-xl
      [:i.fa-solid.fa-circle-exclamation]
-     [:h3.font-bold (str status (when (and status status-text) ": ") status-text)]]
+     [:h3.font-bold
+      (str status (when (and status status-text) ": ") status-text)]]
     (when parse-error
       [:span (:status-text parse-error)])
     [:div.flex.justify-center.gap-x-3

+ 19 - 12
src/frontend/tubo/components/navigation.cljs

@@ -1,7 +1,6 @@
 (ns tubo.components.navigation
   (:require
    [re-frame.core :as rf]
-   [reagent.core :as r]
    [reitit.frontend.easy :as rfe]
    [tubo.components.layout :as layout]
    [tubo.kiosks.views :as kiosks]
@@ -17,35 +16,43 @@
     [:span {:class (when active? "font-bold")} label]]])
 
 (defn mobile-nav
-  [show-mobile-nav? service-color services available-kiosks & {:keys [service-id] :as kiosk-args}]
+  [show-mobile-nav? service-color services available-kiosks &
+   {:keys [service-id] :as kiosk-args}]
   [:<>
    [layout/focus-overlay #(rf/dispatch [:toggle-mobile-nav]) show-mobile-nav?]
    [:div.fixed.overflow-x-hidden.min-h-screen.w-60.top-0.transition-all.ease-in-out.delay-75.bg-white.dark:bg-neutral-900.z-20
     {:class [(if show-mobile-nav? "left-0" "left-[-245px]")]}
-    [:div.flex.justify-center.py-4.items-center.text-white {:style {:background service-color}}
+    [:div.flex.justify-center.py-4.items-center.text-white
+     {:style {:background service-color}}
      [layout/logo :height 75 :width 75]
      [:h3.text-3xl.font-bold "Tubo"]]
     [services/services-dropdown services service-id service-color]
     [:div.relative.py-4
      [:ul.flex.flex-col
       (for [[i kiosk] (map-indexed vector available-kiosks)]
-        ^{:key i} [mobile-nav-item
-                   (rfe/href :kiosk-page nil {:serviceId service-id :kioskId kiosk})
-                   [:i.fa-solid.fa-fire] kiosk
-                   :active? (kiosks/kiosk-active? (assoc kiosk-args :kiosk kiosk))])]]
+        ^{:key i}
+        [mobile-nav-item
+         (rfe/href :kiosk-page nil {:serviceId service-id :kioskId kiosk})
+         [:i.fa-solid.fa-fire] kiosk
+         :active? (kiosks/kiosk-active? (assoc kiosk-args :kiosk kiosk))])]]
     [:div.relative.dark:border-neutral-800.border-gray-300.pt-4
      {:class "border-t-[1px]"}
      [:ul.flex.flex-col
-      [mobile-nav-item (rfe/href :bookmarks-page) [:i.fa-solid.fa-bookmark] "Bookmarks"]
-      [mobile-nav-item (rfe/href :settings-page) [:i.fa-solid.fa-cog] "Settings"]]]]])
+      [mobile-nav-item (rfe/href :bookmarks-page) [:i.fa-solid.fa-bookmark]
+       "Bookmarks"]
+      [mobile-nav-item (rfe/href :settings-page) [:i.fa-solid.fa-cog]
+       "Settings"]]]]])
 
 (defn navbar
   [{{:keys [kioskId]} :query-params path :path}]
   (let [service-id                               @(rf/subscribe [:service-id])
-        service-color                            @(rf/subscribe [:service-color])
+        service-color                            @(rf/subscribe
+                                                   [:service-color])
         services                                 @(rf/subscribe [:services])
-        show-mobile-nav?                         @(rf/subscribe [:show-mobile-nav])
-        show-search-form?                        @(rf/subscribe [:show-search-form])
+        show-mobile-nav?                         @(rf/subscribe
+                                                   [:show-mobile-nav])
+        show-search-form?                        @(rf/subscribe
+                                                   [:show-search-form])
         {:keys [default-service]}                @(rf/subscribe [:settings])
         {:keys [available-kiosks default-kiosk]} @(rf/subscribe [:kiosks])]
     [:nav.sticky.flex.items-center.px-2.h-14.top-0.z-20

+ 90 - 59
src/frontend/tubo/components/player.cljs

@@ -1,19 +1,22 @@
 (ns tubo.components.player
   (:require
+   [clojure.string :as str]
    [reagent.core :as r]
    [re-frame.core :as rf]
-   [reagent.core :as r]
-   [reagent.dom :as rdom]
    ["@vidstack/react" :refer (MediaPlayer MediaProvider Poster)]
-   ["@vidstack/react/player/layouts/default" :refer (defaultLayoutIcons DefaultVideoLayout DefaultAudioLayout)]))
+   ["@vidstack/react/player/layouts/default" :refer
+    (defaultLayoutIcons DefaultVideoLayout DefaultAudioLayout)]))
 
 (defn get-video-player-sources
   [available-streams service-id]
   (if available-streams
     (if (= service-id 3)
-      (map (fn [{:keys [content]}] {:src content :type "video/mp4"}) (reverse available-streams))
+      (map (fn [{:keys [content]}] {:src content :type "video/mp4"})
+           (reverse available-streams))
       (->> available-streams
-           (filter #(and (not= (:format %) "WEBMA_OPUS") (not= (:format %) "OPUS") (not= (:format %) "M4A")))
+           (filter #(and (not= (:format %) "WEBMA_OPUS")
+                         (not= (:format %) "OPUS")
+                         (not= (:format %) "M4A")))
            (sort-by :bitrate)
            (#(if (empty? (filter (fn [x] (= (:format x) "MP3")) %))
                (reverse %)
@@ -23,28 +26,34 @@
     []))
 
 (defn video-player
-  [stream !player]
-  (let [!elapsed-time @(rf/subscribe [:elapsed-time])
+  [_stream _!player]
+  (let [!elapsed-time       @(rf/subscribe [:elapsed-time])
         !main-player-first? (r/atom true)]
     (r/create-class
      {:component-will-unmount #(rf/dispatch [:main-player/ready false])
       :reagent-render
-      (fn [{:keys [name video-streams audio-streams thumbnail-url service-id]} !player]
+      (fn [{:keys [name video-streams audio-streams thumbnail-url service-id]}
+           !player]
         (let [show-main-player? @(rf/subscribe [:main-player/show])]
           [:> MediaPlayer
            {:title          name
-            :src            (get-video-player-sources (into video-streams audio-streams) service-id)
+            :src            (get-video-player-sources (into video-streams
+                                                            audio-streams)
+                                                      service-id)
             :poster         thumbnail-url
             :class          "w-full xl:w-3/5 overflow-hidden"
             :playsInline    true
             :ref            #(reset! !player %)
-            :loop           (when show-main-player? (= @(rf/subscribe [:loop-playback]) :stream))
+            :loop           (when show-main-player?
+                              (= @(rf/subscribe [:loop-playback]) :stream))
             :onSeeked       (when show-main-player?
                               #(reset! !elapsed-time (.-currentTime @!player)))
             :onTimeUpdate   (when show-main-player?
                               #(reset! !elapsed-time (.-currentTime @!player)))
             :onEnded        #(when show-main-player?
-                               (rf/dispatch [:queue/change-pos (inc @(rf/subscribe [:queue-pos]))])
+                               (rf/dispatch [:queue/change-pos
+                                             (inc @(rf/subscribe
+                                                    [:queue-pos]))])
                                (reset! !elapsed-time 0))
             :onLoadedData   (fn []
                               (when show-main-player?
@@ -56,9 +65,10 @@
             :onSourceChange #(when-not @!main-player-first?
                                (reset! !elapsed-time 0))}
            [:> MediaProvider
-            [:> Poster {:src   thumbnail-url
-                        :alt   name
-                        :class :vds-poster}]]
+            [:> Poster
+             {:src   thumbnail-url
+              :alt   name
+              :class :vds-poster}]]
            [:> DefaultVideoLayout {:icons defaultLayoutIcons}]]))})))
 
 (defn get-audio-player-sources
@@ -71,13 +81,13 @@
     []))
 
 (defn audio-player
-  [stream !player]
+  [_stream _!player]
   (let [!elapsed-time     @(rf/subscribe [:elapsed-time])
         !bg-player-first? (r/atom nil)]
     (r/create-class
-     {:component-will-unmount #(rf/dispatch [:background-player/ready false])
+     {:component-will-unmount #(rf/dispatch [:bg-player/ready false])
       :reagent-render
-      (fn [{:keys [name audio-streams thumbnail-url]} !player]
+      (fn [{:keys [name audio-streams]} !player]
         [:> MediaPlayer
          {:title          name
           :class          "invisible fixed"
@@ -86,19 +96,20 @@
           :viewType       "audio"
           :ref            #(reset! !player %)
           :loop           (= @(rf/subscribe [:loop-playback]) :stream)
-          :onCanPlay      #(rf/dispatch [:background-player/ready true])
+          :onCanPlay      #(rf/dispatch [:bg-player/ready true])
           :onSeeked       #(reset! !elapsed-time (.-currentTime @!player))
           :onTimeUpdate   #(reset! !elapsed-time (.-currentTime @!player))
           :onEnded        (fn []
-                            (rf/dispatch [:queue/change-pos (inc @(rf/subscribe [:queue-pos]))])
+                            (rf/dispatch [:queue/change-pos
+                                          (inc @(rf/subscribe [:queue-pos]))])
                             (reset! !elapsed-time 0))
-          :onPlay         #(rf/dispatch [:background-player/play])
+          :onPlay         #(rf/dispatch [:bg-player/play])
           :onReplay       (fn []
-                            (rf/dispatch [:background-player/set-paused false])
+                            (rf/dispatch [:bg-player/set-paused false])
                             (reset! !elapsed-time 0))
-          :onPause        #(rf/dispatch [:background-player/set-paused true])
+          :onPause        #(rf/dispatch [:bg-player/set-paused true])
           :onLoadedData   (fn []
-                            (rf/dispatch [:background-player/start])
+                            (rf/dispatch [:bg-player/start])
                             (when-not @!bg-player-first?
                               (reset! !bg-player-first? true)))
           :onSourceChange #(when @!bg-player-first?
@@ -107,7 +118,8 @@
          [:> DefaultAudioLayout {:icons defaultLayoutIcons}]])})))
 
 (defonce base-slider-classes
-  ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300" "dark:bg-neutral-600"
+  ["h-2" "cursor-pointer" "appearance-none" "bg-neutral-300"
+   "dark:bg-neutral-600"
    "rounded-full" "overflow-hidden" "focus:outline-none"
    "[&::-webkit-slider-thumb]:appearance-none"
    "[&::-webkit-slider-thumb]:border-0"
@@ -124,34 +136,50 @@
 (defn get-slider-shadow-classes
   [service-color]
   (case service-color
-    "#cc0000" ["[&::-webkit-slider-thumb]:shadow-[#cc0000]" "[&::-moz-range-thumb]:shadow-[#cc0000]"]
-    "#ff7700" ["[&::-webkit-slider-thumb]:shadow-[#ff7700]" "[&::-moz-range-thumb]:shadow-[#ff7700]"]
-    "#333333" ["[&::-webkit-slider-thumb]:shadow-[#333333]" "[&::-moz-range-thumb]:shadow-[#333333]"]
-    "#F2690D" ["[&::-webkit-slider-thumb]:shadow-[#F2690D]" "[&::-moz-range-thumb]:shadow-[#F2690D]"]
-    "#629aa9" ["[&::-webkit-slider-thumb]:shadow-[#629aa9]" "[&::-moz-range-thumb]:shadow-[#629aa9]"]
-    ["[&::-webkit-slider-thumb]:shadow-neutral-300" "[&::-moz-range-thumb]:shadow-neutral-300"]))
+    "#cc0000" ["[&::-webkit-slider-thumb]:shadow-[#cc0000]"
+               "[&::-moz-range-thumb]:shadow-[#cc0000]"]
+    "#ff7700" ["[&::-webkit-slider-thumb]:shadow-[#ff7700]"
+               "[&::-moz-range-thumb]:shadow-[#ff7700]"]
+    "#333333" ["[&::-webkit-slider-thumb]:shadow-[#333333]"
+               "[&::-moz-range-thumb]:shadow-[#333333]"]
+    "#F2690D" ["[&::-webkit-slider-thumb]:shadow-[#F2690D]"
+               "[&::-moz-range-thumb]:shadow-[#F2690D]"]
+    "#629aa9" ["[&::-webkit-slider-thumb]:shadow-[#629aa9]"
+               "[&::-moz-range-thumb]:shadow-[#629aa9]"]
+    ["[&::-webkit-slider-thumb]:shadow-neutral-300"
+     "[&::-moz-range-thumb]:shadow-neutral-300"]))
 
 (defn get-slider-bg-classes
   [service-color]
   (case service-color
-    "#cc0000" ["[&::-webkit-slider-thumb]:bg-[#cc0000]" "[&::-moz-range-thumb]:bg-[#cc0000]"]
-    "#ff7700" ["[&::-webkit-slider-thumb]:bg-[#ff7700]" "[&::-moz-range-thumb]:bg-[#ff7700]"]
-    "#333333" ["[&::-webkit-slider-thumb]:bg-[#333333]" "[&::-moz-range-thumb]:bg-[#333333]"]
-    "#F2690D" ["[&::-webkit-slider-thumb]:bg-[#F2690D]" "[&::-moz-range-thumb]:bg-[#F2690D]"]
-    "#629aa9" ["[&::-webkit-slider-thumb]:bg-[#629aa9]" "[&::-moz-range-thumb]:bg-[#629aa9]"]
-    ["[&::-webkit-slider-thumb]:bg-neutral-300" "[&::-moz-range-thumb]:bg-neutral-300"]))
+    "#cc0000" ["[&::-webkit-slider-thumb]:bg-[#cc0000]"
+               "[&::-moz-range-thumb]:bg-[#cc0000]"]
+    "#ff7700" ["[&::-webkit-slider-thumb]:bg-[#ff7700]"
+               "[&::-moz-range-thumb]:bg-[#ff7700]"]
+    "#333333" ["[&::-webkit-slider-thumb]:bg-[#333333]"
+               "[&::-moz-range-thumb]:bg-[#333333]"]
+    "#F2690D" ["[&::-webkit-slider-thumb]:bg-[#F2690D]"
+               "[&::-moz-range-thumb]:bg-[#F2690D]"]
+    "#629aa9" ["[&::-webkit-slider-thumb]:bg-[#629aa9]"
+               "[&::-moz-range-thumb]:bg-[#629aa9]"]
+    ["[&::-webkit-slider-thumb]:bg-neutral-300"
+     "[&::-moz-range-thumb]:bg-neutral-300"]))
 
-(defn time-slider [!player !elapsed-time service-color]
-  (let [styles (concat base-slider-classes
-                       (get-slider-bg-classes service-color)
-                       (get-slider-shadow-classes service-color))
-        bg-player-ready? @(rf/subscribe [:background-player/ready])]
+(defn time-slider
+  [!player !elapsed-time service-color]
+  (let [styles           (concat base-slider-classes
+                                 (get-slider-bg-classes service-color)
+                                 (get-slider-shadow-classes service-color))
+        bg-player-ready? @(rf/subscribe [:bg-player/ready])]
     [:input.w-full
      {:class     styles
       :type      "range"
       :on-input  #(reset! !elapsed-time (.. % -target -value))
-      :on-change #(when (and bg-player-ready? @!player) (set! (.-currentTime @!player) @!elapsed-time))
-      :max       (if (and bg-player-ready? @!player (not (js/isNaN (.-duration @!player))))
+      :on-change #(when (and bg-player-ready? @!player)
+                    (set! (.-currentTime @!player) @!elapsed-time))
+      :max       (if (and bg-player-ready?
+                          @!player
+                          (not (js/isNaN (.-duration @!player))))
                    (.floor js/Math (.-duration @!player))
                    100)
       :value     @!elapsed-time}]))
@@ -159,29 +187,30 @@
 (defn button
   [& {:keys [icon on-click disabled? show-on-mobile? extra-classes]}]
   [:button.outline-none.focus:ring-transparent.px-2.pt-1
-   {:class (into (into (when disabled? [:opacity-50 :cursor-auto])
-                       (when-not show-on-mobile? [:hidden :lg:block]))
-                  extra-classes)
+   {:class    (into (into (when disabled? [:opacity-50 :cursor-auto])
+                          (when-not show-on-mobile? [:hidden :lg:block]))
+                    extra-classes)
     :on-click on-click}
    icon])
 
 (defn loop-button
   [loop-playback color show-on-mobile?]
   [button
-   :icon [:div.relative.flex.items-center
-          [:i.fa-solid.fa-repeat
-           {:style {:color (when loop-playback color)}}]
-          (when (= loop-playback :stream)
-            [:div.absolute.w-full.h-full.flex.justify-center.items-center.font-bold
-             {:class "text-[6px]"
-              :style {:color (when loop-playback color)}}
-             "1"])]
+   :icon
+   [:div.relative.flex.items-center
+    [:i.fa-solid.fa-repeat
+     {:style {:color (when loop-playback color)}}]
+    (when (= loop-playback :stream)
+      [:div.absolute.w-full.h-full.flex.justify-center.items-center.font-bold
+       {:class "text-[6px]"
+        :style {:color (when loop-playback color)}}
+       "1"])]
    :on-click #(rf/dispatch [:player/loop])
    :extra-classes [:text-sm]
    :show-on-mobile? show-on-mobile?])
 
 (defn volume-slider
-  [player volume-level muted? service-color]
+  [_player _volume-level _muted? _service-color]
   (let [show-slider? (r/atom nil)]
     (fn [player volume-level muted? service-color]
       (let [styles (concat ["rotate-[270deg]"]
@@ -192,13 +221,15 @@
          {:on-mouse-over #(reset! show-slider? true)
           :on-mouse-out  #(reset! show-slider? false)}
          [button
-          :icon (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low])
-          :on-click #(rf/dispatch [:background-player/mute (not muted?) player])
+          :icon
+          (if muted? [:i.fa-solid.fa-volume-xmark] [:i.fa-solid.fa-volume-low])
+          :on-click #(rf/dispatch [:bg-player/mute (not muted?) player])
           :extra-classes [:pl-3 :pr-2]]
          (when @show-slider?
            [:input.absolute.w-24.ml-2.m-1.bottom-16
-            {:class    (clojure.string/join " " styles)
+            {:class    (str/join " " styles)
              :type     "range"
-             :on-input #(rf/dispatch [:player/change-volume (.. % -target -value) player])
+             :on-input #(rf/dispatch [:player/change-volume
+                                      (.. % -target -value) player])
              :max      100
              :value    volume-level}])]))))

+ 1 - 4
src/frontend/tubo/core.cljs

@@ -16,7 +16,4 @@
   (routes/start-routes!)
   (.render root (r/as-element [(fn [] views/app)])))
 
-(defn ^:export init
-  []
-  (rf/dispatch-sync [:initialize-db])
-  (mount-root))
+(defn ^:export init [] (rf/dispatch-sync [:initialize-db]) (mount-root))

+ 46 - 32
src/frontend/tubo/events.cljs

@@ -30,26 +30,31 @@
  (fn [{:keys [store]} _]
    (let [if-nil #(if (nil? %1) %2 %1)]
      {:db
-      {:paused                 true
-       :muted                  (:muted store)
-       :queue                  (if-nil (:queue store) [])
-       :service-id             (if-nil (:service-id store) 0)
-       :loop-playback          (if-nil (:loop-playback store) :playlist)
-       :queue-pos              (if-nil (:queue-pos store) 0)
-       :volume-level           (if-nil (:volume-level store) 100)
-       :background-player/show (:background-player/show store)
-       :bookmarks
-       (if-nil (:bookmarks store) [{:id (nano-id) :name "Liked Streams"}])
-       :settings
-       {:theme            (if-nil (:theme store) "auto")
-        :show-comments    (if-nil (:show-comments store) true)
-        :show-related     (if-nil (:show-related store) true)
-        :show-description (if-nil (:show-description store) true)
-        :default-service  (if-nil (:default-service store)
-                            {:service-id       0
-                             :id               "YouTube"
-                             :default-kiosk    "Trending"
-                             :available-kiosks ["Trending"]})}}})))
+      {:paused         true
+       :muted          (:muted store)
+       :queue          (if-nil (:queue store) [])
+       :service-id     (if-nil (:service-id store) 0)
+       :loop-playback  (if-nil (:loop-playback store) :playlist)
+       :queue-pos      (if-nil (:queue-pos store) 0)
+       :volume-level   (if-nil (:volume-level store) 100)
+       :bg-player/show (:bg-player/show store)
+       :bookmarks      (if-nil (:bookmarks store)
+                               [{:id (nano-id) :name "Liked Streams"}])
+       :settings       {:theme            (if-nil (:theme store) "auto")
+                        :show-comments    (if-nil (:show-comments store)
+                                                  true)
+                        :show-related     (if-nil (:show-related store)
+                                                  true)
+                        :show-description (if-nil (:show-description
+                                                   store)
+                                                  true)
+                        :default-service  (if-nil
+                                           (:default-service store)
+                                           {:service-id 0
+                                            :id "YouTube"
+                                            :default-kiosk "Trending"
+                                            :available-kiosks
+                                            ["Trending"]})}}})))
 
 (rf/reg-fx
  :scroll-to-top
@@ -74,7 +79,7 @@
 
 (rf/reg-event-fx
  :scroll-into-view
- (fn [{:keys [db]} [_ element]]
+ (fn [_ [_ element]]
    {:scroll-into-view! element}))
 
 (rf/reg-fx
@@ -118,10 +123,12 @@
       :fx            [(when (:main-player/show db)
                         [:dispatch [:player/switch-from-main]])
                       [:dispatch [:queue/show false]]
-                      [:dispatch [:services/fetch-all
-                                  [:services/load] [:bad-response]]]
-                      [:dispatch [:kiosks/fetch-all (:service-id db)
-                                  [:kiosks/load] [:bad-response]]]]})))
+                      [:dispatch
+                       [:services/fetch-all
+                        [:services/load] [:bad-response]]]
+                      [:dispatch
+                       [:kiosks/fetch-all (:service-id db)
+                        [:kiosks/load] [:bad-response]]]]})))
 
 (defonce timeouts! (r/atom {}))
 
@@ -132,18 +139,20 @@
      (js/clearTimeout existing)
      (swap! timeouts! dissoc id))
    (when (some? event)
-     (swap! timeouts! assoc id
-            (js/setTimeout #(rf/dispatch event) time)))))
+     (swap! timeouts! assoc
+       id
+       (js/setTimeout #(rf/dispatch event) time)))))
 
 (rf/reg-event-fx
  :bad-response
- (fn [{:keys [db]} [_ res]]
+ (fn [_ [_ res]]
    {:fx [[:dispatch [:notifications/add res]]]}))
 
 (rf/reg-fx
  :file-download
  (fn [{:keys [data name mime-type]}]
-   (let [file  (.createObjectURL js/URL (js/Blob. (array data) {:type mime-type}))
+   (let [file  (.createObjectURL js/URL
+                                 (js/Blob. (array data) {:type mime-type}))
          !link (.createElement js/document "a")]
      (set! (.-href !link) file)
      (set! (.-download !link) name)
@@ -155,15 +164,20 @@
  (fn [{:keys [db]} [_ res]]
    (let [updated-db (assoc db :services (js->clj res :keywordize-keys true))
          service-id (:id (first
-                          (filter #(= (-> db :settings :default-service :id)
-                                      (-> % :info :name))
+                          (filter #(= (-> db
+                                          :settings
+                                          :default-service
+                                          :id)
+                                      (-> %
+                                          :info
+                                          :name))
                                   (:services updated-db))))]
      {:fx [[:dispatch [:kiosks/fetch-default-page service-id]]
            [:dispatch [:services/change-id service-id]]]})))
 
 (rf/reg-event-fx
  :fetch-homepage
- (fn [{:keys [db]} _]
+ (fn [_ _]
    {:fx [[:dispatch [:services/fetch-all [:load-homepage] [:bad-response]]]]}))
 
 (rf/reg-event-fx

+ 53 - 32
src/frontend/tubo/kiosks/events.cljs

@@ -11,28 +11,34 @@
 
 (rf/reg-event-fx
  :kiosks/fetch
- (fn [{:keys [db]} [_ service-id kiosk-id on-success on-error params]]
-   (api/get-request (str "/services/" service-id "/kiosks/"
+ (fn [_ [_ service-id kiosk-id on-success on-error params]]
+   (api/get-request (str "/services/" service-id
+                         "/kiosks/"
                          (js/encodeURIComponent kiosk-id))
-                    on-success on-error params)))
+                    on-success
+                    on-error
+                    params)))
 
 (rf/reg-event-fx
  :kiosks/fetch-default
- (fn [{:keys [db]} [_ service-id on-success on-error]]
+ (fn [_ [_ service-id on-success on-error]]
    (api/get-request (str "/services/" service-id "/default-kiosk")
-                    on-success on-error)))
+                    on-success
+                    on-error)))
 
 (rf/reg-event-fx
  :kiosks/fetch-all
- (fn [{:keys [db]} [_ id on-success on-error]]
+ (fn [_ [_ id on-success on-error]]
    (api/get-request (str "/services/" id "/kiosks")
-                    on-success on-error)))
+                    on-success
+                    on-error)))
 
 (rf/reg-event-fx
  :kiosks/load-page
  (fn [{:keys [db]} [_ res]]
    (let [kiosk-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db :kiosk kiosk-res
+     {:db (assoc db
+                 :kiosk             kiosk-res
                  :show-page-loading false)
       :fx [[:dispatch [:services/fetch kiosk-res]]
            [:document-title (:id kiosk-res)]]})))
@@ -40,11 +46,13 @@
 (rf/reg-event-fx
  :kiosks/bad-page-response
  (fn [{:keys [db]} [_ service-id kiosk-id res]]
-   {:fx [[:dispatch [:change-view
-                     #(layout/error
-                       res (if kiosk-id
-                             [:kiosks/fetch-page service-id kiosk-id]
-                             [:kiosks/fetch-default-page service-id]))]]]
+   {:fx [[:dispatch
+          [:change-view
+           #(layout/error
+             res
+             (if kiosk-id
+               [:kiosks/fetch-page service-id kiosk-id]
+               [:kiosks/fetch-default-page service-id]))]]]
     :db (assoc db :show-page-loading false)}))
 
 (rf/reg-event-fx
@@ -52,38 +60,50 @@
  (fn [{:keys [db]} [_ service-id kiosk-id]]
    {:db (assoc db
                :show-page-loading true
-               :kiosk nil)
-    :fx [[:dispatch (if kiosk-id
-                      [:kiosks/fetch service-id kiosk-id
-                       [:kiosks/load-page] [:kiosks/bad-page-response service-id kiosk-id]]
-                      [:kiosks/fetch-default-page service-id])]]}))
+               :kiosk             nil)
+    :fx [[:dispatch
+          (if kiosk-id
+            [:kiosks/fetch service-id kiosk-id
+             [:kiosks/load-page]
+             [:kiosks/bad-page-response service-id kiosk-id]]
+            [:kiosks/fetch-default-page service-id])]]}))
 
 (rf/reg-event-fx
  :kiosks/fetch-default-page
  (fn [{:keys [db]} [_ service-id]]
    (let [default-kiosk-id
          (when (= (js/parseInt service-id)
-                  (-> db :settings :default-service :service-id))
-           (-> db :settings :default-service :default-kiosk))]
-     {:fx [[:dispatch (if default-kiosk-id
-                        [:kiosks/fetch-page service-id default-kiosk-id]
-                        [:kiosks/fetch-default service-id
-                         [:kiosks/load-page] [:kiosks/bad-page-response service-id nil]])]]})))
+                  (-> db
+                      :settings
+                      :default-service
+                      :service-id))
+           (-> db
+               :settings
+               :default-service
+               :default-kiosk))]
+     {:fx [[:dispatch
+            (if default-kiosk-id
+              [:kiosks/fetch-page service-id default-kiosk-id]
+              [:kiosks/fetch-default service-id
+               [:kiosks/load-page]
+               [:kiosks/bad-page-response service-id nil]])]]})))
 
 (rf/reg-event-fx
  :kiosks/change-page
- (fn [{:keys [db]} [_ service-id]]
+ (fn [_ [_ service-id]]
    {:fx [[:dispatch [:services/change-id service-id]]
          [:dispatch
-          [:navigate {:name   :kiosk-page
-                      :params {}
-                      :query  {:serviceId service-id}}]]]}))
+          [:navigate
+           {:name   :kiosk-page
+            :params {}
+            :query  {:serviceId service-id}}]]]}))
 
 (rf/reg-event-db
  :kiosks/load-paginated
  (fn [db [_ res]]
    (-> db
-       (update-in [:kiosk :related-streams] #(into %1 %2)
+       (update-in [:kiosk :related-streams]
+                  #(into %1 %2)
                   (:related-streams (js->clj res :keywordize-keys true)))
        (assoc-in [:kiosk :next-page]
                  (:next-page (js->clj res :keywordize-keys true)))
@@ -95,6 +115,7 @@
    (if (empty? next-page-url)
      {:db (assoc db :show-pagination-loading false)}
      {:db (assoc db :show-pagination-loading true)
-      :fx [[:dispatch [:kiosks/fetch service-id kiosk-id
-                       [:kiosks/load-paginated] [:bad-response]
-                       {:nextPage (js/encodeURIComponent next-page-url)}]]]})))
+      :fx [[:dispatch
+            [:kiosks/fetch service-id kiosk-id
+             [:kiosks/load-paginated] [:bad-response]
+             {:nextPage (js/encodeURIComponent next-page-url)}]]]})))

+ 13 - 11
src/frontend/tubo/kiosks/views.cljs

@@ -7,7 +7,7 @@
 
 (defn kiosk-active?
   [& {:keys [kiosk kiosk-id service-id default-service default-kiosk path]}]
-  (or (and (= kiosk-id kiosk))
+  (or (= kiosk-id kiosk)
       (and (= path "/kiosk")
            (not kiosk-id)
            (not= (js/parseInt service-id)
@@ -22,19 +22,21 @@
   [:ul.flex.items-center.px-4.text-white
    (for [kiosk kiosks]
      [:li.px-3 {:key kiosk}
-      [:a {:href  (rfe/href :kiosk-page nil {:serviceId service-id
-                                             :kioskId   kiosk})
-           :class (when (kiosk-active? (assoc kiosk-args :kiosk kiosk))
-                    :font-bold)}
+      [:a
+       {:href  (rfe/href :kiosk-page
+                         nil
+                         {:serviceId service-id
+                          :kioskId   kiosk})
+        :class (when (kiosk-active? (assoc kiosk-args :kiosk kiosk))
+                 :font-bold)}
        kiosk]])])
 
 (defn kiosk
-  [{{:keys [serviceId kioskId]} :query-params}]
-  (let [{:keys [id url related-streams
-                next-page]} @(rf/subscribe [:kiosk])
-        next-page-url       (:url next-page)
-        service-color       @(rf/subscribe [:service-color])
-        service-id          (or @(rf/subscribe [:service-id]) serviceId)
+  [{{:keys [serviceId]} :query-params}]
+  (let [{:keys [id related-streams next-page]}
+        @(rf/subscribe [:kiosk])
+        next-page-url (:url next-page)
+        service-id (or @(rf/subscribe [:service-id]) serviceId)
         scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
     (when scrolled-to-bottom?
       (rf/dispatch [:kiosks/fetch-paginated service-id id next-page-url]))

+ 14 - 7
src/frontend/tubo/modals/events.cljs

@@ -16,21 +16,28 @@
 (rf/reg-event-fx
  :modals/hide
  (fn [{:keys [db]} _]
-   {:db (update db :modals
-                #(map-indexed (fn [i modal]
-                                (if (= i (- (count %) 1))
-                                  (assoc modal :show? false :child nil)))
-                              %))
+   {:db            (update db
+                           :modals
+                           #(map-indexed
+                             (fn [i modal]
+                               (when (= i (- (count %) 1))
+                                 (assoc modal :show? false :child nil)))
+                             %))
     :body-overflow false}))
 
 (rf/reg-event-fx
  :modals/close
  (fn [{:keys [db]} _]
-   {:fx [[:dispatch [:modals/delete (-> (:modals db) last :id)]]]
+   {:fx            [[:dispatch
+                     [:modals/delete
+                      (-> (:modals db)
+                          last
+                          :id)]]]
     :body-overflow false}))
 
 (rf/reg-event-fx
  :modals/open
  (fn [_ [_ child]]
-   {:fx            [[:dispatch [:modals/add {:show? true :child child :id (nano-id)}]]]
+   {:fx            [[:dispatch
+                     [:modals/add {:show? true :child child :id (nano-id)}]]]
     :body-overflow true}))

+ 1 - 1
src/frontend/tubo/modals/views.cljs

@@ -28,7 +28,7 @@
 (defn modal
   []
   (fn []
-    (let [modals @(rf/subscribe [:modals])
+    (let [modals        @(rf/subscribe [:modals])
           visible-modal (last (filter :show? modals))]
       (when visible-modal
         [modal-panel visible-modal]))))

+ 14 - 8
src/frontend/tubo/notifications/events.cljs

@@ -6,21 +6,27 @@
 (rf/reg-event-fx
  :notifications/add
  (fn [{:keys [db]} [_ data time]]
-   (let [id (nano-id)
-         updated-db (update db :notifications #(into [] (conj %1 %2))
+   (let [id         (nano-id)
+         updated-db (update db
+                            :notifications
+                            #(into [] (conj %1 %2))
                             (assoc data :id id))]
      {:db updated-db
       :fx (if (false? time)
-              []
-              [[:timeout {:id    id
-                          :event [:notifications/remove id]
-                          :time  (or time 2000)}]])})))
+            []
+            [[:timeout
+              {:id    id
+               :event [:notifications/remove id]
+               :time  (or time 2000)}]])})))
 
 (rf/reg-event-db
  :notifications/remove
  (fn [db [_ id]]
-   (update db :notifications #(remove (fn [notification]
-                                        (= (:id notification) id)) %))))
+   (update db
+           :notifications
+           #(remove (fn [notification]
+                      (= (:id notification) id))
+                    %))))
 
 (rf/reg-event-db
  :notifications/clear

+ 4 - 2
src/frontend/tubo/notifications/views.cljs

@@ -9,7 +9,8 @@
      {:class (case failure
                :success ["bg-green-600/90" :text-white]
                :error   ["bg-red-600/90" :text-white]
-               ["dark:bg-stone-800" "dark:text-white" :bg-neutral-300 :text-neutral-800])}
+               ["dark:bg-stone-800" "dark:text-white" :bg-neutral-300
+                :text-neutral-800])}
      [:div.flex.items-center.gap-x-4
       (case failure
         :success [:i.fa-solid.fa-circle-check]
@@ -20,7 +21,8 @@
         {:on-click
          #(rf/dispatch [:notifications/remove (:id notification)])}
         [:i.fa-solid.fa-close]]
-       [:span.font-bold (str status (when (and status status-text) ": ") status-text)]
+       [:span.font-bold
+        (str status (when (and status status-text) ": ") status-text)]
        (when parse-error
          [:span.line-clamp-1 (:status-text parse-error)])]]]))
 

+ 95 - 80
src/frontend/tubo/player/events.cljs

@@ -1,7 +1,5 @@
 (ns tubo.player.events
   (:require
-   [tubo.stream.views :as stream]
-   [tubo.utils :as utils]
    [goog.object :as gobj]
    [re-frame.core :as rf]
    [vimsical.re-frame.cofx.inject :as inject]))
@@ -20,7 +18,7 @@
 
 (rf/reg-fx
  :src
- (fn [{:keys [player src current-pos]}]
+ (fn [{:keys [player src]}]
    (set! (.-source @player) (clj->js src))))
 
 (rf/reg-fx
@@ -34,16 +32,16 @@
    (set! (.-currentTime @player) time)))
 
 (rf/reg-event-fx
- :background-player/seek
+ :bg-player/seek
  [(rf/inject-cofx ::inject/sub [:player])]
  (fn [{:keys [db player]} [_ time]]
-   (when (:background-player/ready db)
+   (when (:bg-player/ready db)
      {:current-time {:time time :player player}})))
 
 (rf/reg-event-fx
  :main-player/seek
  [(rf/inject-cofx ::inject/sub [:main-player])]
- (fn [{:keys [db main-player]} [_ time]]
+ (fn [{:keys [main-player]} [_ time]]
    {:current-time {:time time :player main-player}}))
 
 (rf/reg-fx
@@ -53,14 +51,14 @@
      (set! (.-paused @player) paused?))))
 
 (rf/reg-event-db
- :background-player/set-paused
+ :bg-player/set-paused
  (fn [db [_ val]]
    (assoc db :paused val)))
 
 (rf/reg-event-fx
- :background-player/pause
+ :bg-player/pause
  [(rf/inject-cofx ::inject/sub [:player])]
- (fn [{:keys [db player]} [_ paused?]]
+ (fn [{:keys [player]} [_ paused?]]
    {:pause! {:paused? paused?
              :player  player}}))
 
@@ -73,12 +71,12 @@
                :player  main-player}})))
 
 (rf/reg-event-fx
- :background-player/play
+ :bg-player/play
  [(rf/inject-cofx ::inject/sub [:elapsed-time])
   (rf/inject-cofx ::inject/sub [:main-player])]
  (fn [{:keys [db elapsed-time main-player]}]
-   {:fx [[:dispatch [:background-player/set-paused false]]
-         [:dispatch [:background-player/seek @elapsed-time]]
+   {:fx [[:dispatch [:bg-player/set-paused false]]
+         [:dispatch [:bg-player/seek @elapsed-time]]
          (when (and (:main-player/ready db) @main-player)
            [:dispatch [:main-player/pause true]])]}))
 
@@ -86,23 +84,23 @@
  :main-player/play
  [(rf/inject-cofx ::inject/sub [:elapsed-time])
   (rf/inject-cofx ::inject/sub [:player])]
- (fn [{:keys [db elapsed-time player]}]
-   {:fx [(when (and (:background-player/ready db) @player)
-           [:dispatch [:background-player/pause true]])]}))
+ (fn [{:keys [db player]}]
+   {:fx [(when (and (:bg-player/ready db) @player)
+           [:dispatch [:bg-player/pause true]])]}))
 
 (rf/reg-event-fx
- :background-player/stop
- (fn [{:keys [db]}]
-   {:fx [[:dispatch [:background-player/pause true]]
-         [:dispatch [:background-player/seek 0]]]}))
+ :bg-player/stop
+ (fn [_]
+   {:fx [[:dispatch [:bg-player/pause true]]
+         [:dispatch [:bg-player/seek 0]]]}))
 
 (rf/reg-event-fx
- :background-player/start
+ :bg-player/start
  [(rf/inject-cofx ::inject/sub [:player])
   (rf/inject-cofx ::inject/sub [:elapsed-time])]
- (fn [{:keys [db player elapsed-time]} _]
-   {:fx [[:dispatch [:background-player/set-paused true]]
-         [:dispatch [:background-player/pause false]]
+ (fn [{:keys [db player]} _]
+   {:fx [[:dispatch [:bg-player/set-paused true]]
+         [:dispatch [:bg-player/pause false]]
          [:dispatch [:player/change-volume (:volume-level db) player]]]}))
 
 (rf/reg-event-fx
@@ -110,7 +108,7 @@
  [(rf/inject-cofx ::inject/sub [:elapsed-time])]
  (fn [{:keys [db elapsed-time]} _]
    {:fx [[:dispatch [:main-player/pause false]]
-         (when (and (:main-player/show db) (not (:background-player/ready db)))
+         (when (and (:main-player/show db) (not (:bg-player/ready db)))
            [:dispatch [:main-player/seek @elapsed-time]])]}))
 
 (rf/reg-fx
@@ -122,7 +120,7 @@
 
 (rf/reg-fx
  :media-session-handlers
- (fn [{:keys [current-pos player stream]}]
+ (fn [{:keys [current-pos player]}]
    (when (gobj/containsKey js/navigator "mediaSession")
      (let [current-time (and @player (.-currentTime @player))
            update-position
@@ -136,11 +134,16 @@
             "pause"         #(.pause @player)
             "previoustrack" #(rf/dispatch [:queue/change-pos (dec current-pos)])
             "nexttrack"     #(rf/dispatch [:queue/change-pos (inc current-pos)])
-            "seekbackward"  (fn [^js/navigator.MediaSessionActionDetails details]
-                              (seek (- current-time (or (.-seekOffset details) 10))))
-            "seekforward"   (fn [^js/navigator.MediaSessionActionDetails details]
-                              (seek (+ current-time (or (.-seekOffset details) 10))))
-            "seekto"        (fn [^js/navigator.MediaSessionActionDetails details]
+            "seekbackward"  (fn [^js/navigator.MediaSessionActionDetails
+                                 details]
+                              (seek (- current-time
+                                       (or (.-seekOffset details) 10))))
+            "seekforward"   (fn [^js/navigator.MediaSessionActionDetails
+                                 details]
+                              (seek (+ current-time
+                                       (or (.-seekOffset details) 10))))
+            "seekto"        (fn [^js/navigator.MediaSessionActionDetails
+                                 details]
                               (seek (.-seekTime details)))
             "stop"          #(seek 0)}]
        (doseq [[action cb] events]
@@ -155,7 +158,7 @@
     :volume {:player player :volume value}}))
 
 (rf/reg-event-fx
- :background-player/mute
+ :bg-player/mute
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ value player]]
    {:db    (assoc db :muted value)
@@ -163,11 +166,11 @@
     :mute  {:player player :muted? value}}))
 
 (rf/reg-event-fx
- :background-player/hide
+ :bg-player/hide
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} _]
-   {:db (assoc db :background-player/show false)
-    :store (assoc store :background-player/show false)}))
+   {:db    (assoc db :bg-player/show false)
+    :store (assoc store :bg-player/show false)}))
 
 (rf/reg-event-fx
  :player/loop
@@ -181,7 +184,7 @@
       :store (assoc store :loop-playback loop-state)})))
 
 (rf/reg-event-fx
- :background-player/dispose
+ :bg-player/dispose
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} _]
    (let [remove-entries
@@ -191,14 +194,14 @@
                (assoc :queue-pos 0)))]
      {:db    (remove-entries db)
       :store (remove-entries store)
-      :fx    [[:dispatch [:background-player/pause true]]
-              [:dispatch [:background-player/seek 0]]
-              [:dispatch [:background-player/hide]]]})))
+      :fx    [[:dispatch [:bg-player/pause true]]
+              [:dispatch [:bg-player/seek 0]]
+              [:dispatch [:bg-player/hide]]]})))
 
 (rf/reg-event-db
- :background-player/ready
+ :bg-player/ready
  (fn [db [_ ready]]
-   (assoc db :background-player/ready ready)))
+   (assoc db :bg-player/ready ready)))
 
 (rf/reg-event-db
  :main-player/ready
@@ -213,38 +216,40 @@
          idx        (.indexOf (:queue updated-db) stream)]
      {:db    updated-db
       :store (assoc store :queue (:queue updated-db))
-      :fx    [[:dispatch [:player/fetch-stream
-                          (:url stream) idx (= (count (:queue db)) 0)]]
+      :fx    [[:dispatch
+               [:player/fetch-stream
+                (:url stream) idx (= (count (:queue db)) 0)]]
               (when (and notify? (not (= (count (:queue db)) 0)))
-                [:dispatch [:notifications/add
-                            {:status-text "Added stream to queue"
-                             :failure     :info}]])]})))
+                [:dispatch
+                 [:notifications/add
+                  {:status-text "Added stream to queue"
+                   :failure     :info}]])]})))
 
 (rf/reg-event-fx
  :player/show-main-player
  (fn [{:keys [db]} [_ val]]
-   {:db (assoc db :main-player/show val)
+   {:db            (assoc db :main-player/show val)
     :body-overflow val}))
 
 (rf/reg-event-fx
  :player/switch-from-main
  [(rf/inject-cofx ::inject/sub [:elapsed-time])]
- (fn [{:keys [db elapsed-time]} [_ stream]]
-   {:db (assoc db :background-player/show true)
+ (fn [{:keys [db]} _]
+   {:db (assoc db :bg-player/show true)
     :fx [[:dispatch [:player/show-main-player false]]
          [:dispatch [:main-player/pause true]]]}))
 
 (rf/reg-event-fx
  :player/switch-to-main
  [(rf/inject-cofx :store)]
- (fn [{:keys [db]} [_ stream]]
-   {:fx [[:dispatch [:player/show-main-player true]]]
-    :db (assoc db :background-player/show false)
+ (fn [{:keys [db]} _]
+   {:fx            [[:dispatch [:player/show-main-player true]]]
+    :db            (assoc db :bg-player/show false)
     :scroll-to-top nil}))
 
 (rf/reg-event-fx
  :player/load-related-streams
- (fn [{:keys [db]} [_ res]]
+ (fn [_ [_ res]]
    (let [{:keys [related-streams]} (js->clj res :keywordize-keys true)]
      {:fx [[:dispatch [:queue/add-n related-streams]]]})))
 
@@ -254,45 +259,53 @@
   (rf/inject-cofx ::inject/sub [:player])]
  (fn [{:keys [db store player]} [_ idx play? res]]
    (let [stream-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db
-                 :background-player/show (not (:main-player/show db))
-                 :background-player/loading false)
-      :store (assoc store :background-player/show (not (:main-player/show db)))
-      :fx (apply conj [(when play? [:dispatch [:queue/change-stream stream-res idx]])]
-                 (when (and (:background-player/ready db) play?)
-                   [[:media-session-metadata
-                     {:title   (:name stream-res)
-                      :artist  (:uploader-name stream-res)
-                      :artwork [{:src (:thumbnail-url stream-res)}]}]
-                    [:media-session-handlers
-                     {:current-pos idx
-                      :player      player}]]))})))
+     {:db    (assoc db
+                    :bg-player/show    (not (:main-player/show db))
+                    :bg-player/loading false)
+      :store (assoc store :bg-player/show (not (:main-player/show db)))
+      :fx    (apply conj
+                    [(when play?
+                       [:dispatch [:queue/change-stream stream-res idx]])]
+                    (when (and (:bg-player/ready db) play?)
+                      [[:media-session-metadata
+                        {:title   (:name stream-res)
+                         :artist  (:uploader-name stream-res)
+                         :artwork [{:src (:thumbnail-url stream-res)}]}]
+                       [:media-session-handlers
+                        {:current-pos idx
+                         :player      player}]]))})))
 
 (rf/reg-event-fx
  :player/bad-response
  (fn [{:keys [db]} [_ idx play? res]]
    {:db (assoc db
-               :background-player/loading false)
+               :bg-player/loading
+               false)
     :fx [[:dispatch [:bad-response res]]
          (when play?
-           (if (> (-> db :queue count) 1)
+           (if (> (-> db
+                      :queue
+                      count)
+                  1)
              [:dispatch [:queue/change-pos (inc idx)]]
-             [:dispatch [:background-player/dispose]]))]}))
+             [:dispatch [:bg-player/dispose]]))]}))
 
 (rf/reg-event-fx
  :player/fetch-related-streams
  (fn [{:keys [db]} [_ url]]
-   {:fx [[:dispatch [:stream/fetch url
-                     [:player/load-related-streams]] [:bad-response]]]
-    :db (assoc db :background-player/loading true)}))
+   {:fx [[:dispatch
+          [:stream/fetch url
+           [:player/load-related-streams]] [:bad-response]]]
+    :db (assoc db :bg-player/loading true)}))
 
 (rf/reg-event-fx
  :player/fetch-stream
  (fn [{:keys [db]} [_ url idx play?]]
-   {:fx [[:dispatch [:stream/fetch url
-                     [:player/load-stream idx play?]
-                     [:player/bad-response idx play?]]]]
-    :db (assoc db :background-player/loading play?)}))
+   {:fx [[:dispatch
+          [:stream/fetch url
+           [:player/load-stream idx play?]
+           [:player/bad-response idx play?]]]]
+    :db (assoc db :bg-player/loading play?)}))
 
 (rf/reg-event-fx
  :player/start-radio
@@ -301,12 +314,14 @@
          (when (not= (count (:queue db)) 0)
            [:dispatch [:queue/change-pos (count (:queue db))]])
          [:dispatch [:player/fetch-related-streams (:url stream)]]
-         [:dispatch [:notifications/add
-                     {:status-text "Started stream radio"
-                      :failure     :info}]]]}))
+         [:dispatch
+          [:notifications/add
+           {:status-text "Started stream radio"
+            :failure     :info}]]]}))
 
 (rf/reg-event-db
  :main-player/toggle-layout
  (fn [db [_ layout]]
-   (assoc-in db [:queue (:queue-pos db) layout]
-             (not (get-in db [:queue (:queue-pos db) layout])))))
+   (assoc-in db
+    [:queue (:queue-pos db) layout]
+    (not (get-in db [:queue (:queue-pos db) layout])))))

+ 9 - 9
src/frontend/tubo/player/subs.cljs

@@ -9,18 +9,18 @@
 
 (rf/reg-sub
  :player
- (fn [db _]
+ (fn [_ _]
    !player))
 
 (rf/reg-sub
  :main-player
- (fn [db _]
+ (fn [_ _]
    !main-player))
 
 (rf/reg-sub
- :background-player/ready
+ :bg-player/ready
  (fn [db _]
-   (:background-player/ready db)))
+   (:bg-player/ready db)))
 
 (rf/reg-sub
  :main-player/ready
@@ -28,14 +28,14 @@
    (:main-player/ready db)))
 
 (rf/reg-sub
- :background-player/show
+ :bg-player/show
  (fn [db _]
-   (:background-player/show db)))
+   (:bg-player/show db)))
 
 (rf/reg-sub
- :background-player/loading
+ :bg-player/loading
  (fn [db _]
-   (:background-player/loading db)))
+   (:bg-player/loading db)))
 
 (rf/reg-sub
  :loop-playback
@@ -59,7 +59,7 @@
 
 (rf/reg-sub
  :elapsed-time
- (fn [db _]
+ (fn [_ _]
    !elapsed-time))
 
 (rf/reg-sub

+ 62 - 44
src/frontend/tubo/player/views.cljs

@@ -8,8 +8,7 @@
    [tubo.components.player :as player]
    [tubo.queue.views :as queue]
    [tubo.stream.views :as stream]
-   [tubo.utils :as utils]
-   ["@vidstack/react" :refer (useStore MediaPlayerInstance)]))
+   [tubo.utils :as utils]))
 
 (defn stream-metadata
   [{:keys [thumbnail-url url name uploader-url uploader-name]}]
@@ -29,15 +28,13 @@
 
 (defn main-controls
   [!player color]
-  (let [queue              @(rf/subscribe [:queue])
-        queue-pos          @(rf/subscribe [:queue-pos])
-        loading?           @(rf/subscribe [:background-player/loading])
-        loop-playback      @(rf/subscribe [:loop-playback])
-        !main-player       @(rf/subscribe [:main-player])
-        bg-player-ready?   @(rf/subscribe [:background-player/ready])
-        main-player-ready? @(rf/subscribe [:main-player/ready])
-        paused?            @(rf/subscribe [:paused])
-        !elapsed-time      @(rf/subscribe [:elapsed-time])]
+  (let [queue            @(rf/subscribe [:queue])
+        queue-pos        @(rf/subscribe [:queue-pos])
+        loading?         @(rf/subscribe [:bg-player/loading])
+        loop-playback    @(rf/subscribe [:loop-playback])
+        bg-player-ready? @(rf/subscribe [:bg-player/ready])
+        paused?          @(rf/subscribe [:paused])
+        !elapsed-time    @(rf/subscribe [:elapsed-time])]
     [:div.flex.flex-col.items-center.ml-auto
      [:div.flex.justify-end
       [player/loop-button loop-playback color]
@@ -47,33 +44,39 @@
        :disabled? (not (and queue (not= queue-pos 0)))]
       [player/button
        :icon [:i.fa-solid.fa-backward]
-       :on-click #(rf/dispatch [:background-player/seek (- @!elapsed-time 5)])]
+       :on-click #(rf/dispatch [:bg-player/seek (- @!elapsed-time 5)])]
       [player/button
-       :icon (if (and (not loading?) @!player)
-               (if paused?
-                 [:i.fa-solid.fa-play]
-                 [:i.fa-solid.fa-pause])
-               [layout/loading-icon color "lg:text-2xl"])
-       :on-click #(rf/dispatch [:background-player/pause (not (.-paused @!player))])
+       :icon
+       (if (and (not loading?) @!player)
+         (if paused?
+           [:i.fa-solid.fa-play]
+           [:i.fa-solid.fa-pause])
+         [layout/loading-icon color "lg:text-2xl"])
+       :on-click
+       #(rf/dispatch [:bg-player/pause (not (.-paused @!player))])
        :show-on-mobile? true
        :extra-classes ["lg:text-2xl"]]
       [player/button
        :icon [:i.fa-solid.fa-forward]
-       :on-click #(rf/dispatch [:background-player/seek (+ @!elapsed-time 5)])]
+       :on-click #(rf/dispatch [:bg-player/seek (+ @!elapsed-time 5)])]
       [player/button
        :icon [:i.fa-solid.fa-forward-step]
        :on-click #(rf/dispatch [:queue/change-pos (inc queue-pos)])
        :disabled? (not (and queue (< (inc queue-pos) (count queue))))]]
      [:div.hidden.lg:flex.items-center.text-sm
       [:span.mx-2
-       (if (and bg-player-ready? @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
+       (if (and bg-player-ready? @!player @!elapsed-time)
+         (utils/format-duration @!elapsed-time)
+         "--:--")]
       [:div.w-20.lg:w-64.mx-2.flex.items-center
        [player/time-slider !player !elapsed-time color]]
       [:span.mx-2
-       (if (and bg-player-ready? @!player) (utils/format-duration (.-duration @!player)) "--:--")]]]))
+       (if (and bg-player-ready? @!player)
+         (utils/format-duration (.-duration @!player))
+         "--:--")]]]))
 
 (defn extra-controls
-  [!player {:keys [url uploader-url] :as stream} color]
+  [_!player _stream _color]
   (let [!menu-active? (r/atom nil)]
     (fn [!player {:keys [url uploader-url] :as stream} color]
       (let [muted?    @(rf/subscribe [:muted])
@@ -81,7 +84,10 @@
             queue     @(rf/subscribe [:queue])
             queue-pos @(rf/subscribe [:queue-pos])
             bookmarks @(rf/subscribe [:bookmarks])
-            liked?    (some #(= (:url %) url) (-> bookmarks first :items))
+            liked?    (some #(= (:url %) url)
+                            (-> bookmarks
+                                first
+                                :items))
             bookmark  #(rf/dispatch [:modals/open [modals/add-to-bookmark %]])]
         [:div.flex.lg:justify-end.lg:flex-1
          [player/volume-slider !player volume muted? color]
@@ -92,8 +98,10 @@
           :extra-classes [:!pl-4 :!pr-3]]
          [layout/popover-menu !menu-active?
           [{:label    (if liked? "Remove favorite" "Favorite")
-            :icon     [:i.fa-solid.fa-heart (when liked? {:style {:color color}})]
-            :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) stream])}
+            :icon     [:i.fa-solid.fa-heart
+                       (when liked? {:style {:color color}})]
+            :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add)
+                                     stream])}
            {:label    "Play radio"
             :icon     [:i.fa-solid.fa-tower-cell]
             :on-click #(rf/dispatch [:player/start-radio stream])}
@@ -117,22 +125,30 @@
                                       :query  {:url uploader-url}}])}
            {:label    "Close player"
             :icon     [:i.fa-solid.fa-close]
-            :on-click #(rf/dispatch [:background-player/dispose])}]
+            :on-click #(rf/dispatch [:bg-player/dispose])}]
           :menu-styles {:bottom "30px" :top nil :right "10px"}
           :extra-classes [:pt-1 :!pl-4 :px-3]]]))))
 
 (defn background-player
   []
-  (let [!player       @(rf/subscribe [:player])
-        stream        @(rf/subscribe [:queue-stream])
-        show-queue?   @(rf/subscribe [:show-queue])
-        show-player?  @(rf/subscribe [:background-player/show])
-        dark-theme?   @(rf/subscribe [:dark-theme])
-        muted?        @(rf/subscribe [:muted])
-        loop-playback @(rf/subscribe [:loop-playback])
-        color         (-> stream :service-id utils/get-service-color)
-        bg-color      (str "rgba(" (if dark-theme? "23,23,23" "255,255,255") ",0.95)")
-        bg-image      (str "linear-gradient(" bg-color "," bg-color "),url(" (:thumbnail-url stream) ")")]
+  (let [!player      @(rf/subscribe [:player])
+        stream       @(rf/subscribe [:queue-stream])
+        show-queue?  @(rf/subscribe [:show-queue])
+        show-player? @(rf/subscribe [:bg-player/show])
+        dark-theme?  @(rf/subscribe [:dark-theme])
+        color        (-> stream
+                         :service-id
+                         utils/get-service-color)
+        bg-color     (str "rgba("
+                          (if dark-theme? "23,23,23" "255,255,255")
+                          ",0.95)")
+        bg-image     (str "linear-gradient("
+                          bg-color
+                          ","
+                          bg-color
+                          "),url("
+                          (:thumbnail-url stream)
+                          ")")]
     (when show-player?
       [:div.sticky.absolute.left-0.bottom-0.z-10.p-3.transition-all.ease-in.relative
        {:style
@@ -148,15 +164,17 @@
         [main-controls !player color]
         [extra-controls !player stream color]]])))
 
-(defn main-player []
-  (let [queue                           @(rf/subscribe [:queue])
-        queue-pos                       @(rf/subscribe [:queue-pos])
-        bookmarks                       @(rf/subscribe [:bookmarks])
-        !player                         @(rf/subscribe [:main-player])
-        {:keys [service-id] :as stream} @(rf/subscribe [:queue-stream])
-        show-player?                    @(rf/subscribe [:main-player/show])]
+(defn main-player
+  []
+  (let [queue        @(rf/subscribe [:queue])
+        queue-pos    @(rf/subscribe [:queue-pos])
+        bookmarks    @(rf/subscribe [:bookmarks])
+        !player      @(rf/subscribe [:main-player])
+        stream       @(rf/subscribe [:queue-stream])
+        show-player? @(rf/subscribe [:main-player/show])]
     [:div.fixed.w-full.bg-neutral-100.dark:bg-neutral-900.overflow-auto.z-10.transition-all.ease-in-out
-     {:class ["h-[calc(100%-56px)]" (if show-player? "translate-y-0" "translate-y-full")]}
+     {:class ["h-[calc(100%-56px)]"
+              (if show-player? "translate-y-0" "translate-y-full")]}
      [:div.sticky.z-10.right-0.top-0
       [:button.absolute.text-white.m-8.text-2xl.z-10.right-0
        {:on-click #(rf/dispatch [:player/switch-from-main nil])}

+ 16 - 10
src/frontend/tubo/playlist/events.cljs

@@ -5,15 +5,18 @@
 
 (rf/reg-event-fx
  :playlist/fetch
- (fn [{:keys [db]} [_ url on-success on-error params]]
+ (fn [_ [_ url on-success on-error params]]
    (api/get-request (str "/playlists/" (js/encodeURIComponent url))
-                    on-success on-error params)))
+                    on-success
+                    on-error
+                    params)))
 
 (rf/reg-event-db
  :playlist/load-paginated
  (fn [db [_ res]]
    (-> db
-       (update-in [:playlist :related-streams] #(apply conj %1 %2)
+       (update-in [:playlist :related-streams]
+                  #(apply conj %1 %2)
                   (:related-streams (js->clj res :keywordize-keys true)))
        (assoc-in [:playlist :next-page]
                  (:next-page (js->clj res :keywordize-keys true)))
@@ -24,16 +27,18 @@
  (fn [{:keys [db]} [_ url next-page-url]]
    (if (empty? next-page-url)
      {:db (assoc db :show-pagination-loading false)}
-     {:fx [[:dispatch [:playlist/fetch url
-                       [:playlist/load-paginated] [:bad-response]
-                       {:nextPage (js/encodeURIComponent next-page-url)}]]]
+     {:fx [[:dispatch
+            [:playlist/fetch url
+             [:playlist/load-paginated] [:bad-response]
+             {:nextPage (js/encodeURIComponent next-page-url)}]]]
       :db (assoc db :show-pagination-loading true)})))
 
 (rf/reg-event-fx
  :playlist/load-page
  (fn [{:keys [db]} [_ res]]
    (let [playlist-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db :playlist playlist-res
+     {:db (assoc db
+                 :playlist          playlist-res
                  :show-page-loading false)
       :fx [[:dispatch [:services/fetch playlist-res]]
            [:document-title (:name playlist-res)]]})))
@@ -41,8 +46,9 @@
 (rf/reg-event-fx
  :playlist/fetch-page
  (fn [{:keys [db]} [_ url]]
-   {:fx [[:dispatch [:playlist/fetch url
-                     [:playlist/load-page] [:bad-response]]]]
+   {:fx [[:dispatch
+          [:playlist/fetch url
+           [:playlist/load-page] [:bad-response]]]]
     :db (assoc db
                :show-page-loading true
-               :playlist nil)}))
+               :playlist          nil)}))

+ 8 - 6
src/frontend/tubo/playlist/views.cljs

@@ -11,11 +11,11 @@
   [{{:keys [url]} :query-params}]
   (let [!menu-active? (r/atom nil)]
     (fn []
-      (let [{:keys [id name playlist-type thumbnail-url banner-url next-page
-                    uploader-name uploader-url related-streams
-                    stream-count]} @(rf/subscribe [:playlist])
-            next-page-url          (:url next-page)
-            scrolled-to-bottom?    @(rf/subscribe [:scrolled-to-bottom])]
+      (let [{:keys [name next-page uploader-name uploader-url related-streams
+                    stream-count]}
+            @(rf/subscribe [:playlist])
+            next-page-url (:url next-page)
+            scrolled-to-bottom? @(rf/subscribe [:scrolled-to-bottom])]
         (when scrolled-to-bottom?
           (rf/dispatch [:playlist/fetch-paginated url next-page-url]))
         [layout/content-container
@@ -28,7 +28,9 @@
                 :on-click #(rf/dispatch [:queue/add-n related-streams true])}
                {:label    "Add to playlist"
                 :icon     [:i.fa-solid.fa-plus]
-                :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark related-streams]])}]])]
+                :on-click #(rf/dispatch [:modals/open
+                                         [modals/add-to-bookmark
+                                          related-streams]])}]])]
           [:div.flex.items-center.justify-between.my-4.gap-x-4
            [:div.flex.items-center
             [layout/uploader-avatar playlist]

+ 29 - 19
src/frontend/tubo/queue/events.cljs

@@ -16,29 +16,38 @@
      {:db    updated-db
       :store (assoc store :queue (:queue updated-db))
       :fx    (if notify?
-               [[:dispatch [:notifications/add
-                            {:status-text "Added stream to queue"
-                             :failure     :info}]]]
+               [[:dispatch
+                 [:notifications/add
+                  {:status-text "Added stream to queue"
+                   :failure     :info}]]]
                [])})))
 
 (rf/reg-event-fx
  :queue/add-n
  [(rf/inject-cofx :store)]
- (fn [{:keys [db store]} [_ streams notify?]]
-   {:fx    (into (map (fn [stream] [:dispatch [:queue/add stream]]) streams)
-                 [[:dispatch [:player/fetch-stream (-> streams first :url)
-                              (count (:queue db)) (= (count (:queue db)) 0)]]
-                  (when notify?
-                    [:dispatch [:notifications/add
-                                {:status-text (str "Added " (count streams)
-                                                   " streams to queue")
-                                 :failure     :info}]])])}))
+ (fn [{:keys [db]} [_ streams notify?]]
+   {:fx (into (map (fn [stream] [:dispatch [:queue/add stream]]) streams)
+              [[:dispatch
+                [:player/fetch-stream
+                 (-> streams
+                     first
+                     :url)
+                 (count (:queue db)) (= (count (:queue db)) 0)]]
+               (when notify?
+                 [:dispatch
+                  [:notifications/add
+                   {:status-text (str "Added "
+                                      (count streams)
+                                      " streams to queue")
+                    :failure     :info}]])])}))
 
 (rf/reg-event-fx
  :queue/remove
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ pos]]
-   (let [updated-db   (update db :queue #(into (subvec % 0 pos) (subvec % (inc pos))))
+   (let [updated-db   (update db
+                              :queue
+                              #(into (subvec % 0 pos) (subvec % (inc pos))))
          queue-pos    (:queue-pos db)
          queue-length (count (:queue updated-db))]
      {:db    updated-db
@@ -48,11 +57,12 @@
                     (or (< pos queue-pos)
                         (= pos queue-pos)
                         (= queue-pos queue-length)))
-               [[:dispatch [:queue/change-pos
-                            (cond
-                              (= pos queue-length) 0
-                              (= pos queue-pos)    pos
-                              :else                (dec queue-pos))]]]
+               [[:dispatch
+                 [:queue/change-pos
+                  (cond
+                    (= pos queue-length) 0
+                    (= pos queue-pos)    pos
+                    :else                (dec queue-pos))]]]
                (= (count (:queue updated-db)) 0)
                [[:dispatch [:player/dispose]]
                 [:dispatch [:queue/show false]]]
@@ -61,7 +71,7 @@
 (rf/reg-event-fx
  :queue/change-pos
  [(rf/inject-cofx :store)]
- (fn [{:keys [db store]} [_ i]]
+ (fn [{:keys [db]} [_ i]]
    (let [idx    (if (< i (count (:queue db)))
                   i
                   (when (= (:loop-playback db) :playlist) 0))

+ 40 - 29
src/frontend/tubo/queue/views.cljs

@@ -4,7 +4,6 @@
    [re-frame.core :as rf]
    [reitit.frontend.easy :as rfe]
    [tubo.bookmarks.modals :as modals]
-   [tubo.components.items :as items]
    [tubo.components.layout :as layout]
    [tubo.components.player :as player]
    [tubo.utils :as utils]))
@@ -29,11 +28,16 @@
 
 (defn popover
   [{:keys [url service-id uploader-url] :as item} i menu-active? bookmarks]
-  (let [liked? (some #(= (:url %) url) (-> bookmarks first :items))]
+  (let [liked? (some #(= (:url %) url)
+                     (-> bookmarks
+                         first
+                         :items))]
     [:div.absolute.right-0.top-0.min-h-full.flex.items-center
      [layout/popover-menu menu-active?
       [{:label    (if liked? "Remove favorite" "Favorite")
-        :icon     [:i.fa-solid.fa-heart (when liked? {:style {:color (utils/get-service-color service-id)}})]
+        :icon     [:i.fa-solid.fa-heart
+                   (when liked?
+                     {:style {:color (utils/get-service-color service-id)}})]
         :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) item])}
        {:label    "Play radio"
         :icon     [:i.fa-solid.fa-tower-cell]
@@ -54,12 +58,13 @@
       :extra-classes [:px-7 :py-2]]]))
 
 (defn queue-item
-  [item queue queue-pos i bookmarks]
-  (let [!menu-active? (r/atom false)
+  [_item _queue _queue-pos _i _bookmarks]
+  (let [!menu-active?     (r/atom false)
         show-main-player? @(rf/subscribe [:main-player/show])]
     (fn [item queue queue-pos i bookmarks]
       [:div.relative.w-full
-       {:ref #(when (and queue (= queue-pos i) (not show-main-player?)) (rf/dispatch [:scroll-into-view %]))}
+       {:ref #(when (and queue (= queue-pos i) (not show-main-player?))
+                (rf/dispatch [:scroll-into-view %]))}
        [item-metadata item queue-pos i]
        [popover item i !menu-active? bookmarks]])))
 
@@ -76,24 +81,26 @@
     uploader-name]])
 
 (defn main-controls
-  [{:keys [service-id]} queue queue-pos color]
-  (let [loop-playback      @(rf/subscribe [:loop-playback])
-        !player            @(rf/subscribe [:player])
-        !main-player       @(rf/subscribe [:main-player])
-        loading?           @(rf/subscribe [:background-player/loading])
-        bg-player-ready?   @(rf/subscribe [:background-player/ready])
-        main-player-ready? @(rf/subscribe [:main-player/ready])
-        paused?            @(rf/subscribe [:paused])
-        !elapsed-time      @(rf/subscribe [:elapsed-time])
-        queue              @(rf/subscribe [:queue])
-        queue-pos          @(rf/subscribe [:queue-pos])]
+  [color]
+  (let [loop-playback    @(rf/subscribe [:loop-playback])
+        !player          @(rf/subscribe [:player])
+        loading?         @(rf/subscribe [:bg-player/loading])
+        bg-player-ready? @(rf/subscribe [:bg-player/ready])
+        paused?          @(rf/subscribe [:paused])
+        !elapsed-time    @(rf/subscribe [:elapsed-time])
+        queue            @(rf/subscribe [:queue])
+        queue-pos        @(rf/subscribe [:queue-pos])]
     [:<>
      [:div.flex.flex-auto.py-2.w-full.items-center.text-sm
       [:span.mr-4.whitespace-nowrap
-       (if (and bg-player-ready? @!player @!elapsed-time) (utils/format-duration @!elapsed-time) "--:--")]
+       (if (and bg-player-ready? @!player @!elapsed-time)
+         (utils/format-duration @!elapsed-time)
+         "--:--")]
       [player/time-slider !player !elapsed-time color]
       [:span.ml-4.whitespace-nowrap
-       (if (and bg-player-ready? @!player) (utils/format-duration (.-duration @!player)) "--:--")]]
+       (if (and bg-player-ready? @!player)
+         (utils/format-duration (.-duration @!player))
+         "--:--")]]
      [:div.flex.justify-center.items-center
       [player/loop-button loop-playback color true]
       [player/button
@@ -104,21 +111,23 @@
        :show-on-mobile? true]
       [player/button
        :icon [:i.fa-solid.fa-backward]
-       :on-click #(rf/dispatch [:background-player/seek (- @!elapsed-time 5)])
+       :on-click #(rf/dispatch [:bg-player/seek (- @!elapsed-time 5)])
        :extra-classes [:text-xl]
        :show-on-mobile? true]
       [player/button
-       :icon (if (and (not loading?) @!player)
-               (if paused?
-                 [:i.fa-solid.fa-play]
-                 [:i.fa-solid.fa-pause])
-               [layout/loading-icon color :text-3xl])
-       :on-click #(rf/dispatch [:background-player/pause (not (.-paused @!player))])
+       :icon
+       (if (and (not loading?) @!player)
+         (if paused?
+           [:i.fa-solid.fa-play]
+           [:i.fa-solid.fa-pause])
+         [layout/loading-icon color :text-3xl])
+       :on-click
+       #(rf/dispatch [:bg-player/pause (not (.-paused @!player))])
        :show-on-mobile? true
        :extra-classes [:text-3xl]]
       [player/button
        :icon [:i.fa-solid.fa-forward]
-       :on-click #(rf/dispatch [:background-player/seek (+ @!elapsed-time 5)])
+       :on-click #(rf/dispatch [:bg-player/seek (+ @!elapsed-time 5)])
        :extra-classes [:text-xl]
        :show-on-mobile? true]
       [player/button
@@ -140,7 +149,9 @@
         bookmarks  @(rf/subscribe [:bookmarks])
         queue-pos  @(rf/subscribe [:queue-pos])
         queue      @(rf/subscribe [:queue])
-        color      (-> stream :service-id utils/get-service-color)]
+        color      (-> stream
+                       :service-id
+                       utils/get-service-color)]
     [:div.fixed.flex.flex-col.items-center.min-w-full.w-full.z-10.backdrop-blur
      {:class ["dark:bg-neutral-900/90" "bg-neutral-100/90"
               "min-h-[calc(100dvh-56px)]" "h-[calc(100dvh-56px)]"
@@ -154,4 +165,4 @@
          ^{:key i} [queue-item item queue queue-pos i bookmarks])]
       [:div.flex.flex-col.py-4.shrink-0.px-5
        [queue-metadata stream]
-       [main-controls stream queue queue-pos color]]]]))
+       [main-controls color]]]]))

+ 51 - 40
src/frontend/tubo/routes.cljs

@@ -13,46 +13,57 @@
 
 (def router
   (ref/router
-   [["/" {:view        kiosk/kiosk
-          :name        :homepage
-          :controllers [{:start #(rf/dispatch [:fetch-homepage])}]}]
-    ["/search" {:view        search/search
-                :name        :search-page
-                :controllers [{:parameters {:query [:q :serviceId]}
-                               :start      (fn [{{:keys [serviceId q]} :query}]
-                                             (rf/dispatch [:search/fetch-page serviceId q]))
-                               :stop       #(rf/dispatch [:search/show-form false])}]}]
-    ["/stream" {:view        stream/stream
-                :name        :stream-page
-                :controllers [{:parameters {:query [:url]}
-                               :start      (fn [{{:keys [url]} :query}]
-                                             (rf/dispatch [:stream/fetch-page url]))}]}]
-    ["/channel" {:view        channel/channel
-                 :name        :channel-page
-                 :controllers [{:parameters {:query [:url]}
-                                :start      (fn [{{:keys [url]} :query}]
-                                              (rf/dispatch [:channel/fetch-page url]))}]}]
-    ["/playlist" {:view        playlist/playlist
-                  :name        :playlist-page
-                  :controllers [{:parameters {:query [:url]}
-                                 :start      (fn [{{:keys [url]} :query}]
-                                               (rf/dispatch [:playlist/fetch-page url]))}]}]
-    ["/kiosk" {:view        kiosk/kiosk
-               :name        :kiosk-page
-               :controllers [{:parameters {:query [:kioskId :serviceId]}
-                              :start      (fn [{{:keys [serviceId kioskId]} :query}]
-                                            (rf/dispatch [:kiosks/fetch-page serviceId kioskId]))}]}]
-    ["/settings" {:view        settings/settings
-                  :name        :settings-page
-                  :controllers [{:start #(rf/dispatch [:settings/fetch-page])}]}]
-    ["/bookmark" {:view        bookmarks/bookmark
-                  :name        :bookmark-page
-                  :controllers [{:parameters {:query [:id]}
-                                 :start      (fn [{{:keys [id]} :query}]
-                                               (rf/dispatch [:bookmark/fetch-page id]))}]}]
-    ["/bookmarks" {:view        bookmarks/bookmarks
-                   :name        :bookmarks-page
-                   :controllers [{:start #(rf/dispatch [:bookmarks/fetch-page])}]}]]))
+   [["/"
+     {:view        kiosk/kiosk
+      :name        :homepage
+      :controllers [{:start #(rf/dispatch [:fetch-homepage])}]}]
+    ["/search"
+     {:view        search/search
+      :name        :search-page
+      :controllers [{:parameters {:query [:q :serviceId]}
+                     :start      (fn [{{:keys [serviceId q]} :query}]
+                                   (rf/dispatch [:search/fetch-page serviceId
+                                                 q]))
+                     :stop       #(rf/dispatch [:search/show-form false])}]}]
+    ["/stream"
+     {:view        stream/stream
+      :name        :stream-page
+      :controllers [{:parameters {:query [:url]}
+                     :start      (fn [{{:keys [url]} :query}]
+                                   (rf/dispatch [:stream/fetch-page url]))}]}]
+    ["/channel"
+     {:view        channel/channel
+      :name        :channel-page
+      :controllers [{:parameters {:query [:url]}
+                     :start      (fn [{{:keys [url]} :query}]
+                                   (rf/dispatch [:channel/fetch-page url]))}]}]
+    ["/playlist"
+     {:view        playlist/playlist
+      :name        :playlist-page
+      :controllers [{:parameters {:query [:url]}
+                     :start      (fn [{{:keys [url]} :query}]
+                                   (rf/dispatch [:playlist/fetch-page url]))}]}]
+    ["/kiosk"
+     {:view        kiosk/kiosk
+      :name        :kiosk-page
+      :controllers [{:parameters {:query [:kioskId :serviceId]}
+                     :start      (fn [{{:keys [serviceId kioskId]} :query}]
+                                   (rf/dispatch [:kiosks/fetch-page serviceId
+                                                 kioskId]))}]}]
+    ["/settings"
+     {:view        settings/settings
+      :name        :settings-page
+      :controllers [{:start #(rf/dispatch [:settings/fetch-page])}]}]
+    ["/bookmark"
+     {:view        bookmarks/bookmark
+      :name        :bookmark-page
+      :controllers [{:parameters {:query [:id]}
+                     :start      (fn [{{:keys [id]} :query}]
+                                   (rf/dispatch [:bookmark/fetch-page id]))}]}]
+    ["/bookmarks"
+     {:view        bookmarks/bookmarks
+      :name        :bookmarks-page
+      :controllers [{:start #(rf/dispatch [:bookmarks/fetch-page])}]}]]))
 
 (defn on-navigate
   [new-match]

+ 26 - 14
src/frontend/tubo/search/events.cljs

@@ -6,22 +6,27 @@
 
 (rf/reg-event-fx
  :search/fetch
- (fn [{:keys  [db]} [_ service-id on-success on-error params]]
+ (fn [_ [_ service-id on-success on-error params]]
    (api/get-request (str "/services/" service-id "/search")
-                    on-success on-error params)))
+                    on-success
+                    on-error
+                    params)))
 
 (rf/reg-event-fx
  :search/load-page
  (fn [{:keys [db]} [_ res]]
    (let [search-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db :search-results search-res
+     {:db (assoc db
+                 :search-results    search-res
                  :show-page-loading false)
       :fx [[:dispatch [:services/fetch search-res]]]})))
 
 (rf/reg-event-fx
  :search/bad-page-response
  (fn [{:keys [db]} [_ service-id query res]]
-   {:fx [[:dispatch [:change-view #(layout/error res [:search/fetch-page service-id query])]]]
+   {:fx [[:dispatch
+          [:change-view
+           #(layout/error res [:search/fetch-page service-id query])]]]
     :db (assoc db :show-page-loading false)}))
 
 (rf/reg-event-fx
@@ -29,10 +34,12 @@
  (fn [{:keys [db]} [_ service-id query]]
    {:db (assoc db
                :show-page-loading true
-               :show-search-form true
-               :search-results nil)
-    :fx [[:dispatch [:search/fetch service-id
-                     [:search/load-page] [:search/bad-page-response service-id query] {:q query}]]
+               :show-search-form  true
+               :search-results    nil)
+    :fx [[:dispatch
+          [:search/fetch service-id
+           [:search/load-page] [:search/bad-page-response service-id query]
+           {:q query}]]
          [:document-title (str "Search for \"" query "\"")]]}))
 
 (rf/reg-event-db
@@ -44,7 +51,8 @@
            (assoc-in [:search-results :next-page] nil)
            (assoc :show-pagination-loading false))
        (-> db
-           (update-in [:search-results :items] #(apply conj %1 %2)
+           (update-in [:search-results :items]
+                      #(apply conj %1 %2)
                       (:items search-res))
            (assoc-in [:search-results :next-page] (:next-page search-res))
            (assoc :show-pagination-loading false))))))
@@ -54,16 +62,20 @@
  (fn [{:keys [db]} [_ query id next-page-url]]
    (if (empty? next-page-url)
      {:db (assoc db :show-pagination-loading false)}
-     {:fx [[:dispatch [:search/fetch id
-                       [:search/load-paginated] [:bad-response]
-                       {:q        query
-                        :nextPage (js/encodeURIComponent next-page-url)}]]]
+     {:fx [[:dispatch
+            [:search/fetch id
+             [:search/load-paginated] [:bad-response]
+             {:q        query
+              :nextPage (js/encodeURIComponent next-page-url)}]]]
       :db (assoc db :show-pagination-loading true)})))
 
 (rf/reg-event-db
  :search/show-form
  (fn [db [_ show?]]
-   (when-not (= (-> db :current-match :path) "search")
+   (when-not (= (-> db
+                    :current-match
+                    :path)
+                "search")
      (assoc db :show-search-form show?))))
 
 (rf/reg-event-db

+ 7 - 9
src/frontend/tubo/search/views.cljs

@@ -2,11 +2,11 @@
   (:require
    [re-frame.core :as rf]
    [reagent.core :as r]
-   [reitit.frontend.easy :as rfe]
    [tubo.components.items :as items]
    [tubo.components.layout :as layout]))
 
-(defn search-form []
+(defn search-form
+  []
   (let [!query (r/atom "")
         !input (r/atom nil)]
     (fn []
@@ -14,7 +14,7 @@
             show-search-form? @(rf/subscribe [:show-search-form])
             service-id        @(rf/subscribe [:service-id])]
         [:form.relative.flex.items-center.text-white.ml-4
-         {:class (when-not show-search-form? "hidden")
+         {:class     (when-not show-search-form? "hidden")
           :on-submit #(do (.preventDefault %)
                           (when-not (empty? @!query)
                             (rf/dispatch [:navigate
@@ -49,12 +49,10 @@
 
 (defn search
   [{{:keys [q serviceId]} :query-params}]
-  (let [{:keys [items next-page]
-         :as   search-results} @(rf/subscribe [:search-results])
-        next-page-url          (:url next-page)
-        services               @(rf/subscribe [:services])
-        service-id             (or @(rf/subscribe [:service-id]) serviceId)
-        scrolled-to-bottom?    @(rf/subscribe [:scrolled-to-bottom])]
+  (let [{:keys [items next-page]} @(rf/subscribe [:search-results])
+        next-page-url             (:url next-page)
+        service-id                (or @(rf/subscribe [:service-id]) serviceId)
+        scrolled-to-bottom?       @(rf/subscribe [:scrolled-to-bottom])]
     (when scrolled-to-bottom?
       (rf/dispatch [:search/fetch-paginated q service-id next-page-url]))
     [layout/content-container

+ 4 - 3
src/frontend/tubo/services/events.cljs

@@ -8,8 +8,9 @@
  (fn [{:keys [db]} [_ {:keys [service-id]}]]
    {:db db
     :fx [[:dispatch [:services/change-id service-id]]
-         [:dispatch [:kiosks/fetch-all service-id
-                     [:kiosks/load] [:bad-response]]]]}))
+         [:dispatch
+          [:kiosks/fetch-all service-id
+           [:kiosks/load] [:bad-response]]]]}))
 
 (rf/reg-event-fx
  :services/change-id
@@ -20,7 +21,7 @@
 
 (rf/reg-event-fx
  :services/fetch-all
- (fn [{:keys [db]} [_ on-success on-error]]
+ (fn [_ [_ on-success on-error]]
    (api/get-request "/services" on-success on-error)))
 
 (rf/reg-event-db

+ 8 - 4
src/frontend/tubo/services/views.cljs

@@ -8,13 +8,17 @@
    {:style {:background service-color}}
    [:div.w-full.box-border.z-10.lg:z-0
     [:select.border-none.focus:ring-transparent.bg-blend-color-dodge.font-bold.w-full
-     {:on-change #(rf/dispatch [:kiosks/change-page (js/parseInt (.. % -target -value))])
+     {:on-change #(rf/dispatch [:kiosks/change-page
+                                (js/parseInt (.. % -target -value))])
       :value     service-id
       :style     {:background :transparent}}
      (when services
        (for [[i service] (map-indexed vector services)]
-         ^{:key i} [:option.text-white.bg-neutral-900.border-none
-                    {:value (:id service)}
-                    (-> service :info :name)]))]]
+         ^{:key i}
+         [:option.text-white.bg-neutral-900.border-none
+          {:value (:id service)}
+          (-> service
+              :info
+              :name)]))]]
    [:div.flex.items-center.justify-end.absolute.min-h-full.top-0.right-4.lg:right-0.z-0
     [:i.fa-solid.fa-caret-down]]])

+ 34 - 15
src/frontend/tubo/settings/events.cljs

@@ -15,27 +15,38 @@
  [(rf/inject-cofx :store)]
  (fn [{:keys [db store]} [_ service-name service-id res]]
    (let [kiosks-res            (js->clj res :keywordize-keys true)
-         default-service-kiosk (-> db :settings :default-service :default-kiosk)
+         default-service-kiosk (-> db
+                                   :settings
+                                   :default-service
+                                   :default-kiosk)
          default-kiosk         (if (some #(= % default-service-kiosk)
                                          (:available-kiosks kiosks-res))
                                  default-service-kiosk
                                  (:default-kiosk kiosks-res))]
-     {:db    (update-in db [:settings :default-service] assoc
-                        :id service-name
-                        :service-id service-id
+     {:db    (update-in db
+                        [:settings :default-service]
+                        assoc
+                        :id               service-name
+                        :service-id       service-id
                         :available-kiosks (:available-kiosks kiosks-res)
-                        :default-kiosk default-kiosk)
-      :store (update-in store [:default-service] assoc
-                        :id service-name
-                        :service-id service-id
+                        :default-kiosk    default-kiosk)
+      :store (update-in store
+                        [:default-service]
+                        assoc
+                        :id               service-name
+                        :service-id       service-id
                         :available-kiosks (:available-kiosks kiosks-res)
-                        :default-kiosk default-kiosk)})))
+                        :default-kiosk    default-kiosk)})))
 
 (rf/reg-event-fx
  :settings/change-service
  [(rf/inject-cofx :store)]
- (fn [{:keys [db store]} [_ val]]
-   (let [service-id (-> (filter #(= val (-> % :info :name)) (:services db))
+ (fn [{:keys [db]} [_ val]]
+   (let [service-id (-> (filter #(= val
+                                    (-> %
+                                        :info
+                                        :name))
+                                (:services db))
                         first
                         :id)]
      (api/get-request (str "/services/" service-id "/kiosks")
@@ -52,9 +63,17 @@
 (rf/reg-event-fx
  :settings/fetch-page
  (fn [{:keys [db]} _]
-   (let [id         (-> db :settings :default-service :id)
-         service-id (-> db :settings :default-service :service-id)]
+   (let [id         (-> db
+                        :settings
+                        :default-service
+                        :id)
+         service-id (-> db
+                        :settings
+                        :default-service
+                        :service-id)]
      (assoc
       (api/get-request (str "/services/" service-id "/kiosks")
-                       [:settings/load-kiosks id service-id] [:bad-response])
-      :document-title "Settings"))))
+                       [:settings/load-kiosks id service-id]
+                       [:bad-response])
+      :document-title
+      "Settings"))))

+ 10 - 7
src/frontend/tubo/settings/views.cljs

@@ -16,20 +16,23 @@
 
 (defn settings
   []
-  (let [{:keys [theme themes show-comments show-related show-description
-                default-service]} @(rf/subscribe [:settings])
-        service-color             @(rf/subscribe [:service-color])
-        services                  @(rf/subscribe [:services])]
+  (let [{:keys [theme show-comments show-related show-description
+                default-service]}
+        @(rf/subscribe [:settings])
+        services @(rf/subscribe [:services])]
     [layout/content-container
      [layout/content-header "Settings"]
      [:form.flex.flex-wrap.py-4
       [select-input "Theme" :theme theme #{:auto :light :dark}]
       [select-input "Default service" :default-service (:id default-service)
-       (map #(-> % :info :name) services)
-       #(rf/dispatch [:settings/change-service (..  % -target -value)])]
+       (map #(-> %
+                 :info
+                 :name)
+            services)
+       #(rf/dispatch [:settings/change-service (.. % -target -value)])]
       [select-input "Default kiosk" :default-service
        (:default-kiosk default-service) (:available-kiosks default-service)
-       #(rf/dispatch [:settings/change-kiosk (..  % -target -value)])]
+       #(rf/dispatch [:settings/change-kiosk (.. % -target -value)])]
       [boolean-input "Show description" :show-description show-description]
       [boolean-input "Show comments" :show-comments show-comments]
       [boolean-input "Show related videos" :show-related show-related]]]))

+ 19 - 9
src/frontend/tubo/stream/events.cljs

@@ -6,17 +6,21 @@
 
 (rf/reg-event-fx
  :stream/fetch
- (fn [{:keys [db]} [_ url on-success on-error]]
+ (fn [_ [_ url on-success on-error]]
    (api/get-request (str "/streams/" (js/encodeURIComponent url))
-                    on-success on-error)))
+                    on-success
+                    on-error)))
 
 (rf/reg-event-fx
  :stream/load-page
  (fn [{:keys [db]} [_ res]]
    (let [stream-res (js->clj res :keywordize-keys true)]
-     {:db (assoc db :stream stream-res
+     {:db (assoc db
+                 :stream            stream-res
                  :show-page-loading false)
-      :fx [(when (and (-> db :settings :show-comments))
+      :fx [(when (-> db
+                     :settings
+                     :show-comments)
              [:dispatch [:comments/fetch-page (:url stream-res)]])
            [:dispatch [:services/fetch stream-res]]
            [:document-title (:name stream-res)]]})))
@@ -24,19 +28,25 @@
 (rf/reg-event-fx
  :stream/bad-page-response
  (fn [{:keys [db]} [_ url res]]
-   {:fx [[:dispatch [:change-view #(layout/error res [:stream/fetch-page url])]]]
+   {:fx [[:dispatch
+          [:change-view #(layout/error res [:stream/fetch-page url])]]]
     :db (assoc db :show-page-loading false)}))
 
 (rf/reg-event-fx
  :stream/fetch-page
  (fn [{:keys [db]} [_ url]]
-   {:fx [[:dispatch [:stream/fetch url
-                     [:stream/load-page] [:stream/bad-page-response url]]]]
+   {:fx [[:dispatch
+          [:stream/fetch url
+           [:stream/load-page] [:stream/bad-page-response url]]]]
     :db (assoc db
                :show-page-loading true
-               :stream nil)}))
+               :stream            nil)}))
 
 (rf/reg-event-db
  :stream/toggle-layout
  (fn [db [_ layout]]
-   (assoc-in db [:stream layout] (not (-> db :stream layout)))))
+   (assoc-in db
+    [:stream layout]
+    (not (-> db
+             :stream
+             layout)))))

+ 20 - 11
src/frontend/tubo/stream/views.cljs

@@ -15,7 +15,10 @@
   (let [!menu-active? (r/atom nil)]
     (fn [{:keys [service-id url] :as stream}]
       (let [bookmarks @(rf/subscribe [:bookmarks])
-            liked?    (some #(= (:url %) url) (-> bookmarks first :items))]
+            liked?    (some #(= (:url %) url)
+                            (-> bookmarks
+                                first
+                                :items))]
         [layout/popover-menu !menu-active?
          [{:label    "Add to queue"
            :icon     [:i.fa-solid.fa-headphones]
@@ -28,13 +31,15 @@
                        [:i.fa-solid.fa-heart
                         {:style {:color (utils/get-service-color service-id)}}]
                        [:i.fa-solid.fa-heart])
-           :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) stream true])}
+           :on-click #(rf/dispatch [(if liked? :likes/remove :likes/add) stream
+                                    true])}
           {:label "Original"
            :link  {:route url :external? true}
            :icon  [:i.fa-solid.fa-external-link-alt]}
           {:label    "Add to playlist"
            :icon     [:i.fa-solid.fa-plus]
-           :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark stream]])}]]))))
+           :on-click #(rf/dispatch [:modals/open
+                                    [modals/add-to-bookmark stream]])}]]))))
 
 (defn metadata-uploader
   [{:keys [uploader-url uploader-name subscriber-count] :as stream}]
@@ -84,7 +89,7 @@
 (defn description
   [{:keys [description show-description]}]
   (let [show? (:show-description @(rf/subscribe [:settings]))]
-    (when (and show? (not (empty? description)))
+    (when (and show? (seq description))
       [layout/show-more-container show-description description
        #(rf/dispatch [(if @(rf/subscribe [:main-player/show])
                         :main-player/toggle-layout
@@ -95,7 +100,7 @@
   [{:keys [comments-page show-comments show-comments-loading url] :as stream}]
   (let [show?         (:show-comments @(rf/subscribe [:settings]))
         service-color @(rf/subscribe [:service-color])]
-    (when (and comments-page (not (empty? (:comments comments-page))) show?)
+    (when (and comments-page (seq (:comments comments-page)) show?)
       [layout/accordeon
        {:label     "Comments"
         :on-open   #(if show-comments
@@ -115,27 +120,31 @@
   (let [!menu-active? (r/atom nil)]
     (fn [{:keys [related-streams show-related]}]
       (let [show? (:show-related @(rf/subscribe [:settings]))]
-        (when (and show? (not (empty? related-streams)))
+        (when (and show? (seq related-streams))
           [layout/accordeon
            {:label        "Suggested"
             :on-open      #(rf/dispatch [(if @(rf/subscribe [:main-player/show])
                                            :main-player/toggle-layout
-                                           :stream/toggle-layout) :show-related])
+                                           :stream/toggle-layout)
+                                         :show-related])
             :open?        (not show-related)
             :left-icon    "fa-solid fa-list"
             :right-button [layout/popover-menu !menu-active?
                            [{:label    "Add to queue"
                              :icon     [:i.fa-solid.fa-headphones]
-                             :on-click #(rf/dispatch [:queue/add-n related-streams true])}
+                             :on-click #(rf/dispatch [:queue/add-n
+                                                      related-streams true])}
                             {:label    "Add to playlist"
                              :icon     [:i.fa-solid.fa-plus]
-                             :on-click #(rf/dispatch [:modals/open [modals/add-to-bookmark related-streams]])}]]}
+                             :on-click #(rf/dispatch [:modals/open
+                                                      [modals/add-to-bookmark
+                                                       related-streams]])}]]}
            [items/related-streams related-streams nil]])))))
 
 (defn stream
   []
-  (let [{:keys [audio-streams video-streams name thumbnail-url] :as stream} @(rf/subscribe [:stream])
-        !player @(rf/subscribe [:main-player])
+  (let [stream        @(rf/subscribe [:stream])
+        !player       @(rf/subscribe [:main-player])
         page-loading? @(rf/subscribe [:show-page-loading])]
     [:<>
      (when-not page-loading?

+ 0 - 2
src/frontend/tubo/subs.cljs

@@ -2,7 +2,6 @@
   (:require
    [reagent.core :as r]
    [re-frame.core :as rf]
-   [tubo.utils :as utils]
    [tubo.bookmarks.subs]
    [tubo.channel.subs]
    [tubo.kiosks.subs]
@@ -42,7 +41,6 @@
                        #(reset! theme (if (.-matches %) "dark" "light")))
     theme))
 
-
 (rf/reg-sub
  :is-window-visible
  (fn [_ _]

+ 12 - 4
src/frontend/tubo/utils.cljs

@@ -31,7 +31,9 @@
 
 (defn format-date-ago
   [date]
-  (if (-> date js/Date.parse js/isNaN)
+  (if (-> date
+          js/Date.parse
+          js/isNaN)
     date
     (timeago/format date)))
 
@@ -39,11 +41,17 @@
   [num]
   (.format
    (js/Intl.NumberFormat
-    "en-US" #js {"notation" "compact" "maximumFractionDigits" 1})
+    "en-US"
+    #js {"notation" "compact" "maximumFractionDigits" 1})
    num))
 
 (defn format-duration
   [num]
   (let [duration (and (not (js/isNaN num)) (js/Date. (* num 1000)))
-        slice    (and duration #(.slice % (if (>= (.getUTCHours duration) 1) 11 14) 19))]
-    (if slice (-> duration (.toISOString) slice) "--:--")))
+        slice    (and duration
+                      #(.slice % (if (>= (.getUTCHours duration) 1) 11 14) 19))]
+    (if slice
+      (-> duration
+          (.toISOString)
+          slice)
+      "--:--")))

+ 3 - 1
src/frontend/tubo/views.cljs

@@ -15,7 +15,9 @@
       [navigation/navbar current-match]
       [notifications/notifications-panel]
       [:div.flex.flex-col.flex-auto.justify-between.relative
-       (when-let [view (-> current-match :data :view)]
+       (when-let [view (-> current-match
+                           :data
+                           :view)]
          [view current-match])
        [queue/queue]
        [player/main-player]