diff options
| author | Tim Keller <tjkeller.xyz> | 2025-05-23 21:46:20 -0500 | 
|---|---|---|
| committer | Tim Keller <tjkeller.xyz> | 2025-05-23 21:46:20 -0500 | 
| commit | 865911f8ffdac7d1d9773570216c0bd35fc601d9 (patch) | |
| tree | 2c29ff9cfe78d95e095a39163ade4e83e4d56b92 /static | |
| download | mintube-865911f8ffdac7d1d9773570216c0bd35fc601d9.tar.xz mintube-865911f8ffdac7d1d9773570216c0bd35fc601d9.zip  | |
initial commit
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 +}  | 
