From 7ddf157e974e9df2dccb2c45ca31c15e4b3cee74 Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Mon, 30 Oct 2023 01:02:17 +0100 Subject: feat: add initial support for bookmarks --- src/backend/tubo/routes.clj | 1 + src/frontend/tubo/components/items.cljs | 117 ++++++++++++++++---------------- src/frontend/tubo/events.cljs | 19 +++++- src/frontend/tubo/routes.cljs | 5 +- src/frontend/tubo/subs.cljs | 5 ++ src/frontend/tubo/views.cljs | 11 ++- src/frontend/tubo/views/bookmarks.cljs | 21 ++++++ src/frontend/tubo/views/stream.cljs | 16 ++++- 8 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 src/frontend/tubo/views/bookmarks.cljs diff --git a/src/backend/tubo/routes.clj b/src/backend/tubo/routes.clj index e22f35f..2bf56f8 100644 --- a/src/backend/tubo/routes.clj +++ b/src/backend/tubo/routes.clj @@ -20,6 +20,7 @@ ["/playlist" handler/index] ["/kiosk" handler/index] ["/settings" handler/index] + ["/bookmarks" handler/index] ["/api" ["/services" ["" {:get handler/services}] diff --git a/src/frontend/tubo/components/items.cljs b/src/frontend/tubo/components/items.cljs index 1881d06..6d2663e 100644 --- a/src/frontend/tubo/components/items.cljs +++ b/src/frontend/tubo/components/items.cljs @@ -7,8 +7,8 @@ [tubo.util :as util])) (defn thumbnail - [thumbnail-url route url name duration] - [:div.flex.py-2.box-border.h-44.xs:h-28 + [thumbnail-url route url name duration & {:keys [classes] :or {classes "h-44 xs:h-28"}}] + [:div.flex.py-2.box-border {:class classes} [:div.relative.min-w-full [:a.absolute.min-w-full.min-h-full.z-10 {:href route :title name}] [:img.rounded.object-cover.min-h-full.max-h-full.min-w-full {:src thumbnail-url}] @@ -23,88 +23,89 @@ [{:keys [type url name thumbnail-url description subscriber-count stream-count verified? key uploader-name uploader-url uploader-avatar upload-date short-description view-count - duration]} - item-route service-color] - [:<> - (when name - [:div.flex.items-center.my-2 - [:a {:href item-route :title name} - [:h1.line-clamp-2.my-1.break-words name]] - (when (and verified? (not uploader-url)) - [:i.fa-solid.fa-circle-check.pl-2])]) - [:div.flex.justify-between - [:div.flex.items-center.my-2 - (if uploader-url - [:a {:href (rfe/href :tubo.routes/channel nil {:url uploader-url}) :title uploader-name} - [:h1.line-clamp-1.text-neutral-800.dark:text-gray-300.font-bold.pr-2.break-all uploader-name]] - [:h1.line-clamp-1.text-neutral-800.dark:text-gray-300.font-bold.pr-2 uploader-name]) - (when (and uploader-url verified?) - [:i.fa-solid.fa-circle-check])] - (when (= type "stream") - [:button.pl-4.focus:outline-none - {:on-click #(rf/dispatch [::events/switch-to-audio-player - {:duration duration - :thumbnail-url thumbnail-url - :uploader-name uploader-name - :uploader-url uploader-url - :name name - :url url - :service-color service-color}])} - [:i.fa-solid.fa-headphones]])] - (when subscriber-count + duration audio-streams video-streams] :as item} + item-route service-color bookmarks] + (let [stream? (or (= type "stream") audio-streams video-streams)] + [:<> + (when name + [:div.flex.items-center.my-2 + [:a {:href item-route :title name} + [:h1.line-clamp-2.my-1.break-words {:style {:wordBreak "break-word"}} name]] + (when (and verified? (not uploader-url)) + [:i.fa-solid.fa-circle-check.pl-2])]) + [:div.flex.justify-between + [:div.flex.items-center.my-2 + (if uploader-url + [:a {:href (rfe/href :tubo.routes/channel nil {:url uploader-url}) :title uploader-name} + [:h1.line-clamp-1.text-neutral-800.dark:text-gray-300.font-bold.pr-2.break-all uploader-name]] + [:h1.line-clamp-1.text-neutral-800.dark:text-gray-300.font-bold.pr-2 uploader-name]) + (when (and uploader-url verified?) + [:i.fa-solid.fa-circle-check])] [:div.flex.items-center - [:i.fa-solid.fa-users.text-xs] - [:p.mx-2 (util/format-quantity subscriber-count)]]) - (when stream-count - [:div.flex.items-center - [:i.fa-solid.fa-video.text-xs] - [:p.mx-2 (util/format-quantity stream-count)]]) - [:div.flex.my-1.justify-between - [:p (util/format-date upload-date)] - (when view-count - [:div.flex.items-center.h-full.pl-2 - [:i.fa-solid.fa-eye.text-xs] - [:p.pl-1.5 (util/format-quantity view-count)]])]]) + (when stream? + [:button.ml-2.focus:outline-none + {:on-click #(rf/dispatch [::events/switch-to-audio-player item service-color])} + [:i.fa-solid.fa-headphones]]) + (when (some #{(dissoc item :key)} bookmarks) + [:button.ml-4.focus:outline-none + {:on-click #(rf/dispatch [::events/remove-from-bookmarks (dissoc item :key)])} + [:i.fa-solid.fa-trash]])]] + (when (and subscriber-count (not stream?)) + [:div.flex.items-center + [:i.fa-solid.fa-users.text-xs] + [:p.mx-2 (util/format-quantity subscriber-count)]]) + (when stream-count + [:div.flex.items-center + [:i.fa-solid.fa-video.text-xs] + [:p.mx-2 (util/format-quantity stream-count)]]) + [:div.flex.my-1.justify-between + [:p (util/format-date upload-date)] + (when view-count + [:div.flex.items-center.h-full.pl-2 + [:i.fa-solid.fa-eye.text-xs] + [:p.pl-1.5 (util/format-quantity view-count)]])]])) (defn stream-item - [{:keys [url name thumbnail-url duration] :as item} service-color] + [{:keys [url name thumbnail-url duration] :as item} service-color bookmarks] [:<> [thumbnail thumbnail-url (rfe/href :tubo.routes/stream nil {:url url}) url name duration] - [item-content item (rfe/href :tubo.routes/stream nil {:url url}) service-color]]) + [item-content item (rfe/href :tubo.routes/stream nil {:url url}) service-color bookmarks]]) (defn channel-item - [{:keys [url name thumbnail-url] :as item} service-color] + [{:keys [url name thumbnail-url] :as item} service-color bookmarks] [:<> [thumbnail thumbnail-url (rfe/href :tubo.routes/channel nil {:url url}) url name nil] - [item-content item (rfe/href :tubo.routes/channel nil {:url url}) service-color]]) + [item-content item (rfe/href :tubo.routes/channel nil {:url url}) service-color bookmarks]]) (defn playlist-item - [{:keys [url name thumbnail-url] :as item} service-color] + [{:keys [url name thumbnail-url] :as item} service-color bookmarks] [:<> [thumbnail thumbnail-url (rfe/href :tubo.routes/playlist nil {:url url}) url name nil] - [item-content item (rfe/href :tubo.routes/playlist nil {:url url}) service-color]]) + [item-content item (rfe/href :tubo.routes/playlist nil {:url url}) service-color bookmarks]]) (defn generic-item - [item service-color] + [item service-color bookmarks] [:div.w-full.h-80.xs:h-72.my-2 {:key key} [:div.px-5.py-2.m-2.flex.flex-col.max-w-full.min-h-full.max-h-full (case (:type item) - "stream" [stream-item item service-color] - "channel" [channel-item item service-color] - "playlist" [playlist-item item service-color])]]) + "stream" [stream-item item service-color bookmarks] + "channel" [channel-item item service-color bookmarks] + "playlist" [playlist-item item service-color bookmarks] + [stream-item item service-color bookmarks])]]) (defn related-streams [related-streams next-page-url] (let [service-color @(rf/subscribe [:service-color]) - pagination-loading? @(rf/subscribe [:show-pagination-loading])] - [:div.flex.flex-col.justify-center.items-center.flex-auto.my-2.md:my-8 + pagination-loading? @(rf/subscribe [:show-pagination-loading]) + bookmarks @(rf/subscribe [:bookmarks])] + [:div.flex.flex-col.items-center.flex-auto.my-2.md:my-8 (if (empty? related-streams) - [:div.flex.items-center + [:div.flex.items-center.flex-auto [:p "No available streams"]] [:div.grid.w-full - {:class "grid-cols-[repeat(auto-fit,_minmax(200px,_1fr))]"} + {:class "grid-cols-[repeat(auto-fill,_minmax(200px,_1fr))]"} (for [[i item] (map-indexed vector related-streams) :let [keyed-item (assoc item :key i)]] - [generic-item keyed-item service-color])]) + [generic-item keyed-item service-color bookmarks])]) (when-not (empty? next-page-url) [loading/loading-icon service-color "text-2xl" (when-not pagination-loading? "invisible")])])) diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index cebd707..8254cd4 100644 --- a/src/frontend/tubo/events.cljs +++ b/src/frontend/tubo/events.cljs @@ -15,7 +15,7 @@ (fn [{:keys [store]} _] (let [{:keys [current-theme show-comments show-related show-description media-queue media-queue-pos show-audio-player - loop-file loop-playlist]} store] + loop-file loop-playlist volume-level muted bookmarks]} store] {:db {:search-query "" :service-id 0 @@ -27,6 +27,7 @@ :media-queue (if (nil? media-queue) [] media-queue) :media-queue-pos (if (nil? media-queue-pos) 0 media-queue-pos) :volume-level (if (nil? volume-level) 100 volume-level) + :bookmarks (if (nil? bookmarks) [] bookmarks) :muted (if (nil? muted) false muted) :current-match nil :show-audio-player (if (nil? show-audio-player) false show-audio-player) @@ -291,6 +292,22 @@ (conj {:service-color service-color} %)]]) streams))})) +(rf/reg-event-fx + ::add-to-bookmarks + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ bookmark]] + (when-not (some #(= (:url %) (:url bookmark)) (:bookmarks db)) + (let [updated-db (update db :bookmarks conj bookmark)] + {:db updated-db + :store (assoc store :bookmarks (:bookmarks updated-db))})))) + +(rf/reg-event-fx + ::remove-from-bookmarks + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ bookmark]] + (let [updated-db (update db :bookmarks #(remove (fn [item] (= (:url item) (:url bookmark))) %))] + {:db updated-db + :store (assoc store :bookmarks (:bookmarks updated-db))}))) (rf/reg-event-db ::load-services diff --git a/src/frontend/tubo/routes.cljs b/src/frontend/tubo/routes.cljs index 1049812..4cf97c3 100644 --- a/src/frontend/tubo/routes.cljs +++ b/src/frontend/tubo/routes.cljs @@ -4,6 +4,7 @@ [reitit.frontend.easy :as rfe] [re-frame.core :as rf] [tubo.events :as events] + [tubo.views.bookmarks :as bookmarks] [tubo.views.channel :as channel] [tubo.views.kiosk :as kiosk] [tubo.views.playlist :as playlist] @@ -43,7 +44,9 @@ :start (fn [{{:keys [serviceId kioskId]} :query}] (rf/dispatch [::events/get-kiosk-page serviceId kioskId]))}]}] ["/settings" {:view settings/settings-page - :name ::settings}]])) + :name ::settings}] + ["/bookmarks" {:view bookmarks/bookmarks-page + :name ::bookmarks}]])) (defn on-navigate [new-match] diff --git a/src/frontend/tubo/subs.cljs b/src/frontend/tubo/subs.cljs index 622984a..017697c 100644 --- a/src/frontend/tubo/subs.cljs +++ b/src/frontend/tubo/subs.cljs @@ -156,6 +156,11 @@ (fn [[queue pos] _] (and (not-empty queue) (nth queue pos)))) +(rf/reg-sub + :bookmarks + (fn [db _] + (:bookmarks db))) + (rf/reg-sub :show-audio-player (fn [db _] diff --git a/src/frontend/tubo/views.cljs b/src/frontend/tubo/views.cljs index 8f052ca..28a3e82 100644 --- a/src/frontend/tubo/views.cljs +++ b/src/frontend/tubo/views.cljs @@ -51,7 +51,11 @@ [:span.ml-7 kiosk]]])]] [:div.relative.dark:border-neutral-800.border-gray-300.pt-4 {:class "border-t-[1px]"} - [:ul.flex.font-roboto + [:ul.flex.flex-col.font-roboto + [:li.px-5.py-2 + [:a {:href (rfe/href ::routes/bookmarks)} + [:i.fa-solid.fa-bookmark.text-neutral-600.dark:text-neutral-300] + [:span.ml-8 "Bookmarks"]]] [:li.px-5.py-2 [:a {:href (rfe/href ::routes/settings)} [:i.fa-solid.fa-cog.text-neutral-600.dark:text-neutral-300] @@ -113,7 +117,10 @@ [:i.fa-solid.fa-search]] [:a.mx-2.hidden.ml:block {:href (rfe/href ::routes/settings)} - [:i.fa-solid.fa-cog]]]] + [:i.fa-solid.fa-cog]] + [:a.mx-2.hidden.ml:block + {:href (rfe/href ::routes/bookmarks)} + [:i.fa-solid.fa-bookmark]]]] [:div.cursor-pointer.px-2.ml:hidden.text-white {:on-click #(rf/dispatch [::events/toggle-mobile-nav])} [:i.fa-solid.fa-bars]] diff --git a/src/frontend/tubo/views/bookmarks.cljs b/src/frontend/tubo/views/bookmarks.cljs new file mode 100644 index 0000000..cae383c --- /dev/null +++ b/src/frontend/tubo/views/bookmarks.cljs @@ -0,0 +1,21 @@ +(ns tubo.views.bookmarks + (:require + [re-frame.core :as rf] + [tubo.components.items :as items] + [tubo.components.navigation :as navigation] + [tubo.events :as events])) + +(defn bookmarks-page + [] + (let [service-color @(rf/subscribe [:service-color]) + bookmarks @(rf/subscribe [:bookmarks])] + [:div.flex.flex-col.items-center.px-5.py-2.flex-auto + [:div.flex.flex-col.flex-auto {:class "ml:w-4/5 xl:w-3/5"} + [navigation/back-button service-color] + [:div.flex.justify-between + [:h1.text-2xl.font-bold.py-6 "Bookmarks"] + [:button + {:on-click #(rf/dispatch [::events/enqueue-related-streams bookmarks service-color])} + [:i.fa-solid.fa-headphones] + [:span.mx-2.text-neutral-600.dark:text-neutral-300 "Background"]]] + [items/related-streams bookmarks]]])) diff --git a/src/frontend/tubo/views/stream.cljs b/src/frontend/tubo/views/stream.cljs index 1727da1..c4546b8 100644 --- a/src/frontend/tubo/views/stream.cljs +++ b/src/frontend/tubo/views/stream.cljs @@ -24,7 +24,8 @@ available-streams (apply conj audio-streams video-streams) {:keys [content id] :as stream-format} @(rf/subscribe [:stream-format]) page-loading? @(rf/subscribe [:show-page-loading]) - service-color @(rf/subscribe [:service-color])] + service-color @(rf/subscribe [:service-color]) + bookmarks @(rf/subscribe [:bookmarks])] [:div.flex.flex-col.items-center.justify-center.dark:text-white.flex-auto (if page-loading? [loading/loading-icon service-color "text-5xl"] @@ -45,8 +46,17 @@ [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300 {:on-click #(rf/dispatch [::events/switch-to-audio-player stream service-color])} [:i.fa-solid.fa-headphones] - [:span.mx-3.text-neutral-600.dark:text-neutral-300 "Background"]] - [:button.px-3.py-1.mx-2 + [:span.mx-3 "Background"]] + (if (some #(= (:url %) url) bookmarks) + [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300 + {:on-click #(rf/dispatch [::events/remove-from-bookmarks stream])} + [:i.fa-solid.fa-bookmark] + [:span.mx-3 "Bookmarked"]] + [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300 + {:on-click #(rf/dispatch [::events/add-to-bookmarks stream])} + [:i.fa-regular.fa-bookmark] + [:span.mx-3 "Bookmark"]]) + [:button.sm:px-2.py-1.text-sm.sm:text-base.text-neutral-600.dark:text-neutral-300 [:a.block.sm:inline-block {:href url} [:i.fa-solid.fa-external-link-alt]] [:span.mx-3 "Original"]] -- cgit v1.2.3