Hand over the whole item from the info panel
The panel is where you go to see exactly what the server said about a video, and every time that is worth reporting somewhere it has to be retyped from the screen. Copy JSON hands over the object instead: the listing item, plus the extractor's payload once that has resolved. What it copies is built where the rows are built, so the button and the panel can't disagree about what "this video" means -- and it is the values rather than the rendering, so a duration of 0 stays 0 instead of becoming the panel's dash. navigator.clipboard needs a secure context, which the app has on https and does not on a plain-http LAN address, so the old execCommand path sits behind it. The test drives the real clipboard rather than the function, and the awkward parts of a real item -- nested http_headers, a tag array, a zero, an empty string -- are in the fixture for that reason. It and two others also pick up smoke_grid's lean Chromium flags, without which they get themselves killed when run alongside everything else. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MPZFnNdHbPGDTqQUNiE4ZN
This commit is contained in:
@@ -69,6 +69,62 @@ App.ui = App.ui || {};
|
||||
// Which video the panel is currently showing, so a slow resolve that lands
|
||||
// after the user moved on doesn't redraw someone else's panel.
|
||||
let infoVideo = null;
|
||||
// Exactly what the panel is showing, as one object -- what the copy button
|
||||
// hands over. Kept beside the rendering rather than rebuilt on click, so
|
||||
// the two can't disagree about what "this video" means.
|
||||
let infoPayload = null;
|
||||
|
||||
// The clipboard proper needs a secure context, which a home-screen app on
|
||||
// https has and a plain-http LAN address does not -- hence the old
|
||||
// execCommand path behind it, which only works on a selection in the
|
||||
// document.
|
||||
const copyText = async function(text) {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
return true;
|
||||
} catch (err) { /* fall through to the old way */ }
|
||||
}
|
||||
const scratch = document.createElement('textarea');
|
||||
scratch.value = text;
|
||||
scratch.setAttribute('readonly', '');
|
||||
// Off-screen but focusable: display:none or visibility:hidden would
|
||||
// leave nothing to select, and a visible one would scroll the page.
|
||||
scratch.style.position = 'fixed';
|
||||
scratch.style.top = '-1000px';
|
||||
scratch.style.opacity = '0';
|
||||
document.body.appendChild(scratch);
|
||||
try {
|
||||
scratch.select();
|
||||
return document.execCommand('copy');
|
||||
} catch (err) {
|
||||
return false;
|
||||
} finally {
|
||||
scratch.remove();
|
||||
}
|
||||
};
|
||||
|
||||
let copyResetTimer = null;
|
||||
|
||||
App.ui.copyInfoJson = async function() {
|
||||
const button = document.getElementById('info-copy');
|
||||
if (!infoPayload) return false;
|
||||
const copied = await copyText(JSON.stringify(infoPayload, null, 2));
|
||||
if (!copied) {
|
||||
App.ui.showError('Could not copy to the clipboard.');
|
||||
return false;
|
||||
}
|
||||
if (button) {
|
||||
button.classList.add('is-copied');
|
||||
button.textContent = 'Copied';
|
||||
if (copyResetTimer) clearTimeout(copyResetTimer);
|
||||
copyResetTimer = setTimeout(() => {
|
||||
button.classList.remove('is-copied');
|
||||
button.textContent = 'Copy JSON';
|
||||
}, 1600);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const appendInfoHeading = function(list, label) {
|
||||
const heading = document.createElement('div');
|
||||
@@ -131,12 +187,16 @@ App.ui = App.ui || {};
|
||||
|
||||
if (title) title.textContent = item.title || (resolved && resolved.title) || 'Video Info';
|
||||
|
||||
// `meta` gets its own section below rather than a row of JSON.
|
||||
const own = Object.assign({}, item);
|
||||
delete own.meta;
|
||||
const section = opts.info ? 'extractor' : 'resolved';
|
||||
infoPayload = Object.assign({}, own);
|
||||
if (resolved && typeof resolved === 'object') infoPayload[section] = resolved;
|
||||
|
||||
let rows = 0;
|
||||
if (list) {
|
||||
list.innerHTML = "";
|
||||
// `meta` gets its own section below rather than a row of JSON.
|
||||
const own = Object.assign({}, item);
|
||||
delete own.meta;
|
||||
rows += appendInfoRows(list, own);
|
||||
|
||||
if (resolved && typeof resolved === 'object') {
|
||||
@@ -166,6 +226,13 @@ App.ui = App.ui || {};
|
||||
// nothing (or at a spinner) while it does.
|
||||
App.ui.openInfo = function(video) {
|
||||
infoVideo = video;
|
||||
// A fresh panel, so the button stops saying it copied the last one.
|
||||
const copyBtn = document.getElementById('info-copy');
|
||||
if (copyBtn) {
|
||||
if (copyResetTimer) clearTimeout(copyResetTimer);
|
||||
copyBtn.classList.remove('is-copied');
|
||||
copyBtn.textContent = 'Copy JSON';
|
||||
}
|
||||
const canResolve = !!(App.videos && typeof App.videos.fetchFullInfo === 'function');
|
||||
App.ui.showInfo(video, { pending: canResolve });
|
||||
if (!canResolve) return;
|
||||
@@ -179,6 +246,7 @@ App.ui = App.ui || {};
|
||||
const modal = document.getElementById('info-modal');
|
||||
if (!modal) return;
|
||||
infoVideo = null;
|
||||
infoPayload = null;
|
||||
modal.classList.remove('open');
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
};
|
||||
@@ -1098,5 +1166,12 @@ App.ui = App.ui || {};
|
||||
App.ui.closeInfo();
|
||||
});
|
||||
}
|
||||
|
||||
const infoCopy = document.getElementById('info-copy');
|
||||
if (infoCopy) {
|
||||
infoCopy.addEventListener('click', () => {
|
||||
App.ui.copyInfoJson();
|
||||
});
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user