Updated styling, formatting, structure of frontend

This commit is contained in:
rockerBOO
2025-04-30 20:04:21 -04:00
parent 18ce902aa0
commit 71a30512c9
60 changed files with 3683 additions and 2451 deletions

View File

@@ -1,6 +1,6 @@
var video_data = JSON.parse(document.getElementById('video_data').textContent);
var spinnerHTML = '<h3 style="text-align:center"><div class="loading"><i class="icon ion-ios-refresh"></i></div></h3>';
var spinnerHTML = '<div class="loading"><i class="icon ion-ios-refresh"></i></div>';
var spinnerHTMLwithHR = spinnerHTML + '<hr>';
String.prototype.supplant = function (o) {
@@ -11,14 +11,14 @@ String.prototype.supplant = function (o) {
};
function toggle_comments(event) {
var target = event.target;
var body = target.parentNode.parentNode.parentNode.children[1];
if (body.style.display === 'none') {
target.textContent = '[ ]';
body.style.display = '';
const target = event.target;
const comments = document.querySelector(".comments");
if (comments.style.display === 'none') {
target.textContent = '';
comments.style.display = '';
} else {
target.textContent = '[ + ]';
body.style.display = 'none';
target.textContent = '+';
comments.style.display = 'none';
}
}
@@ -39,6 +39,7 @@ function hide_youtube_replies(event) {
function show_youtube_replies(event) {
var target = event.target;
console.log(target);
var sub_text = target.getAttribute('data-inner-text');
var inner_text = target.getAttribute('data-sub-text');
@@ -75,23 +76,24 @@ function get_youtube_comments() {
helpers.xhr('GET', url, {retries: 5, entity_name: 'comments'}, {
on200: function (response) {
var commentInnerHtml = ' \
<div> \
<h3> \
<a href="javascript:void(0)">[ ]</a> \
<nav class="comments-header"> \
<ul> \
<li> \
<button class="secondary" id="toggle-comments"></button> \
{commentsText} \
</h3> \
<b> \
'
</li> \
\
<li>'
if (video_data.support_reddit) {
commentInnerHtml += ' <a href="javascript:void(0)" data-comments="reddit"> \
commentInnerHtml += ' <button data-comments="reddit"> \
{redditComments} \
</a> \
</button> \
'
}
commentInnerHtml += ' </b> \
</div> \
<div>{contentHtml}</div> \
<hr>'
commentInnerHtml += ' </li> \
</ul> \
</nav> \
<div class="comments">{contentHtml}</div>'
commentInnerHtml = commentInnerHtml.supplant({
contentHtml: response.contentHtml,
redditComments: video_data.reddit_comments_text,
@@ -104,9 +106,9 @@ function get_youtube_comments() {
})
});
comments.innerHTML = commentInnerHtml;
comments.children[0].children[0].children[0].onclick = toggle_comments;
document.getElementById("toggle-comments").onclick = toggle_comments;
if (video_data.support_reddit) {
comments.children[0].children[1].children[0].onclick = swap_comments;
comments.children[1].children[1].onclick = swap_comments;
}
},
onNon200: onNon200, // declared above
@@ -122,7 +124,7 @@ function get_youtube_comments() {
function get_youtube_replies(target, load_more, load_replies) {
var continuation = target.getAttribute('data-continuation');
var body = target.parentNode.parentNode;
var body = target.parentNode;
var fallback = body.innerHTML;
body.innerHTML = spinnerHTML;
var baseUrl = video_data.base_url || '/api/v1/comments/'+ video_data.id
@@ -140,26 +142,24 @@ function get_youtube_replies(target, load_more, load_replies) {
helpers.xhr('GET', url, {}, {
on200: function (response) {
if (load_more) {
body = body.parentNode.parentNode;
body = body.parentNode;
body.removeChild(body.lastElementChild);
body.insertAdjacentHTML('beforeend', response.contentHtml);
} else {
body.removeChild(body.lastElementChild);
var p = document.createElement('p');
var a = document.createElement('a');
p.appendChild(a);
var div = document.createElement('div');
var button = document.createElement('button');
div.appendChild(button);
a.href = 'javascript:void(0)';
a.onclick = hide_youtube_replies;
a.setAttribute('data-sub-text', video_data.hide_replies_text);
a.setAttribute('data-inner-text', video_data.show_replies_text);
a.textContent = video_data.hide_replies_text;
button.onclick = hide_youtube_replies;
button.setAttribute('data-sub-text', video_data.hide_replies_text);
button.setAttribute('data-inner-text', video_data.show_replies_text);
button.textContent = video_data.hide_replies_text;
var div = document.createElement('div');
div.innerHTML = response.contentHtml;
body.appendChild(p);
body.appendChild(div);
}
},
@@ -171,4 +171,4 @@ function get_youtube_replies(target, load_more, load_replies) {
body.innerHTML = fallback;
}
});
}
}

View File

@@ -58,13 +58,13 @@
el.onclick = function () { mark_unwatched(el); };
});
document.querySelectorAll('[data-onclick="add_playlist_video"]').forEach(function (el) {
el.onclick = function () { add_playlist_video(el); };
el.onclick = function (e) { add_playlist_video(e); };
});
document.querySelectorAll('[data-onclick="add_playlist_item"]').forEach(function (el) {
el.onclick = function () { add_playlist_item(el); };
el.onclick = function (e) { add_playlist_item(e); };
});
document.querySelectorAll('[data-onclick="remove_playlist_item"]').forEach(function (el) {
el.onclick = function () { remove_playlist_item(el); };
el.onclick = function (e) { remove_playlist_item(e); };
});
document.querySelectorAll('[data-onclick="revoke_token"]').forEach(function (el) {
el.onclick = function () { revoke_token(el); };

View File

@@ -1,131 +1,197 @@
'use strict';
var notification_data = JSON.parse(document.getElementById('notification_data').textContent);
"use strict";
var notification_data = JSON.parse(
document.getElementById("notification_data").textContent,
);
/** Boolean meaning 'some tab have stream' */
const STORAGE_KEY_STREAM = 'stream';
const STORAGE_KEY_STREAM = "stream";
/** Number of notifications. May be increased or reset */
const STORAGE_KEY_NOTIF_COUNT = 'notification_count';
const STORAGE_KEY_NOTIF_COUNT = "notification_count";
var notifications, delivered;
var notifications_mock = { close: function () { } };
var notifications_mock = { close: function () {} };
function get_subscriptions() {
helpers.xhr('GET', '/api/v1/auth/subscriptions', {
async function get_subscriptions_call() {
return new Promise((resolve) => {
helpers.xhr(
"GET",
"/api/v1/auth/subscriptions",
{
retries: 5,
entity_name: 'subscriptions'
}, {
on200: create_notification_stream
});
entity_name: "subscriptions",
},
{
on200: function (subscriptions) {
create_notification_stream(subscriptions);
resolve(subscriptions);
},
},
);
});
}
// Start the retry mechanism
const get_subscriptions = exponential_backoff(get_subscriptions_call, 100, 1000);
function create_notification_stream(subscriptions) {
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
notifications = new SSE(
'/api/v1/auth/notifications', {
withCredentials: true,
payload: 'topics=' + subscriptions.map(function (subscription) { return subscription.authorId; }).join(','),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
delivered = [];
// sse.js can't be replaced to EventSource in place as it lack support of payload and headers
// see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/EventSource
notifications = new SSE("/api/v1/auth/notifications", {
withCredentials: true,
payload:
"topics=" +
subscriptions
.map(function (subscription) {
return subscription.authorId;
})
.join(","),
headers: { "Content-Type": "application/x-www-form-urlencoded" },
});
delivered = [];
var start_time = Math.round(new Date() / 1000);
var start_time = Math.round(new Date() / 1000);
notifications.onmessage = function (event) {
if (!event.id) return;
notifications.onmessage = function (event) {
if (!event.id) return;
var notification = JSON.parse(event.data);
console.info('Got notification:', notification);
var notification = JSON.parse(event.data);
console.info("Got notification:", notification);
// Ignore not actual and delivered notifications
if (start_time > notification.published || delivered.includes(notification.videoId)) return;
// Ignore not actual and delivered notifications
if (
start_time > notification.published ||
delivered.includes(notification.videoId)
)
return;
delivered.push(notification.videoId);
delivered.push(notification.videoId);
let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
notification_count++;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
let notification_count = helpers.storage.get(STORAGE_KEY_NOTIF_COUNT) || 0;
notification_count++;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
update_ticker_count();
update_ticker_count();
// permission for notifications handled on settings page. JS handler is in handlers.js
if (window.Notification && Notification.permission === 'granted') {
var notification_text = notification.liveNow ? notification_data.live_now_text : notification_data.upload_text;
notification_text = notification_text.replace('`x`', notification.author);
// permission for notifications handled on settings page. JS handler is in handlers.js
if (window.Notification && Notification.permission === "granted") {
var notification_text = notification.liveNow
? notification_data.live_now_text
: notification_data.upload_text;
notification_text = notification_text.replace("`x`", notification.author);
var system_notification = new Notification(notification_text, {
body: notification.title,
icon: '/ggpht' + new URL(notification.authorThumbnails[2].url).pathname,
img: '/ggpht' + new URL(notification.authorThumbnails[4].url).pathname
});
var system_notification = new Notification(notification_text, {
body: notification.title,
icon: "/ggpht" + new URL(notification.authorThumbnails[2].url).pathname,
img: "/ggpht" + new URL(notification.authorThumbnails[4].url).pathname,
});
system_notification.onclick = function (e) {
open('/watch?v=' + notification.videoId, '_blank');
};
}
};
system_notification.onclick = function (e) {
open("/watch?v=" + notification.videoId, "_blank");
};
}
};
notifications.addEventListener('error', function (e) {
console.warn('Something went wrong with notifications, trying to reconnect...');
notifications = notifications_mock;
setTimeout(get_subscriptions, 1000);
});
notifications.addEventListener("error", function (e) {
console.warn(
"Something went wrong with notifications, trying to reconnect...",
);
notifications = notifications_mock;
notifications.stream();
});
notifications.stream();
}
function update_ticker_count() {
var notification_ticker = document.getElementById('notification_ticker');
var notification_ticker = document.getElementById("notification_ticker");
const notification_count = helpers.storage.get(STORAGE_KEY_STREAM);
if (notification_count > 0) {
notification_ticker.innerHTML =
'<span id="notification_count">' + notification_count + '</span> <i class="icon ion-ios-notifications"></i>';
} else {
notification_ticker.innerHTML =
'<i class="icon ion-ios-notifications-outline"></i>';
}
const notification_count = helpers.storage.get(STORAGE_KEY_STREAM);
if (notification_count > 0) {
notification_ticker.innerHTML =
'<span id="notification_count">' +
notification_count +
'</span> <i class="icon ion-ios-notifications"></i>';
} else {
notification_ticker.innerHTML =
'<i class="icon ion-ios-notifications-outline"></i>';
}
}
function start_stream_if_needed() {
// random wait for other tabs set 'stream' flag
setTimeout(function () {
if (!helpers.storage.get(STORAGE_KEY_STREAM)) {
// if no one set 'stream', set it by yourself and start stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
notifications = notifications_mock;
get_subscriptions();
}
}, Math.random() * 1000 + 50); // [0.050 .. 1.050) second
// random wait for other tabs set 'stream' flag
setTimeout(
function () {
if (!helpers.storage.get(STORAGE_KEY_STREAM)) {
// if no one set 'stream', set it by yourself and start stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
notifications = notifications_mock;
get_subscriptions();
}
},
Math.random() * 1000 + 50,
); // [0.050 .. 1.050) second
}
addEventListener("storage", function (e) {
if (e.key === STORAGE_KEY_NOTIF_COUNT) update_ticker_count();
addEventListener('storage', function (e) {
if (e.key === STORAGE_KEY_NOTIF_COUNT)
update_ticker_count();
// if 'stream' key was removed
if (e.key === STORAGE_KEY_STREAM && !helpers.storage.get(STORAGE_KEY_STREAM)) {
if (notifications) {
// restore it if we have active stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
} else {
start_stream_if_needed();
}
// if 'stream' key was removed
if (
e.key === STORAGE_KEY_STREAM &&
!helpers.storage.get(STORAGE_KEY_STREAM)
) {
if (notifications) {
// restore it if we have active stream
helpers.storage.set(STORAGE_KEY_STREAM, true);
} else {
start_stream_if_needed();
}
}
});
addEventListener('load', function () {
var notification_count_el = document.getElementById('notification_count');
var notification_count = notification_count_el ? parseInt(notification_count_el.textContent) : 0;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
addEventListener("load", function () {
var notification_count_el = document.getElementById("notification_count");
var notification_count = notification_count_el
? parseInt(notification_count_el.textContent)
: 0;
helpers.storage.set(STORAGE_KEY_NOTIF_COUNT, notification_count);
if (helpers.storage.get(STORAGE_KEY_STREAM))
helpers.storage.remove(STORAGE_KEY_STREAM);
start_stream_if_needed();
if (helpers.storage.get(STORAGE_KEY_STREAM))
helpers.storage.remove(STORAGE_KEY_STREAM);
start_stream_if_needed();
});
addEventListener('unload', function () {
// let chance to other tabs to be a streamer via firing 'storage' event
if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM);
addEventListener("unload", function () {
// let chance to other tabs to be a streamer via firing 'storage' event
if (notifications) helpers.storage.remove(STORAGE_KEY_STREAM);
});
function exponential_backoff(
fn,
maxRetries = 5,
initialDelay = 1000,
randomnessFactor = 0.5,
) {
let attempt = 0;
return function tryFunction() {
fn()
.then((response) => {
attempt = 0;
})
.catch((error) => {
if (attempt < maxRetries) {
attempt++;
let delay = initialDelay * Math.pow(2, attempt); // Exponential backoff
let randomMultiplier = 1 + Math.random() * randomnessFactor;
delay = delay * randomMultiplier;
console.log(
`Attempt ${attempt} failed. Retrying in ${(delay / 1000).toPrecision(2)} seconds...`,
);
setTimeout(tryFunction, delay); // Retry after delay
} else {
console.log("Max retries reached. Operation failed:", error);
}
});
}
}

View File

@@ -2,8 +2,9 @@
var playlist_data = JSON.parse(document.getElementById('playlist_data').textContent);
var payload = 'csrf_token=' + playlist_data.csrf_token;
function add_playlist_video(target) {
var select = target.parentNode.children[0].children[1];
function add_playlist_video(event) {
const target = event.target;
var select = document.querySelector("#playlists");
var option = select.children[select.selectedIndex];
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
@@ -12,37 +13,43 @@ function add_playlist_video(target) {
helpers.xhr('POST', url, {payload: payload}, {
on200: function (response) {
option.textContent = '✓' + option.textContent;
option.textContent = '✓ ' + option.textContent;
}
});
}
function add_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
function add_playlist_item(event) {
event.preventDefault();
const target = event.target;
const video_id = target.getAttribute('data-id');
var card = document.querySelector(`#video-card-${video_id}`);
card.classList.add("hide");
var url = '/playlist_ajax?action_add_video=1&redirect=false' +
'&video_id=' + target.getAttribute('data-id') +
'&video_id=' + video_id +
'&playlist_id=' + target.getAttribute('data-plid');
helpers.xhr('POST', url, {payload: payload}, {
onNon200: function (xhr) {
tile.style.display = '';
card.classList.remove("hide");
}
});
}
function remove_playlist_item(target) {
var tile = target.parentNode.parentNode.parentNode.parentNode.parentNode;
tile.style.display = 'none';
function remove_playlist_item(event) {
event.preventDefault();
const target = event.target;
const video_index = target.getAttribute('data-index');
const card = document.querySelector(`.video-card [data-index="${video_index}"]`)
card.classList.add("hide");
var url = '/playlist_ajax?action_remove_video=1&redirect=false' +
'&set_video_id=' + target.getAttribute('data-index') +
'&set_video_id=' + video_index +
'&playlist_id=' + target.getAttribute('data-plid');
helpers.xhr('POST', url, {payload: payload}, {
onNon200: function (xhr) {
tile.style.display = '';
card.classList.remove("hide");
}
});
}

View File

@@ -3,7 +3,6 @@ var subscribe_data = JSON.parse(document.getElementById('subscribe_data').textCo
var payload = 'csrf_token=' + subscribe_data.csrf_token;
var subscribe_button = document.getElementById('subscribe');
subscribe_button.parentNode.action = 'javascript:void(0)';
if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = subscribe;
@@ -11,10 +10,29 @@ if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.onclick = unsubscribe;
}
function subscribe() {
var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
function toggleSubscribeButton() {
subscribe_button.classList.remove("primary");
subscribe_button.classList.remove("secondary");
subscribe_button.classList.remove("unsubscribe");
subscribe_button.classList.remove("subscribe");
if (subscribe_button.getAttribute('data-type') === 'subscribe') {
subscribe_button.textContent = subscribe_data.unsubscribe_text + ' | ' + subscribe_data.sub_count_text;
subscribe_button.onclick = unsubscribe;
subscribe_button.classList.add("secondary");
subscribe_button.classList.add("unsubscribe");
} else {
subscribe_button.textContent = subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text;
subscribe_button.onclick = subscribe;
subscribe_button.classList.add("primary");
subscribe_button.classList.add("subscribe");
}
}
function subscribe(e) {
e.preventDefault();
var fallback = subscribe_button.textContent;
toggleSubscribeButton();
var url = '/subscription_ajax?action_create_subscription_to_channel=1&redirect=false' +
'&c=' + subscribe_data.ucid;
@@ -22,15 +40,15 @@ function subscribe() {
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'subscribe request'}, {
onNon200: function (xhr) {
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = fallback;
subscribe_button.textContent = fallback;
}
});
}
function unsubscribe() {
var fallback = subscribe_button.innerHTML;
subscribe_button.onclick = subscribe;
subscribe_button.innerHTML = '<b>' + subscribe_data.subscribe_text + ' | ' + subscribe_data.sub_count_text + '</b>';
function unsubscribe(e) {
e.preventDefault();
var fallback = subscribe_button.textContent;
toggleSubscribeButton();
var url = '/subscription_ajax?action_remove_subscriptions=1&redirect=false' +
'&c=' + subscribe_data.ucid;
@@ -38,7 +56,7 @@ function unsubscribe() {
helpers.xhr('POST', url, {payload: payload, retries: 5, entity_name: 'unsubscribe request'}, {
onNon200: function (xhr) {
subscribe_button.onclick = unsubscribe;
subscribe_button.innerHTML = fallback;
subscribe_button.textContent = fallback;
}
});
}

44
assets/js/theme.js Normal file
View File

@@ -0,0 +1,44 @@
const themeSelector = document.querySelector("#theme-selector");
themeSelector.addEventListener("change", (event) => {
const select = event.target;
const selected = select.options[select.selectedIndex].text;
applyTheme(selected);
});
const colorSchemeSelector = document.querySelector("#color-scheme");
colorSchemeSelector.addEventListener("change", (event) => {
const select = event.target;
const selected = select.options[select.selectedIndex].text;
applyColorScheme(selected);
});
function applyTheme(theme) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = `/css/theme-${theme}.css`;
link.id = "theme-css";
const themeCss = document.querySelector("#theme-css");
if (themeCss) {
themeCss.parentNode.removeChild(themeCss);
}
const head = document.getElementsByTagName("head")[0];
head.appendChild(link);
}
function applyColorScheme(colorScheme) {
document.body.classList.remove("dark-theme");
document.body.classList.remove("light-theme");
if (colorScheme === "dark" || colorScheme === "light") {
document.body.classList.add(`${colorScheme}-theme`);
}
}
applyTheme(themeSelector.options[themeSelector.selectedIndex].text);
applyColorScheme("dark");
// <link rel="stylesheet" href="/css/theme-dracula.css" />
// <link rel="stylesheet" href="/css/theme-catppuccin-latte.css" />
// <link rel="stylesheet" href="/css/ionicons.min.css" />

View File

@@ -1,13 +1,13 @@
'use strict';
var toggle_theme = document.getElementById('toggle_theme');
toggle_theme.href = 'javascript:void(0)';
const STORAGE_KEY_THEME = 'dark_mode';
const THEME_DARK = 'dark';
const THEME_LIGHT = 'light';
// TODO: theme state controlled by system
toggle_theme.addEventListener('click', function () {
toggle_theme.addEventListener('click', function (e) {
e.preventDefault();
const isDarkTheme = helpers.storage.get(STORAGE_KEY_THEME) === THEME_DARK;
const newTheme = isDarkTheme ? THEME_LIGHT : THEME_DARK;
setTheme(newTheme);

View File

@@ -69,6 +69,8 @@ function get_playlist(plid) {
helpers.xhr('GET', plid_url, {retries: 5, entity_name: 'playlist'}, {
on200: function (response) {
if (response === null) return;
playlist.innerHTML = response.playlistHtml;
if (!response.nextVideo) return;