From 480032492a6a55911a66884286d1fce0a3828555 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Mon, 1 Sep 2025 13:21:41 +0100 Subject: [PATCH] D'oh! Added `japji.cljs` --- .gitignore | 32 ++--- resources/public/scripts/japji.cljs | 184 ++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+), 15 deletions(-) create mode 100644 resources/public/scripts/japji.cljs diff --git a/.gitignore b/.gitignore index bbabc6d..eb26c83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,21 @@ +.calva/ +/checkouts/ +*.class +/classes/ +.clj-kondo/ # ---> Clojure +.cpcache/ +japji.code-workspace +*.jar +.lein-deps-sum +.lein-failures +.lein-plugins/ +.lein-repl-history +/lib/ +.lsp/ +.nrepl-port pom.xml pom.xml.asc -*.jar -*.class -/lib/ -/classes/ -/target/ -/checkouts/ -.lein-deps-sum -.lein-repl-history -.lein-plugins/ -.lein-failures -.nrepl-port -.cpcache/ -.clj-kondo/ -.lsp/ .portal/ -japji.code-workspace +.shadow-cljs/ +/target/ diff --git a/resources/public/scripts/japji.cljs b/resources/public/scripts/japji.cljs new file mode 100644 index 0000000..009e7a3 --- /dev/null +++ b/resources/public/scripts/japji.cljs @@ -0,0 +1,184 @@ +(ns japji + (:require [reagent.core :as r] + [reagent.dom :as rdom])) + +(declare launch-popup markup-phrase data-js) + +(def recording (.getElementById js/document "japji-bindranwale")) + +(def data (js->clj data-js)) + +(.log js/console (str "Data has " (count data) " entries")) + +;; bits of Scittle stuff that need to be set up for recordStudentSound to +;; work + +(def student-recordings (atom (apply vector (repeat (count data) nil)))) + +(defn enable-play-button! [phrase-no]) + +(defn record-student-sound! + [phrase-no] + (.info js/console "Recording student sound for phrase " phrase-no) + + (try + (.then (.getUserMedia (.mediaDevices js/navigator) {:audio true}) + (fn [arg] + (let [media-recorder (js/MediaRecorder. arg) + audio-chunks (atom [])] + (.start media-recorder) + (set! (.-onerror media-recorder) + (fn [s] + (.log js/console (str "Error while recording sound: " s)))) + (.addEventListener media-recorder "dataavailable" + (fn [event] + (.info js/console "Audio recorded...") + (swap! audio-chunks conj (.-data event)))) + (set! (.-onstop media-recorder) + (fn [e] + (js/console.log "data available after MediaRecorder.stop() called.") + (if (> (count @audio-chunks) 0) + (do + ;; Store the blob in the student-recordings data structure + (swap! student-recordings assoc phrase-no + (js/Blob. (clj->js @audio-chunks))) + (enable-play-button! phrase-no)))))))) + (catch js/Error e + (.log js/console + (str "Error thrown while recording sound: " (.-message e)))) + (catch :default x + (str "Unexpected object thrown while recording " x)))) + +(defn interval-watcher + "Returns a closure over this `audio` and this `end`, + which pauses and clears down the interval of the audio when it is + called after the current time of the `audio` exceeds `end`." + [audio end] + (fn [] + (let [audio-time (.-currentTime audio)] + (.log js/console (str "interval: current time now " + audio-time "; end " end)) + (when (> audio-time end) + (.log js/console (str "current time now " + (.-currentTime audio) "; end " end "; pausing recording")) + (.pause audio) + (.clearInterval js/window (.-int audio)))))) + +(defn play-segment [audio start end] + ;; adapted from https://stackoverflow.com/questions/5932412/html5-audio-how-to-play-only-a-selected-portion-of-an-audio-file-audio-sprite + (.log js/console (str "Playing recording from " start " to " end)) + (let [clone (.cloneNode audio true)] + (set! (.-currentTime clone) start) + (.log js/console (str "current time now set to " (.-currentTime clone))) + (.play clone) + (set! (.-int clone) + (.setInterval js/window + (interval-watcher clone end) + 10)))) + +(defn markup-word [record phrase-no word-no] + [:span {:class "word, gurmukhi" + :id (str "word-" phrase-no "-" word-no) + :lang "pa" + :on-click #(play-segment recording (:start record) (:end record))} + (str " " (:text record))]) + +(defn close-popup [] + (let [popup (.getElementById js/document "popup")] + (when popup + (rdom/render "" popup)))) + +(def popup-control-style {:border "thin solid #331f16" + :padding "0 0.5em"}) + +(defn play-student! + [phrase-no] + (.play (js/Audio. (.createObjectURL js/URL (@student-recordings phrase-no))))) + +(defn animate-progress-bar! + [id duration] + (let [progress-step (int (* duration 10))] + (do + (.info js/console (str "Duration is: " duration "; step is: " progress-step)) + (loop [i 1 + e (.getElementById js/document id)] + (when (< i 99) + (js/setTimeout + #(do (.setAttribute e "value" i) + (.info js/console (str "progress-width updated to " i))) (* i progress-step)) + (recur (inc i))))))) + +(defn record-student! [phrase-no] + (let [phrase-data (nth data phrase-no) + duration (- (:lineEnd phrase-data) (:lineStart phrase-data))] + (record-student-sound! phrase-no) + (animate-progress-bar! "progress-bar" duration))) + +(defn launch-popup [phrase-no event] + (.log js/console (str "Launching popup for phrase " phrase-no)) + ;; OK, there's a problem here. The popup is not getting style from the + ;; style element, probably because it doesn't exist when the page is + ;; initially styled; so style needs to be embedded. + (let [popup (.getElementById js/document "popup") + phrase-data (nth data phrase-no) + duration (- (:lineEnd phrase-data) (:lineStart phrase-data)) + progress-width (atom 0) + progress-step (int (* duration 10)) + content [:div {:id "popup-content" + :style {:border "thin solid #331f16" + :background-color "whitesmoke" + :left (- (.-pageX event) 100) + :top (.-pageY event) + :position "absolute" + :display "block" + :z-index 10}} + [:div {:style {:background-color "#331f16" + :color "red" + :height "1.2em" + :width "100%"}} + [:span {:id "popup-closebox" :on-click #(close-popup) + :title "Close popup" + :style {:float "right"}} "x"]] + (apply vector (concat [:p {:id "popup-phrase" :class "gurmukhi"}] + (map #(markup-word %1 phrase-no %2) (:words phrase-data) (range)))) + [:table {:id "popup-controls" + :summary "Controls for audio playback and recording" + :style {:border "thin solid #331f16" + :padding "0.25em 1em" + :width "100%"}} + [:tr [:td "Tutor"] [:td {:id "popup-play-tutor" + :on-click #(play-segment recording + (:lineStart phrase-data) + (:lineEnd phrase-data)) + :style popup-control-style + :title "Play the tutor's recording"} + "►"]] + [:tr [:td "You"] + (when (@student-recordings phrase-no) + [:td {:id "popup-play-student" + :on-click #(play-student! phrase-no) + :style popup-control-style + :title "Play your own recording"} "►"]) + [:td {:id "popup-record-stop" + :on-click #(record-student! phrase-no) + :style popup-control-style + :title "Record yourself saying this phrase"} + "⏺"]] + [:tr [:td {:id "progress" + :colspan 3} + [:progress {:id "progress-bar" + :max 100 + :value 0 + :style {:width "100%"}}]]]]]] + (rdom/render content popup))) + +(defn markup-phrase [record n] + [:div {:class "phrase" :id (str "phrase-" n)} + [:span {:class "play-control" :lang "en-GB" + :on-click #(play-segment recording (:lineStart record) (:lineEnd record))} "•"] + (map #(markup-word %1 n %2) (:words record) (range)) + [:span {:class "popup-launcher" + :on-click #(launch-popup n %)} "..."]]) + +(let [content (map markup-phrase data (range))] + (rdom/render content (.getElementById js/document "content"))) \ No newline at end of file