|
1 | 1 | /* ============================================================ |
2 | | - OpenCut CEP Panel - Main Controller v1.9.27 |
| 2 | + OpenCut CEP Panel - Main Controller v1.9.28 |
3 | 3 | 6-Tab Professional Toolkit |
4 | 4 | ============================================================ */ |
5 | 5 | (function () { |
|
628 | 628 | el.sessionContext = $("sessionContext"); |
629 | 629 | el.sessionContextBody = $("sessionContextBody"); |
630 | 630 | el.sessionContextDismiss = $("sessionContextDismiss"); |
| 631 | + el.journalList = $("journalList"); |
| 632 | + el.journalRefreshBtn = $("journalRefreshBtn"); |
| 633 | + el.journalClearBtn = $("journalClearBtn"); |
631 | 634 |
|
632 | 635 | // Clip |
633 | 636 | el.clipSelect = $("clipSelect"); |
|
1357 | 1360 | }, |
1358 | 1361 | isProjectSaved: function (cb) { |
1359 | 1362 | jsx("isProjectSaved()", cb); |
| 1363 | + }, |
| 1364 | + // Journal inverse helpers (v1.9.28) |
| 1365 | + removeSequenceMarkers: function (payload, cb) { |
| 1366 | + var json = JSON.stringify(payload); |
| 1367 | + jsx("ocRemoveSequenceMarkers('" + json.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "')", cb); |
| 1368 | + }, |
| 1369 | + unrenameItems: function (payload, cb) { |
| 1370 | + var json = JSON.stringify(payload); |
| 1371 | + jsx("ocUnrenameItems('" + json.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "')", cb); |
| 1372 | + }, |
| 1373 | + removeImportedSequence: function (payload, cb) { |
| 1374 | + var json = JSON.stringify(payload); |
| 1375 | + jsx("ocRemoveImportedSequence('" + json.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "')", cb); |
| 1376 | + }, |
| 1377 | + removeImportedItem: function (payload, cb) { |
| 1378 | + var json = JSON.stringify(payload); |
| 1379 | + jsx("ocRemoveImportedItem('" + json.replace(/\\/g, "\\\\").replace(/'/g, "\\'") + "')", cb); |
1360 | 1380 | } |
1361 | 1381 | }; |
1362 | 1382 |
|
| 1383 | + // ================================================================ |
| 1384 | + // Operation Journal (v1.9.28) — frontend record + rollback |
| 1385 | + // ================================================================ |
| 1386 | + // Every ExtendScript operation that mutates the Premiere project calls |
| 1387 | + // journalRecord() after success. The Journal sub-tab under Settings |
| 1388 | + // renders the history and dispatches inverse calls via PremiereBridge. |
| 1389 | + function journalRecord(action, label, inversePayload, clipPath) { |
| 1390 | + if (!connected) return; |
| 1391 | + api("POST", "/journal/record", { |
| 1392 | + action: action, |
| 1393 | + label: label || "", |
| 1394 | + clip_path: clipPath || "", |
| 1395 | + inverse: inversePayload || {} |
| 1396 | + }, function (err, entry) { |
| 1397 | + if (err) { |
| 1398 | + // Never surface journal-record failures to the user — the |
| 1399 | + // forward operation already succeeded. |
| 1400 | + try { console.warn("journal record failed:", err); } catch (_) {} |
| 1401 | + return; |
| 1402 | + } |
| 1403 | + // Nudge the journal tab if it's currently visible. |
| 1404 | + if (typeof renderJournalList === "function" && el.journalList && |
| 1405 | + el.journalList.offsetParent !== null) { |
| 1406 | + renderJournalList(); |
| 1407 | + } |
| 1408 | + }); |
| 1409 | + } |
| 1410 | + |
| 1411 | + // Helpers to extract the label/inverse from a forward-operation result. |
| 1412 | + function _journalLabelForMarkers(count, clipName) { |
| 1413 | + var n = count | 0; |
| 1414 | + return n + " marker" + (n === 1 ? "" : "s") + |
| 1415 | + (clipName ? " on '" + clipName + "'" : ""); |
| 1416 | + } |
| 1417 | + function _journalLabelForRename(count) { |
| 1418 | + var n = count | 0; |
| 1419 | + return "Renamed " + n + " project item" + (n === 1 ? "" : "s"); |
| 1420 | + } |
| 1421 | + |
1363 | 1422 | // ================================================================ |
1364 | 1423 | // Backend Communication |
1365 | 1424 | // ================================================================ |
|
3121 | 3180 | showAlert("Import error: " + r.error); |
3122 | 3181 | } else if (r.sequence_name) { |
3123 | 3182 | showAlert("Opened: " + r.sequence_name); |
| 3183 | + journalRecord( |
| 3184 | + "import_sequence", |
| 3185 | + _sessionCtxOpText(job) + " → '" + r.sequence_name + "'", |
| 3186 | + { name: r.sequence_name }, |
| 3187 | + selectedPath |
| 3188 | + ); |
3124 | 3189 | } |
3125 | 3190 | } catch (e) { console.error("XML import parse error:", e); } |
3126 | 3191 | }); |
3127 | 3192 | lastXmlPath = xmlPath; |
3128 | 3193 | } |
3129 | | - |
| 3194 | +
|
3130 | 3195 | // Styled caption overlay video (.mov with alpha) |
3131 | 3196 | var overlayPath = job.result.overlay_path; |
3132 | 3197 | if (overlayPath) { |
|
5933 | 5998 | _sessionCtxDismissedAt = Date.now(); |
5934 | 5999 | } |
5935 | 6000 |
|
| 6001 | + // ================================================================ |
| 6002 | + // Operation Journal UI (v1.9.28) |
| 6003 | + // ================================================================ |
| 6004 | + function _journalActionLabel(action) { |
| 6005 | + return ({ |
| 6006 | + add_markers: "Add markers", |
| 6007 | + batch_rename: "Batch rename", |
| 6008 | + import_sequence: "Import sequence", |
| 6009 | + import_overlay: "Import overlay", |
| 6010 | + import_captions: "Import captions", |
| 6011 | + create_smart_bins: "Create bins" |
| 6012 | + })[action] || action; |
| 6013 | + } |
| 6014 | + |
| 6015 | + function renderJournalList() { |
| 6016 | + if (!el.journalList) return; |
| 6017 | + el.journalList.innerHTML = '<div class="journal-empty">Loading recent operations…</div>'; |
| 6018 | + api("GET", "/journal/list?limit=30", null, function (err, data) { |
| 6019 | + if (err) { |
| 6020 | + el.journalList.innerHTML = |
| 6021 | + '<div class="journal-empty journal-error">Couldn\'t load journal: ' + |
| 6022 | + esc(err.error || err.message || "Unknown error") + '</div>'; |
| 6023 | + return; |
| 6024 | + } |
| 6025 | + if (!Array.isArray(data) || !data.length) { |
| 6026 | + el.journalList.innerHTML = |
| 6027 | + '<div class="journal-empty">No operations recorded yet. Run an action that writes to your Premiere project and it will appear here.</div>'; |
| 6028 | + return; |
| 6029 | + } |
| 6030 | + el.journalList.innerHTML = ""; |
| 6031 | + var frag = document.createDocumentFragment(); |
| 6032 | + for (var i = 0; i < data.length; i++) { |
| 6033 | + frag.appendChild(_buildJournalRow(data[i])); |
| 6034 | + } |
| 6035 | + el.journalList.appendChild(frag); |
| 6036 | + }); |
| 6037 | + } |
| 6038 | + |
| 6039 | + function _buildJournalRow(entry) { |
| 6040 | + var row = document.createElement("div"); |
| 6041 | + row.className = "journal-row" + (entry.reverted ? " journal-row-reverted" : ""); |
| 6042 | + row.setAttribute("data-id", entry.id); |
| 6043 | + |
| 6044 | + var copy = document.createElement("div"); |
| 6045 | + copy.className = "journal-row-copy"; |
| 6046 | + |
| 6047 | + var title = document.createElement("div"); |
| 6048 | + title.className = "journal-row-title"; |
| 6049 | + title.textContent = _journalActionLabel(entry.action); |
| 6050 | + |
| 6051 | + var meta = document.createElement("div"); |
| 6052 | + meta.className = "journal-row-meta"; |
| 6053 | + var parts = [_sessionCtxRelativeTime(entry.created_at)]; |
| 6054 | + if (entry.label) parts.push(entry.label); |
| 6055 | + meta.textContent = parts.join(" · "); |
| 6056 | + |
| 6057 | + copy.appendChild(title); |
| 6058 | + copy.appendChild(meta); |
| 6059 | + row.appendChild(copy); |
| 6060 | + |
| 6061 | + var actions = document.createElement("div"); |
| 6062 | + actions.className = "journal-row-actions"; |
| 6063 | + |
| 6064 | + if (entry.reverted) { |
| 6065 | + var pill = document.createElement("span"); |
| 6066 | + pill.className = "journal-pill journal-pill-reverted"; |
| 6067 | + pill.textContent = "Reverted"; |
| 6068 | + actions.appendChild(pill); |
| 6069 | + } else if (!entry.revertible) { |
| 6070 | + var pill2 = document.createElement("span"); |
| 6071 | + pill2.className = "journal-pill journal-pill-info"; |
| 6072 | + pill2.textContent = "No auto-revert"; |
| 6073 | + pill2.title = "This action type has no ExtendScript inverse. It's recorded for context only."; |
| 6074 | + actions.appendChild(pill2); |
| 6075 | + } else { |
| 6076 | + var revertBtn = document.createElement("button"); |
| 6077 | + revertBtn.type = "button"; |
| 6078 | + revertBtn.className = "btn btn-secondary btn-sm journal-revert-btn"; |
| 6079 | + revertBtn.textContent = "Revert"; |
| 6080 | + revertBtn.addEventListener("click", function () { |
| 6081 | + _journalRevert(entry, revertBtn); |
| 6082 | + }); |
| 6083 | + actions.appendChild(revertBtn); |
| 6084 | + } |
| 6085 | + |
| 6086 | + row.appendChild(actions); |
| 6087 | + return row; |
| 6088 | + } |
| 6089 | + |
| 6090 | + function _journalRevert(entry, btn) { |
| 6091 | + if (!inPremiere) { showAlert("Premiere Pro connection required to revert."); return; } |
| 6092 | + if (!entry.revertible) { return; } |
| 6093 | + |
| 6094 | + btn.disabled = true; |
| 6095 | + btn.textContent = "Reverting…"; |
| 6096 | + |
| 6097 | + var dispatch = { |
| 6098 | + add_markers: function (p, cb) { PremiereBridge.removeSequenceMarkers(p, cb); }, |
| 6099 | + batch_rename: function (p, cb) { PremiereBridge.unrenameItems(p, cb); }, |
| 6100 | + import_sequence: function (p, cb) { PremiereBridge.removeImportedSequence(p, cb); }, |
| 6101 | + import_overlay: function (p, cb) { PremiereBridge.removeImportedItem(p, cb); } |
| 6102 | + }[entry.action]; |
| 6103 | + |
| 6104 | + if (!dispatch) { |
| 6105 | + btn.disabled = false; |
| 6106 | + btn.textContent = "Revert"; |
| 6107 | + showAlert("This action can't be reverted automatically."); |
| 6108 | + return; |
| 6109 | + } |
| 6110 | + |
| 6111 | + dispatch(entry.inverse || {}, function (result) { |
| 6112 | + var r; |
| 6113 | + try { r = JSON.parse(result || "{}"); } catch (e) { r = { error: result || "Parse error" }; } |
| 6114 | + if (r.error) { |
| 6115 | + btn.disabled = false; |
| 6116 | + btn.textContent = "Revert"; |
| 6117 | + showAlert("Revert failed: " + r.error); |
| 6118 | + return; |
| 6119 | + } |
| 6120 | + // Mark server-side so the UI reflects the new state. |
| 6121 | + api("POST", "/journal/mark-reverted/" + entry.id, {}, function (err) { |
| 6122 | + if (err) { |
| 6123 | + showToast("Reverted in Premiere but couldn't update the journal", "warn"); |
| 6124 | + } else { |
| 6125 | + showToast("Reverted: " + _journalActionLabel(entry.action), "success"); |
| 6126 | + } |
| 6127 | + renderJournalList(); |
| 6128 | + }); |
| 6129 | + }); |
| 6130 | + } |
| 6131 | + |
| 6132 | + function initJournal() { |
| 6133 | + if (!el.journalList) return; |
| 6134 | + if (el.journalRefreshBtn) { |
| 6135 | + el.journalRefreshBtn.addEventListener("click", renderJournalList); |
| 6136 | + } |
| 6137 | + if (el.journalClearBtn) { |
| 6138 | + el.journalClearBtn.addEventListener("click", function () { |
| 6139 | + if (!confirm("Clear all journal entries? This does not undo anything in Premiere.")) return; |
| 6140 | + api("POST", "/journal/clear", {}, function (err) { |
| 6141 | + if (err) { showAlert("Could not clear: " + (err.error || err)); return; } |
| 6142 | + showToast("Journal cleared", "success"); |
| 6143 | + renderJournalList(); |
| 6144 | + }); |
| 6145 | + }); |
| 6146 | + } |
| 6147 | + // Lazy render the first time the user actually opens Settings. |
| 6148 | + var settingsTab = document.querySelector('[data-nav="settings"]'); |
| 6149 | + if (settingsTab) { |
| 6150 | + settingsTab.addEventListener("click", function () { |
| 6151 | + setTimeout(renderJournalList, 50); |
| 6152 | + }); |
| 6153 | + } |
| 6154 | + } |
| 6155 | + |
5936 | 6156 | function esc(s) { |
5937 | 6157 | if (!s) return ""; |
5938 | 6158 | return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); |
|
9423 | 9643 | try { |
9424 | 9644 | var r = JSON.parse(result); |
9425 | 9645 | showToast("Added " + (r.added || beatMarkerTimes.length) + " markers", "success"); |
| 9646 | + if (!r.error) { |
| 9647 | + // Journal the op for one-click rollback. Fingerprint each |
| 9648 | + // marker by its {time, comment} pair so the inverse can |
| 9649 | + // find+delete exactly these rows. |
| 9650 | + var fingerprints = []; |
| 9651 | + for (var k = 0; k < markers.length; k++) { |
| 9652 | + fingerprints.push({ |
| 9653 | + time: markers[k].time, |
| 9654 | + comment: markers[k].name || "Beat" |
| 9655 | + }); |
| 9656 | + } |
| 9657 | + journalRecord( |
| 9658 | + "add_markers", |
| 9659 | + _journalLabelForMarkers(markers.length, selectedName), |
| 9660 | + { markers: fingerprints }, |
| 9661 | + selectedPath |
| 9662 | + ); |
| 9663 | + } |
9426 | 9664 | } catch (e) { showAlert("Error adding markers: " + (result || e.message)); } |
9427 | 9665 | }); |
9428 | 9666 | } |
|
9618 | 9856 | try { |
9619 | 9857 | var r = JSON.parse(result); |
9620 | 9858 | showToast("Renamed " + (r.renamed || renames.length) + " items", "success"); |
| 9859 | + if (!r.error) { |
| 9860 | + // Journal each (nodeId -> oldName) pair so Unrename |
| 9861 | + // can restore the previous names. |
| 9862 | + var reverseList = []; |
| 9863 | + for (var k = 0; k < renames.length; k++) { |
| 9864 | + var idx = -1; |
| 9865 | + for (var j = 0; j < renameItemsData.length; j++) { |
| 9866 | + if ((renameItemsData[j].nodeId || renameItemsData[j].id || |
| 9867 | + renameItemsData[j].path) === renames[k].nodeId) { |
| 9868 | + idx = j; break; |
| 9869 | + } |
| 9870 | + } |
| 9871 | + if (idx >= 0) { |
| 9872 | + reverseList.push({ |
| 9873 | + nodeId: renames[k].nodeId, |
| 9874 | + oldName: renameItemsData[idx].name, |
| 9875 | + currentName: renames[k].newName |
| 9876 | + }); |
| 9877 | + } |
| 9878 | + } |
| 9879 | + journalRecord( |
| 9880 | + "batch_rename", |
| 9881 | + _journalLabelForRename(reverseList.length), |
| 9882 | + { renames: reverseList } |
| 9883 | + ); |
| 9884 | + } |
9621 | 9885 | } catch (e) { showAlert("Error: " + (result || e.message)); } |
9622 | 9886 | }); |
9623 | 9887 | }); |
|
10992 | 11256 | initFavorites, initPreviewModal, initAudioPreview, initContextMenu, |
10993 | 11257 | initWizard, initOutputBrowser, initBatchPicker, initDepDashboard, |
10994 | 11258 | initSettingsIO, initWorkflowBuilder, loadWorkflowPresets, |
10995 | | - initCollapsibleCards, initI18n, initProjectTemplates |
| 11259 | + initCollapsibleCards, initI18n, initProjectTemplates, |
| 11260 | + initJournal |
10996 | 11261 | ]; |
10997 | 11262 | for (var fi = 0; fi < _featureInits.length; fi++) { |
10998 | 11263 | try { _featureInits[fi](); } |
|
0 commit comments