Populating metadata
This commit is contained in:
251
background.js
251
background.js
@@ -10,6 +10,8 @@ const WAVEFORM_PATH_RE = /\.wv\.json$/i;
|
|||||||
const SCRAMBLED_PATH_RE = /-scrambled\//i;
|
const SCRAMBLED_PATH_RE = /-scrambled\//i;
|
||||||
const HASH_RE = /^[a-f0-9]{64}$/i;
|
const HASH_RE = /^[a-f0-9]{64}$/i;
|
||||||
const GRAPHQL_URL = "https://surfaces-graphql.splice.com/graphql";
|
const GRAPHQL_URL = "https://surfaces-graphql.splice.com/graphql";
|
||||||
|
const DEBUG_TRACE = true;
|
||||||
|
const DEBUG_TRACE_MARKER = "S3DBG_MAPTRACE_V1";
|
||||||
|
|
||||||
function isDomainAllowed(pageUrl) {
|
function isDomainAllowed(pageUrl) {
|
||||||
return typeof pageUrl === "string" && SPLICE_DOMAIN_RE.test(pageUrl);
|
return typeof pageUrl === "string" && SPLICE_DOMAIN_RE.test(pageUrl);
|
||||||
@@ -75,6 +77,28 @@ function normalizeUrl(url) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeName(value) {
|
||||||
|
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugMapTrace(stage, entry) {
|
||||||
|
if (!DEBUG_TRACE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const safe = entry && typeof entry === "object" ? entry : {};
|
||||||
|
console.debug(DEBUG_TRACE_MARKER, stage, {
|
||||||
|
sampleId: safe.sampleId || "",
|
||||||
|
fileName: safe.fileName || "",
|
||||||
|
key: safe.key || "",
|
||||||
|
bpm: safe.bpm || "",
|
||||||
|
tagsCount: Array.isArray(safe.tags) ? safe.tags.length : 0,
|
||||||
|
coverImageUrl: safe.coverImageUrl || "",
|
||||||
|
previewUrl: safe.previewUrl || "",
|
||||||
|
fullUrl: safe.fullUrl || "",
|
||||||
|
source: safe.source || ""
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function extractSampleIdFromUrl(url) {
|
function extractSampleIdFromUrl(url) {
|
||||||
try {
|
try {
|
||||||
const parsed = new URL(url);
|
const parsed = new URL(url);
|
||||||
@@ -128,7 +152,8 @@ function maybeClearTabState(tabId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mergeEntry(tabState, inputEntry, source) {
|
function mergeEntry(tabState, inputEntry, source, options) {
|
||||||
|
const setLatest = !options || options.setLatest !== false;
|
||||||
const sampleId = inputEntry.sampleId || extractSampleIdFromUrl(inputEntry.previewUrl || inputEntry.fullUrl || "");
|
const sampleId = inputEntry.sampleId || extractSampleIdFromUrl(inputEntry.previewUrl || inputEntry.fullUrl || "");
|
||||||
if (!sampleId) {
|
if (!sampleId) {
|
||||||
return null;
|
return null;
|
||||||
@@ -138,6 +163,11 @@ function mergeEntry(tabState, inputEntry, source) {
|
|||||||
sampleId,
|
sampleId,
|
||||||
previewUrl: "",
|
previewUrl: "",
|
||||||
fullUrl: "",
|
fullUrl: "",
|
||||||
|
fileName: "",
|
||||||
|
key: "",
|
||||||
|
bpm: "",
|
||||||
|
tags: [],
|
||||||
|
coverImageUrl: "",
|
||||||
source: source || "unknown",
|
source: source || "unknown",
|
||||||
capturedAt: Date.now()
|
capturedAt: Date.now()
|
||||||
};
|
};
|
||||||
@@ -158,9 +188,26 @@ function mergeEntry(tabState, inputEntry, source) {
|
|||||||
} else if (!merged.fullUrl && FULL_URL_BY_SAMPLE.has(sampleId)) {
|
} else if (!merged.fullUrl && FULL_URL_BY_SAMPLE.has(sampleId)) {
|
||||||
merged.fullUrl = FULL_URL_BY_SAMPLE.get(sampleId);
|
merged.fullUrl = FULL_URL_BY_SAMPLE.get(sampleId);
|
||||||
}
|
}
|
||||||
|
if (inputEntry.fileName) {
|
||||||
|
merged.fileName = String(inputEntry.fileName).trim();
|
||||||
|
}
|
||||||
|
if (typeof inputEntry.key === "string" && inputEntry.key.trim()) {
|
||||||
|
merged.key = inputEntry.key.trim();
|
||||||
|
}
|
||||||
|
if (typeof inputEntry.bpm === "string" && inputEntry.bpm.trim()) {
|
||||||
|
merged.bpm = inputEntry.bpm.trim();
|
||||||
|
}
|
||||||
|
if (Array.isArray(inputEntry.tags) && inputEntry.tags.length) {
|
||||||
|
merged.tags = Array.from(new Set(inputEntry.tags.map((x) => String(x).trim()).filter(Boolean)));
|
||||||
|
}
|
||||||
|
if (typeof inputEntry.coverImageUrl === "string" && inputEntry.coverImageUrl.trim()) {
|
||||||
|
merged.coverImageUrl = inputEntry.coverImageUrl.trim();
|
||||||
|
}
|
||||||
|
|
||||||
tabState.entriesBySample.set(sampleId, merged);
|
tabState.entriesBySample.set(sampleId, merged);
|
||||||
|
if (setLatest) {
|
||||||
tabState.latestSampleId = sampleId;
|
tabState.latestSampleId = sampleId;
|
||||||
|
}
|
||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,12 +223,28 @@ function upsertPreviewContext(tabId, context, source) {
|
|||||||
}
|
}
|
||||||
return mergeEntry(
|
return mergeEntry(
|
||||||
tabState,
|
tabState,
|
||||||
{ sampleId, previewUrl, fullUrl: context.fullUrl ? normalizeUrl(context.fullUrl) : "" },
|
{
|
||||||
source || "graphql_preview"
|
sampleId,
|
||||||
|
previewUrl,
|
||||||
|
fullUrl: context.fullUrl ? normalizeUrl(context.fullUrl) : "",
|
||||||
|
fileName: context.fileName || "",
|
||||||
|
key: context.key || "",
|
||||||
|
bpm: context.bpm || "",
|
||||||
|
tags: Array.isArray(context.tags) ? context.tags : [],
|
||||||
|
coverImageUrl: context.coverImageUrl || ""
|
||||||
|
},
|
||||||
|
source || "graphql_preview",
|
||||||
|
{ setLatest: false }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function upsertUrl(tabId, url, source) {
|
function upsertUrl(tabId, url, source) {
|
||||||
|
const sourceTag = source && typeof source === "object" ? source.tag || "" : source || "";
|
||||||
|
const sourceFileName = source && typeof source === "object" ? source.fileName || "" : "";
|
||||||
|
const sourceKey = source && typeof source === "object" ? source.key || "" : "";
|
||||||
|
const sourceBpm = source && typeof source === "object" ? source.bpm || "" : "";
|
||||||
|
const sourceTags = source && typeof source === "object" && Array.isArray(source.tags) ? source.tags : [];
|
||||||
|
const sourceCoverImageUrl = source && typeof source === "object" ? source.coverImageUrl || "" : "";
|
||||||
const normalized = normalizeUrl(url);
|
const normalized = normalizeUrl(url);
|
||||||
if (!normalized || !isAudioSampleUrl(normalized) || isWaveformUrl(normalized)) {
|
if (!normalized || !isAudioSampleUrl(normalized) || isWaveformUrl(normalized)) {
|
||||||
return null;
|
return null;
|
||||||
@@ -195,11 +258,16 @@ function upsertUrl(tabId, url, source) {
|
|||||||
const entry = {
|
const entry = {
|
||||||
sampleId,
|
sampleId,
|
||||||
previewUrl: isPreviewUrl(normalized) ? normalized : "",
|
previewUrl: isPreviewUrl(normalized) ? normalized : "",
|
||||||
fullUrl: isFullUrl(normalized) ? normalized : ""
|
fullUrl: isFullUrl(normalized) ? normalized : "",
|
||||||
|
fileName: sourceFileName,
|
||||||
|
key: sourceKey,
|
||||||
|
bpm: sourceBpm,
|
||||||
|
tags: sourceTags,
|
||||||
|
coverImageUrl: sourceCoverImageUrl
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof tabId === "number" && tabId >= 0) {
|
if (typeof tabId === "number" && tabId >= 0) {
|
||||||
return mergeEntry(ensureTabState(tabId), entry, source);
|
return mergeEntry(ensureTabState(tabId), entry, sourceTag);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (entry.fullUrl) {
|
if (entry.fullUrl) {
|
||||||
@@ -216,14 +284,32 @@ async function appendHistoryRows(rows) {
|
|||||||
const data = await browser.storage.local.get(HISTORY_KEY);
|
const data = await browser.storage.local.get(HISTORY_KEY);
|
||||||
const current = Array.isArray(data[HISTORY_KEY]) ? data[HISTORY_KEY] : [];
|
const current = Array.isArray(data[HISTORY_KEY]) ? data[HISTORY_KEY] : [];
|
||||||
const combined = [...current];
|
const combined = [...current];
|
||||||
const seen = new Set(current.map((item) => `${item.sampleId}|${item.url}`));
|
const seenMap = new Map();
|
||||||
|
current.forEach((item, idx) => {
|
||||||
|
seenMap.set(`${item.sampleId}|${item.url}`, idx);
|
||||||
|
});
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const key = `${row.sampleId}|${row.url}`;
|
const key = `${row.sampleId}|${row.url}`;
|
||||||
if (!row.url || seen.has(key)) {
|
if (!row.url) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
seen.add(key);
|
if (seenMap.has(key)) {
|
||||||
|
const existingIdx = seenMap.get(key);
|
||||||
|
const existing = combined[existingIdx] || {};
|
||||||
|
const hasCoverImageUrl = Object.prototype.hasOwnProperty.call(row, "coverImageUrl");
|
||||||
|
combined[existingIdx] = {
|
||||||
|
...existing,
|
||||||
|
...row,
|
||||||
|
fileName: row.fileName || existing.fileName || "",
|
||||||
|
key: row.key || existing.key || "",
|
||||||
|
bpm: row.bpm || existing.bpm || "",
|
||||||
|
tags: Array.isArray(row.tags) && row.tags.length ? row.tags : (existing.tags || []),
|
||||||
|
coverImageUrl: hasCoverImageUrl ? String(row.coverImageUrl || "") : (existing.coverImageUrl || "")
|
||||||
|
};
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
seenMap.set(key, combined.length);
|
||||||
combined.push(row);
|
combined.push(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +322,11 @@ async function appendDownloadHistory(entry, result) {
|
|||||||
sampleId: resolvedEntry.sampleId || "",
|
sampleId: resolvedEntry.sampleId || "",
|
||||||
previewUrl: resolvedEntry.previewUrl || "",
|
previewUrl: resolvedEntry.previewUrl || "",
|
||||||
fullUrl: resolvedEntry.fullUrl || "",
|
fullUrl: resolvedEntry.fullUrl || "",
|
||||||
|
fileName: resolvedEntry.fileName || "",
|
||||||
|
key: resolvedEntry.key || "",
|
||||||
|
bpm: resolvedEntry.bpm || "",
|
||||||
|
tags: Array.isArray(resolvedEntry.tags) ? resolvedEntry.tags : [],
|
||||||
|
coverImageUrl: resolvedEntry.coverImageUrl || "",
|
||||||
url: result.historyUrl || resolvedEntry.fullUrl || "",
|
url: result.historyUrl || resolvedEntry.fullUrl || "",
|
||||||
source: result.mode === "full" ? "full_download" : "decoded_preview",
|
source: result.mode === "full" ? "full_download" : "decoded_preview",
|
||||||
capturedAt: Date.now(),
|
capturedAt: Date.now(),
|
||||||
@@ -253,6 +344,11 @@ function makeDecodedPlaceholderRow(entry, source) {
|
|||||||
sampleId,
|
sampleId,
|
||||||
previewUrl: entry.previewUrl || "",
|
previewUrl: entry.previewUrl || "",
|
||||||
fullUrl: entry.fullUrl || "",
|
fullUrl: entry.fullUrl || "",
|
||||||
|
fileName: entry.fileName || "",
|
||||||
|
key: entry.key || "",
|
||||||
|
bpm: entry.bpm || "",
|
||||||
|
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
||||||
|
coverImageUrl: entry.coverImageUrl || "",
|
||||||
url: `decoded://${sampleId}`,
|
url: `decoded://${sampleId}`,
|
||||||
source: source || entry.source || "captured_preview",
|
source: source || entry.source || "captured_preview",
|
||||||
capturedAt: entry.capturedAt || Date.now(),
|
capturedAt: entry.capturedAt || Date.now(),
|
||||||
@@ -293,14 +389,28 @@ function decodeSpliceAudio(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function deriveFilename(entry, mode) {
|
function deriveFilename(entry, mode) {
|
||||||
const base = entry.sampleId || `splice-${Date.now()}`;
|
const sampleBase = entry.sampleId || `splice-${Date.now()}`;
|
||||||
|
const rawName = typeof entry.fileName === "string" ? entry.fileName.trim() : "";
|
||||||
|
const cleanName = rawName ? rawName.replace(/[<>:"/\\|?*\u0000-\u001f]/g, "_") : "";
|
||||||
|
const hasExt = cleanName.lastIndexOf(".") > 0;
|
||||||
|
const stem = hasExt ? cleanName.slice(0, cleanName.lastIndexOf(".")) : cleanName;
|
||||||
|
const ext = hasExt ? cleanName.slice(cleanName.lastIndexOf(".")).toLowerCase() : "";
|
||||||
|
|
||||||
|
const safeStem = stem || sampleBase;
|
||||||
if (mode === "full") {
|
if (mode === "full") {
|
||||||
const full = entry.fullUrl || "";
|
if (ext === ".wav" || ext === ".mp3" || ext === ".aif" || ext === ".aiff" || ext === ".flac") {
|
||||||
|
return `${safeStem}${ext}`;
|
||||||
|
}
|
||||||
|
const full = (entry.fullUrl || "").toLowerCase();
|
||||||
if (full.endsWith(".wav") || full.includes(".wav?")) {
|
if (full.endsWith(".wav") || full.includes(".wav?")) {
|
||||||
return `${base}.wav`;
|
return `${safeStem}.wav`;
|
||||||
}
|
}
|
||||||
|
if (full.endsWith(".mp3") || full.includes(".mp3?")) {
|
||||||
|
return `${safeStem}.mp3`;
|
||||||
}
|
}
|
||||||
return `${base}-decoded.mp3`;
|
return `${safeStem}.wav`;
|
||||||
|
}
|
||||||
|
return `${safeStem}_decoded.mp3`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function downloadDirectUrl(url, entry) {
|
async function downloadDirectUrl(url, entry) {
|
||||||
@@ -421,7 +531,12 @@ function findEntryInTabStates(sampleId) {
|
|||||||
return {
|
return {
|
||||||
sampleId: entry.sampleId || sampleId,
|
sampleId: entry.sampleId || sampleId,
|
||||||
previewUrl: normalizeUrl(entry.previewUrl || ""),
|
previewUrl: normalizeUrl(entry.previewUrl || ""),
|
||||||
fullUrl: normalizeUrl(entry.fullUrl || "")
|
fullUrl: normalizeUrl(entry.fullUrl || ""),
|
||||||
|
fileName: entry.fileName || "",
|
||||||
|
key: entry.key || "",
|
||||||
|
bpm: entry.bpm || "",
|
||||||
|
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
||||||
|
coverImageUrl: entry.coverImageUrl || ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,7 +554,16 @@ async function hydrateEntryFromHistory(sampleId) {
|
|||||||
const previewUrl = normalizeUrl(row.previewUrl || "");
|
const previewUrl = normalizeUrl(row.previewUrl || "");
|
||||||
const fullUrl = normalizeUrl(row.fullUrl || "");
|
const fullUrl = normalizeUrl(row.fullUrl || "");
|
||||||
if (previewUrl || fullUrl) {
|
if (previewUrl || fullUrl) {
|
||||||
return { sampleId, previewUrl, fullUrl };
|
return {
|
||||||
|
sampleId,
|
||||||
|
previewUrl,
|
||||||
|
fullUrl,
|
||||||
|
fileName: row.fileName || "",
|
||||||
|
key: row.key || "",
|
||||||
|
bpm: row.bpm || "",
|
||||||
|
tags: Array.isArray(row.tags) ? row.tags : [],
|
||||||
|
coverImageUrl: row.coverImageUrl || ""
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -451,7 +575,12 @@ async function resolveAndDownloadEntry(entryInput) {
|
|||||||
let normalizedEntry = {
|
let normalizedEntry = {
|
||||||
sampleId,
|
sampleId,
|
||||||
previewUrl: normalizeUrl(entry.previewUrl || ""),
|
previewUrl: normalizeUrl(entry.previewUrl || ""),
|
||||||
fullUrl: normalizeUrl(entry.fullUrl || "")
|
fullUrl: normalizeUrl(entry.fullUrl || ""),
|
||||||
|
fileName: entry.fileName || "",
|
||||||
|
key: entry.key || "",
|
||||||
|
bpm: entry.bpm || "",
|
||||||
|
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
||||||
|
coverImageUrl: entry.coverImageUrl || ""
|
||||||
};
|
};
|
||||||
|
|
||||||
if (normalizedEntry.sampleId && !normalizedEntry.previewUrl && !normalizedEntry.fullUrl) {
|
if (normalizedEntry.sampleId && !normalizedEntry.previewUrl && !normalizedEntry.fullUrl) {
|
||||||
@@ -499,6 +628,11 @@ function serializeEntry(entry) {
|
|||||||
sampleId: entry.sampleId || "",
|
sampleId: entry.sampleId || "",
|
||||||
previewUrl: entry.previewUrl || "",
|
previewUrl: entry.previewUrl || "",
|
||||||
fullUrl: entry.fullUrl || "",
|
fullUrl: entry.fullUrl || "",
|
||||||
|
fileName: entry.fileName || "",
|
||||||
|
key: entry.key || "",
|
||||||
|
bpm: entry.bpm || "",
|
||||||
|
tags: Array.isArray(entry.tags) ? entry.tags : [],
|
||||||
|
coverImageUrl: entry.coverImageUrl || "",
|
||||||
source: entry.source || "",
|
source: entry.source || "",
|
||||||
capturedAt: entry.capturedAt || Date.now()
|
capturedAt: entry.capturedAt || Date.now()
|
||||||
};
|
};
|
||||||
@@ -526,26 +660,33 @@ browser.runtime.onMessage.addListener((message, sender) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const contexts = Array.isArray(message.contexts) ? message.contexts : [];
|
const contexts = Array.isArray(message.contexts) ? message.contexts : [];
|
||||||
const rows = [];
|
|
||||||
for (const context of contexts) {
|
for (const context of contexts) {
|
||||||
const merged = upsertPreviewContext(tabId, context, "graphql_preview");
|
const merged = upsertPreviewContext(tabId, context, "graphql_preview");
|
||||||
if (merged) {
|
if (merged) {
|
||||||
const row = makeDecodedPlaceholderRow(merged, "graphql_preview");
|
debugMapTrace("CAPTURE_PREVIEW_CONTEXT_MERGED", merged);
|
||||||
if (row) {
|
|
||||||
rows.push(row);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
return Promise.resolve({ ok: true, added: contexts.length });
|
||||||
return appendHistoryRows(rows).then(() => ({ ok: true, added: contexts.length }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.type === "CAPTURE_URLS") {
|
if (message.type === "CAPTURE_URLS") {
|
||||||
const tabId = tabIdFromSender(sender);
|
const tabId = tabIdFromSender(sender);
|
||||||
const urls = Array.isArray(message.urls) ? message.urls : [];
|
const urls = Array.isArray(message.urls) ? message.urls : [];
|
||||||
|
const canAttachPlaybackMeta = urls.length === 1;
|
||||||
const rows = [];
|
const rows = [];
|
||||||
for (const raw of urls) {
|
for (const raw of urls) {
|
||||||
const merged = upsertUrl(tabId, raw, message.source || "network_preview");
|
const merged = upsertUrl(tabId, raw, {
|
||||||
if (merged && merged.previewUrl) {
|
tag: message.source || "network_preview",
|
||||||
|
fileName: canAttachPlaybackMeta ? (message.fileName || "") : "",
|
||||||
|
key: "",
|
||||||
|
bpm: "",
|
||||||
|
tags: [],
|
||||||
|
coverImageUrl: ""
|
||||||
|
});
|
||||||
|
if (merged) {
|
||||||
|
debugMapTrace("CAPTURE_URLS_MERGED", merged);
|
||||||
|
}
|
||||||
|
if (merged && merged.previewUrl && canAttachPlaybackMeta) {
|
||||||
const row = makeDecodedPlaceholderRow(merged, message.source || "network_preview");
|
const row = makeDecodedPlaceholderRow(merged, message.source || "network_preview");
|
||||||
if (row) {
|
if (row) {
|
||||||
rows.push(row);
|
rows.push(row);
|
||||||
@@ -597,6 +738,70 @@ browser.runtime.onMessage.addListener((message, sender) => {
|
|||||||
.catch((error) => ({ ok: false, error: error.message || "Download failed." }));
|
.catch((error) => ({ ok: false, error: error.message || "Download failed." }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "UPDATE_ACTIVE_PLAYBACK_META" || message.type === "UPDATE_ACTIVE_FILENAME") {
|
||||||
|
const tabId = tabIdFromSender(sender);
|
||||||
|
const state = TAB_CAPTURED.get(tabId);
|
||||||
|
if (!state || !state.latestSampleId) {
|
||||||
|
return Promise.resolve({ ok: false });
|
||||||
|
}
|
||||||
|
const latest = state.entriesBySample.get(state.latestSampleId);
|
||||||
|
if (!latest) {
|
||||||
|
return Promise.resolve({ ok: false });
|
||||||
|
}
|
||||||
|
const fileName = typeof message.fileName === "string" ? message.fileName.trim() : "";
|
||||||
|
const key = typeof message.key === "string" ? message.key.trim() : "";
|
||||||
|
const bpm = typeof message.bpm === "string" ? message.bpm.trim() : "";
|
||||||
|
const tags = Array.isArray(message.tags) ? message.tags.map((x) => String(x).trim()).filter(Boolean) : [];
|
||||||
|
const coverImageUrl = typeof message.coverImageUrl === "string" ? message.coverImageUrl.trim() : "";
|
||||||
|
const entries = Array.from(state.entriesBySample.values());
|
||||||
|
let targetEntry = null;
|
||||||
|
if (fileName) {
|
||||||
|
const wantedName = normalizeName(fileName);
|
||||||
|
const matches = entries
|
||||||
|
.filter((entry) => normalizeName(entry.fileName) === wantedName)
|
||||||
|
.sort((a, b) => (b.capturedAt || 0) - (a.capturedAt || 0));
|
||||||
|
if (matches.length) {
|
||||||
|
targetEntry = matches[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetEntry && state.latestSampleId) {
|
||||||
|
const latestEntry = state.entriesBySample.get(state.latestSampleId);
|
||||||
|
const freshEnough = latestEntry && (Date.now() - (latestEntry.capturedAt || 0) < 15000);
|
||||||
|
const canHydrate = latestEntry && !latestEntry.fileName;
|
||||||
|
if (freshEnough && canHydrate) {
|
||||||
|
targetEntry = latestEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!targetEntry) {
|
||||||
|
return Promise.resolve({ ok: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fileName) {
|
||||||
|
targetEntry.fileName = fileName;
|
||||||
|
}
|
||||||
|
if (key) {
|
||||||
|
targetEntry.key = key;
|
||||||
|
}
|
||||||
|
if (bpm) {
|
||||||
|
targetEntry.bpm = bpm;
|
||||||
|
}
|
||||||
|
if (tags.length) {
|
||||||
|
targetEntry.tags = Array.from(new Set(tags));
|
||||||
|
}
|
||||||
|
if (coverImageUrl) {
|
||||||
|
targetEntry.coverImageUrl = coverImageUrl;
|
||||||
|
}
|
||||||
|
targetEntry.capturedAt = Date.now();
|
||||||
|
state.entriesBySample.set(targetEntry.sampleId, targetEntry);
|
||||||
|
state.latestSampleId = targetEntry.sampleId;
|
||||||
|
debugMapTrace("UPDATE_ACTIVE_PLAYBACK_META_TARGET", targetEntry);
|
||||||
|
const row = makeDecodedPlaceholderRow(targetEntry, targetEntry.source || "playback_meta");
|
||||||
|
if (!row) {
|
||||||
|
return Promise.resolve({ ok: true });
|
||||||
|
}
|
||||||
|
return appendHistoryRows([row]).then(() => ({ ok: true }));
|
||||||
|
}
|
||||||
|
|
||||||
if (message.type === "DOWNLOAD_LATEST") {
|
if (message.type === "DOWNLOAD_LATEST") {
|
||||||
const tabId = tabIdFromSender(sender);
|
const tabId = tabIdFromSender(sender);
|
||||||
const latest = getLatestEntryForTab(tabId);
|
const latest = getLatestEntryForTab(tabId);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const SPLICE_DOMAIN_RE = /^https?:\/\/([a-z0-9-]+\.)*splice\.com(\/|$)/i;
|
|||||||
const GRAPHQL_URL_FRAGMENT = "surfaces-graphql.splice.com/graphql";
|
const GRAPHQL_URL_FRAGMENT = "surfaces-graphql.splice.com/graphql";
|
||||||
const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE";
|
const PROBE_EVENT_SOURCE = "S3_CAPTURE_PROBE";
|
||||||
const DEBUG_CAPTURE = true;
|
const DEBUG_CAPTURE = true;
|
||||||
|
const DEBUG_PLAYBACK_META_MARKER = "S3DBG_PLAYBAR_META_V2";
|
||||||
let knownUrls = new Set();
|
let knownUrls = new Set();
|
||||||
let knownPerformanceUrls = new Set();
|
let knownPerformanceUrls = new Set();
|
||||||
let observer = null;
|
let observer = null;
|
||||||
@@ -14,6 +15,13 @@ let performanceObserver = null;
|
|||||||
let fetchPatched = false;
|
let fetchPatched = false;
|
||||||
let xhrPatched = false;
|
let xhrPatched = false;
|
||||||
let probeListenerInstalled = false;
|
let probeListenerInstalled = false;
|
||||||
|
let currentPlaybackFilename = "";
|
||||||
|
let currentPlaybackKey = "";
|
||||||
|
let currentPlaybackBpm = "";
|
||||||
|
let currentPlaybackTags = [];
|
||||||
|
let currentPlaybackCoverImageUrl = "";
|
||||||
|
let lastPlaybackMetaSignature = "";
|
||||||
|
let lastPlaybackMetaApplied = false;
|
||||||
|
|
||||||
function isDomainAllowed(url) {
|
function isDomainAllowed(url) {
|
||||||
return typeof url === "string" && SPLICE_DOMAIN_RE.test(url);
|
return typeof url === "string" && SPLICE_DOMAIN_RE.test(url);
|
||||||
@@ -55,6 +63,121 @@ function normalizeUrl(rawUrl) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeFileName(raw) {
|
||||||
|
if (typeof raw !== "string") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const parts = trimmed.split("/");
|
||||||
|
return parts[parts.length - 1] || trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackRoot() {
|
||||||
|
const playbar = document.querySelector(TARGET_SELECTOR);
|
||||||
|
if (playbar) {
|
||||||
|
return playbar;
|
||||||
|
}
|
||||||
|
const filenameNode = document.querySelector('[data-qa="filename"]');
|
||||||
|
if (filenameNode && typeof filenameNode.closest === "function") {
|
||||||
|
return filenameNode.closest(".playbar-playback-info-bar");
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function queryPlaybackNode(selector) {
|
||||||
|
const root = getPlaybackRoot();
|
||||||
|
if (!root) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return root.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackFilenameFromDom() {
|
||||||
|
const el = queryPlaybackNode('[data-qa="filename"]');
|
||||||
|
if (!el) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return normalizeFileName(el.textContent || "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackKeyFromDom() {
|
||||||
|
const el = queryPlaybackNode('[data-qa="key"]');
|
||||||
|
return el ? String(el.textContent || "").trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackBpmFromDom() {
|
||||||
|
const el = queryPlaybackNode('[data-qa="bpm"]');
|
||||||
|
return el ? String(el.textContent || "").trim() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackTagsFromDom() {
|
||||||
|
const root = queryPlaybackNode('[data-qa="tags"]');
|
||||||
|
if (!root) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const tags = [];
|
||||||
|
const spans = root.querySelectorAll("span.tag");
|
||||||
|
for (const span of spans) {
|
||||||
|
const value = String(span.textContent || "").trim();
|
||||||
|
if (value) {
|
||||||
|
tags.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(new Set(tags));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPlaybackCoverImageFromDom() {
|
||||||
|
const img = queryPlaybackNode('[data-qa="cover-art-image"]');
|
||||||
|
if (!img) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const src = img.getAttribute("src") || img.currentSrc || "";
|
||||||
|
return normalizeUrl(src);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCoverImageFromGraphqlItem(item) {
|
||||||
|
if (!item || typeof item !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
const ownFiles = Array.isArray(item.files) ? item.files : [];
|
||||||
|
for (const file of ownFiles) {
|
||||||
|
if (!file || typeof file !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const typeSlug = String(file.asset_file_type_slug || "").toLowerCase();
|
||||||
|
if (typeSlug !== "cover_image") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = normalizeUrl(file.url || "");
|
||||||
|
if (url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!item.parents || !Array.isArray(item.parents.items)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
for (const parent of item.parents.items) {
|
||||||
|
const files = parent && Array.isArray(parent.files) ? parent.files : [];
|
||||||
|
for (const file of files) {
|
||||||
|
if (!file || typeof file !== "object") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const typeSlug = String(file.asset_file_type_slug || "").toLowerCase();
|
||||||
|
if (typeSlug !== "cover_image") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const url = normalizeUrl(file.url || "");
|
||||||
|
if (url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
function logDebug(message, extra) {
|
function logDebug(message, extra) {
|
||||||
if (!DEBUG_CAPTURE) {
|
if (!DEBUG_CAPTURE) {
|
||||||
return;
|
return;
|
||||||
@@ -142,6 +265,13 @@ function extractPreviewContextsFromGraphql(json) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const sampleId = typeof item.uuid === "string" ? item.uuid.toLowerCase() : "";
|
const sampleId = typeof item.uuid === "string" ? item.uuid.toLowerCase() : "";
|
||||||
|
const itemName = normalizeFileName(String(item.name || ""));
|
||||||
|
const itemKey = typeof item.key === "string" ? item.key.trim().toUpperCase() : "";
|
||||||
|
const itemBpm = item.bpm == null ? "" : String(item.bpm).trim();
|
||||||
|
const itemTags = Array.isArray(item.tags)
|
||||||
|
? Array.from(new Set(item.tags.map((tag) => (tag && tag.label ? String(tag.label).trim() : "")).filter(Boolean)))
|
||||||
|
: [];
|
||||||
|
const itemCoverImageUrl = extractCoverImageFromGraphqlItem(item);
|
||||||
const files = Array.isArray(item.files) ? item.files : [];
|
const files = Array.isArray(item.files) ? item.files : [];
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
@@ -155,7 +285,12 @@ function extractPreviewContextsFromGraphql(json) {
|
|||||||
}
|
}
|
||||||
contexts.push({
|
contexts.push({
|
||||||
sampleId: sampleId || extractSampleIdFromUrl(fileUrl),
|
sampleId: sampleId || extractSampleIdFromUrl(fileUrl),
|
||||||
previewUrl: fileUrl
|
previewUrl: fileUrl,
|
||||||
|
fileName: itemName,
|
||||||
|
key: itemKey,
|
||||||
|
bpm: itemBpm,
|
||||||
|
tags: itemTags,
|
||||||
|
coverImageUrl: itemCoverImageUrl
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -221,7 +356,8 @@ function captureUrls(inputUrls) {
|
|||||||
browser.runtime.sendMessage({
|
browser.runtime.sendMessage({
|
||||||
type: "CAPTURE_URLS",
|
type: "CAPTURE_URLS",
|
||||||
urls: newUrls,
|
urls: newUrls,
|
||||||
source: "content_capture"
|
source: "content_capture",
|
||||||
|
fileName: getPlaybackFilenameFromDom()
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
// Ignore transient extension context failures.
|
// Ignore transient extension context failures.
|
||||||
});
|
});
|
||||||
@@ -499,6 +635,7 @@ function startUiObserver() {
|
|||||||
}
|
}
|
||||||
observer = new MutationObserver(() => {
|
observer = new MutationObserver(() => {
|
||||||
injectControls();
|
injectControls();
|
||||||
|
syncPlaybackMeta();
|
||||||
});
|
});
|
||||||
observer.observe(document.documentElement, { childList: true, subtree: true });
|
observer.observe(document.documentElement, { childList: true, subtree: true });
|
||||||
}
|
}
|
||||||
@@ -527,6 +664,71 @@ function startCaptureObserver() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function syncPlaybackMeta() {
|
||||||
|
const fileName = getPlaybackFilenameFromDom();
|
||||||
|
const key = getPlaybackKeyFromDom();
|
||||||
|
const bpm = getPlaybackBpmFromDom();
|
||||||
|
const tags = getPlaybackTagsFromDom();
|
||||||
|
const coverImageUrl = getPlaybackCoverImageFromDom();
|
||||||
|
|
||||||
|
const sameTags =
|
||||||
|
tags.length === currentPlaybackTags.length &&
|
||||||
|
tags.every((tag, idx) => tag === currentPlaybackTags[idx]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
(!fileName || fileName === currentPlaybackFilename) &&
|
||||||
|
key === currentPlaybackKey &&
|
||||||
|
bpm === currentPlaybackBpm &&
|
||||||
|
sameTags &&
|
||||||
|
coverImageUrl === currentPlaybackCoverImageUrl
|
||||||
|
) {
|
||||||
|
const signature = JSON.stringify({
|
||||||
|
fileName: currentPlaybackFilename,
|
||||||
|
key: currentPlaybackKey,
|
||||||
|
bpm: currentPlaybackBpm,
|
||||||
|
tags: currentPlaybackTags,
|
||||||
|
coverImageUrl: currentPlaybackCoverImageUrl
|
||||||
|
});
|
||||||
|
if (lastPlaybackMetaApplied && signature === lastPlaybackMetaSignature) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (fileName) {
|
||||||
|
currentPlaybackFilename = fileName;
|
||||||
|
}
|
||||||
|
currentPlaybackKey = key;
|
||||||
|
currentPlaybackBpm = bpm;
|
||||||
|
currentPlaybackTags = tags;
|
||||||
|
currentPlaybackCoverImageUrl = coverImageUrl;
|
||||||
|
const payload = {
|
||||||
|
type: "UPDATE_ACTIVE_PLAYBACK_META",
|
||||||
|
fileName: currentPlaybackFilename,
|
||||||
|
key: currentPlaybackKey,
|
||||||
|
bpm: currentPlaybackBpm,
|
||||||
|
tags: currentPlaybackTags,
|
||||||
|
coverImageUrl: currentPlaybackCoverImageUrl
|
||||||
|
};
|
||||||
|
if (DEBUG_CAPTURE) {
|
||||||
|
console.debug(DEBUG_PLAYBACK_META_MARKER, {
|
||||||
|
fileName: payload.fileName,
|
||||||
|
key: payload.key,
|
||||||
|
bpm: payload.bpm,
|
||||||
|
tagsCount: Array.isArray(payload.tags) ? payload.tags.length : 0,
|
||||||
|
coverImageUrl: payload.coverImageUrl
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const signature = JSON.stringify(payload);
|
||||||
|
lastPlaybackMetaSignature = signature;
|
||||||
|
lastPlaybackMetaApplied = false;
|
||||||
|
browser.runtime.sendMessage(payload).then((result) => {
|
||||||
|
// Keep retrying on the next interval until background confirms attachment.
|
||||||
|
lastPlaybackMetaApplied = Boolean(result && result.ok);
|
||||||
|
}).catch(() => {
|
||||||
|
lastPlaybackMetaApplied = false;
|
||||||
|
// Ignore transient extension messaging failures.
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function installPageProbeListener() {
|
function installPageProbeListener() {
|
||||||
if (probeListenerInstalled) {
|
if (probeListenerInstalled) {
|
||||||
return;
|
return;
|
||||||
@@ -584,15 +786,21 @@ function init() {
|
|||||||
|
|
||||||
installPageProbeListener();
|
installPageProbeListener();
|
||||||
injectPageProbeScript();
|
injectPageProbeScript();
|
||||||
|
currentPlaybackFilename = getPlaybackFilenameFromDom();
|
||||||
|
currentPlaybackKey = getPlaybackKeyFromDom();
|
||||||
|
currentPlaybackBpm = getPlaybackBpmFromDom();
|
||||||
|
currentPlaybackTags = getPlaybackTagsFromDom();
|
||||||
|
currentPlaybackCoverImageUrl = getPlaybackCoverImageFromDom();
|
||||||
|
lastPlaybackMetaSignature = "";
|
||||||
|
lastPlaybackMetaApplied = false;
|
||||||
scanNodeForUrls(document);
|
scanNodeForUrls(document);
|
||||||
patchFetch();
|
patchFetch();
|
||||||
patchXhr();
|
patchXhr();
|
||||||
capturePerformanceResourceUrls();
|
// Avoid buffered resource sweeps; they can re-capture stale previews.
|
||||||
startPerformanceObserver();
|
|
||||||
window.setInterval(capturePerformanceResourceUrls, 2000);
|
|
||||||
injectControls();
|
injectControls();
|
||||||
startUiObserver();
|
startUiObserver();
|
||||||
startCaptureObserver();
|
startCaptureObserver();
|
||||||
|
window.setInterval(syncPlaybackMeta, 700);
|
||||||
}
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
|
|||||||
@@ -35,6 +35,15 @@ p {
|
|||||||
background: #151a26;
|
background: #151a26;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.history-cover-image {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 6px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.history-url {
|
.history-url {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
word-break: break-all;
|
word-break: break-all;
|
||||||
|
|||||||
27
history.js
27
history.js
@@ -11,18 +11,38 @@ function formatDate(ms) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMeta(entry) {
|
||||||
|
const key = entry && entry.key ? entry.key : "n/a";
|
||||||
|
const bpm = entry && entry.bpm ? entry.bpm : "n/a";
|
||||||
|
const tags = entry && Array.isArray(entry.tags) && entry.tags.length ? entry.tags.join(", ") : "n/a";
|
||||||
|
return `Key: ${key} | BPM: ${bpm} | Tags: ${tags}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildItem(entry) {
|
function buildItem(entry) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "history-item";
|
li.className = "history-item";
|
||||||
|
|
||||||
|
if (entry.coverImageUrl) {
|
||||||
|
const cover = document.createElement("img");
|
||||||
|
cover.className = "history-cover-image";
|
||||||
|
cover.src = entry.coverImageUrl;
|
||||||
|
cover.alt = "Cover art";
|
||||||
|
cover.loading = "lazy";
|
||||||
|
li.appendChild(cover);
|
||||||
|
}
|
||||||
|
|
||||||
const url = document.createElement("p");
|
const url = document.createElement("p");
|
||||||
url.className = "history-url";
|
url.className = "history-url";
|
||||||
url.textContent = entry.fullUrl || entry.url || "";
|
url.textContent = entry.fileName || entry.sampleId || "(unknown filename)";
|
||||||
|
|
||||||
const meta = document.createElement("p");
|
const meta = document.createElement("p");
|
||||||
meta.className = "history-meta";
|
meta.className = "history-meta";
|
||||||
const mode = entry.fullUrl ? "full candidate" : "preview fallback";
|
const mode = entry.mode === "full" || entry.fullUrl ? "full candidate" : "preview fallback";
|
||||||
meta.textContent = `Captured: ${formatDate(entry.capturedAt)} | Mode: ${mode} | Source: ${entry.source || "unknown"}`;
|
meta.textContent = `Captured: ${formatDate(entry.capturedAt)} | Mode: ${mode} | Source: ${entry.source || "unknown"} | ID: ${entry.sampleId || "n/a"}`;
|
||||||
|
|
||||||
|
const details = document.createElement("p");
|
||||||
|
details.className = "history-meta";
|
||||||
|
details.textContent = formatMeta(entry);
|
||||||
|
|
||||||
const download = document.createElement("button");
|
const download = document.createElement("button");
|
||||||
download.type = "button";
|
download.type = "button";
|
||||||
@@ -41,6 +61,7 @@ function buildItem(entry) {
|
|||||||
|
|
||||||
li.appendChild(url);
|
li.appendChild(url);
|
||||||
li.appendChild(meta);
|
li.appendChild(meta);
|
||||||
|
li.appendChild(details);
|
||||||
li.appendChild(download);
|
li.appendChild(download);
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,15 @@ button.secondary {
|
|||||||
padding: 8px;
|
padding: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.cover-image {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 4px;
|
||||||
|
object-fit: cover;
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.url-text {
|
.url-text {
|
||||||
margin: 0 0 8px;
|
margin: 0 0 8px;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
|
|||||||
25
popup.js
25
popup.js
@@ -12,17 +12,37 @@ function entryLabel(entry) {
|
|||||||
return entry && entry.fullUrl ? "full candidate" : "preview fallback";
|
return entry && entry.fullUrl ? "full candidate" : "preview fallback";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatMeta(entry) {
|
||||||
|
const key = entry && entry.key ? entry.key : "n/a";
|
||||||
|
const bpm = entry && entry.bpm ? entry.bpm : "n/a";
|
||||||
|
const tags = entry && Array.isArray(entry.tags) && entry.tags.length ? entry.tags.join(", ") : "n/a";
|
||||||
|
return `Key: ${key} | BPM: ${bpm} | Tags: ${tags}`;
|
||||||
|
}
|
||||||
|
|
||||||
function buildRow(entry) {
|
function buildRow(entry) {
|
||||||
const li = document.createElement("li");
|
const li = document.createElement("li");
|
||||||
li.className = "url-item";
|
li.className = "url-item";
|
||||||
|
|
||||||
|
if (entry.coverImageUrl) {
|
||||||
|
const cover = document.createElement("img");
|
||||||
|
cover.className = "cover-image";
|
||||||
|
cover.src = entry.coverImageUrl;
|
||||||
|
cover.alt = "Cover art";
|
||||||
|
cover.loading = "lazy";
|
||||||
|
li.appendChild(cover);
|
||||||
|
}
|
||||||
|
|
||||||
const p = document.createElement("p");
|
const p = document.createElement("p");
|
||||||
p.className = "url-text";
|
p.className = "url-text";
|
||||||
p.textContent = entry.previewUrl || entry.fullUrl || "";
|
p.textContent = entry.fileName || entry.sampleId || "(unknown filename)";
|
||||||
|
|
||||||
const status = document.createElement("p");
|
const status = document.createElement("p");
|
||||||
status.className = "status";
|
status.className = "status";
|
||||||
status.textContent = `Source: ${entry.source || "unknown"} | Mode: ${entryLabel(entry)}`;
|
status.textContent = `Source: ${entry.source || "unknown"} | Mode: ${entryLabel(entry)} | ID: ${entry.sampleId || "n/a"}`;
|
||||||
|
|
||||||
|
const meta = document.createElement("p");
|
||||||
|
meta.className = "status";
|
||||||
|
meta.textContent = formatMeta(entry);
|
||||||
|
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "actions";
|
actions.className = "actions";
|
||||||
@@ -52,6 +72,7 @@ function buildRow(entry) {
|
|||||||
actions.appendChild(copy);
|
actions.appendChild(copy);
|
||||||
li.appendChild(p);
|
li.appendChild(p);
|
||||||
li.appendChild(status);
|
li.appendChild(status);
|
||||||
|
li.appendChild(meta);
|
||||||
li.appendChild(actions);
|
li.appendChild(actions);
|
||||||
return li;
|
return li;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user