فهرست منبع

feat: youtube timestamp embed (#2810)

feat: youtube timestamp
Weihua 4 سال پیش
والد
کامیت
15cafea8c5

+ 8 - 0
src/main/frontend/commands.cljs

@@ -7,6 +7,7 @@
             [frontend.handler.draw :as draw]
             [frontend.handler.notification :as notification]
             [frontend.handler.plugin :as plugin-handler]
+            [frontend.extensions.video.youtube :as youtube]
             [frontend.search :as search]
             [frontend.state :as state]
             [frontend.util :as util]
@@ -269,6 +270,8 @@
      ["Embed Youtube Video" [[:editor/input "{{youtube }}" {:last-pattern slash
                                                             :backward-pos 2}]]]
 
+     ["Embed Youtube Timestamp" [[:youtube/insert-timestamp]]]
+
      ["Embed Vimeo Video" [[:editor/input "{{vimeo }}" {:last-pattern slash
                                                         :backward-pos 2}]]]
 
@@ -563,6 +566,11 @@
 (defmethod handle-step :editor/show-zotero [[_]]
   (state/set-editor-show-zotero! true))
 
+(defmethod handle-step :youtube/insert-timestamp [[_]]
+  (let [input-id (state/get-edit-input-id)
+        macro (youtube/gen-youtube-ts-macro)]
+    (insert! input-id macro {})))
+
 (defmethod handle-step :editor/show-date-picker [[_ type]]
   (if (and
        (contains? #{:scheduled :deadline} type)

+ 6 - 11
src/main/frontend/components/block.cljs

@@ -26,6 +26,7 @@
             [frontend.extensions.sci :as sci]
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.zotero :as zotero]
+            [frontend.extensions.video.youtube :as youtube]
             [frontend.format.block :as block]
             [frontend.format.mldoc :as mldoc]
             [frontend.components.plugins :as plugins]
@@ -1084,17 +1085,11 @@
                                     :else
                                     (nth (util/safe-re-find YouTube-regex url) 5))]
               (when-not (string/blank? youtube-id)
-                (let [width (min (- (util/get-width) 96)
-                                 560)
-                      height (int (* width (/ 315 560)))]
-                  [:iframe
-                   {:allow-full-screen "allowfullscreen"
-                    :allow
-                    "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
-                    :frame-border "0"
-                    :src (str "https://www.youtube.com/embed/" youtube-id)
-                    :height height
-                    :width width}])))))
+                (youtube/youtube-video youtube-id)))))
+
+        (= name "youtube-timestamp")
+        (when-let [seconds (first arguments)]
+          (youtube/timestamp seconds))
 
         (= name "tutorial-video")
         (tutorial-video)

+ 9 - 0
src/main/frontend/components/svg.cljs

@@ -518,6 +518,15 @@
    [:path {:d "M2 0.5H6.78272L13.5 7.69708V18C13.5 18.8284 12.8284 19.5 12 19.5H2C1.17157 19.5 0.5 18.8284 0.5 18V2C0.5 1.17157 1.17157 0.5 2 0.5Z", :fill "var(--ls-active-primary-color)"}]
    [:path {:d "M7 5.5V0L14 7.5H9C7.89543 7.5 7 6.60457 7 5.5Z", :fill "var(--ls-active-secondary-color)"}]])
 
+(def clock
+  [:svg.h-5.w-5
+   {:fill "currentColor", :viewBox "0 0 20 20"}
+   [:path
+    {:clip-rule "evenodd",
+     :d
+     "M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z",
+     :fill-rule "evenodd"}]])
+
 (def online
   (hero-icon "M8.111 16.404a5.5 5.5 0 017.778 0M12 20h.01m-7.08-7.071c3.904-3.905 10.236-3.905 14.141 0M1.394 9.393c5.857-5.857 15.355-5.857 21.213 0"))
 

+ 98 - 0
src/main/frontend/extensions/video/youtube.cljs

@@ -0,0 +1,98 @@
+(ns frontend.extensions.video.youtube
+  (:require [rum.core :as rum]
+            [cljs.core.async :refer [<! >! chan go go-loop] :as a]
+            [frontend.components.svg :as svg]
+            [frontend.state :as state]
+            [frontend.util :as util]
+            [goog.object :as gobj]
+            [clojure.string :as str]))
+
+(defn- load-yt-script []
+  (js/console.log "load yt script")
+  (let [tag              (js/document.createElement "script")
+        first-script-tag (first (js/document.getElementsByTagName "script"))
+        parent-node      (.-parentNode first-script-tag)]
+    (set! (.-src tag) "https://www.youtube.com/iframe_api")
+    (.insertBefore parent-node tag first-script-tag)))
+
+(defn load-youtube-api []
+  (let [c (chan)]
+    (if js/window.YT
+      (a/close! c)
+      (do
+        (set! js/window.onYouTubeIframeAPIReady #(a/close! c))
+        (load-yt-script)))
+    c))
+
+(defn register-player [state]
+  (let [id (first (:rum/args state))
+        player (js/window.YT.Player.
+                (rum/dom-node state)
+                (clj->js
+                 {:events
+                  {"onReady" (fn [e] (js/console.log id " ready"))}}))]
+    (state/update-state! [:youtube/players]
+                         (fn [players]
+                           (assoc players id player)))))
+
+(rum/defcs youtube-video <
+  rum/reactive
+  (rum/local nil ::player)
+  {:did-mount
+   (fn [state]
+     (go
+       (<! (load-youtube-api))
+       (register-player state))
+     state)}
+  [state id]
+  (let [width  (min (- (util/get-width) 96)
+                    560)
+        height (int (* width (/ 315 560)))]
+    [:iframe
+     {:id                (str "youtube-player-" id)
+      :allow-full-screen "allowfullscreen"
+      :allow "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+      :frame-border      "0"
+      :src               (str "https://www.youtube.com/embed/" id "?enablejsapi=1")
+      :height            height
+      :width             width}]))
+
+(defn seconds->display [seconds]
+  (let [seconds (int seconds)
+        minutes (Math/floor (/ seconds 60))
+        remaining-seconds (- seconds (* 60 minutes))
+        remaining-seconds (if (zero? remaining-seconds) "00" remaining-seconds)]
+    (str minutes ":" remaining-seconds)))
+
+(defn dom-after-video-node? [video-node target]
+  (not (zero?
+        (bit-and
+         (.compareDocumentPosition video-node target)
+         js/Node.DOCUMENT_POSITION_FOLLOWING))))
+
+(defn get-player [target]
+  (when-let [iframe (->> (js/document.getElementsByTagName "iframe")
+                         (filter
+                          (fn [node]
+                            (let [src (gobj/get node "src" "")]
+                              (str/includes? src "youtube.com"))))
+                         (filter #(dom-after-video-node? % target))
+                         last)]
+    (let [id (gobj/get iframe "id" "")
+          id (str/replace-first id #"youtube-player-" "")]
+      (get (get @state/state :youtube/players) id))))
+
+
+(rum/defc timestamp
+  [seconds]
+  [:a.svg-small.youtube-timestamp
+   {:on-click (fn [e]
+                (util/stop e)
+                (when-let [player (get-player (.-target e))]
+                  (.seekTo ^js player seconds true)))}
+   svg/clock
+   (seconds->display seconds)])
+
+(defn gen-youtube-ts-macro []
+  (when-let [player (get-player (state/get-input))]
+    (util/format "{{youtube-timestamp %s}}" (Math/floor (.getCurrentTime ^js player)))))

+ 2 - 0
src/main/frontend/state.cljs

@@ -160,6 +160,8 @@
                                                  #{})
       :date-picker/date nil
 
+      :youtube/players {}
+
       ;; command palette
       :command-palette/commands []