diff --git a/.github/workflows/prod.yml b/.github/workflows/prod.yml index 5c824379..e7630bbb 100644 --- a/.github/workflows/prod.yml +++ b/.github/workflows/prod.yml @@ -6,9 +6,8 @@ on: jobs: test: - - name: Build and Run Mix Test - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 # Use a specific version for stability + name: Build OTP ${{matrix.otp}} / Elixir ${{matrix.elixir}} && Run Mix Test services: postgres: image: postgres:latest @@ -18,48 +17,55 @@ jobs: POSTGRES_USER: postgres ports: - 5432:5432 - # Set health checks to wait until postgres has started options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + strategy: + matrix: + otp: ['26.2'] + elixir: ['1.16.3'] steps: - - uses: actions/checkout@v2 - - name: Install dependecies for build - run: sudo apt-get install -y libncurses-dev libtinfo5 - - name: Set up Elixir - uses: erlef/setup-elixir@885971a72ed1f9240973bd92ab57af8c1aa68f24 - with: - elixir-version: '1.15.7' # Define the elixir version [required] - otp-version: '25' # Define the OTP version [required] - - name: Restore dependencies cache - uses: actions/cache@v2 - with: - path: deps - key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} - restore-keys: ${{ runner.os }}-mix- - - name: Install dependencies - working-directory: ./ - run: mix deps.get - - name: Run tests - env: - # use localhost for the host here because we are running the job on the VM. - # If we were running the job on in a container this would be postgres - POSTGRES_HOST: localhost - POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} - working-directory: ./ - run: mix test + - name: Checkout code + uses: actions/checkout@v4 # Pin to a specific version for stability + + - name: Install dependencies for build + run: sudo apt-get update && sudo apt-get install -y libncurses-dev libtinfo5 + - name: Setup Elixir and OTP + uses: erlef/setup-beam@v1 + with: + otp-version: ${{matrix.otp}} + elixir-version: ${{matrix.elixir}} + + - name: Restore dependencies cache + uses: actions/cache@v4 + with: + path: deps + key: ${{ runner.os }}-mix-${{ hashFiles('**/mix.lock') }} + restore-keys: ${{ runner.os }}-mix- + + - name: Install dependencies + run: mix deps.get + + - name: Run tests + env: + POSTGRES_HOST: localhost + POSTGRES_PORT: ${{ job.services.postgres.ports[5432] }} + run: mix test deploy: needs: test name: Build & Deploy to Fly - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 # Use a specific version for stability steps: - - uses: actions/checkout@v2 - - uses: superfly/flyctl-actions@master + - name: Checkout code + uses: actions/checkout@v4 # Pin to a specific version for stability + + - name: Deploy to Fly.io + uses: superfly/flyctl-actions@master env: FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} FLY_APP: vyasa diff --git a/assets/css/app.css b/assets/css/app.css index 8f4956a6..bc948eb1 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -65,3 +65,54 @@ .emphasized-verse { @apply bg-brandAccentLight border-b-0 border-l-8 border-black p-4 pl-8 rounded-sm; } + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.eot"); + src: url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.woff") format("woff"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.ttf") format("truetype"), url("et-book/et-book-roman-line-figures/et-book-roman-line-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.eot"); + src: url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.woff") format("woff"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.ttf") format("truetype"), url("et-book/et-book-display-italic-old-style-figures/et-book-display-italic-old-style-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: italic; + font-display: swap; +} + +@font-face { + font-family: "et-book"; + src: url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.eot"); + src: url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.woff") format("woff"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.ttf") format("truetype"), url("et-book/et-book-bold-line-figures/et-book-bold-line-figures.svg#etbookromanosf") format("svg"); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: "et-book-roman-old-style"; + src: url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.eot"); + src: url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.eot?#iefix") format("embedded-opentype"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.woff") format("woff"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.ttf") format("truetype"), url("et-book/et-book-roman-old-style-figures/et-book-roman-old-style-figures.svg#etbookromanosf") format("svg"); + font-weight: normal; + font-style: normal; + font-display: swap; +} +/* This file is for your main application CSS */ + +.hoverune, +.marginote { + float: right; + clear: right; + margin-right: -60%; + width: 50%; + margin-top: 0.3rem; + margin-bottom: 0; + font-size: 1.1rem; + line-height: 1.3; + vertical-align: baseline; + position: relative; +} diff --git a/assets/js/app.js b/assets/js/app.js index 0ef6eb47..dc1bd3b0 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -36,11 +36,20 @@ let liveSocket = new LiveSocket("/live", Socket, { hooks: Hooks, }); + // Show progress bar on live navigation and form submits topbar.config({ barColors: { 0: "#29d" }, shadowColor: "rgba(0, 0, 0, .3)" }); window.addEventListener("phx:page-loading-start", (_info) => topbar.show(300)); window.addEventListener("phx:page-loading-stop", (_info) => topbar.hide()); +// Stream our server logs directly to our browser’s console +window.addEventListener("phx:live_reload:attached", ({detail: reloader}) => { + // enable server log streaming to client. + // disable with reloader.disableServerLogs() + reloader.enableServerLogs() + +}) + // connect if there are any LiveViews on the page liveSocket.connect(); diff --git a/assets/js/hooks/apply_modal.js b/assets/js/hooks/apply_modal.js new file mode 100644 index 00000000..ada9399b --- /dev/null +++ b/assets/js/hooks/apply_modal.js @@ -0,0 +1,15 @@ +const ApplyModal = () => { + const modal = document.querySelector('[data-selector="vyasa_modal_message"]') + + if (!modal) return + + const encodedReturnTo = encodeURIComponent(document.location.pathname) + + modal.querySelectorAll('a[data-phx-link="redirect"]').forEach(val => { + const url = new URL(val.href, document.location.origin) + url.searchParams.set('return_to', encodedReturnTo) + val.href = `${url.href}` + }) +} + +export default ApplyModal diff --git a/assets/js/hooks/audio_player.js b/assets/js/hooks/audio_player.js index 3ee4fc05..614cf56a 100644 --- a/assets/js/hooks/audio_player.js +++ b/assets/js/hooks/audio_player.js @@ -12,57 +12,73 @@ * general player-agnostic fashion. "Playback" and actual playback (i.e. audio or video playback) is decoupled, allowing * us the ability to reconcile bufferring states and other edge cases, mediated by the Media Bridge. * */ -let rand = (min, max) => Math.floor(Math.random() * (max - min) + min) -let isVisible = (el) => !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length > 0) +import { + seekTimeBridge, + playPauseBridge, + heartbeatBridge, + playbackMetaBridge, +} from "./mediaEventBridges"; let execJS = (selector, attr) => { - document.querySelectorAll(selector).forEach(el => liveSocket.execJS(el, el.getAttribute(attr))) -} + document + .querySelectorAll(selector) + .forEach((el) => liveSocket.execJS(el, el.getAttribute(attr))); +}; -import {seekTimeBridge, playPauseBridge, heartbeatBridge} from "./media_bridge.js" -import {formatDisplayTime, nowMs} from "../utils/time_utils.js" +import { formatDisplayTime, nowMs } from "../utils/time_utils.js"; AudioPlayer = { mounted() { this.isFollowMode = false; - this.playbackBeganAt = null - this.player = this.el.querySelector("audio") - - document.addEventListener("click", () => this.enableAudio()) - - this.player.addEventListener("loadedmetadata", e => this.handleMetadataLoad(e)) - - this.handleEvent("initSession", (sess) => this.initSession(sess)) + this.playbackBeganAt = null; + this.player = this.el.querySelector("audio"); + this.player.addEventListener("canplaythrough", (e) => + this.handlePlayableState(e), + ); /// Audio playback events: - this.handleEvent("stop", () => this.stop()) + this.handleEvent("stop", () => this.stop()); /// maps eventName to its deregisterer: this.eventBridgeDeregisterers = { - seekTime: seekTimeBridge.sub(payload => this.handleExternalSeekTime(payload)), - playPause: playPauseBridge.sub(payload => this.handleMediaPlayPause(payload)), - heartbeat: heartbeatBridge.sub(payload => this.echoHeartbeat(payload)), - } + seekTime: seekTimeBridge.sub((payload) => + this.handleExternalSeekTime(payload), + ), + playPause: playPauseBridge.sub((payload) => + this.handleMediaPlayPause(payload), + ), + heartbeat: heartbeatBridge.sub((payload) => this.echoHeartbeat(payload)), + playbackMeta: playbackMetaBridge.sub((playback) => + this.handlePlaybackMetaUpdate(playback), + ), + }; }, - /// Handlers: + /** + * Loads the audio onto the audio player and inits the MediaSession as soon as playback information is received. + * This allows the metadata and audio load to happen independently of users' + * actions that effect playback (e.g. play/pause) -- bufferring gets init a lot earlier + * as a result. + * */ + handlePlaybackMetaUpdate(playback) { + const { meta: playbackMeta } = playback; + const { file_path: filePath } = playbackMeta; + this.loadAudio(filePath); + this.initMediaSession(playback); + }, + /// Handlers for events received via the events bridge: handleMediaPlayPause(payload) { - console.log("[playPauseBridge::audio_player::playpause] payload:", payload) - const { - cmd, - playback, - } = payload + const { cmd, playback } = payload; if (cmd === "play") { - this.playMedia(playback) + this.playMedia(playback); } if (cmd === "pause") { - this.pause() + this.pause(); } }, handleExternalSeekTime(payload) { - console.log("[audio_player::seekTimeBridgeSub::seekTimeHandler] payload:", payload); - const {seekToMs: timeMs} = payload; - this.seekToMs(timeMs) + const { seekToMs: timeMs } = payload; + this.seekToMs(timeMs); }, /** * Returns information about the current playback. @@ -71,109 +87,97 @@ AudioPlayer = { * is documented to be in s. * */ readCurrentPlaybackInfo() { - const currentTimeMs = this.player.currentTime * 1000 - const durationMs = this.player.duration * 1000 + const currentTimeMs = this.player.currentTime * 1000; + const durationMs = this.player.duration * 1000; return { - isPlaying: !this.player.paused, - currentTimeMs, - durationMs, - } + isPlaying: !this.player.paused, + currentTimeMs, + durationMs, + }; }, echoHeartbeat(heartbeatPayload) { const shouldIgnoreSignal = heartbeatPayload.originator === "AudioPlayer"; - if(shouldIgnoreSignal) { - return + if (shouldIgnoreSignal) { + return; } - - console.log("[heartbeatBridge::audio_player] payload:", heartbeatPayload) const echoPayload = { originator: "AudioPlayer", - currentPlaybackInfo: this.readCurrentPlaybackInfo() - } - heartbeatBridge.pub(echoPayload) - }, - initSession(sess) { - localStorage.setItem("session", JSON.stringify(sess)) + currentPlaybackInfo: this.readCurrentPlaybackInfo(), + }; + heartbeatBridge.pub(echoPayload); }, - handleMetadataLoad(e) { - console.log("Loaded metadata!", { - duration: this.player.duration, - event: e, - }) + handlePlayableState(e) { + // TODO: consider if a handler is needed for the "canplaythrough" event + console.log( + "TRACE HandlePlayableState -- the audio can be played through completely now.", + e, + ); }, handlePlayPause() { - console.log("{play_pause event triggerred} player:", this.player) - if(this.player.paused){ - this.play() - } - }, - /** - * This "init" behaviour has been mimicked from live_beats. - * It is likely there to enable the audio player bufferring. - * */ - enableAudio() { - if(this.player.src){ - document.removeEventListener("click", this.enableAudio) - const hasNothingToPlay = this.player.readyState === 0; - if(hasNothingToPlay){ - this.player.play().catch(error => null) - this.player.pause() - } + if (this.player.paused) { + this.play(); } }, playMedia(playback) { - console.log("PlayMedia", playback) - const {meta: playbackMeta, "playing?": isPlaying, elapsed} = playback; - const { title, duration, file_path: filePath, artists } = playbackMeta; - const artist = artists ? artists[0] : "myArtist" // FIXME: this should be ready once seeding has been update to properly add in artist names - - const beginTime = nowMs() - elapsed - this.playbackBeganAt = beginTime - let currentSrc = this.player.src.split("?")[0] - - const isLoadedAndPaused = currentSrc === filePath && !isPlaying && this.player.paused; - if(isLoadedAndPaused){ - this.play({sync: true}) - } else if(currentSrc !== filePath) { - currentSrc = filePath - this.player.src = currentSrc - this.play({sync: true}) - } + const { meta: playbackMeta, "playing?": isPlaying, elapsed } = playback; + const { file_path: filePath } = playbackMeta; + this.updateMediaSession(playback); - // TODO: supply necessary info for media sessions api here... - const isMediaSessionApiSupported = "mediaSession" in navigator; - if(isMediaSessionApiSupported){ - navigator.mediaSession.metadata = new MediaMetadata({artist, title}) + const beginTime = nowMs() - elapsed; + this.playbackBeganAt = beginTime; + let currentSrc = this.getCurrentSrc(); + const isLoadedAndPaused = + currentSrc === filePath && !isPlaying && this.player.paused; + if (isLoadedAndPaused) { + this.play({ sync: true }); + } else if (currentSrc !== filePath) { + this.loadAudio(filePath); + this.play({ sync: true }); } }, - play(opts = {}){ - console.log("Triggered playback, check params", { - player: this.player, - opts, - }) + loadAudio(src) { + const isSrcAlreadyLoaded = src === this.getCurrentSrc(); + if (isSrcAlreadyLoaded) { + return; + } + this.player.src = src; + }, + getCurrentSrc() { + if (!this?.player?.src) { + return null; + } + // since the html5 player's src value is typically a url string, it will have url encodings e.g. ContentType. + // Therefore, we strip away these urlencodes: + const src = this.player.src.split("?")[0]; - let {sync} = opts + return src; + }, + play(opts = {}) { + let { sync } = opts; - this.player.play().then(() => { - if(sync) { - const currentTimeMs = nowMs() - this.playbackBeganAt; + this.player.play().then( + () => { + if (sync) { + const currentTimeMs = nowMs() - this.playbackBeganAt; - this.player.currentTime = currentTimeMs / 1000; - const formattedCurrentTime = formatDisplayTime(currentTimeMs); - } - }, error => { - if(error.name === "NotAllowedError"){ - execJS("#enable-audio", "data-js-show") - } - }) + this.player.currentTime = currentTimeMs / 1000; + const formattedCurrentTime = formatDisplayTime(currentTimeMs); + } + }, + (error) => { + if (error.name === "NotAllowedError") { + execJS("#enable-audio", "data-js-show"); + } + }, + ); }, - pause(){ - this.player.pause() + pause() { + this.player.pause(); }, - stop(){ - this.player.pause() - this.player.currentTime = 0 + stop() { + this.player.pause(); + this.player.currentTime = 0; }, /** * The exposed api for html5 audio player is such that currentTime is a number value @@ -183,15 +187,71 @@ AudioPlayer = { * This preserves as much precision as possible. * */ seekToMs(timeMs) { - const beginTime = nowMs() - timeMs + const beginTime = nowMs() - timeMs; this.playbackBeganAt = beginTime; this.player.currentTime = timeMs / 1000; if (!this.player.paused) { - this.player.play() // force a play event if is not paused + this.player.play(); // force a play event if is not paused + } + }, + /** + * At the point of init, we register some action handlers and update the media session's metadata + * */ + initMediaSession(playback) { + // TODO: register action handlers + this.registerActionHandlers(playback); + this.updateMediaSession(playback); + }, + registerActionHandlers(playback) { + const isSupported = "mediaSession" in navigator; + if (!isSupported) { + return; + } + + const session = navigator.mediaSession; + const playPauseEvents = ["play", "pause"]; + playPauseEvents.forEach((e) => + session.setActionHandler(e, (e) => this.dispatchPlayPauseToServer(e)), + ); + }, + dispatchPlayPauseToServer(_e) { + this.pushEvent("play_pause"); + }, + updateMediaSession(playback) { + const isSupported = "mediaSession" in navigator; + if (!isSupported) { + return; } + const payload = this.createMediaMetadataPayload(playback); + navigator.mediaSession.metadata = new MediaMetadata(payload); }, -} + createMediaMetadataPayload(playback) { + if (!playback) { + return {}; + } + const { meta } = playback; + const sessionMetadata = navigator?.mediaSession?.metadata; + const oldMetadata = sessionMetadata + ? { + title: sessionMetadata.title, + artist: sessionMetadata.artist, + album: sessionMetadata.album, + artwork: sessionMetadata.artwork, + } + : {}; + const artist = meta?.artists + ? meta.artists.join(", ") + : (sessionMetadata.artist ?? "Unknown artist"); + const metadata = { + ...oldMetadata, + ...meta, + artist, + }; + + return metadata; + }, +}; export default AudioPlayer; diff --git a/assets/js/hooks/floater.js b/assets/js/hooks/floater.js index 33bb2e68..37f5efd9 100644 --- a/assets/js/hooks/floater.js +++ b/assets/js/hooks/floater.js @@ -2,6 +2,12 @@ * Ideally generic hook for floating logic. */ import {isMobileDevice} from "../utils/uncategorised_utils.js"; +import { + computePosition, + autoPlacement, + shift, + offset, +} from "floating-ui.dom.umd.min"; Floater = { mounted() { @@ -48,13 +54,6 @@ Floater = { return } - const { - computePosition, - autoPlacement, - shift, - offset, - } = window.FloatingUIDOM; - computePosition(reference, floater, { placement: 'right', // NOTE: order of middleware matters. diff --git a/assets/js/hooks/hoverune.js b/assets/js/hooks/hoverune.js new file mode 100644 index 00000000..47905347 --- /dev/null +++ b/assets/js/hooks/hoverune.js @@ -0,0 +1,92 @@ +import { computePosition, offset, inline, autoUpdate } from "floating-ui.dom.umd.min"; + +const findParent = (el, attr, stopper) => { + // need identifier for marginoted content + if (el && el.localName == "body") return null + if (el && el.getAttribute(attr) == stopper) return el + if (!el) return null + return findParent(el.parentElement, attr, stopper) +} + +function floatHoveRune({clientX, clientY}) { + const selection = window.getSelection() + var getSelectRect = selection.getRangeAt(0).getBoundingClientRect() + const virtualEl = { + getBoundingClientRect() { + return getSelectRect + }, + contextElement: document.querySelector('#verses'), + }; + const hoverune = document.getElementById("hoverune"); + + computePosition(virtualEl, hoverune, {placement: 'top-end', middleware: [inline(getSelectRect.x, getSelectRect.y), offset(5)]}).then(({x, y}) => { + // Position the floating element relative to the click + hoverune.classList.remove("hidden") + Object.assign(hoverune.style, { + left: `${getSelectRect.x}px`, + top: `${y}px`, + }) + }); + + // computePosition(virtualEl, hoverune, {placement: 'top-end', middleware: [inline(getSelectRect.x, getSelectRect.y), offset(5)]}).then(({x, y}) => { + // hoverune.classList.remove("hidden") + // Object.assign(hoverune.style, { + // left: `${getSelectRect.x}px`, + // top: `${y}px`, + // }); + // }) +} + +const findHook = el => findParent(el, "phx-hook", "HoveRune") +const findMarginote = el => findParent(el, "phx-hook", "MargiNote") +const findNode = el => el && el.getAttribute('node') +const forgeBinding = (el, attrs) => attrs.reduce((acc, attr) => { + acc[attr] = el.getAttribute(attr) + return acc +}, {}) +// const marginoteParent = el => findParent(el, "data-marginote", "parent") + +export default HoveRune = { + mounted() { + const t = this.el + const targetEvents = ['pointerdown', 'pointerup'] + targetEvents.forEach(e => window.addEventListener(e, ({ target }) => { + var selection = window.getSelection() + if (!selection || selection.rangeCount <= 0) { + return + } + + var getSelectRect = selection.getRangeAt(0).getBoundingClientRect(); + const getSelectText = selection.toString() + //const validElem = findHook(target) + // const isMarginote = findMarginote(target) + const isNode = findNode(target) + + if (isNode) { + binding = forgeBinding(target, ["node", "node_id", "field", "verse_id"]) + binding["selection"] = getSelectText + + this.pushEvent("bindHoveRune", {"binding": binding}) + + console.log(binding) + + + computePosition(target, hoverune, {placement: 'top-end', middleware: [inline(getSelectRect.x, getSelectRect.y), offset(5)]}).then(({x, y}) => { + hoverune.classList.remove("hidden") + Object.assign(hoverune.style, { + left: `${getSelectRect.x}px`, + top: `${y}px`, + }); + }) + + } + else { + hoverune.classList.add("hidden") + } + })) + + }, + + updated() { + } +} diff --git a/assets/js/hooks/index.js b/assets/js/hooks/index.js index 25afeba0..2e312973 100644 --- a/assets/js/hooks/index.js +++ b/assets/js/hooks/index.js @@ -7,8 +7,11 @@ import MiniPlayer from "./mini_player.js"; import MediaBridge from "./media_bridge.js"; import AudioPlayer from "./audio_player.js"; import ProgressBar from "./progress_bar.js"; -import Floater from "./floater.js" - +import Floater from "./floater.js"; +import ApplyModal from "./apply_modal.js"; +import MargiNote from "./marginote.js"; +import HoveRune from "./hoverune.js"; +import Scrolling from "./scrolling.js"; let Hooks = { ShareQuoteButton, @@ -19,6 +22,10 @@ let Hooks = { AudioPlayer, ProgressBar, Floater, + ApplyModal, + MargiNote, + HoveRune, + Scrolling, }; export default Hooks; diff --git a/assets/js/hooks/marginote.js b/assets/js/hooks/marginote.js new file mode 100644 index 00000000..6e0e619c --- /dev/null +++ b/assets/js/hooks/marginote.js @@ -0,0 +1,35 @@ +import { computePosition, offset, autoPlacement } from "floating-ui.dom.umd.min"; + +const findParents = (el, parents = []) => { + if (el && el.localName == "body") return parents + if (el && el.getAttribute("phx-hook") == "Marginote") return findParents(el.parentElement, parents.concat([el])) + if (!el) return parents + return findParents(el.parentElement, parents) +} + +export default MargiNote = { + func: 0, + mounted() { + const t = this.el + const marginoteParent = document.querySelector("[data-marginote='parent']") + t.addEventListener("click", ev => { + const selection = window.getSelection().toString() + + if (selection !== "") return + ev.stopPropagation() + + if (!marginoteParent) return + + const parents = findParents(t.parentElement, []).map(f => f.id.replace("marginote-id-", "")) + + + this.pushEvent("show-quoted-comment", {id: t.id, parents}, () => { + computePosition(this.el, marginoteParent, { + middleware: [offset(10), autoPlacement()], + }).then(({ x, y}) => { + this.pushEvent("adjust-marginote", {top: `${y}px`, left: `${x}px`}) + }) + }) + }) + }, +} diff --git a/assets/js/hooks/media/bridged.js b/assets/js/hooks/mediaEventBridges/bridged.js similarity index 100% rename from assets/js/hooks/media/bridged.js rename to assets/js/hooks/mediaEventBridges/bridged.js diff --git a/assets/js/hooks/mediaEventBridges/index.js b/assets/js/hooks/mediaEventBridges/index.js new file mode 100644 index 00000000..97b886f9 --- /dev/null +++ b/assets/js/hooks/mediaEventBridges/index.js @@ -0,0 +1,17 @@ +/** + * This file contains definitions for custom event bridges and keeps + * the exporting of these clean. + * */ + +import { bridged } from "./bridged.js"; + +export const seekTimeBridge = bridged("seekTime"); +export const playPauseBridge = bridged("playPause"); +export const heartbeatBridge = bridged("heartbeat"); +/** + * The playbackMetaBridge is the channel through which playback-metadata related + * messages are passed. + * An example would be when a voice first gets loaded/registered and we can + * update the mediasessions api even if the user has not started the actual playback. + * */ +export const playbackMetaBridge = bridged("playback"); diff --git a/assets/js/hooks/media_bridge.js b/assets/js/hooks/media_bridge.js index 757ceb74..5582d035 100644 --- a/assets/js/hooks/media_bridge.js +++ b/assets/js/hooks/media_bridge.js @@ -6,221 +6,252 @@ * * Event-handling is done using custom bridged events as a proxy. * */ -import { bridged } from "./media/bridged.js"; -import { formatDisplayTime } from "../utils/time_utils.js" - -// TODO: consider switching to a map of bridges to support other key events -export const seekTimeBridge = bridged("seekTime"); -export const playPauseBridge = bridged("playPause") -export const heartbeatBridge = bridged("heartbeat") +import { formatDisplayTime } from "../utils/time_utils.js"; +import { + seekTimeBridge, + playPauseBridge, + heartbeatBridge, + playbackMetaBridge, +} from "./mediaEventBridges"; MediaBridge = { mounted() { - this.currentTime = this.el.querySelector("#player-time") + this.currentTime = this.el.querySelector("#player-time"); this.isFollowMode = false; - this.duration = this.el.querySelector("#player-duration") - this.progress = this.el.querySelector("#player-progress") - - const emphasizedChapterPreamble = this.emphasizeChapterPreamble() - this.emphasizedDomNode = { - prev: null, - current: emphasizedChapterPreamble, - } - this.el.addEventListener("update_display_value", e => this.handleUpdateDisplayValue(e)) - this.handleEvent("media_bridge:registerEventsTimeline", params => this.registerEventsTimeline(params)) - + this.duration = this.el.querySelector("#player-duration"); + this.progress = this.el.querySelector("#player-progress"); + this.emphasizedDomNode = this.initEmphasizedNode(); + this.el.addEventListener("update_display_value", (e) => + this.handleUpdateDisplayValue(e), + ); + this.handleEvent("media_bridge:registerEventsTimeline", (params) => + this.registerEventsTimeline(params), + ); + this.handleEvent("media_bridge:registerPlayback", (params) => + this.registerPlaybackInfo(params), + ); + this.handleEvent("initSession", (sess) => this.initSession(sess)); // pub: external action // this callback pubs to others - this.handleEvent("media_bridge:seekTime", (seekTimePayload) => { - const { - originator, - } = seekTimePayload; - console.assert(originator === "MediaBridge", "This event may only originate from the MediaBridge server.") - - seekTimeBridge.pub(seekTimePayload) - }) - - this.handleEvent("media_bridge:play_pause", (playPausePayload) => { - const { - originator, - } = playPausePayload; - console.assert(originator === "MediaBridge", "This event may only originate from the MediaBridge server.") - - console.log("media_bridge:play_pause", playPausePayload) - playPauseBridge.pub(playPausePayload) - }) - - this.handleEvent("toggleFollowMode", () => this.toggleFollowMode()) // TODO: candidate for shifting to media_bridge.js? - + this.handleEvent("media_bridge:play_pause", (payload) => + this.receivePlayPauseFromServer(payload), + ); + this.handleEvent("media_bridge:seekTime", (payload) => + this.receiveSeekTimeFromServer(payload), + ); + this.handleEvent("toggleFollowMode", () => this.toggleFollowMode()); // TODO: candidate for shifting to media_bridge.js? // this callback: is internal to media_bridge // internal action this.eventBridgeDeregisterers = { - seekTime: seekTimeBridge.sub(payload => this.handleSeekTime(payload)), - playPause: playPauseBridge.sub(payload => this.handlePlayPause(payload)), - heartbeat: heartbeatBridge.sub(payload => this.handleHeartbeat(payload)), - } + seekTime: seekTimeBridge.sub((payload) => this.handleSeekTime(payload)), + playPause: playPauseBridge.sub((payload) => + this.handlePlayPause(payload), + ), + heartbeat: heartbeatBridge.sub((payload) => + this.handleHeartbeat(payload), + ), + }; + }, + /** + * Saves current session id + * */ + initSession(sess) { + localStorage.setItem("session", JSON.stringify(sess)); }, toggleFollowMode() { this.isFollowMode = !this.isFollowMode; }, handleHeartbeat(payload) { - console.log("[MediaBridge::handleHeartbeat]", payload) const shouldIgnoreSignal = payload.originator === "MediaBridge"; - if(shouldIgnoreSignal) { + if (shouldIgnoreSignal) { return; } // originator is expected to be audio player - console.assert(payload.originator === "AudioPlayer", "MediaBridge only expects heartbeat acks to come from AudioPlayer"); - console.log(">>> progress update, payload:", {payload, eventsTimeline: this.eventsTimeline}) - const { - currentTimeMs, - durationMs, - } = payload.currentPlaybackInfo || {}; + console.assert( + payload.originator === "AudioPlayer", + "MediaBridge only expects heartbeat acks to come from AudioPlayer", + ); + const { currentTimeMs, durationMs } = payload.currentPlaybackInfo || {}; - this.updateTimeDisplay(currentTimeMs, durationMs) - this.emphasizeActiveEvent(currentTimeMs, this.eventsTimeline) + this.updateTimeDisplay(currentTimeMs, durationMs); + this.emphasizeActiveEvent(currentTimeMs, this.eventsTimeline); + }, + /** + * Returns the map that gets used to init the preamble as the emphasized node. + * This happens when the hook mounts. + * */ + initEmphasizedNode() { + const emphasizedChapterPreamble = this.emphasizeChapterPreamble(); + return { + prev: null, + current: emphasizedChapterPreamble, + }; }, /** * Emphasizes then returns the node reference to the chapter's preamble. * This is so that @ mount, at least the chapter preamble shall be emphasized * */ emphasizeChapterPreamble() { - const preambleNode = document.querySelector("#chapter-preamble") + const preambleNode = document.querySelector("#chapter-preamble"); if (!preambleNode) { - console.log("[EMPHASIZE], no preamble node found") - return null + console.warn("[EMPHASIZE], no preamble node found"); + return null; } - preambleNode.classList.add("emphasized-verse") + preambleNode.classList.add("emphasized-verse"); - console.log("[EMPHASIZE], preamble node:", preambleNode) - - return preambleNode + return preambleNode; }, emphasizeActiveEvent(currentTimeMs, events) { if (!events) { - console.log("No events found") return; } - const activeEvent = events.find(event => currentTimeMs >= event.origin && - currentTimeMs < (event.origin + event.duration)) + const activeEvent = events.find( + (event) => + currentTimeMs >= event.origin && + currentTimeMs < event.origin + event.duration, + ); if (!activeEvent) { - console.log("No active event found @ time = ", currentTime) return; } - const { - verse_id: verseId - } = activeEvent; + const { verse_id: verseId } = activeEvent; if (!verseId) { - return + return; } - const { - prev: prevDomNode, - current: currDomNode, - } = this.emphasizedDomNode; // @ this point it wouldn't have been updated yet + const { prev: prevDomNode, current: currDomNode } = this.emphasizedDomNode; // @ this point it wouldn't have been updated yet - const updatedEmphasizedDomNode = {} - if(currDomNode) { - currDomNode.classList.remove("emphasized-verse") + const updatedEmphasizedDomNode = {}; + if (currDomNode) { + currDomNode.classList.remove("emphasized-verse"); updatedEmphasizedDomNode.prev = currDomNode; } - const targetDomId = `verse-${verseId}` - const targetNode = document.getElementById(targetDomId) - targetNode.classList.add("emphasized-verse") + const targetDomId = `verse-${verseId}`; + const targetNode = document.getElementById(targetDomId); + targetNode.classList.add("emphasized-verse"); updatedEmphasizedDomNode.current = targetNode; - if(this.isFollowMode) { - targetNode.focus() + if (this.isFollowMode) { + targetNode.focus(); targetNode.scrollIntoView({ - behavior: 'smooth', - block: 'start', + behavior: "smooth", + block: "start", }); } this.emphasizedDomNode = updatedEmphasizedDomNode; }, startHeartbeat() { - const heartbeatInterval = 100 // 10fps, comfortable for human eye - console.log("Starting heartbeat!") + const heartbeatInterval = 100; // 10fps, comfortable for human eye const heartbeatPayload = { originator: "MediaBridge", - } - const heartbeatTimer = setInterval(() => heartbeatBridge.pub(heartbeatPayload), heartbeatInterval) - console.log("Started Heartbeat with:", {heartbeatTimer, heartbeatPayload, heartbeatInterval}) - - this.heartbeatTimer = heartbeatTimer + }; + const heartbeatTimer = setInterval( + () => heartbeatBridge.pub(heartbeatPayload), + heartbeatInterval, + ); + this.heartbeatTimer = heartbeatTimer; }, killHeartbeat() { - console.log("Killing heartbeat!", {heartbeatTimer: this.heartbeatTimer}) - clearInterval(this.heartbeatTimer) + clearInterval(this.heartbeatTimer); }, - updateTimeDisplay(timeMs, durationMs=null) { + updateTimeDisplay(timeMs, durationMs = null) { const currentTimeDisplay = formatDisplayTime(timeMs); - this.currentTime.innerText = currentTimeDisplay - console.log("Updated time display to", currentTimeDisplay); + this.currentTime.innerText = currentTimeDisplay; - if(durationMs) { - const durationDisplay = formatDisplayTime(durationMs) - this.duration.innerText = durationDisplay + if (durationMs) { + const durationDisplay = formatDisplayTime(durationMs); + this.duration.innerText = durationDisplay; } }, seekToMs(originator, timeMs) { - console.log("media_bridge.js::seekToMs", {timeMs, originator}) - const knownOriginators = ["ProgressBar", "MediaBridge"] // temp-list, will be removed + const knownOriginators = ["ProgressBar", "MediaBridge"]; // temp-list, will be removed if (!knownOriginators.includes(originator)) { - console.warn(`originator ${originator} is not a known originator. Is not one of ${knownOriginators}.`) + console.warn( + `originator ${originator} is not a known originator. Is not one of ${knownOriginators}.`, + ); } this.updateTimeDisplay(timeMs); }, handleUpdateDisplayValue(e) { - const { - detail, - } = e - const [key, val, extraKey] = detail?.payload + const { detail } = e; + const [key, val, extraKey] = detail?.payload; if (extraKey === "innerText") { this[key][extraKey] = val; } if (extraKey === "style.width") { - this[key].style.width = val + this[key].style.width = val; } }, registerEventsTimeline(params) { - console.log("Register Events Timeline", params); - this.eventsTimeline = params.voice_events + const { voice_events } = params; + this.eventsTimeline = voice_events; }, - handleSeekTime(payload) { - console.log("[media_bridge::seekTimeBridgeSub::seekTimeHandler] payload", payload); - const { - seekToMs: timeMs, - originator, - } = payload; - this.seekToMs(originator, timeMs) + /** + * First registers the playback information about a playable medium (e.g. voice). + * The intent of this is to separate out tasks for interfacing with things like MediaSessions api + * from interfacing with the concrete players (e.g. play pause event on the audio player). + * */ + registerPlaybackInfo(params) { + const { playback } = params; + playbackMetaBridge.pub(playback); + }, + /** + * Receives event pushed from the server, then pubs through the + * */ + receivePlayPauseFromServer(playPausePayload) { + const { originator } = playPausePayload; + console.assert( + originator === "MediaBridge", + "This event may only originate from the MediaBridge server.", + ); + + this.updateHeartbeatFromPlayPause(playPausePayload); + playPauseBridge.pub(playPausePayload); }, handlePlayPause(payload) { - console.log("[playPauseBridge::media_bridge:playpause] payload:", payload) - const { - cmd, - playback, - originator, - } = payload - - // TODO: implement handler for actions emitted via interaction with youtube player - console.log(">> [media_bridge.js::playPauseBridge], received a signal", payload) + const { originator } = payload; + + const shouldIgnoreSignal = originator === "MediaBridge"; + if (shouldIgnoreSignal) { + return; + } + + this.updateHeartbeatFromPlayPause(payload); + }, + /** + * Updates the MediaBridgeHook's internal heartbeat timer depending on the command + * given (play or pause). + * + * NOTE: This doesn't guard for the originator of the command. + * */ + updateHeartbeatFromPlayPause(payload) { + const { cmd } = payload; if (cmd === "play") { - this.startHeartbeat() + this.startHeartbeat(); } if (cmd === "pause") { - this.killHeartbeat() + this.killHeartbeat(); } - } - -} + }, + receiveSeekTimeFromServer(seekTimePayload) { + const { originator } = seekTimePayload; + console.assert( + originator === "MediaBridge", + "This event may only originate from the MediaBridge server.", + ); + seekTimeBridge.pub(seekTimePayload); + }, + handleSeekTime(payload) { + const { seekToMs: timeMs, originator } = payload; + this.seekToMs(originator, timeMs); + }, +}; export default MediaBridge; diff --git a/assets/js/hooks/mini_player.js b/assets/js/hooks/mini_player.js index f92d5982..c8c68ed0 100644 --- a/assets/js/hooks/mini_player.js +++ b/assets/js/hooks/mini_player.js @@ -34,17 +34,16 @@ const bindListenersToEventsOnEl = (el, listeners, events) => { }); }; +import { autoUpdate} from "floating-ui.dom.umd.min"; + const autoUpdatePosition = () => { const { verseContainer, youtubePlayerContainer, } = getRelevantElements(); - const { - autoUpdate - } = window.FloatingUIDOM - window.FloatingUIDOM.cleanupAutoPositioning = autoUpdate( + autoUpdate( verseContainer, youtubePlayerContainer, alignMiniPlayer, @@ -71,15 +70,15 @@ const getRelevantElements = () => { return {button, youtubePlayerContainer, verseContainer} } -const alignMiniPlayer = () => { - const {verseContainer, youtubePlayerContainer} = getRelevantElements(); - - const { +import { computePosition, autoPlacement, shift, offset, - } = window.FloatingUIDOM; + } from "floating-ui.dom.umd.min"; + +const alignMiniPlayer = () => { + const {verseContainer, youtubePlayerContainer} = getRelevantElements(); computePosition(verseContainer, youtubePlayerContainer, { diff --git a/assets/js/hooks/progress_bar.js b/assets/js/hooks/progress_bar.js index ad1e05a0..68db4f4f 100644 --- a/assets/js/hooks/progress_bar.js +++ b/assets/js/hooks/progress_bar.js @@ -2,66 +2,57 @@ * Progress Bar hooks intended to sync playback related actions * */ -import {seekTimeBridge, heartbeatBridge} from "./media_bridge.js" +import { seekTimeBridge, heartbeatBridge } from "./mediaEventBridges"; ProgressBar = { mounted() { this.el.addEventListener("click", (e) => { e.preventDefault(); - this.handleProgressBarClick(e) + this.handleProgressBarClick(e); }); - const heartbeatDeregisterer = heartbeatBridge.sub(payload => this.handleHeartbeat(payload)) - const seekTimeDeregisterer = seekTimeBridge.sub(payload => this.handleExternalSeekTime(payload)) + const heartbeatDeregisterer = heartbeatBridge.sub((payload) => + this.handleHeartbeat(payload), + ); + const seekTimeDeregisterer = seekTimeBridge.sub((payload) => + this.handleExternalSeekTime(payload), + ); this.eventBridgeDeregisterers = { seekTime: seekTimeDeregisterer, heartbeat: heartbeatDeregisterer, - } + }; }, handleExternalSeekTime(payload) { - console.log("[progress_bar::seekTimeBridgeSub::seekTimeHandler] this:", {payload}); - const { - seekToMs: timeMs, - originator, - } = payload; + const { seekToMs: timeMs, originator } = payload; const shouldIgnoreSignal = originator === "ProgressBar"; if (shouldIgnoreSignal) { - console.info("Ignoring signal for seekTime", payload) - return; } - const maxTime = this.el.dataset?.max || this.maxTime - if(!maxTime) { - console.warn("Max time not available in element's state or dataset, ignoring progress bar update.") - return + const maxTime = this.el.dataset?.max || this.maxTime; + if (!maxTime) { + console.warn( + "Max time not available in element's state or dataset, ignoring progress bar update.", + ); + return; } - const playbackPercentage = (timeMs / maxTime) - const progressStyleWidth = `${(playbackPercentage * 100)}%` - console.log("[DEBUG]", { - maxTime, - playbackPercentage, - }) - this.setProgressBarWidth(progressStyleWidth) + const playbackPercentage = timeMs / maxTime; + const progressStyleWidth = `${playbackPercentage * 100}%`; + this.setProgressBarWidth(progressStyleWidth); }, handleHeartbeat(payload) { - console.log("[ProgressBar::handleHeartbeat]", payload) const shouldIgnoreSignal = payload.originator === "MediaBridge"; - if(shouldIgnoreSignal) { + if (shouldIgnoreSignal) { return; } - const { - currentTimeMs, - durationMs, - } = payload.currentPlaybackInfo || {}; + const { currentTimeMs, durationMs } = payload.currentPlaybackInfo || {}; - const playbackPercentage = (currentTimeMs / durationMs) - const progressStyleWidth = `${(playbackPercentage * 100)}%` - console.log("handleHeartbeat, set progress bar width", progressStyleWidth) - this.setProgressBarWidth(progressStyleWidth) + const playbackPercentage = currentTimeMs / durationMs; + const progressStyleWidth = `${playbackPercentage * 100}%`; + this.setProgressBarWidth(progressStyleWidth); }, /* // The progress bar is measured in milliseconds, @@ -86,54 +77,43 @@ ProgressBar = { // }) */ handleProgressBarClick(e) { - const { max: maxTime } = this.el.dataset + const { max: maxTime } = this.el.dataset; if (!maxTime) { - console.log("unable to seek position, payload is incorrect") - return + return; } - const containerNode = document.getElementById("player-progress-container") - const maxOffset = containerNode.offsetWidth + const containerNode = document.getElementById("player-progress-container"); + const maxOffset = containerNode.offsetWidth; this.maxTime = maxTime; this.maxOffset = maxOffset; const currXOffset = e.offsetX; - const maxPlaybackMs = Number(maxTime) - const playbackPercentage = (currXOffset / maxOffset) - const positionMs = maxPlaybackMs * playbackPercentage - const progressStyleWidth = `${(playbackPercentage * 100)}%` - this.setProgressBarWidth(progressStyleWidth) + const maxPlaybackMs = Number(maxTime); + const playbackPercentage = currXOffset / maxOffset; + const positionMs = maxPlaybackMs * playbackPercentage; + const progressStyleWidth = `${playbackPercentage * 100}%`; + this.setProgressBarWidth(progressStyleWidth); // Optimistic update this.el.value = positionMs; - console.log("seek attempt @ positionMs:", { - checkThis: this, - elem: this.el, - event: e, - maxOffset, - currXOffset, - playbackPercentage, - maxPlaybackMs, - positionMs, - }) - // pubs & dispatches this position const seekTimePayload = { seekToMs: positionMs, originator: "ProgressBar", - } - seekTimeBridge.dispatch(this, seekTimePayload, "#media-player-container") + }; + seekTimeBridge.dispatch(this, seekTimePayload, "#media-player-container"); return; }, - setProgressBarWidth(progressStyleWidth, selector="#player-progress") { - console.log("setting progress bar width:", progressStyleWidth) - const progressBarNode = document.querySelector(selector) - console.assert(!!progressBarNode, "progress bar node must always be present in the dom.") + setProgressBarWidth(progressStyleWidth, selector = "#player-progress") { + const progressBarNode = document.querySelector(selector); + console.assert( + !!progressBarNode, + "progress bar node must always be present in the dom.", + ); progressBarNode.style.width = progressStyleWidth; - } + }, }; - export default ProgressBar; diff --git a/assets/js/hooks/scrolling.js b/assets/js/hooks/scrolling.js new file mode 100644 index 00000000..68af5873 --- /dev/null +++ b/assets/js/hooks/scrolling.js @@ -0,0 +1,11 @@ +Scrolling = { + mounted() { + console.log("SCROLLING MOUNTED"); + this.handleEvent("scroll-to-top", this.handleScrollToTop); + }, + handleScrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); + }, +}; + +export default Scrolling; diff --git a/assets/js/hooks/share_quote.js b/assets/js/hooks/share_quote.js index 3e6436a5..0194bda7 100644 --- a/assets/js/hooks/share_quote.js +++ b/assets/js/hooks/share_quote.js @@ -3,13 +3,13 @@ */ import { getSharer } from "./web_share.js"; - ShareQuoteButton = { mounted() { initTooltip(); let callback = () => console.log("share quote!"); - if ("share" in navigator) { // uses webshare api: + if ("share" in navigator) { + // uses webshare api: callback = () => { const shareTitle = this.el.getAttribute("data-share-title"); const shareUrl = window.location.href; @@ -21,9 +21,9 @@ ShareQuoteButton = { window.shareUrl = sharer; window.shareUrl(shareUrl); }; - } else if ("clipboard" in navigator) { // copies to clipboard: + } else if ("clipboard" in navigator) { + // copies to clipboard: callback = () => { - console.log(">> see me:", {"floating ui:": window}) const { chapter_number: chapterNum, verse_number: verseNum, @@ -31,7 +31,7 @@ ShareQuoteButton = { text, } = JSON.parse(this.el.getAttribute("data-verse")); - const content = `[Gita Chapter ${chapterNum} Verse ${verseNum}] \n${text}\n${transliteration}\nRead more at ${document.URL}` + const content = `[Gita Chapter ${chapterNum} Verse ${verseNum}] \n${text}\n${transliteration}\nRead more at ${document.URL}`; navigator.clipboard.writeText(content); }; } @@ -41,66 +41,50 @@ ShareQuoteButton = { }; function showTooltip() { - const {tooltip} = getButtonAndTooltip(); - tooltip.style.display = 'block'; + const { tooltip } = getButtonAndTooltip(); + tooltip.style.display = "block"; alignTooltip(); } function hideTooltip() { - const {tooltip} = getButtonAndTooltip(); - tooltip.style.display = ''; + const { tooltip } = getButtonAndTooltip(); + tooltip.style.display = ""; } const initTooltip = () => { - const {button} = getButtonAndTooltip(); + const { button } = getButtonAndTooltip(); [ - ['mouseenter', showTooltip], - ['mouseleave', hideTooltip], - ['focus', showTooltip], - ['blur', hideTooltip], + ["mouseenter", showTooltip], + ["mouseleave", hideTooltip], + ["focus", showTooltip], + ["blur", hideTooltip], ].forEach(([event, listener]) => { button.addEventListener(event, listener); }); -} +}; const getButtonAndTooltip = () => { - const id = "ShareQuoteButton" - const button = document.getElementById(id) - const tooltip = document.getElementById(`tooltip-${id}`) - return {button, tooltip} - -} + const id = "ShareQuoteButton"; + const button = document.getElementById(id); + const tooltip = document.getElementById(`tooltip-${id}`); + return { button, tooltip }; +}; +import { computePosition, flip, shift, offset } from "floating-ui.dom.umd.min"; const alignTooltip = () => { - const {button, tooltip} = getButtonAndTooltip() - console.log(">>> found?", { - button, - tooltip, - }) - const { - computePosition, - flip, - shift, - offset, - } = window.FloatingUIDOM; - + const { button, tooltip } = getButtonAndTooltip(); computePosition(button, tooltip, { - placement: 'right', + placement: "right", // NOTE: order of middleware matters. - middleware: [offset(6), flip(), shift({padding: 16})], - }).then(({x, y}) => { - console.log(">>> computed new position!", { - x, - y - }) + middleware: [offset(6), flip(), shift({ padding: 16 })], + }).then(({ x, y }) => { Object.assign(tooltip.style, { left: `${x}px`, top: `${y}px`, }); }); -} - +}; export default ShareQuoteButton; diff --git a/assets/js/hooks/youtube_player.js b/assets/js/hooks/youtube_player.js index 20cdcb79..4b55453f 100644 --- a/assets/js/hooks/youtube_player.js +++ b/assets/js/hooks/youtube_player.js @@ -2,61 +2,57 @@ * Follower * Validates if required parameters exist. * */ -import {seekTimeBridge, playPauseBridge} from "./media_bridge.js" -import { isMobileDevice } from "../utils/uncategorised_utils.js" +import { seekTimeBridge, playPauseBridge } from "./mediaEventBridges"; + +import { isMobileDevice } from "../utils/uncategorised_utils.js"; const isYouTubeFnCallable = (dataset) => { - const {functionName, eventName} = dataset; - const areFnAndEventNamesProvided = functionName && eventName - if(!areFnAndEventNamesProvided) { + const { functionName, eventName } = dataset; + const areFnAndEventNamesProvided = functionName && eventName; + if (!areFnAndEventNamesProvided) { console.warn("Need to provide both valid function name and event name"); - return false + return false; } - const supportedEvents = ["click", "mouseover"] + const supportedEvents = ["click", "mouseover"]; if (!supportedEvents.includes(eventName)) { - console.warn(`${eventName} is not a supported event. Supported events include ${supportedEvents}.`); - return false + console.warn( + `${eventName} is not a supported event. Supported events include ${supportedEvents}.`, + ); + return false; } - const supportedFunctionNames = Object.keys(youtubePlayerCallbacks) + const supportedFunctionNames = Object.keys(youtubePlayerCallbacks); if (!supportedFunctionNames.includes(functionName)) { - console.warn(`${functionName} is not a supported youtube function. Supported functions include ${supportedFunctionNames}.`); + console.warn( + `${functionName} is not a supported youtube function. Supported functions include ${supportedFunctionNames}.`, + ); return false; } - - return true -} + return true; +}; // NOTE: the player interface can be found @ https://developers.google.com/youtube/iframe_api_reference#Functions const youtubePlayerCallbacks = { - seekTo: function(options) { - const {targetTimeStamp, player} = options; - const target = Number(targetTimeStamp) - console.log("seeking to: ", target) - return player.seekTo(target) + seekTo: function (options) { + const { targetTimeStamp, player } = options; + const target = Number(targetTimeStamp); + return player.seekTo(target); }, - loadVideoById: function(options) { - const { - targetTimeStamp: startSeconds, - videoId, - player, - } = options; - console.log(`Loading video with id ${videoId} at t=${startSeconds}s`) - player.loadVideoById({videoId, startSeconds}) + loadVideoById: function (options) { + const { targetTimeStamp: startSeconds, videoId, player } = options; + player.loadVideoById({ videoId, startSeconds }); }, - getAllStats: function(options) { // this is a custom function - const { - hook, - player, - } = options; + getAllStats: function (options) { + // this is a custom function + const { hook, player } = options; const stats = { duration: player.getDuration(), videoUrl: player.getVideoUrl(), currentTime: player.getCurrentTime(), - } - hook.pushEventTo("#statsHover", "reportVideoStatus", stats) - } -} + }; + hook.pushEventTo("#statsHover", "reportVideoStatus", stats); + }, +}; /** * Contains client-side logic for the youtube iframe embeded player. @@ -65,68 +61,48 @@ const youtubePlayerCallbacks = { */ export const RenderYouTubePlayer = { mounted() { - const { - videoId, - playerConfig: serialisedPlayerConfig, - } = this.el.dataset; - console.log("Check dataset", this.el.dataset) + const { videoId, playerConfig: serialisedPlayerConfig } = this.el.dataset; - const playerConfig = JSON.parse(serialisedPlayerConfig) - const updatedConfig = overrideConfigForMobile(playerConfig) - injectIframeDownloadScript() - injectYoutubeInitialiserScript(videoId, updatedConfig) + const playerConfig = JSON.parse(serialisedPlayerConfig); + const updatedConfig = overrideConfigForMobile(playerConfig); + injectIframeDownloadScript(); + injectYoutubeInitialiserScript(videoId, updatedConfig); // TODO: capture youtube player events (play state changes and pub to the same event bridges, so as to control overall playback) this.eventBridgeDeregisterers = { seekTime: seekTimeBridge.sub((payload) => this.handleSeekTime(payload)), - playPause: playPauseBridge.sub(payload => this.handlePlayPause(payload)), - } - this.handleEvent("stop", () => this.stop()) + playPause: playPauseBridge.sub((payload) => + this.handlePlayPause(payload), + ), + }; + this.handleEvent("stop", () => this.stop()); }, handlePlayPause(payload) { - console.log("[playPauseBridge::audio_player::playpause] payload:", payload) - const { - cmd, - playback, - } = payload + const { cmd, playback } = payload; if (cmd === "play") { - this.playMedia(playback) + this.playMedia(playback); } if (cmd === "pause") { - this.pauseMedia() + this.pauseMedia(); } }, handleSeekTime(payload) { - console.log("[youtube_player::seekTimeBridgeSub::seekTimeHandler] check params:", {payload} ); - let {seekToMs: timeMs} = payload; - this.seekToMs(timeMs) + let { seekToMs: timeMs } = payload; + this.seekToMs(timeMs); }, - playMedia(playback) { - console.log("youtube player playMedia triggerred", playback) - const {meta: playbackMeta, "playing?": isPlaying, elapsed} = playback; - const { title, duration, file_path: filePath, artists } = playbackMeta; - const artist = artists ? artists[0] : "myArtist" // FIXME: this should be ready once seeding has been update to properly add in artist names - + playMedia(_playback) { // TODO: consider if the elapsed ms should be used here for better sync(?) - window.youtubePlayer.playVideo() + window.youtubePlayer.playVideo(); }, pauseMedia() { - console.log("youtube player pauseMedia triggerred") - window.youtubePlayer.pauseVideo() - }, - stop() { - console.log("youtube player stop triggerred") + window.youtubePlayer.pauseVideo(); }, + stop() {}, seekToMs(timeMs) { const timeS = timeMs / 1000; - console.log("youtube player seekto triggerred", { - timeS, - player: window.youtubePlayer - }) - window.youtubePlayer.seekTo(timeS); - } + }, }; /** @@ -134,11 +110,11 @@ export const RenderYouTubePlayer = { * so that it gets fired before any other script. * */ const injectIframeDownloadScript = () => { - const tag = document.createElement("script"); - tag.src = "https://www.youtube.com/iframe_api"; - const firstScriptTag = document.getElementsByTagName("script")?.[0]; - firstScriptTag && firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); -} + const tag = document.createElement("script"); + tag.src = "https://www.youtube.com/iframe_api"; + const firstScriptTag = document.getElementsByTagName("script")?.[0]; + firstScriptTag && firstScriptTag.parentNode.insertBefore(tag, firstScriptTag); +}; /** * Injects a script that contains initialisation logic for the Youtube Player object. @@ -152,13 +128,13 @@ const injectYoutubeInitialiserScript = (videoId, playerConfig) => { videoId: videoId, events: { onReady: onPlayerReady, - } - } - window.youtubePlayer = new YT.Player("player", assimilatedConfig) - } + }, + }; + window.youtubePlayer = new YT.Player("player", assimilatedConfig); + }; window.callbackOnPlayerReady = (event) => { event.target.playVideo(); - } + }; const stringifiedScript = ` function onYouTubeIframeAPIReady() { @@ -166,40 +142,43 @@ const injectYoutubeInitialiserScript = (videoId, playerConfig) => { } function onPlayerReady(event) { window.callbackOnPlayerReady(event) - }` + }`; const functionCode = document.createTextNode(stringifiedScript); - iframeInitialiserScript.appendChild(functionCode) -} + iframeInitialiserScript.appendChild(functionCode); +}; export const TriggerYouTubeFunction = { mounted() { if (!isYouTubeFnCallable(this.el.dataset)) { - console.warn("YouTube function can not be triggerred.") - return + console.warn("YouTube function can not be triggerred."); + return; } - const {functionName, eventName} = this.el.dataset - const callback = youtubePlayerCallbacks[functionName] - const getOptions = () => ({hook: this,...this.el.dataset, player: window.youtubePlayer}) - this.el.addEventListener(eventName, () => callback(getOptions())) - } -} + const { functionName, eventName } = this.el.dataset; + const callback = youtubePlayerCallbacks[functionName]; + const getOptions = () => ({ + hook: this, + ...this.el.dataset, + player: window.youtubePlayer, + }); + this.el.addEventListener(eventName, () => callback(getOptions())); + }, +}; /// FIXME: this is a temp fix, that overrides the dimensions if it's a mobile. // there has to be a better, more generic way of handling this. // Alternatively, if we can reverse engineer a custom PIP mode (with resize and all that), then // we won't need to fix this. const overrideConfigForMobile = (playerConfig) => { - let overridedConfig = {...playerConfig} - if(isMobileDevice()) { - overridedConfig["height"] = "150", - overridedConfig["width"] = "200", - console.log("[iframe] updating the player config:", { - before: playerConfig, - after: overridedConfig, - - }) + let overridedConfig = { ...playerConfig }; + if (isMobileDevice()) { + (overridedConfig["height"] = "150"), + (overridedConfig["width"] = "200"), + console.log("[iframe] updating the player config:", { + before: playerConfig, + after: overridedConfig, + }); } return overridedConfig; -} +}; diff --git a/assets/tailwind.config.js b/assets/tailwind.config.js index 83e22aa7..8e66d50d 100644 --- a/assets/tailwind.config.js +++ b/assets/tailwind.config.js @@ -1,36 +1,37 @@ // See the Tailwind configuration guide for advanced usage // https://tailwindcss.com/docs/configuration -const plugin = require("tailwindcss/plugin") -const fs = require("fs") -const path = require("path") +const plugin = require("tailwindcss/plugin"); +const fs = require("fs"); +const path = require("path"); module.exports = { content: [ "./js/**/*.js", "../lib/vyasa_web.ex", - "../lib/vyasa_web/**/*.*ex" + "../lib/vyasa/display/user_mode.ex", + "../lib/vyasa_web/**/*.*ex", ], theme: { extend: { fontFamily: { - 'dn': ['"Gotu"', 'sans-serif'], - 'tl': ['"Tamil Font"', 'sans-serif'], + dn: ['"Gotu"', "sans-serif"], + tl: ['"Tamil Font"', "sans-serif"], }, colors: { - primary: 'var(--color-primary)', - primaryAccent: 'var(--color-primary-accent)', - secondary: 'var(--color-secondary)', - primaryBackground: 'var(--color-primary-background)', - secondary: 'var(--color-secondary)', - brand: 'var(--color-brand)', - brandLight: 'var(--color-brand-light)', - brandExtraLight: 'var(--color-brand-extra-light)', - brandAccent: 'var(--color-brand-accent)', - brandAccentLight: 'var(--color-brand-accent-light)', - brandDark: 'var(--color-brand-dark)', - brandExtraDark: 'var(--color-brand-extra-dark)', - } + primary: "var(--color-primary)", + primaryAccent: "var(--color-primary-accent)", + secondary: "var(--color-secondary)", + primaryBackground: "var(--color-primary-background)", + secondary: "var(--color-secondary)", + brand: "var(--color-brand)", + brandLight: "var(--color-brand-light)", + brandExtraLight: "var(--color-brand-extra-light)", + brandAccent: "var(--color-brand-accent)", + brandAccentLight: "var(--color-brand-accent-light)", + brandDark: "var(--color-brand-dark)", + brandExtraDark: "var(--color-brand-extra-dark)", + }, }, }, plugins: [ @@ -41,44 +42,70 @@ module.exports = { // //
v<%= Application.spec(:vyasa, :vsn) %>
@@ -21,9 +21,8 @@
+ <%= c.pointer.quote %> +
+<%= c.body %>
+<%= s.body %>
+- <%= if @voice && Map.has_key?(@voice, "artist") do %> - <%= @voice.artist %> - <% else %> - artist - <% end %> -
-+ <%= if @playback && @playback.meta.artists do %> + <%= Enum.join(@playback.meta.artists, ", ") %> + <% else %> + Unknown artist + <% end %> +
+ + +- <%= verse.body |> String.split("।।") |> List.first() %> -
- - <:item title={"Transliteration"}> -- <%= hd(verse.translations).target.body_translit %> -
- - <:item title={"Transliteration Meaning"}> -- <%= hd(verse.translations).target.body_translit_meant %> -
- - <:item title={"Translation"}> -- <%= hd(verse.translations).target.body %> -
- - + field={[:body]} + verseup={:big}/> + <:edge + node={hd(verse.translations)} + field={[:target, :body_translit]} + verseup={:mid}/> + + <:edge + node={hd(verse.translations)} + field={[:target, :body_translit_meant]} + verseup={:mid}/> + + <:edge + node={hd(verse.translations)} + field={[:target, :body]} + verseup={:mid}/> + +