feat: webui

This commit is contained in:
2024-03-22 22:01:30 +07:00
parent cc57d5c8dc
commit 3fc3f1badd
9 changed files with 651 additions and 147 deletions

123
web/assets/index.js Normal file
View File

@ -0,0 +1,123 @@
const tokenInput = document.querySelector('#token');
const rememberTokenCheckbox = document.querySelector('#remember-token');
const setTokenButton = document.querySelector('#set-token');
const lessonInput = document.querySelector('#lesson-query');
const getLessonButton = document.querySelector('#get-lesson');
let token = localStorage.getItem('token') || '';
if (token !== '') {
tokenInput.value = token;
rememberTokenCheckbox.checked = true;
fetch("/api/v1/token", {
method: 'POST',
body: token,
}).then(rsp => {
if (rsp.status !== 200) {
setTokenButton.innerHTML = `:( (${rsp.status})`;
}
});
}
function randomString(length) {
return `a${Math.random().toString(36).slice(2, length + 2)}`;
}
// React simulator
function getResult(id) {
const element = document.createElement("div");
element.className = "block";
document.body.appendChild(document.createElement("br"));
document.body.appendChild(element);
return element;
}
setTokenButton.addEventListener('click', async () => {
token = tokenInput.value;
const rsp = await fetch("/api/v1/token", {
method: 'POST',
body: token,
});
if (rsp.status !== 200) {
setTokenButton.innerHTML = `:( (${rsp.status})`;
return;
}
if (rememberTokenCheckbox.checked) {
localStorage.setItem('token', token);
} else {
localStorage.removeItem('token');
}
});
getLessonButton.addEventListener('click', async () => {
const input = lessonInput.value;
let id = Number.parseInt(input);
if (Number.isNaN(id)) {
const url = input.split('/');
url.pop();
id = Number.parseInt(url.pop());
}
const rsp = await fetch(`/api/v1/lessons/${id}`);
const result = getResult("lesson");
const data = await rsp.json();
const ids = {};
// React :nerd:
result.innerHTML = "<h2>Lesson Information</h2>\n";
result.innerHTML += `Name: ${decodeURIComponent(data.name)}`;
result.innerHTML += "<h3>Videos available</h3>";
for (const video of data.videos) {
const uniqueId = randomString(15);
ids[uniqueId] = video;
result.innerHTML += `
<input type="checkbox" id="${uniqueId}" name="${uniqueId}" value="${uniqueId}">
<label for="${uniqueId}">${decodeURIComponent(video.name)}</label>
<br>`;
}
let videoDlResult = null;
const downloadSelectedButton = document.createElement("button");
downloadSelectedButton.innerHTML = "Download Selected";
downloadSelectedButton.addEventListener('click', async () => {
for (const [id, video] of Object.entries(ids)) {
const checkbox = document.querySelector(`#${id}`);
if (!checkbox.checked) {
continue;
}
const videoRsp = await fetch(`/api/v1/videos/${video.id}`);
if (videoRsp.status !== 200) {
console.error(`Failed to get video ${video.name}`);
continue;
}
const videoData = await videoRsp.json();
if (videoDlResult === null) {
result.appendChild(document.createElement("br"));
result.appendChild(document.createElement("br"));
videoDlResult = document.createElement("div");
videoDlResult.className = "block";
result.appendChild(videoDlResult);
}
const progressId = randomString(15);
const progressStrId = randomString(15);
videoDlResult.innerHTML += `<span>${video.name}.mp4 <progress id="${progressId}" value="0" max="100"></progress> <span id="${progressStrId}">0%</span></span>`
const rsp = await fetch(`/api/v1/download-video?${new URLSearchParams({
url: encodeURIComponent(videoData.m3u8),
output: encodeURIComponent(`videos/${video.name}.mp4`),
})}`);
const reader = rsp.body.getReader();
while (true) {
// wait for next encoded chunk
const { done, value } = await reader.read();
// check if stream is done
if (done) break;
// Decodes data chunk and yields it
const progress = Math.floor(Number.parseFloat(new TextDecoder().decode(value)));
document.querySelector(`#${progressId}`).value = progress;
document.querySelector(`#${progressStrId}`).innerText = `${progress}%`;
}
if (rsp.status !== 200) {
console.error(`Failed to download ${video.name}`);
}
}
});
result.appendChild(downloadSelectedButton);
// result.innerHTML += "<button>Download All</button>";
});

61
web/assets/style.css Normal file
View File

@ -0,0 +1,61 @@
* {
margin: 0;
padding: 0;
font-family: "Be Vietnam Pro", sans-serif;
}
.theme-dark {
background-color: #303030;
color: #fff;
}
header {
font-size: 32px;
padding: 10px;
background-color: #ffffff50;
}
.block {
width: fit-content;
border: 1px solid #fff;
border-radius: 10px;
margin: auto;
padding: 10px;
justify-content: center;
align-items: center;
}
button {
padding: 5px;
background-color: #303030;
color: #fff;
border: 1px solid #fff;
border-radius: 10px;
transition-duration: 0.2s;
cursor: pointer;
}
button:hover {
background-color: #444;
}
button:active {
background-color: #222;
}
input[type="password"],
input[type="text"] {
padding: 5px;
margin: 5px;
background-color: #303030;
color: #fff;
border: 1px solid #fff;
border-radius: 10px;
}
input[type="checkbox"] {
margin-bottom: 10px;
background-color: #303030;
color: #fff;
border: 1px solid #fff;
}