mirror of
https://github.com/GetOpenScript/Reddit.openscript.git
synced 2026-09-18 08:05:45 +00:00
Fix mounting timing on playlists and initial page refresh
This commit is contained in:
136
YouTube.os.js
136
YouTube.os.js
@@ -1,6 +1,6 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name Reddit Comments on YouTube
|
// @name Reddit Comments on YouTube
|
||||||
// @version 1.0.0
|
// @version 1.0.1
|
||||||
// @description View Reddit discussion threads and comments directly above YouTube comments.
|
// @description View Reddit discussion threads and comments directly above YouTube comments.
|
||||||
// @match https://www.youtube.com/*
|
// @match https://www.youtube.com/*
|
||||||
// @run-at document_idle
|
// @run-at document_idle
|
||||||
@@ -8,13 +8,11 @@
|
|||||||
|
|
||||||
const MOUNT_ID = 'os-reddit-mount';
|
const MOUNT_ID = 'os-reddit-mount';
|
||||||
const STYLE_ID = 'os-reddit-style';
|
const STYLE_ID = 'os-reddit-style';
|
||||||
const COMMENTS_SELECTOR = '#comments';
|
|
||||||
const DESC_SELECTOR = '#description, #description-inline-expander, ytd-text-inline-expander';
|
const DESC_SELECTOR = '#description, #description-inline-expander, ytd-text-inline-expander';
|
||||||
|
|
||||||
let currentVideoId = null;
|
let currentVideoId = null;
|
||||||
let activePostId = null;
|
let activePostId = null;
|
||||||
let cachedPosts = [];
|
let cachedPosts = [];
|
||||||
let abortController = null;
|
|
||||||
|
|
||||||
const ensureStyles = () => {
|
const ensureStyles = () => {
|
||||||
if (document.getElementById(STYLE_ID)) return;
|
if (document.getElementById(STYLE_ID)) return;
|
||||||
@@ -22,7 +20,7 @@ const ensureStyles = () => {
|
|||||||
style.id = STYLE_ID;
|
style.id = STYLE_ID;
|
||||||
style.textContent = `
|
style.textContent = `
|
||||||
#${MOUNT_ID} {
|
#${MOUNT_ID} {
|
||||||
margin-bottom: 24px;
|
margin: 16px 0 24px;
|
||||||
font-family: Roboto, Arial, sans-serif;
|
font-family: Roboto, Arial, sans-serif;
|
||||||
color: var(--yt-spec-text-primary, #0f0f0f);
|
color: var(--yt-spec-text-primary, #0f0f0f);
|
||||||
}
|
}
|
||||||
@@ -109,9 +107,6 @@ const ensureStyles = () => {
|
|||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
.os-comment-inner {
|
|
||||||
padding: 4px 0;
|
|
||||||
}
|
|
||||||
.os-comment-tagline {
|
.os-comment-tagline {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -189,10 +184,21 @@ const ensureStyles = () => {
|
|||||||
background: var(--yt-spec-badge-chip-background, rgba(0,0,0,0.05));
|
background: var(--yt-spec-badge-chip-background, rgba(0,0,0,0.05));
|
||||||
}
|
}
|
||||||
.os-status {
|
.os-status {
|
||||||
padding: 12px 0;
|
padding: 10px 14px;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--yt-spec-badge-chip-background, rgba(0,0,0,0.04));
|
||||||
color: var(--yt-spec-text-secondary, #606060);
|
color: var(--yt-spec-text-secondary, #606060);
|
||||||
font-style: italic;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.os-retry-btn {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--yt-spec-brand-link-text, #065fd4);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.append(style);
|
document.head.append(style);
|
||||||
@@ -221,7 +227,7 @@ const timeAgo = utc => {
|
|||||||
|
|
||||||
const decodeHtml = str => {
|
const decodeHtml = str => {
|
||||||
const txt = document.createElement('textarea');
|
const txt = document.createElement('textarea');
|
||||||
txt.innerHTML = str;
|
txt.innerHTML = str || '';
|
||||||
return txt.value;
|
return txt.value;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -243,13 +249,42 @@ const getOfficialSub = () => {
|
|||||||
|
|
||||||
const getMount = () => {
|
const getMount = () => {
|
||||||
let mount = document.getElementById(MOUNT_ID);
|
let mount = document.getElementById(MOUNT_ID);
|
||||||
if (mount) return mount;
|
if (mount && mount.isConnected) return mount;
|
||||||
const target = document.querySelector(COMMENTS_SELECTOR);
|
|
||||||
if (!target || !target.parentNode) return null;
|
const comments = document.querySelector('#comments, ytd-comments');
|
||||||
|
if (comments && comments.parentNode) {
|
||||||
mount = document.createElement('div');
|
mount = document.createElement('div');
|
||||||
mount.id = MOUNT_ID;
|
mount.id = MOUNT_ID;
|
||||||
target.parentNode.insertBefore(mount, target);
|
comments.parentNode.insertBefore(mount, comments);
|
||||||
return mount;
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = document.querySelector('#below > ytd-watch-metadata, #below > #watch-metadata');
|
||||||
|
if (meta && meta.parentNode) {
|
||||||
|
mount = document.createElement('div');
|
||||||
|
mount.id = MOUNT_ID;
|
||||||
|
meta.parentNode.insertBefore(mount, meta.nextSibling);
|
||||||
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
const below = document.querySelector('#below');
|
||||||
|
if (below) {
|
||||||
|
mount = document.createElement('div');
|
||||||
|
mount.id = MOUNT_ID;
|
||||||
|
below.append(mount);
|
||||||
|
return mount;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const waitForMount = async (maxAttempts = 40) => {
|
||||||
|
for (let i = 0; i < maxAttempts; i++) {
|
||||||
|
const m = getMount();
|
||||||
|
if (m) return m;
|
||||||
|
await new Promise(r => setTimeout(r, 250));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderComments = (children, postAuthor) => {
|
const renderComments = (children, postAuthor) => {
|
||||||
@@ -258,7 +293,7 @@ const renderComments = (children, postAuthor) => {
|
|||||||
|
|
||||||
for (const child of children) {
|
for (const child of children) {
|
||||||
if (child.kind === 'more') {
|
if (child.kind === 'more') {
|
||||||
const { count, children: moreIds, id, parent_id } = child.data;
|
const { count, children: moreIds } = child.data;
|
||||||
if (!moreIds?.length) continue;
|
if (!moreIds?.length) continue;
|
||||||
const btn = document.createElement('button');
|
const btn = document.createElement('button');
|
||||||
btn.className = 'os-load-more';
|
btn.className = 'os-load-more';
|
||||||
@@ -273,8 +308,7 @@ const renderComments = (children, postAuthor) => {
|
|||||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
const items = json?.json?.data?.things?.map(t => t.data) || [];
|
const items = json?.json?.data?.things?.map(t => t.data) || [];
|
||||||
const rendered = renderFlatComments(items, postAuthor);
|
btn.replaceWith(renderFlatComments(items, postAuthor));
|
||||||
btn.replaceWith(rendered);
|
|
||||||
} catch {
|
} catch {
|
||||||
btn.textContent = 'Failed to load comments';
|
btn.textContent = 'Failed to load comments';
|
||||||
}
|
}
|
||||||
@@ -384,10 +418,7 @@ const loadPostComments = async (post, postContainer) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderThread = post => {
|
const renderThread = (post, mount) => {
|
||||||
const mount = getMount();
|
|
||||||
if (!mount) return;
|
|
||||||
|
|
||||||
let contentArea = mount.querySelector('.os-content-area');
|
let contentArea = mount.querySelector('.os-content-area');
|
||||||
if (!contentArea) {
|
if (!contentArea) {
|
||||||
contentArea = document.createElement('div');
|
contentArea = document.createElement('div');
|
||||||
@@ -422,22 +453,16 @@ const renderThread = post => {
|
|||||||
loadPostComments(post, commentsArea);
|
loadPostComments(post, commentsArea);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTabs = posts => {
|
const renderTabs = (posts, mount) => {
|
||||||
const mount = getMount();
|
mount.innerHTML = '';
|
||||||
if (!mount) return;
|
|
||||||
|
|
||||||
const officialSub = getOfficialSub();
|
const officialSub = getOfficialSub();
|
||||||
if (officialSub) {
|
if (officialSub) {
|
||||||
posts.sort((a, b) => (a.subreddit.toLowerCase() === officialSub ? -1 : b.subreddit.toLowerCase() === officialSub ? 1 : 0));
|
posts.sort((a, b) => (a.subreddit.toLowerCase() === officialSub ? -1 : b.subreddit.toLowerCase() === officialSub ? 1 : 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
let tabs = mount.querySelector('.os-tabs');
|
const tabs = document.createElement('div');
|
||||||
if (!tabs) {
|
|
||||||
tabs = document.createElement('div');
|
|
||||||
tabs.className = 'os-tabs';
|
tabs.className = 'os-tabs';
|
||||||
mount.prepend(tabs);
|
mount.append(tabs);
|
||||||
}
|
|
||||||
tabs.innerHTML = '';
|
|
||||||
|
|
||||||
const activePost = posts.find(p => p.name === activePostId) || posts[0];
|
const activePost = posts.find(p => p.name === activePostId) || posts[0];
|
||||||
activePostId = activePost.name;
|
activePostId = activePost.name;
|
||||||
@@ -465,22 +490,24 @@ const renderTabs = posts => {
|
|||||||
activePostId = post.name;
|
activePostId = post.name;
|
||||||
tabs.querySelectorAll('.os-tab').forEach(t => t.classList.remove('os-active'));
|
tabs.querySelectorAll('.os-tab').forEach(t => t.classList.remove('os-active'));
|
||||||
tab.classList.add('os-active');
|
tab.classList.add('os-active');
|
||||||
renderThread(post);
|
renderThread(post, mount);
|
||||||
};
|
};
|
||||||
|
|
||||||
tabs.append(tab);
|
tabs.append(tab);
|
||||||
});
|
});
|
||||||
|
|
||||||
renderThread(activePost);
|
renderThread(activePost, mount);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateForVideo = async videoId => {
|
const updateForVideo = async videoId => {
|
||||||
ensureStyles();
|
ensureStyles();
|
||||||
const mount = getMount();
|
|
||||||
if (!mount) return;
|
|
||||||
|
|
||||||
mount.innerHTML = '<div class="os-status">Searching Reddit for discussion threads...</div>';
|
|
||||||
cachedPosts = [];
|
cachedPosts = [];
|
||||||
|
activePostId = null;
|
||||||
|
|
||||||
|
const initialMount = getMount();
|
||||||
|
if (initialMount) {
|
||||||
|
initialMount.innerHTML = '<div class="os-status">Searching Reddit for discussion threads...</div>';
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await OpenScript.fetch(
|
const res = await OpenScript.fetch(
|
||||||
@@ -490,20 +517,41 @@ const updateForVideo = async videoId => {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
const posts = data?.data?.children?.map(c => c.data) || [];
|
const posts = data?.data?.children?.map(c => c.data) || [];
|
||||||
|
|
||||||
|
if (videoId !== currentVideoId) return;
|
||||||
|
|
||||||
|
const mount = await waitForMount();
|
||||||
|
if (!mount || videoId !== currentVideoId) return;
|
||||||
|
|
||||||
if (!posts.length) {
|
if (!posts.length) {
|
||||||
mount.innerHTML = '';
|
mount.innerHTML = '<div class="os-status">No Reddit discussions found for this video.</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cachedPosts = posts;
|
cachedPosts = posts;
|
||||||
renderTabs(posts);
|
renderTabs(posts, mount);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
mount.innerHTML = `<div class="os-status">Could not search Reddit (${err.message}).</div>`;
|
const mount = await waitForMount();
|
||||||
|
if (mount && videoId === currentVideoId) {
|
||||||
|
mount.innerHTML = `
|
||||||
|
<div class="os-status">
|
||||||
|
Could not search Reddit (${err.message}).
|
||||||
|
<button class="os-retry-btn">Retry</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
mount.querySelector('.os-retry-btn')?.addEventListener('click', () => updateForVideo(videoId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVideoId = () => {
|
||||||
|
const search = new URLSearchParams(location.search);
|
||||||
|
if (search.get('v')) return search.get('v');
|
||||||
|
const match = location.pathname.match(/\/shorts\/([a-zA-Z0-9_-]+)/);
|
||||||
|
return match ? match[1] : null;
|
||||||
|
};
|
||||||
|
|
||||||
const check = () => {
|
const check = () => {
|
||||||
const id = new URLSearchParams(location.search).get('v');
|
const id = getVideoId();
|
||||||
if (!id) {
|
if (!id) {
|
||||||
currentVideoId = null;
|
currentVideoId = null;
|
||||||
const mount = document.getElementById(MOUNT_ID);
|
const mount = document.getElementById(MOUNT_ID);
|
||||||
@@ -512,15 +560,15 @@ const check = () => {
|
|||||||
}
|
}
|
||||||
if (id === currentVideoId) {
|
if (id === currentVideoId) {
|
||||||
if (cachedPosts.length && !document.getElementById(MOUNT_ID)?.hasChildNodes()) {
|
if (cachedPosts.length && !document.getElementById(MOUNT_ID)?.hasChildNodes()) {
|
||||||
renderTabs(cachedPosts);
|
const mount = getMount();
|
||||||
|
if (mount) renderTabs(cachedPosts, mount);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currentVideoId = id;
|
currentVideoId = id;
|
||||||
activePostId = null;
|
|
||||||
updateForVideo(id);
|
updateForVideo(id);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('yt-navigate-finish', check);
|
document.addEventListener('yt-navigate-finish', check);
|
||||||
setInterval(check, 1200);
|
setInterval(check, 1000);
|
||||||
check();
|
check();
|
||||||
|
|||||||
Reference in New Issue
Block a user