aboutsummaryrefslogtreecommitdiff
path: root/static
diff options
context:
space:
mode:
authorTim Keller <tjk@tjkeller.xyz>2026-05-17 00:29:39 -0500
committerTim Keller <tjk@tjkeller.xyz>2026-05-17 00:29:39 -0500
commitd4f8fc15b7e6a2204e8dda92a083684d93a5fa59 (patch)
tree74196a63e31a244170c2e35863e655af8b06d008 /static
parent1316aa7ca5e1668bbb7967264540bff3c8dbef86 (diff)
downloadembedtube-d4f8fc15b7e6a2204e8dda92a083684d93a5fa59.tar.xz
embedtube-d4f8fc15b7e6a2204e8dda92a083684d93a5fa59.zip
refactor some and client side support for new api + cleanup client code and fix some server bugs
Diffstat (limited to 'static')
-rw-r--r--static/index.js191
1 files changed, 98 insertions, 93 deletions
diff --git a/static/index.js b/static/index.js
index c577050..c753e3f 100644
--- a/static/index.js
+++ b/static/index.js
@@ -1,7 +1,18 @@
-/* setup player */
-let player
+/* static elements */
const titleBar = document.getElementById("title")
+const buttonDetails = document.getElementById("toggle-details")
+const divDetails = document.getElementById("details")
+const buttonComments = document.getElementById("toggle-comments")
+const divComments = document.getElementById("comments")
+const commentTemplate = document.getElementById("template-comment")
+
+/* state */
+let player
+let detailsLoaded = false
+let commentsLoaded = false
+
+/* setup player */
function onPlayerReady(_e) {
document.title = titleBar.textContent = player.videoTitle
}
@@ -15,28 +26,25 @@ function onYouTubeIframeAPIReady() {
})
}
+
/* helpers */
async function getApiReq(path) {
return fetch("/api" + path)
.then(async res => {
- if (!res.ok) {
- const err = await res.text();
- throw new Error(`Server error: ${err}`);
- }
+ if (!res.ok)
+ throw new Error("Server error:", await res.text())
return res.json()
})
- .then(data => {
- return data
- })
- .catch(error => {
- console.error("Fetch error:", error)
- })
+ .catch(e => console.error(e))
}
function toggleVis(el) {
- el.style.display = el.style.display ? null : "none"
+ const v = !!el.style.display
+ el.style.display = v ? null : "none"
+ return v
}
+// TODO this func could be moved to server
function createAnchors(text) {
function _a(href, innerText) {
return `<a href="${href}" rel="noopener noreferrer">${innerText ?? href}</a>`
@@ -47,9 +55,10 @@ function createAnchors(text) {
const timestampRegex = /[0-9]+:[0-9]+/g
return text
+ .replaceAll("\n", "<br>")
.replace(urlRegex, url => {
- const ytRegex = /^(https?:\/\/)?(www\.)?(youtu(be\.com|\.be|be-nocookie\.com))\//gi
- const href = url.replace(ytRegex, `${window.location.origin}/`)
+ const ytWatchRegex = /^(https?:\/\/)?(www\.)?(youtu(be\.com|\.be|be-nocookie\.com))\/watch/gi
+ const href = url.replace(ytWatchRegex, `${window.location.origin}/watch`)
return _a(href, url)
})
.replace(channelRegex, channel => {
@@ -61,113 +70,108 @@ function createAnchors(text) {
})
}
-/* setup video details buttons */
-const buttonDetails = document.getElementById("toggle-details")
-const divDetails = document.getElementById("details")
-let detailsLoaded = false
+function fillFields(data, fieldContainer) {
+ // see html if this doesn't make sense
+ const fieldElements = fieldContainer.querySelectorAll("[data-f]")
+ const fieldElementsMap = {}
+ for (const fe of fieldElements) {
+ const f = fe.dataset.f
+ fieldElementsMap[f] = fe
+ if (fe.dataset.skip !== undefined)
+ continue
+
+ const v = data[f]
+ switch (fe.tagName) {
+ case "DIV":
+ fe.innerHTML = fe.dataset.anchor !== undefined ? createAnchors(v) : v
+ break
+ case "SPAN": // only used for stats
+ fe.textContent = new Intl.NumberFormat().format(v)
+ break
+ case "TIME":
+ fe.textContent = fe.datetime = v
+ break
+ case "A": // only used for channel links
+ fe.href = `https://www.youtube.com/${v}`
+ default:
+ fe.textContent = v
+ }
+ }
+ return fieldElementsMap
+}
+
+/* setup video details */
async function toggleDetails() {
if (!player)
- return
- if (detailsLoaded) {
- toggleVis(divDetails)
- return
- }
- // elements
- const channel = document.getElementById("details-channel")
- const likes = document.getElementById("details-likes")
- const views = document.getElementById("details-views")
- const date = document.getElementById("details-date")
- const desc = document.getElementById("details-desc")
- const tags = document.getElementById("details-tags")
- const comments = document.getElementById("details-comments")
-
- // req
- const d = (await getApiReq(`/details?id=${player.playerInfo.videoData.video_id}`)).items[0]
-
- // populate
- channel.textContent = d.snippet.channelTitle
- channel.href = `https://www.youtube.com/channel/${d.snippet.channelId}`
- views.textContent = new Intl.NumberFormat().format(d.statistics.viewCount)
- likes.textContent = new Intl.NumberFormat().format(d.statistics.likeCount)
- comments.textContent = new Intl.NumberFormat().format(d.statistics.commentCount)
- date.textContent = d.snippet.publishedAt
- if (d.snippet.description)
- desc.innerHTML = createAnchors(d.snippet.description.replaceAll("\n", "<br>"))
- tags.textContent = d.snippet.tags.join(", ")
+ return false
+ if (detailsLoaded)
+ return toggleVis(divDetails)
+
+
+ // data
+ const data = await getApiReq(`/details?id=${player.playerInfo.videoData.video_id}`)
+
+ // pop
+ const fieldElements = fillFields(data, divDetails)
+ fieldElements.author.href = `https://www.youtube.com/channel/${data.channel}`
+ fieldElements.tags.textContent = data.tags.join(", ")
// done
detailsLoaded = true
- toggleVis(divDetails)
+ return toggleVis(divDetails)
}
-/* setup video comments buttons */
-const buttonComments = document.getElementById("toggle-comments")
-const divComments = document.getElementById("comments")
-const commentTemplate = document.getElementById("template-comment")
-let commentsLoaded = false
-
-function genComment(cd, parent, replies) {
- const c = commentTemplate.content.cloneNode(true)
-
- const author = c.querySelector(".author")
- const date = c.querySelector(".date")
- const dateM = c.querySelector(".date.modified")
- const body = c.querySelector(".body")
- const likes = c.querySelector(".likes")
-
- author.textContent = cd.authorDisplayName
- author.href = cd.authorChannelUrl
- date.textContent = cd.publishedAt
- if (cd.publishedAt === cd.updatedAt)
- dateM.remove()
- else
- dateM.textContent = `(Edited ${cd.updatedAt})`
- body.innerHTML = cd.textDisplay
- likes.textContent = new Intl.NumberFormat().format(cd.likeCount)
-
- if (replies) {
- const divReplies = c.querySelector(".replies")
- for (const reply of replies)
- genComment(reply.snippet, divReplies)
- }
- parent.append(c)
-}
+/* setup video comments */
async function toggleComments() {
if (!player)
- return
- if (commentsLoaded) {
- toggleVis(divComments)
- return
+ return false
+ if (commentsLoaded)
+ return toggleVis(divComments)
+
+ // data
+ const data = await getApiReq(`/comments?id=${player.playerInfo.videoData.video_id }`)
+
+ // pop
+ function _genComment(commentData, parent) {
+ const c = commentTemplate.content.cloneNode(true)
+
+ const fieldElements = fillFields(commentData, c)
+ if (commentData.published === commentData.updated)
+ fieldElements.updated.parentElement.remove()
+
+ if (commentData.replies) {
+ const divReplies = c.querySelector(".replies")
+ for (const rc of commentData.replies)
+ _genComment(rc, divReplies)
+ }
+ parent.append(c)
}
- // req
- const d = (await getApiReq(`/comments?id=${player.playerInfo.videoData.video_id }`))
-
- // populate
- for (const cdd of d.items) {
- const cd = cdd.snippet.topLevelComment.snippet
- const replies = cdd.replies ? cdd.replies.comments : null
- genComment(cd, divComments, replies)
- }
+ for (const commentData of data)
+ _genComment(commentData, divComments)
// done
commentsLoaded = true
- toggleVis(divComments)
+ return toggleVis(divComments)
}
+
+/* details button */
buttonDetails.addEventListener("click", () => {
toggleDetails()
toggleComments()
buttonDetails.remove()
})
+
/* timestamps in comments */
function timestampLinkClick(e) {
if (e.target.tagName !== "A")
return
+ // FIXME this needs to check origin as well
const url = new URL(e.target.href)
if (url.pathname !== "/watch" || url.searchParams.get("v") !== player.playerInfo.videoData.video_id)
return
@@ -180,6 +184,7 @@ function timestampLinkClick(e) {
player.seekTo(t)
// change url to match window in case clicking on youtube.com link
+ // FIXME get ridded of replace youtube links w/ host links
url.protocol = window.location.protocol
url.hostname = window.location.hostname
url.port = window.location.port