From 5cfde61805292f4b89c1dc244f8fbdf15c5c687d Mon Sep 17 00:00:00 2001 From: Miguel Ángel Moreno Date: Sun, 18 Feb 2024 17:29:24 +0100 Subject: feat(frontend): add support for bookmark lists (playlists) --- src/backend/tubo/routes.clj | 1 + src/frontend/tubo/components/modals/bookmarks.cljs | 44 ++++++++++++ src/frontend/tubo/components/navigation.cljs | 2 +- src/frontend/tubo/events.cljs | 81 ++++++++++++++++++++-- src/frontend/tubo/routes.cljs | 7 +- src/frontend/tubo/views/bookmarks.cljs | 54 +++++++++++++-- 6 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 src/frontend/tubo/components/modals/bookmarks.cljs (limited to 'src') diff --git a/src/backend/tubo/routes.clj b/src/backend/tubo/routes.clj index 2bf56f8..95fb294 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] + ["/bookmark" handler/index] ["/bookmarks" handler/index] ["/api" ["/services" diff --git a/src/frontend/tubo/components/modals/bookmarks.cljs b/src/frontend/tubo/components/modals/bookmarks.cljs new file mode 100644 index 0000000..c240d85 --- /dev/null +++ b/src/frontend/tubo/components/modals/bookmarks.cljs @@ -0,0 +1,44 @@ +(ns tubo.components.modals.bookmarks + (:require + [reagent.core :as r] + [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] + [tubo.components.modal :as modal] + [tubo.components.layout :as layout])) + +(defn bookmark-list-item + [{:keys [items id name] :as bookmark} item] + [:div.flex.w-full.h-24.rounded.cursor-pointer.hover:bg-gray-100.dark:hover:bg-stone-800.px-2 + {:on-click #(rf/dispatch [:tubo.events/add-to-bookmark-list bookmark item])} + [:div.w-24 + [layout/thumbnail (-> items first :thumbnail-url) nil name nil + :classes "h-24"]] + [:div.flex.flex-col.py-4.px-4 + [:h1.line-clamp-1.font-bold name] + [:span.text-sm (str (count items) " streams")]]]) + +(defn add-bookmark-modal + [item] + (let [!bookmark-name (r/atom "")] + (fn [] + [modal/modal-content "Create New Playlist?" + [layout/text-input "Title" :text-input @!bookmark-name + #(reset! !bookmark-name (.. % -target -value)) "Playlist name"] + [layout/secondary-button "Back" + #(rf/dispatch [:tubo.events/back-to-bookmark-list-modal item])] + [layout/primary-button "Create Playlist" + #(rf/dispatch [:tubo.events/add-bookmark-list-and-back {:name @!bookmark-name} item]) + "fa-solid fa-plus"]]))) + +(defn add-to-bookmark-list-modal + [item] + (let [bookmarks @(rf/subscribe [:bookmarks])] + [modal/modal-content "Add to Playlist" + [:div.flex-auto + [:div.flex.justify-center.items-center.pb-4 + [layout/primary-button "Create New Playlist" + #(rf/dispatch [:tubo.events/open-modal [add-bookmark-modal item]]) + "fa-solid fa-plus"]] + [:div.flex.flex-col.gap-y-2.pr-2 + (for [[i bookmark] (map-indexed vector bookmarks)] + ^{:key i} [bookmark-list-item bookmark item])]]])) diff --git a/src/frontend/tubo/components/navigation.cljs b/src/frontend/tubo/components/navigation.cljs index eeeb665..3f5b508 100644 --- a/src/frontend/tubo/components/navigation.cljs +++ b/src/frontend/tubo/components/navigation.cljs @@ -130,7 +130,7 @@ [:div.relative.dark:border-neutral-800.border-gray-300.pt-4 {:class "border-t-[1px]"} [:ul.flex.flex-col.font-roboto - [mobile-nav-item (rfe/href ::routes/playlists) "fa-solid fa-bookmark" "Bookmarks"] + [mobile-nav-item (rfe/href ::routes/bookmarks) "fa-solid fa-bookmark" "Bookmarks"] [mobile-nav-item (rfe/href ::routes/settings) "fa-solid fa-cog" "Settings"] [mobile-nav-item "https://github.com/migalmoreno/tubo" "fa-brands fa-github" "Source" :new-tab? true]]]]]) diff --git a/src/frontend/tubo/events.cljs b/src/frontend/tubo/events.cljs index e155881..827dec5 100644 --- a/src/frontend/tubo/events.cljs +++ b/src/frontend/tubo/events.cljs @@ -3,10 +3,12 @@ [akiroz.re-frame.storage :refer [reg-co-fx!]] [day8.re-frame.http-fx] [goog.object :as gobj] + [nano-id.core :refer [nano-id]] [re-frame.core :as rf] [reitit.frontend.easy :as rfe] [reitit.frontend.controllers :as rfc] [tubo.api :as api] + [tubo.components.modals.bookmarks :as bookmarks] [vimsical.re-frame.cofx.inject :as inject])) (reg-co-fx! :tubo {:fx :store :cofx :store}) @@ -30,7 +32,11 @@ :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) + :bookmarks (if (nil? bookmarks) + [{:id (nano-id) + :name "Liked Streams" + :items []}] + bookmarks) :muted (if (nil? muted) false muted) :current-match nil :show-audio-player (if (nil? show-audio-player) false show-audio-player) @@ -415,18 +421,77 @@ {:fx [[:dispatch [::modal {:show? true :child child}]]] ::body-overflow! true})) +(rf/reg-event-fx + ::add-bookmark-list-modal + (fn [_ [_ child]] + {:fx [[:dispatch [::open-modal child]]]})) + +(rf/reg-event-fx + ::add-bookmark-list + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ bookmark]] + (let [updated-db (update db :bookmarks conj (assoc bookmark :id (nano-id)))] + {:db updated-db + :store (assoc store :bookmarks (:bookmarks updated-db)) + :fx [[:dispatch [::close-modal]]]}))) + +(rf/reg-event-fx + ::back-to-bookmark-list-modal + (fn [_ [_ item]] + {:fx [[:dispatch [::open-modal [bookmarks/add-to-bookmark-list-modal item]]]]})) + +(rf/reg-event-fx + ::add-bookmark-list-and-back + (fn [_ [_ bookmark item]] + {:fx [[:dispatch [::add-bookmark-list bookmark]] + [:dispatch [::back-to-bookmark-list-modal item]]]})) + +(rf/reg-event-fx + ::remove-bookmark-list + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ id]] + (let [updated-db (update db :bookmarks #(into [] (remove (fn [bookmark] (= (:id bookmark) id)) %)))] + {:db updated-db + :store (assoc store :bookmarks (:bookmarks updated-db))}))) + +(rf/reg-event-fx + ::add-to-likes [(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)] + (when-not (some #(= (:url %) (:url bookmark)) (-> db :bookmarks first :items)) + (let [updated-db (update-in db [:bookmarks 0 :items] #(into [] (conj (into [] %1) %2)) + (assoc bookmark :bookmark-id (-> db :bookmarks first :id)))] {:db updated-db :store (assoc store :bookmarks (:bookmarks updated-db))})))) (rf/reg-event-fx - ::remove-from-bookmarks + ::remove-from-likes [(rf/inject-cofx :store)] (fn [{:keys [db store]} [_ bookmark]] - (let [updated-db (update db :bookmarks #(remove (fn [item] (= (:url item) (:url bookmark))) %))] + (let [updated-db (update-in db [:bookmarks 0 :items] #(remove (fn [item] (= (:url item) (:url bookmark))) %))] + {:db updated-db + :store (assoc store :bookmarks (:bookmarks updated-db))}))) + +(rf/reg-event-fx + ::add-to-bookmark-list + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ bookmark item]] + (let [bookmark-list (first (filter #(= (:id %) (:id bookmark)) (:bookmarks db))) + pos (.indexOf (:bookmarks db) bookmark-list) + updated-db (if (some #(= (:url %) (:url item)) (:items bookmark-list)) + db + (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 [::close-modal]]]}))) + +(rf/reg-event-fx + ::remove-from-bookmark-list + [(rf/inject-cofx :store)] + (fn [{:keys [db store]} [_ bookmark]] + (let [bookmark-list (.indexOf (:bookmarks db) (first (filter #(= (:id %) (:bookmark-id bookmark)) (:bookmarks db)))) + updated-db (update-in db [:bookmarks bookmark-list :items] #(remove (fn [item] (= (:url item) (:url bookmark))) %))] {:db updated-db :store (assoc store :bookmarks (:bookmarks updated-db))}))) @@ -755,3 +820,9 @@ ::get-bookmarks-page (fn [_] {::document-title! "Bookmarks"})) + +(rf/reg-event-fx + ::get-bookmark-page + (fn [{:keys [db]} [_ playlist-id]] + (let [playlist (first (filter #(= (:id %) playlist-id) (:bookmarks db)))] + {::document-title! (:name playlist)}))) diff --git a/src/frontend/tubo/routes.cljs b/src/frontend/tubo/routes.cljs index 6bfb5dc..337a6d6 100644 --- a/src/frontend/tubo/routes.cljs +++ b/src/frontend/tubo/routes.cljs @@ -46,8 +46,13 @@ ["/settings" {:view settings/settings-page :name ::settings :controllers [{:start #(rf/dispatch [::events/get-settings-page])}]}] + ["/bookmark" {:view bookmarks/bookmark-page + :name ::bookmark + :controllers [{:parameters {:query [:id]} + :start (fn [{{:keys [id]} :query}] + (rf/dispatch [::events/get-bookmark-page id]))}]}] ["/bookmarks" {:view bookmarks/bookmarks-page - :name ::playlists + :name ::bookmarks :controllers [{:start #(rf/dispatch [::events/get-bookmarks-page])}]}]])) (defn on-navigate diff --git a/src/frontend/tubo/views/bookmarks.cljs b/src/frontend/tubo/views/bookmarks.cljs index 3f47c02..78959c4 100644 --- a/src/frontend/tubo/views/bookmarks.cljs +++ b/src/frontend/tubo/views/bookmarks.cljs @@ -1,16 +1,56 @@ (ns tubo.views.bookmarks (:require + [reagent.core :as r] [re-frame.core :as rf] + [reitit.frontend.easy :as rfe] [tubo.components.items :as items] [tubo.components.layout :as layout] + [tubo.components.modal :as modal] [tubo.events :as events])) +(defn add-bookmark-modal + [] + (let [!bookmark-name (r/atom "")] + (fn [] + [modal/modal-content "Create New Playlist?" + [layout/text-input "Title" :text-input @!bookmark-name + #(reset! !bookmark-name (.. % -target -value)) "Playlist name"] + [layout/secondary-button "Cancel" + #(rf/dispatch [::events/close-modal])] + [layout/primary-button "Create Playlist" + #(rf/dispatch [::events/add-bookmark-list {:name @!bookmark-name}])]]))) + (defn bookmarks-page [] - (let [service-color @(rf/subscribe [:service-color]) - bookmarks @(rf/subscribe [:bookmarks])] - [layout/content-container - [layout/content-header "Bookmarks" - [layout/primary-button "Enqueue" - #(rf/dispatch [::events/enqueue-related-streams bookmarks service-color]) "fa-solid fa-headphones"]] - [items/related-streams bookmarks]])) + (let [!menu-active? (r/atom nil)] + (let [service-color @(rf/subscribe [:service-color]) + bookmarks @(rf/subscribe [:bookmarks]) + items (map #(assoc % + :url (rfe/href :tubo.routes/bookmark nil {:id (:id %)}) + :thumbnail-url (-> % :items first :thumbnail-url) + :stream-count (count (:items %)) + :bookmark-id (:id %)) bookmarks)] + [layout/content-container + [layout/content-header "Bookmarks" + [layout/more-menu !menu-active? + [{:label "Create playlist" + :icon [:i.fa-solid.fa-plus] + :on-click #(rf/dispatch [::events/open-modal [add-bookmark-modal]])}]]] + [items/related-streams items]]))) + +(defn bookmark-page + [] + (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))] + [layout/content-container + [layout/content-header name + (when-not (empty? items) + [layout/more-menu !menu-active? + [{:label "Add to queue" + :icon [:i.fa-solid.fa-headphones] + :on-click #(rf/dispatch [::events/enqueue-related-streams items])}]])] + [items/related-streams (map #(assoc % :type "stream" :bookmark-id id) items)]])))) -- cgit v1.2.3