diff options
Diffstat (limited to 'static')
-rw-r--r-- | static/index.js | 175 | ||||
-rw-r--r-- | static/style.css | 67 |
2 files changed, 242 insertions, 0 deletions
diff --git a/static/index.js b/static/index.js new file mode 100644 index 0000000..e1c1f41 --- /dev/null +++ b/static/index.js @@ -0,0 +1,175 @@ +/* setup player */ +let player +const titleBar = document.getElementById("title") + +function onPlayerReady(event) { + document.title = titleBar.textContent = player.videoTitle +} + +function onYouTubeIframeAPIReady() { + player = new YT.Player("player", { + host: "https://www.youtube-nocookie.com", + events: { + "onReady": onPlayerReady, + }, + }) +} + +/* helpers */ +function getApiReq(path) { + return fetch("/api" + path) + .then(async res => { + if (!res.ok) { + const err = await res.text(); + throw new Error(`Server error: ${err}`); + } + return res.json() + }) + .then(data => { + return data + }) + .catch(error => { + console.error("Fetch error:", error) + }) +} + +function toggleVis(e) { + e.style.display = e.style.display ? null : "none" + //window.location.hash = e.id && !e.style.display ? e.id : "" +} + +function createAnchors(text) { + const urlRegex = /(https?:\/\/[a-zA-Z0-9#$%&-./=?@_~]+[a-zA-Z0-9/]|@[a-zA-Z0-9.-_]+|[0-9]+:[0-9]+)/gi; + return text.replace(urlRegex, url => { + let href = url + if (url[0] == "@") { + href = `https://youtube.com/${url}` + } else if (parseInt(url[0]) !== NaN) { + const ts = url.split(":").reverse() + let t = 0 + for (let i = 0; i < ts.length; i++) + t += ts[i] * Math.pow(60, i) + href = `https://youtube.com/watch?v=${player.playerInfo.videoData.video_id}&t=${t}` + } + return `<a href="${href}" target="_blank" rel="noopener noreferrer">${url}</a>` + }) +} + +/* setup video details buttons */ +const buttonDetails = document.getElementById("toggle-details") +const divDetails = document.getElementById("details") +let detailsLoaded = false + +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") + + // 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) + date.textContent = d.snippet.publishedAt + if (d.snippet.description) + desc.innerHTML = createAnchors(d.snippet.description.replaceAll("\n", "<br>")) + tags.textContent = d.snippet.tags.join(", ") + + // done + detailsLoaded = true + 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 = `(Modified ${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) +} + +async function toggleComments() { + if (!player) + return + if (commentsLoaded) { + toggleVis(divComments) + return + } + + // 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) + } + + // done + commentsLoaded = true + toggleVis(divComments) +} + +buttonDetails.addEventListener("click", () => { + toggleDetails() + toggleComments() + buttonDetails.remove() +}) + +/* timestamps in comments */ +function timestampLinkClick(e) { + if (e.target.tagName !== "A") + return + + const url = new URL(e.target.href) + if (url.pathname !== "/watch" || url.searchParams.get("v") !== player.playerInfo.videoData.video_id) + return + + e.preventDefault() + + const t = url.searchParams.get("t") + if (!t) + return + player.seekTo(t) +} + +document.addEventListener("click", timestampLinkClick) diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..d9e461f --- /dev/null +++ b/static/style.css @@ -0,0 +1,67 @@ +html { + background: #000; + color: #fff; +} + +body { + margin: 0; +} + +body, pre { + font-family: "Roboto", sans-serif; +} + +#player { + display: block; + width: 100vw; + height: 90vh; + max-height: 80vw; + margin: 1rem auto; + box-sizing: border-box; +} + +#title { + margin: .5rem; +} + +button { + margin: .5rem; + background: #222; + color: #fff; + cursor: pointer; + border: 0; + border-radius: .25rem; + padding: .5rem; +} + +#details, #comments { + padding: 0 1rem; +} + +.replies { + margin-left: 2rem; +} + +a { + color: lightblue; +} + +.comment { + background: #191919; + border-radius: .25rem; + margin: .5rem 0; + padding: .5rem; +} +.comment .author { + text-decoration: none; +} +.comment .date { + color: #aaa; + font-size-adjust: .4; +} +.comment .body { + margin-left: .5rem; +} +.replies .comment { + background: #ffffff0a +} |