Browse Source

feat(frontend): add support for bookmark lists (playlists)

Miguel Ángel Moreno 1 year ago
parent
commit
5cfde61805

+ 1 - 0
.github/README.md

@@ -0,0 +1 @@
+/home/vega/src/projects/tubo/README.md

+ 4 - 4
README

@@ -9,10 +9,10 @@ To retrieve the data, it wraps the excellent [[https://github.com/TeamNewPipe/Ne
 - [X] No ads
 - [X] Audio player
 - [X] Media queue
-- [ ] Playlists
-- [X] Local settings
-- [ ] Local subscriptions
-- [ ] User management
+- [X] Playlists
+- [X] Settings
+- [ ] Subscriptions
+- [ ] User login
 
 ** Instances
 | URL                                     | Country |

+ 5 - 5
README.md

@@ -12,10 +12,10 @@ To retrieve the data, it wraps the excellent [NewPipe Extractor](https://github.
 -   [X] No ads
 -   [X] Audio player
 -   [X] Media queue
--   [ ] Playlists
--   [X] Local settings
--   [ ] Local subscriptions
--   [ ] User management
+-   [X] Playlists
+-   [X] Settings
+-   [ ] Subscriptions
+-   [ ] User login
 
 
 ## Instances
@@ -43,7 +43,7 @@ To retrieve the data, it wraps the excellent [NewPipe Extractor](https://github.
 </tbody>
 </table>
 
-If you consider self-hosting Tubo let me know about your instance via the [contribution methods](#org7e7911e). See [installation](#org0916606) for ways to set up Tubo in your server.  
+If you consider self-hosting Tubo let me know about your instance via the [contribution methods](#org2c797ae). See [installation](#orgc5e387d) for ways to set up Tubo in your server.  
 
 
 ## Installation

+ 2 - 1
deps.edn

@@ -25,6 +25,7 @@
                 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"}}
+                re-frame-utils/re-frame-utils {:mvn/version "0.1.0"}
+                nano-id/nano-id {:mvn/version "1.1.0"}}
    :main-opts ["-m" "shadow.cljs.devtools.cli"]}
   :run {:main-opts ["-m" "tubo.core"]}}}

+ 1 - 0
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"

+ 44 - 0
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])]]]))

+ 1 - 1
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]]]]])

+ 76 - 5
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)})))

+ 6 - 1
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

+ 47 - 7
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)]]))))