aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMiguel Ángel Moreno <mail@migalmoreno.com>2024-02-18 17:41:11 +0100
committerMiguel Ángel Moreno <mail@migalmoreno.com>2024-02-18 17:41:11 +0100
commite041307a6932664ce666a80e341b4bd94d1c263c (patch)
treea86b39f7f51782ab0e80b7d5851f1ee775502163
parent57875a7b1069a2f87c354e0589f12f54fe5e8c4b (diff)
feat(frontend): refactor components with new features
-rw-r--r--resources/src/css/_typography.scss5
-rw-r--r--src/frontend/tubo/components/audio_player.cljs92
-rw-r--r--src/frontend/tubo/components/play_queue.cljs150
-rw-r--r--src/frontend/tubo/views/stream.cljs236
-rw-r--r--tailwind.config.js1
5 files changed, 257 insertions, 227 deletions
diff --git a/resources/src/css/_typography.scss b/resources/src/css/_typography.scss
index f0cc680..a9e24c1 100644
--- a/resources/src/css/_typography.scss
+++ b/resources/src/css/_typography.scss
@@ -9,6 +9,11 @@
}
@font-face {
+ font-family: "nunito-bold";
+ src: url("../fonts/nunito/Nunito-Bold.ttf");
+}
+
+@font-face {
font-family: "roboto-light";
src: url("../fonts/roboto/Roboto-Light.ttf");
}
diff --git a/src/frontend/tubo/components/audio_player.cljs b/src/frontend/tubo/components/audio_player.cljs
index 40f05b1..8935a0d 100644
--- a/src/frontend/tubo/components/audio_player.cljs
+++ b/src/frontend/tubo/components/audio_player.cljs
@@ -6,6 +6,7 @@
[re-frame.core :as rf]
[reitit.frontend.easy :as rfe]
[tubo.components.layout :as layout]
+ [tubo.components.modals.bookmarks :as bookmarks]
[tubo.components.player :as player]
[tubo.events :as events]
[tubo.util :as util]))
@@ -66,7 +67,7 @@
[:i.fa-solid.fa-pause]))
#(rf/dispatch [::events/set-player-paused (not paused?)])
:show-on-mobile? true
- :extra-styles "lg:text-2xl"]
+ :extra-classes "lg:text-2xl"]
[player/button [:i.fa-solid.fa-forward]
#(rf/dispatch [::events/set-player-time (+ @!elapsed-time 5)])]
[player/button [:i.fa-solid.fa-forward-step]
@@ -83,39 +84,56 @@
(defn player
[]
- (let [{:keys
- [uploader-name uploader-url thumbnail-url
- name stream url service-color] :as current-stream}
- @(rf/subscribe [:media-queue-stream])
- show-audio-player? @(rf/subscribe [:show-audio-player])
- show-media-queue? @(rf/subscribe [:show-media-queue])
- volume-level @(rf/subscribe [:volume-level])
- muted? @(rf/subscribe [:muted])
- !player @(rf/subscribe [:player])
- {:keys [theme]} @(rf/subscribe [:settings])
- bg-color (str "rgba(" (if (= theme "dark") "23, 23, 23" "255, 255, 255") ", 0.95)")]
- (when show-audio-player?
- [:div.sticky.bottom-0.z-40.p-3.absolute.box-border.m-0
- {:style
- {:display (when show-media-queue? "none")
- :background-image (str "linear-gradient(0deg, " bg-color "," bg-color "), url(\"" thumbnail-url "\")")
- :backgroundSize "cover"
- :backgroundPosition "center"
- :backgroundRepeat "no-repeat"}}
- [:div.flex.items-center.justify-between
- [:div.flex.items-center.lg:flex-1
- [:div {:style {:height "40px" :width "70px" :maxWidth "70px" :minWidth "70px"}}
- [:img.min-h-full.max-h-full.object-cover.min-w-full.max-w-full.w-full {:src thumbnail-url}]]
- [:div.flex.flex-col.px-2
- [:a.text-xs.line-clamp-1
- {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
- [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
- {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
- [audio-source !player]]
- [main-controls service-color]
- [:div.flex.lg:justify-end.lg:flex-1
- [player/volume-slider !player volume-level muted? service-color]
- [player/button [:i.fa-solid.fa-list] #(rf/dispatch [::events/toggle-media-queue])
- :show-on-mobile? true]
- [player/button [:i.fa-solid.fa-close] #(rf/dispatch [::events/dispose-audio-player])
- :show-on-mobile? true]]]])))
+ (let [!menu-active? (r/atom nil)]
+ (fn []
+ (let [{:keys
+ [uploader-name uploader-url thumbnail-url
+ name stream url service-id] :as current-stream}
+ @(rf/subscribe [:media-queue-stream])
+ show-audio-player? @(rf/subscribe [:show-audio-player])
+ show-media-queue? @(rf/subscribe [:show-media-queue])
+ volume-level @(rf/subscribe [:volume-level])
+ muted? @(rf/subscribe [:muted])
+ bookmarks @(rf/subscribe [:bookmarks])
+ !player @(rf/subscribe [:player])
+ {:keys [theme]} @(rf/subscribe [:settings])
+ service-color (and service-id (util/get-service-color service-id))
+ bg-color (str "rgba(" (if (= theme "dark") "23, 23, 23" "255, 255, 255") ", 0.95)")
+ liked? (some #(= (:url %) url) (-> bookmarks first :items))]
+ (when show-audio-player?
+ [:div.sticky.bottom-0.z-10.p-3.absolute.box-border.m-0
+ {:style
+ {:display (when show-media-queue? "none")
+ :background-image (str "linear-gradient(0deg, " bg-color "," bg-color "), url(\"" thumbnail-url "\")")
+ :backgroundSize "cover"
+ :backgroundPosition "center"
+ :backgroundRepeat "no-repeat"}}
+ [:div.flex.items-center.justify-between
+ [:div.flex.items-center.lg:flex-1
+ [:div {:style {:height "40px" :width "70px" :maxWidth "70px" :minWidth "70px"}}
+ [:img.min-h-full.max-h-full.object-cover.min-w-full.max-w-full.w-full {:src thumbnail-url}]]
+ [:div.flex.flex-col.px-2
+ [:a.text-xs.line-clamp-1
+ {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
+ [:a.text-xs.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
+ {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
+ [audio-source !player]]
+ [main-controls service-color]
+ [:div.flex.lg:justify-end.lg:flex-1
+ [player/volume-slider !player volume-level muted? service-color]
+ [player/button [:i.fa-solid.fa-list] #(rf/dispatch [::events/toggle-media-queue])
+ :show-on-mobile? true]
+ [layout/more-menu !menu-active?
+ [{:label (if liked? "Remove favorite" "Favorite")
+ :icon (if liked?
+ [:i.fa-solid.fa-heart {:style {:color service-color}}]
+ [:i.fa-regular.fa-heart])
+ :on-click #(rf/dispatch [(if liked? ::events/remove-from-likes ::events/add-to-likes) current-stream])}
+ {:label "Add to playlist"
+ :icon [:i.fa-solid.fa-plus]
+ :on-click #(rf/dispatch [::events/add-bookmark-list-modal
+ [bookmarks/add-to-bookmark-list-modal current-stream]])}]
+ :menu-styles {:bottom "0px" :top nil}
+ :extra-classes "pt-1 !pl-2 pr-2 "]
+ [player/button [:i.fa-solid.fa-close] #(rf/dispatch [::events/dispose-audio-player])
+ :show-on-mobile? true]]]])))))
diff --git a/src/frontend/tubo/components/play_queue.cljs b/src/frontend/tubo/components/play_queue.cljs
index a2b1190..8c77d7d 100644
--- a/src/frontend/tubo/components/play_queue.cljs
+++ b/src/frontend/tubo/components/play_queue.cljs
@@ -9,25 +9,19 @@
[tubo.util :as util]))
(defn play-queue-item
- [{:keys [uploader-name uploader-url name duration
+ [{:keys [service-id uploader-name uploader-url name duration
stream url service-color thumbnail-url]} media-queue-pos i]
- (let [service-name (case service-color
- "#cc0000" "YouTube"
- "#ff7700" "SoundCloud"
- "#333333" "media.ccc.de"
- "#F2690D" "PeerTube"
- "#629aa9" "Bandcamp")]
- [:div.flex.w-full.h-24.rounded.cursor-pointer.px-2.my-1
- {:class (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800")
- :on-click #(rf/dispatch [::events/change-media-queue-pos i])}
- [:div.w-56
- [items/thumbnail thumbnail-url nil url name duration {:classes "h-24"}]]
- [:div.flex.flex-col.px-4.py-2.w-full
- [:h1.line-clamp-1 name]
- [:div.text-neutral-600.dark:text-neutral-300.text-sm.flex.flex-col.xs:flex-row
- [:span.line-clamp-1 uploader-name]
- [:span.px-2.hidden.xs:inline-block {:dangerouslySetInnerHTML {:__html "&bull;"}}]
- [:span service-name]]]]))
+ [:div.flex.w-full.h-24.rounded.cursor-pointer.px-2.my-1
+ {:class (when (= i media-queue-pos) "bg-[#f0f0f0] dark:bg-stone-800")
+ :on-click #(rf/dispatch [::events/change-media-queue-pos i])}
+ [:div.w-56
+ [layout/thumbnail thumbnail-url url name duration {:classes "h-24"}]]
+ [:div.flex.flex-col.px-4.py-2.w-full
+ [:h1.line-clamp-1 name]
+ [:div.text-neutral-600.dark:text-neutral-300.text-sm.flex.flex-col.xs:flex-row
+ [:span.line-clamp-1 uploader-name]
+ [:span.px-2.hidden.xs:inline-block {:dangerouslySetInnerHTML {:__html "&bull;"}}]
+ [:span (util/get-service-name service-id)]]]])
(defn queue
[]
@@ -36,72 +30,72 @@
paused? @(rf/subscribe [:paused])
media-queue @(rf/subscribe [:media-queue])
media-queue-pos @(rf/subscribe [:media-queue-pos])
- {:keys [uploader-name uploader-url
- name stream url service-color]
+ {:keys [uploader-name uploader-url name stream url service-id]
:as current-stream} @(rf/subscribe [:media-queue-stream])
+ service-color (and service-id (util/get-service-color service-id))
!elapsed-time @(rf/subscribe [:elapsed-time])
!player @(rf/subscribe [:player])
loop-playback @(rf/subscribe [:loop-playback])
player-ready? (and @!player (> (.-readyState @!player) 0))]
(when (and show-media-queue media-queue)
- [:div.fixed.flex.flex-col.items-center.px-5.py-2.min-w-full.w-full.z-30
+ [:div.fixed.flex.flex-col.items-center.px-5.py-2.min-w-full.w-full.z-10
{:style {:minHeight "calc(100dvh - 56px)" :height "calc(100dvh - 56px)"}
:class "dark:bg-neutral-900/90 bg-white/90 backdrop-blur"}
- [:div.flex.justify-between.items-center.w-full.shrink-0
+ [layout/focus-overlay #(rf/dispatch [::events/toggle-media-queue]) show-media-queue true]
+ [:div.z-20.w-full.flex.flex-col.flex-auto.h-full
{:class "lg:w-4/5 xl:w-3/5"}
- [:h1.text-2xl.font-bold.py-6 "Play Queue"]
- [:button.mx-2
- [:i.fa-solid.fa-close
- {:on-click #(rf/dispatch [::events/toggle-media-queue])}]]]
- [:div.flex.flex-col.pr-2.w-full.overflow-y-auto.flex-auto
- {:class "lg:w-4/5 xl:w-3/5"}
- (for [[i item] (map-indexed vector media-queue)]
- ^{:key i} [play-queue-item item media-queue-pos i])]
- [:div.flex.flex-col.py-4.w-full.shrink-0
- {:class "lg:w-4/5 xl:w-3/5"}
- [:div.flex.flex-col.w-full.py-2
- [:a.text-md.line-clamp-1
- {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
- [:a.text-sm.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
- {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
- [:div.flex.flex-auto.py-2.w-full.items-center
- [:span.mr-2 (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
- [player/time-slider !player !elapsed-time service-color]
- [:span.ml-2 (if player-ready? (util/format-duration (.-duration @!player)) "00:00")]]
- [:div.flex.justify-center.items-center
- [player/loop-button loop-playback service-color true]
- [player/button
- [:i.fa-solid.fa-backward-step]
- #(when (and media-queue (not= media-queue-pos 0))
- (rf/dispatch [::events/change-media-queue-pos
- (- media-queue-pos 1)]))
- :disabled? (not (and media-queue (not= media-queue-pos 0)))
- :extra-styles "text-xl"
- :show-on-mobile? true]
- [player/button
- [:i.fa-solid.fa-backward]
- #(set! (.-currentTime @!player) (- @!elapsed-time 5))
- :extra-styles "text-xl"
- :show-on-mobile? true]
- [player/button
- (if (or loading? (not @!player))
- [layout/loading-icon service-color "text-3xl"]
- (if paused?
- [:i.fa-solid.fa-play]
- [:i.fa-solid.fa-pause]))
- #(rf/dispatch [::events/set-player-paused (not paused?)])
- :extra-styles "text-3xl"
- :show-on-mobile? true]
- [player/button
- [:i.fa-solid.fa-forward]
- #(set! (.-currentTime @!player) (+ @!elapsed-time 5))
- :extra-styles "text-xl"
- :show-on-mobile? true]
- [player/button
- [:i.fa-solid.fa-forward-step]
- #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
- (rf/dispatch [::events/change-media-queue-pos
- (+ media-queue-pos 1)]))
- :disabled? (not (and media-queue (< (+ media-queue-pos 1) (count media-queue))))
- :extra-styles "text-xl"
- :show-on-mobile? true]]]])))
+ [:div.flex.justify-between.items-center.shrink-0
+ [:h1.text-2xl.font-bold.py-6 "Play Queue"]
+ [:button.mx-2
+ [:i.fa-solid.fa-close
+ {:on-click #(rf/dispatch [::events/toggle-media-queue])}]]]
+ [:div.flex.flex-col.pr-2.overflow-y-auto.flex-auto
+ (for [[i item] (map-indexed vector media-queue)]
+ ^{:key i} [play-queue-item item media-queue-pos i])]
+ [:div.flex.flex-col.py-4.shrink-0
+ [:div.flex.flex-col.w-full.py-2
+ [:a.text-md.line-clamp-1
+ {:href (rfe/href :tubo.routes/stream nil {:url url})} name]
+ [:a.text-sm.pt-2.text-neutral-600.dark:text-neutral-300.line-clamp-1
+ {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]]
+ [:div.flex.flex-auto.py-2.w-full.items-center
+ [:span.mr-2 (if @!elapsed-time (util/format-duration @!elapsed-time) "00:00")]
+ [player/time-slider !player !elapsed-time service-color]
+ [:span.ml-2 (if player-ready? (util/format-duration (.-duration @!player)) "00:00")]]
+ [:div.flex.justify-center.items-center
+ [player/loop-button loop-playback service-color true]
+ [player/button
+ [:i.fa-solid.fa-backward-step]
+ #(when (and media-queue (not= media-queue-pos 0))
+ (rf/dispatch [::events/change-media-queue-pos
+ (- media-queue-pos 1)]))
+ :disabled? (not (and media-queue (not= media-queue-pos 0)))
+ :extra-classes "text-xl"
+ :show-on-mobile? true]
+ [player/button
+ [:i.fa-solid.fa-backward]
+ #(set! (.-currentTime @!player) (- @!elapsed-time 5))
+ :extra-classes "text-xl"
+ :show-on-mobile? true]
+ [player/button
+ (if (or loading? (not @!player))
+ [layout/loading-icon service-color "text-3xl"]
+ (if paused?
+ [:i.fa-solid.fa-play]
+ [:i.fa-solid.fa-pause]))
+ #(rf/dispatch [::events/set-player-paused (not paused?)])
+ :extra-classes "text-3xl"
+ :show-on-mobile? true]
+ [player/button
+ [:i.fa-solid.fa-forward]
+ #(set! (.-currentTime @!player) (+ @!elapsed-time 5))
+ :extra-classes "text-xl"
+ :show-on-mobile? true]
+ [player/button
+ [:i.fa-solid.fa-forward-step]
+ #(when (and media-queue (< (+ media-queue-pos 1) (count media-queue)))
+ (rf/dispatch [::events/change-media-queue-pos
+ (+ media-queue-pos 1)]))
+ :disabled? (not (and media-queue (< (+ media-queue-pos 1) (count media-queue))))
+ :extra-classes "text-xl"
+ :show-on-mobile? true]]]]])))
diff --git a/src/frontend/tubo/views/stream.cljs b/src/frontend/tubo/views/stream.cljs
index 29f4bb0..93cd831 100644
--- a/src/frontend/tubo/views/stream.cljs
+++ b/src/frontend/tubo/views/stream.cljs
@@ -1,125 +1,137 @@
(ns tubo.views.stream
(:require
+ [reagent.core :as r]
[re-frame.core :as rf]
[reitit.frontend.easy :as rfe]
[tubo.events :as events]
[tubo.components.items :as items]
[tubo.components.layout :as layout]
[tubo.components.comments :as comments]
+ [tubo.components.modals.bookmarks :as bookmarks]
[tubo.components.video-player :as player]
[tubo.util :as util]))
(defn stream
[match]
- (let [{:keys [name url video-streams audio-streams view-count
- subscriber-count like-count dislike-count
- description uploader-avatar uploader-name
- uploader-url upload-date related-streams
- thumbnail-url show-comments-loading comments-page
- show-comments show-related show-description service-id]
- :as stream} @(rf/subscribe [:stream])
- {show-comments? :show-comments
- show-related? :show-related
- show-description? :show-description}
- @(rf/subscribe [:settings])
- available-streams (apply conj audio-streams video-streams)
- page-loading? @(rf/subscribe [:show-page-loading])
- service-color @(rf/subscribe [:service-color])
- bookmarks @(rf/subscribe [:bookmarks])
- sources (reverse (map (fn [{:keys [content format resolution averageBitrate]}]
- {:src content
- :type "video/mp4"
- :label (str (or resolution "audio-only") " "
- format
- (when-not resolution
- (str " " averageBitrate "kbit/s")))})
- available-streams))
- player-elements ["playToggle" "progressControl"
- "volumePanel" "playbackRateMenuButton"
- "QualitySelector" "fullscreenToggle"]]
- [layout/content-container
- [:div.flex.justify-center.relative
- {:class "h-[300px] md:h-[450px] lg:h-[600px]"}
- [player/player
- {:sources sources
- :poster thumbnail-url
- :controls true
- :controlBar {:children player-elements}
- :preload "metadata"
- :responsive true
- :fill true
- :playbackRates [0.5 1 1.5 2]}]]
- [:div
- [:div.flex.flex-col
- [:div.flex.items-center.justify-between.pt-4
- [:div.flex-auto
- [:h1.text-lg.sm:text-2xl.font-extrabold.line-clamp-1 name]]
- [:div.flex.flex-auto.justify-end.items-center.my-3.ml-4.gap-x-5
- [:button
- {:on-click #(rf/dispatch [::events/switch-to-audio-player stream service-color])}
- [:i.fa-solid.fa-headphones]]
- [:button
- [:a.block.sm:inline-block {:href url :target "__blank"}
- [:i.fa-solid.fa-external-link-alt]]]
- (if (some #(= (:url %) url) bookmarks)
- [:button
- {:on-click #(rf/dispatch [::events/remove-from-bookmarks stream])}
- [:i.fa-solid.fa-bookmark {:style {:color service-color}}]]
- [:button
- {:on-click #(rf/dispatch [::events/add-to-bookmarks stream])}
- [:i.fa-regular.fa-bookmark]])]]
- [:div.flex.justify-between.py-2.flex-nowrap
- [:div.flex.items-center
- [layout/uploader-avatar uploader-avatar uploader-name
- (rfe/href :tubo.routes/channel nil {:url uploader-url})]
- [:div.mx-3
- [:a.line-clamp-1 {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})} uploader-name]
- (when subscriber-count
- [:div.flex.my-2.items-center
- [:i.fa-solid.fa-users.text-xs]
- [:p.mx-2 (util/format-quantity subscriber-count)]])]]
- [:div.flex.flex-col.items-end.flex-auto.justify-center
- (when view-count
- [:div.sm:text-base.text-sm.mb-1
- [:i.fa-solid.fa-eye]
- [:span.ml-2 (.toLocaleString view-count)]])
- [:div.flex
- (when like-count
- [:div.items-center.sm:text-base.text-sm
- [:i.fa-solid.fa-thumbs-up]
- [:span.ml-2 (.toLocaleString like-count)]])
- (when dislike-count
- [:div.ml-2.items-center.sm:text-base.text-sm
- [:i.fa-solid.fa-thumbs-down]
- [:span.ml-2 dislike-count]])]
- (when upload-date
- [:div.sm:text-base.text-sm.mt-1.whitespace-nowrap
- [:i.fa-solid.fa-calendar]
- [:span.ml-2 (util/format-date-string upload-date)]])]]
- (when (and show-description? (not (empty? description)))
- [layout/show-more-container show-description description
- #(rf/dispatch [::events/toggle-stream-layout :show-description])])
- (when (and comments-page (not (empty? (:comments comments-page))) show-comments?)
- [layout/accordeon
- {:label "Comments"
- :on-open #(if show-comments
- (rf/dispatch [::events/toggle-stream-layout :show-comments])
- (if comments-page
- (rf/dispatch [::events/toggle-stream-layout :show-comments])
- (rf/dispatch [::events/get-comments url])))
- :open? show-comments
- :left-icon "fa-solid fa-comments"}
- (if show-comments-loading
- [layout/loading-icon service-color "text-2xl"]
- (when (and show-comments comments-page)
- [comments/comments comments-page uploader-name uploader-avatar url]))])
- (when (and show-related? (not (empty? related-streams)))
- [layout/accordeon
- {:label "Suggested"
- :on-open #(rf/dispatch [::events/toggle-stream-layout :show-related])
- :open? (not show-related)
- :left-icon "fa-solid fa-list"
- :right-button [layout/primary-button "Enqueue"
- #(rf/dispatch [::events/enqueue-related-streams related-streams service-color])
- "fa-solid fa-headphones"]}
- [items/related-streams related-streams nil]])]]]))
+ (let [!stream-menu-active? (r/atom nil)
+ !suggested-menu-active? (r/atom nil)]
+ (fn []
+ (let [{:keys [name url video-streams audio-streams view-count
+ subscriber-count like-count dislike-count
+ description uploader-avatar uploader-name
+ uploader-url upload-date related-streams
+ thumbnail-url show-comments-loading comments-page
+ show-comments show-related show-description service-id]
+ :as stream} @(rf/subscribe [:stream])
+ {show-comments? :show-comments
+ show-related? :show-related
+ show-description? :show-description}
+ @(rf/subscribe [:settings])
+ available-streams (apply conj audio-streams video-streams)
+ page-loading? @(rf/subscribe [:show-page-loading])
+ service-color @(rf/subscribe [:service-color])
+ bookmarks @(rf/subscribe [:bookmarks])
+ liked? (some #(= (:url %) url) (-> bookmarks first :items))
+ sources (reverse (map (fn [{:keys [content format resolution averageBitrate]}]
+ {:src content
+ :type "video/mp4"
+ :label (str (or resolution "audio-only") " "
+ format
+ (when-not resolution
+ (str " " averageBitrate "kbit/s")))})
+ available-streams))
+ player-elements ["playToggle" "progressControl"
+ "volumePanel" "playbackRateMenuButton"
+ "QualitySelector" "fullscreenToggle"]]
+ [layout/content-container
+ [:div.flex.justify-center.relative
+ {:class "h-[300px] md:h-[450px] lg:h-[600px]"}
+ [player/player
+ {:sources sources
+ :poster thumbnail-url
+ :controls true
+ :controlBar {:children player-elements}
+ :preload "metadata"
+ :responsive true
+ :fill true
+ :playbackRates [0.5 1 1.5 2]}]]
+ [:div
+ [:div.flex.flex-col
+ [:div.flex.items-center.justify-between.pt-4.my-3
+ [:div.flex-auto
+ [:h1.text-lg.sm:text-2xl.font-nunito-bold.line-clamp-1 name]]
+ [:div.flex.flex-auto.justify-end.items-center
+ [layout/more-menu !stream-menu-active?
+ [{:label "Add to queue"
+ :icon [:i.fa-solid.fa-headphones]
+ :on-click #(rf/dispatch [::events/switch-to-audio-player stream])}
+ {:label (if liked? "Remove favorite" "Favorite")
+ :icon (if liked?
+ [:i.fa-solid.fa-heart {:style {:color (util/get-service-color service-id)}}]
+ [:i.fa-regular.fa-heart])
+ :on-click #(rf/dispatch [(if liked? ::events/remove-from-likes ::events/add-to-likes) stream])}
+ {: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 [::events/add-bookmark-list-modal
+ [bookmarks/add-to-bookmark-list-modal stream]])}]]]]
+ [:div.flex.justify-between.py-2.flex-nowrap
+ [:div.flex.items-center
+ [layout/uploader-avatar uploader-avatar uploader-name
+ (rfe/href :tubo.routes/channel nil {:url uploader-url})]
+ [:div.mx-3
+ [:a.line-clamp-1.font-nunito-semibold
+ {:href (rfe/href :tubo.routes/channel nil {:url uploader-url})}
+ uploader-name]
+ (when subscriber-count
+ [:div.flex.my-2.items-center
+ [:i.fa-solid.fa-users.text-xs]
+ [:p.mx-2 (util/format-quantity subscriber-count)]])]]
+ [:div.flex.flex-col.items-end.flex-auto.justify-center
+ (when view-count
+ [:div.sm:text-base.text-sm.mb-1
+ [:i.fa-solid.fa-eye]
+ [:span.ml-2 (.toLocaleString view-count)]])
+ [:div.flex
+ (when like-count
+ [:div.items-center.sm:text-base.text-sm
+ [:i.fa-solid.fa-thumbs-up]
+ [:span.ml-2 (.toLocaleString like-count)]])
+ (when dislike-count
+ [:div.ml-2.items-center.sm:text-base.text-sm
+ [:i.fa-solid.fa-thumbs-down]
+ [:span.ml-2 dislike-count]])]
+ (when upload-date
+ [:div.sm:text-base.text-sm.mt-1.whitespace-nowrap
+ [:i.fa-solid.fa-calendar]
+ [:span.ml-2 (util/format-date-string upload-date)]])]]
+ (when (and show-description? (not (empty? description)))
+ [layout/show-more-container show-description description
+ #(rf/dispatch [::events/toggle-stream-layout :show-description])])
+ (when (and comments-page (not (empty? (:comments comments-page))) show-comments?)
+ [layout/accordeon
+ {:label "Comments"
+ :on-open #(if show-comments
+ (rf/dispatch [::events/toggle-stream-layout :show-comments])
+ (if comments-page
+ (rf/dispatch [::events/toggle-stream-layout :show-comments])
+ (rf/dispatch [::events/get-comments url])))
+ :open? show-comments
+ :left-icon "fa-solid fa-comments"}
+ (if show-comments-loading
+ [layout/loading-icon service-color "text-2xl"]
+ (when (and show-comments comments-page)
+ [comments/comments comments-page uploader-name uploader-avatar url]))])
+ (when (and show-related? (not (empty? related-streams)))
+ [layout/accordeon
+ {:label "Suggested"
+ :on-open #(rf/dispatch [::events/toggle-stream-layout :show-related])
+ :open? (not show-related)
+ :left-icon "fa-solid fa-list"
+ :right-button [layout/more-menu !suggested-menu-active?
+ [{:label "Add to queue"
+ :icon [:i.fa-solid.fa-headphones]
+ :on-click #(rf/dispatch [::events/enqueue-related-streams related-streams])}]]}
+ [items/related-streams related-streams nil]])]]]))))
diff --git a/tailwind.config.js b/tailwind.config.js
index dbec746..a8bd0a8 100644
--- a/tailwind.config.js
+++ b/tailwind.config.js
@@ -7,6 +7,7 @@ module.exports = {
fontFamily: {
"nunito": ["nunito-light", "sans-serif"],
"nunito-semibold": ["nunito-semibold", "sans-serif"],
+ "nunito-bold": ["nunito-bold", "sans-serif"],
"roboto": ["roboto-light", "sans-serif"],
"roboto-medium": ["roboto-medium", "sans-serif"],
},