pi-studio 0.9.51 → 0.9.53
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/README.md +12 -8
- package/ROADMAP.md +87 -0
- package/client/studio-client.js +854 -124
- package/client/studio-preview-resource-helpers.js +61 -12
- package/client/studio.css +186 -9
- package/index.ts +547 -70
- package/package.json +2 -1
- package/shared/studio-resource-grants.js +157 -0
- package/shared/studio-side-question-context.js +17 -4
package/client/studio-client.js
CHANGED
|
@@ -242,6 +242,7 @@
|
|
|
242
242
|
|| typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
|
|
243
243
|
|| typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
|
|
244
244
|
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
245
|
+
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds !== "function"
|
|
245
246
|
|| typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
|
|
246
247
|
) {
|
|
247
248
|
throw new Error("Studio preview resource helpers failed to load.");
|
|
@@ -296,6 +297,7 @@
|
|
|
296
297
|
let studioPdfFocusOpenLinkEl = null;
|
|
297
298
|
let studioPdfFocusSystemViewerBtn = null;
|
|
298
299
|
let studioPdfFocusRevealBtn = null;
|
|
300
|
+
let studioPdfFocusCopyPathBtn = null;
|
|
299
301
|
let studioPdfFocusAutoRefreshBtn = null;
|
|
300
302
|
let studioPdfFocusFullscreenBtn = null;
|
|
301
303
|
let studioPdfFocusCloseBtn = null;
|
|
@@ -305,6 +307,7 @@
|
|
|
305
307
|
let studioPdfFocusSourceCard = null;
|
|
306
308
|
let studioPdfFocusStandaloneAutoRefreshState = null;
|
|
307
309
|
const studioPdfCardAutoRefreshStates = new WeakMap();
|
|
310
|
+
const studioPdfActionFeedbackStates = new WeakMap();
|
|
308
311
|
const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
|
|
309
312
|
const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
|
|
310
313
|
let studioHtmlFocusOverlayEl = null;
|
|
@@ -400,8 +403,8 @@
|
|
|
400
403
|
option.disabled = (isEditorOnlyMode && !EDITOR_ONLY_RIGHT_VIEW_ALLOWED.has(option.value)) || (isQuartoOption && !quartoRelevant);
|
|
401
404
|
});
|
|
402
405
|
rightViewSelect.title = isEditorOnlyMode
|
|
403
|
-
? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+Q
|
|
404
|
-
: "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W keep
|
|
406
|
+
? "Editor-only views: Editor Preview, contextual Quarto Preview for .qmd/.md/.markdown files, Changes, Files, REPL, or Side questions. F7 cycles; Cmd/Ctrl+Alt+3/5/6/7/8 switch directly to numbered right-pane views, and Cmd/Ctrl+Alt+F/Q open Files/Side questions."
|
|
407
|
+
: "Right pane view mode. F7 cycles, including contextual Quarto Preview for file-backed .qmd, .md, and .markdown documents; Cmd/Ctrl+Alt+1–8 switches directly between the numbered views. Cmd/Ctrl+Alt+P/E/W/F/Q keep mnemonic shortcuts for Preview, Editor Preview, Working, Files, and Side questions.";
|
|
405
408
|
}
|
|
406
409
|
|
|
407
410
|
function getInitialRightView(source) {
|
|
@@ -476,6 +479,7 @@
|
|
|
476
479
|
let sideQuestionAvailablePiTools = [];
|
|
477
480
|
let sideQuestionPreviewRenderNonce = 0;
|
|
478
481
|
let sideQuestionContextRefreshHandle = null;
|
|
482
|
+
let sideQuestionContextGrantPending = false;
|
|
479
483
|
let sideQuestionMarkdownExportRequest = null;
|
|
480
484
|
const sideQuestionMarkdownRenderCache = new Map();
|
|
481
485
|
let sideQuestionUi = {
|
|
@@ -2291,6 +2295,9 @@
|
|
|
2291
2295
|
relativeDir: "",
|
|
2292
2296
|
parentDir: null,
|
|
2293
2297
|
entries: [],
|
|
2298
|
+
exactFiles: [],
|
|
2299
|
+
locations: [],
|
|
2300
|
+
grantRequiredPath: "",
|
|
2294
2301
|
omitted: 0,
|
|
2295
2302
|
omittedIgnored: 0,
|
|
2296
2303
|
sort: fileBrowserSortMode,
|
|
@@ -3892,8 +3899,12 @@
|
|
|
3892
3899
|
secondaryBtn.className = "studio-decision-secondary";
|
|
3893
3900
|
secondaryBtn.hidden = true;
|
|
3894
3901
|
secondaryBtn.addEventListener("click", () => {
|
|
3895
|
-
const
|
|
3896
|
-
|
|
3902
|
+
const state = studioDecisionState;
|
|
3903
|
+
const handler = state && state.onSecondary;
|
|
3904
|
+
if (typeof handler !== "function") {
|
|
3905
|
+
if (state && state.hasSecondaryValue) finishStudioDecision(state.secondaryValue);
|
|
3906
|
+
return;
|
|
3907
|
+
}
|
|
3897
3908
|
try {
|
|
3898
3909
|
const result = handler();
|
|
3899
3910
|
if (result && typeof result.catch === "function") {
|
|
@@ -3988,6 +3999,8 @@
|
|
|
3988
3999
|
resolve,
|
|
3989
4000
|
returnFocusEl,
|
|
3990
4001
|
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
4002
|
+
hasSecondaryValue: Object.prototype.hasOwnProperty.call(settings, "secondaryValue"),
|
|
4003
|
+
secondaryValue: settings.secondaryValue,
|
|
3991
4004
|
};
|
|
3992
4005
|
studioDecisionState = decisionState;
|
|
3993
4006
|
const schedule = typeof window.requestAnimationFrame === "function"
|
|
@@ -4819,6 +4832,16 @@
|
|
|
4819
4832
|
return;
|
|
4820
4833
|
}
|
|
4821
4834
|
|
|
4835
|
+
const isFilesShortcut = (key.toLowerCase() === "f" || code === "KeyF")
|
|
4836
|
+
&& (event.metaKey || event.ctrlKey)
|
|
4837
|
+
&& event.altKey
|
|
4838
|
+
&& !event.shiftKey;
|
|
4839
|
+
if (isFilesShortcut) {
|
|
4840
|
+
event.preventDefault();
|
|
4841
|
+
switchRightPaneToView("files");
|
|
4842
|
+
return;
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4822
4845
|
const isSideQuestionsShortcut = (key.toLowerCase() === "q" || code === "KeyQ")
|
|
4823
4846
|
&& (event.metaKey || event.ctrlKey)
|
|
4824
4847
|
&& event.altKey
|
|
@@ -6039,7 +6062,8 @@
|
|
|
6039
6062
|
+ " const images = Array.prototype.slice.call(document.querySelectorAll('img[src]'));\n"
|
|
6040
6063
|
+ " images.forEach((image) => {\n"
|
|
6041
6064
|
+ " if (!image || !image.getAttribute) return;\n"
|
|
6042
|
-
+ "
|
|
6065
|
+
+ " const resourceState = image.getAttribute('data-pi-studio-html-resource-resolved') || '';\n"
|
|
6066
|
+
+ " if (resourceState === 'true' || resourceState === 'blocked' || resourceState === 'failed') return;\n"
|
|
6043
6067
|
+ " const raw = String(image.getAttribute('src') || '').trim();\n"
|
|
6044
6068
|
+ " if (!shouldResolveHtmlPreviewResourceUrl(raw)) return;\n"
|
|
6045
6069
|
+ " let resourceId = image.getAttribute('data-pi-studio-html-resource-id') || '';\n"
|
|
@@ -6071,6 +6095,7 @@
|
|
|
6071
6095
|
+ " image.setAttribute('data-pi-studio-html-resource-resolved', 'true');\n"
|
|
6072
6096
|
+ " } else if (typeof result.error === 'string' && result.error) {\n"
|
|
6073
6097
|
+ " image.setAttribute('title', result.error);\n"
|
|
6098
|
+
+ " image.setAttribute('data-pi-studio-html-resource-resolved', result.blocked === true ? 'blocked' : 'failed');\n"
|
|
6074
6099
|
+ " }\n"
|
|
6075
6100
|
+ " htmlResourcePlaceholders.delete(resourceId);\n"
|
|
6076
6101
|
+ " });\n"
|
|
@@ -6521,18 +6546,28 @@
|
|
|
6521
6546
|
const message = payload && typeof payload.error === "string"
|
|
6522
6547
|
? payload.error
|
|
6523
6548
|
: (timeoutLabel || "Local preview resource load") + " failed with HTTP " + response.status + ".";
|
|
6524
|
-
|
|
6549
|
+
const requestError = new Error(message);
|
|
6550
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
6551
|
+
requestError.studioStatus = response.status;
|
|
6552
|
+
throw requestError;
|
|
6525
6553
|
}
|
|
6526
6554
|
return payload.dataUrl;
|
|
6527
6555
|
}
|
|
6528
6556
|
|
|
6529
6557
|
function postHtmlArtifactResourceResults(record, results) {
|
|
6530
6558
|
if (!record || !record.iframe || !record.iframe.isConnected || !record.iframe.contentWindow) return;
|
|
6559
|
+
const publicResults = Array.isArray(results) ? results.map((result) => ({
|
|
6560
|
+
resourceId: result && result.resourceId ? result.resourceId : "",
|
|
6561
|
+
ok: Boolean(result && result.ok === true),
|
|
6562
|
+
dataUrl: result && typeof result.dataUrl === "string" ? result.dataUrl : undefined,
|
|
6563
|
+
error: result && typeof result.error === "string" ? result.error : undefined,
|
|
6564
|
+
blocked: Boolean(result && result.blocked === true),
|
|
6565
|
+
})) : [];
|
|
6531
6566
|
try {
|
|
6532
6567
|
record.iframe.contentWindow.postMessage({
|
|
6533
6568
|
type: "pi-studio-html-artifact-resources-resolved",
|
|
6534
6569
|
id: record.id || "",
|
|
6535
|
-
results:
|
|
6570
|
+
results: publicResults,
|
|
6536
6571
|
}, "*");
|
|
6537
6572
|
} catch {
|
|
6538
6573
|
// Ignore iframe postMessage failures.
|
|
@@ -6550,14 +6585,58 @@
|
|
|
6550
6585
|
);
|
|
6551
6586
|
return { resourceId, ok: true, dataUrl };
|
|
6552
6587
|
} catch (error) {
|
|
6553
|
-
|
|
6588
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
6589
|
+
return {
|
|
6590
|
+
resourceId,
|
|
6591
|
+
ok: false,
|
|
6592
|
+
error: error && error.message ? error.message : String(error || "HTML preview resource load failed."),
|
|
6593
|
+
blocked: Boolean(grantRequest),
|
|
6594
|
+
grantRequest,
|
|
6595
|
+
sourceUrl: item && item.url ? String(item.url) : "",
|
|
6596
|
+
};
|
|
6554
6597
|
}
|
|
6555
6598
|
}
|
|
6556
6599
|
|
|
6600
|
+
function renderHtmlArtifactBlockedResources(record) {
|
|
6601
|
+
if (!record || !record.shell) return;
|
|
6602
|
+
const existing = record.shell.querySelector(".studio-html-artifact-blocked-media");
|
|
6603
|
+
if (existing) existing.remove();
|
|
6604
|
+
const blocked = record.blockedResources instanceof Map ? Array.from(record.blockedResources.values()) : [];
|
|
6605
|
+
if (!blocked.length) return;
|
|
6606
|
+
const container = document.createElement("div");
|
|
6607
|
+
container.className = "studio-html-artifact-blocked-media";
|
|
6608
|
+
blocked.forEach((entry) => {
|
|
6609
|
+
const notice = createStudioBlockedMediaNoticeForRequest(entry.grantRequest, "image", entry.sourceUrl);
|
|
6610
|
+
if (notice) container.appendChild(notice);
|
|
6611
|
+
});
|
|
6612
|
+
if (!container.childNodes.length) return;
|
|
6613
|
+
const frame = record.iframe && record.iframe.parentNode === record.shell ? record.iframe : null;
|
|
6614
|
+
record.shell.insertBefore(container, frame);
|
|
6615
|
+
}
|
|
6616
|
+
|
|
6617
|
+
function syncHtmlArtifactBlockedResources(record, results) {
|
|
6618
|
+
if (!record) return;
|
|
6619
|
+
if (!(record.blockedResources instanceof Map)) record.blockedResources = new Map();
|
|
6620
|
+
(Array.isArray(results) ? results : []).forEach((result) => {
|
|
6621
|
+
const resourceId = result && result.resourceId ? String(result.resourceId) : "";
|
|
6622
|
+
if (!resourceId) return;
|
|
6623
|
+
if (result.blocked === true && result.grantRequest) {
|
|
6624
|
+
record.blockedResources.set(resourceId, {
|
|
6625
|
+
grantRequest: result.grantRequest,
|
|
6626
|
+
sourceUrl: result.sourceUrl || "local image",
|
|
6627
|
+
});
|
|
6628
|
+
} else {
|
|
6629
|
+
record.blockedResources.delete(resourceId);
|
|
6630
|
+
}
|
|
6631
|
+
});
|
|
6632
|
+
renderHtmlArtifactBlockedResources(record);
|
|
6633
|
+
}
|
|
6634
|
+
|
|
6557
6635
|
async function resolveHtmlArtifactResources(record, items) {
|
|
6558
6636
|
if (!record || !Array.isArray(items) || items.length === 0) return;
|
|
6559
6637
|
if (record.detail) record.detail.textContent = "HTML preview · loading local images";
|
|
6560
6638
|
const results = await Promise.all(items.map((item) => fetchHtmlArtifactResource(record, item)));
|
|
6639
|
+
syncHtmlArtifactBlockedResources(record, results);
|
|
6561
6640
|
postHtmlArtifactResourceResults(record, results);
|
|
6562
6641
|
setHtmlArtifactDetailText(record, "HTML preview");
|
|
6563
6642
|
}
|
|
@@ -6618,12 +6697,14 @@
|
|
|
6618
6697
|
const action = typeof data.action === "string" ? data.action : "open";
|
|
6619
6698
|
if (action === "contextmenu") {
|
|
6620
6699
|
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6621
|
-
showPreviewLinkMenu(null, point, context);
|
|
6700
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6622
6701
|
return;
|
|
6623
6702
|
}
|
|
6624
6703
|
const kind = getPreviewLocalLinkKind(context.href);
|
|
6625
6704
|
if (kind === "pdf") {
|
|
6626
|
-
openPreviewPdfLink(context.href, context.title, context)
|
|
6705
|
+
void openPreviewPdfLink(context.href, context.title, context).catch((error) => {
|
|
6706
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
6707
|
+
});
|
|
6627
6708
|
return;
|
|
6628
6709
|
}
|
|
6629
6710
|
if (kind === "image") {
|
|
@@ -6633,9 +6714,8 @@
|
|
|
6633
6714
|
return;
|
|
6634
6715
|
}
|
|
6635
6716
|
if (kind === "text" || kind === "office") {
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
});
|
|
6717
|
+
const point = getHtmlArtifactLocalLinkClientPoint(record, data);
|
|
6718
|
+
void showPreviewLinkMenu(null, point, context);
|
|
6639
6719
|
return;
|
|
6640
6720
|
}
|
|
6641
6721
|
setStatus("Right-click this local HTML preview link for file actions.", "warning");
|
|
@@ -6715,10 +6795,22 @@
|
|
|
6715
6795
|
button.setAttribute("aria-pressed", focused ? "true" : "false");
|
|
6716
6796
|
}
|
|
6717
6797
|
|
|
6798
|
+
function isStudioFullscreenAvailable(element) {
|
|
6799
|
+
return Boolean(
|
|
6800
|
+
element
|
|
6801
|
+
&& typeof element.requestFullscreen === "function"
|
|
6802
|
+
&& typeof document.exitFullscreen === "function"
|
|
6803
|
+
);
|
|
6804
|
+
}
|
|
6805
|
+
|
|
6718
6806
|
function syncStudioHtmlFocusFullscreenButton() {
|
|
6719
6807
|
const button = studioHtmlFocusFullscreenBtn;
|
|
6720
6808
|
if (!button) return;
|
|
6721
6809
|
const shell = studioHtmlFocusShellEl;
|
|
6810
|
+
const available = isStudioFullscreenAvailable(shell);
|
|
6811
|
+
button.hidden = !available;
|
|
6812
|
+
button.setAttribute("aria-hidden", available ? "false" : "true");
|
|
6813
|
+
if (!available) return;
|
|
6722
6814
|
const isFullscreen = Boolean(shell && document.fullscreenElement && document.fullscreenElement === shell);
|
|
6723
6815
|
button.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
6724
6816
|
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
@@ -7027,6 +7119,7 @@
|
|
|
7027
7119
|
mathRenderItemCount: 0,
|
|
7028
7120
|
resourceResolveBatchCount: 0,
|
|
7029
7121
|
resourceResolveItemCount: 0,
|
|
7122
|
+
blockedResources: new Map(),
|
|
7030
7123
|
});
|
|
7031
7124
|
|
|
7032
7125
|
targetEl.appendChild(shell);
|
|
@@ -7191,9 +7284,9 @@
|
|
|
7191
7284
|
systemViewerBtn.type = "button";
|
|
7192
7285
|
systemViewerBtn.className = "studio-pdf-focus-btn studio-pdf-focus-system-viewer";
|
|
7193
7286
|
systemViewerBtn.textContent = "System viewer";
|
|
7194
|
-
systemViewerBtn.title = "Open the local PDF in the
|
|
7287
|
+
systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
|
|
7195
7288
|
systemViewerBtn.addEventListener("click", () => {
|
|
7196
|
-
void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery);
|
|
7289
|
+
void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery, systemViewerBtn);
|
|
7197
7290
|
});
|
|
7198
7291
|
actions.appendChild(systemViewerBtn);
|
|
7199
7292
|
|
|
@@ -7201,12 +7294,22 @@
|
|
|
7201
7294
|
revealBtn.type = "button";
|
|
7202
7295
|
revealBtn.className = "studio-pdf-focus-btn studio-pdf-focus-reveal";
|
|
7203
7296
|
revealBtn.textContent = "Show in folder";
|
|
7204
|
-
revealBtn.title = "Reveal the local PDF in
|
|
7297
|
+
revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
|
|
7205
7298
|
revealBtn.addEventListener("click", () => {
|
|
7206
|
-
void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery);
|
|
7299
|
+
void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery, revealBtn);
|
|
7207
7300
|
});
|
|
7208
7301
|
actions.appendChild(revealBtn);
|
|
7209
7302
|
|
|
7303
|
+
const copyPathBtn = document.createElement("button");
|
|
7304
|
+
copyPathBtn.type = "button";
|
|
7305
|
+
copyPathBtn.className = "studio-pdf-focus-btn studio-pdf-focus-copy-path";
|
|
7306
|
+
copyPathBtn.textContent = "Copy path";
|
|
7307
|
+
copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
|
|
7308
|
+
copyPathBtn.addEventListener("click", () => {
|
|
7309
|
+
void copyStudioPdfResourcePath(studioPdfFocusResourceQuery, copyPathBtn);
|
|
7310
|
+
});
|
|
7311
|
+
actions.appendChild(copyPathBtn);
|
|
7312
|
+
|
|
7210
7313
|
const refreshBtn = document.createElement("button");
|
|
7211
7314
|
refreshBtn.type = "button";
|
|
7212
7315
|
refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
|
|
@@ -7282,6 +7385,7 @@
|
|
|
7282
7385
|
studioPdfFocusOpenLinkEl = openLink;
|
|
7283
7386
|
studioPdfFocusSystemViewerBtn = systemViewerBtn;
|
|
7284
7387
|
studioPdfFocusRevealBtn = revealBtn;
|
|
7388
|
+
studioPdfFocusCopyPathBtn = copyPathBtn;
|
|
7285
7389
|
studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
|
|
7286
7390
|
studioPdfFocusFullscreenBtn = fullscreenBtn;
|
|
7287
7391
|
studioPdfFocusCloseBtn = closeBtn;
|
|
@@ -7576,9 +7680,33 @@
|
|
|
7576
7680
|
return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
|
|
7577
7681
|
}
|
|
7578
7682
|
|
|
7579
|
-
|
|
7683
|
+
function flashStudioPdfActionFeedback(buttonEl, label, level) {
|
|
7684
|
+
if (!(buttonEl instanceof HTMLButtonElement)) return;
|
|
7685
|
+
const existing = studioPdfActionFeedbackStates.get(buttonEl) || null;
|
|
7686
|
+
if (existing && existing.timer) window.clearTimeout(existing.timer);
|
|
7687
|
+
const state = {
|
|
7688
|
+
baselineText: existing ? existing.baselineText : buttonEl.textContent,
|
|
7689
|
+
baselineAriaLabel: existing ? existing.baselineAriaLabel : buttonEl.getAttribute("aria-label"),
|
|
7690
|
+
timer: null,
|
|
7691
|
+
};
|
|
7692
|
+
studioPdfActionFeedbackStates.set(buttonEl, state);
|
|
7693
|
+
buttonEl.textContent = String(label || "Done");
|
|
7694
|
+
buttonEl.dataset.studioActionFeedback = level === "warning" ? "warning" : "success";
|
|
7695
|
+
buttonEl.setAttribute("aria-label", String(label || "Done"));
|
|
7696
|
+
state.timer = window.setTimeout(() => {
|
|
7697
|
+
if (studioPdfActionFeedbackStates.get(buttonEl) !== state) return;
|
|
7698
|
+
buttonEl.textContent = state.baselineText;
|
|
7699
|
+
if (state.baselineAriaLabel === null) buttonEl.removeAttribute("aria-label");
|
|
7700
|
+
else buttonEl.setAttribute("aria-label", state.baselineAriaLabel);
|
|
7701
|
+
delete buttonEl.dataset.studioActionFeedback;
|
|
7702
|
+
studioPdfActionFeedbackStates.delete(buttonEl);
|
|
7703
|
+
}, 2_200);
|
|
7704
|
+
}
|
|
7705
|
+
|
|
7706
|
+
async function runStudioPdfLocalAction(action, resourceQuery, actionButton) {
|
|
7580
7707
|
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7581
7708
|
if (!query) {
|
|
7709
|
+
flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
|
|
7582
7710
|
setStatus("Could not resolve this PDF's local path.", "warning");
|
|
7583
7711
|
return false;
|
|
7584
7712
|
}
|
|
@@ -7589,11 +7717,13 @@
|
|
|
7589
7717
|
method: "POST",
|
|
7590
7718
|
body: JSON.stringify(query),
|
|
7591
7719
|
});
|
|
7720
|
+
flashStudioPdfActionFeedback(actionButton, openInSystemViewer ? "Opened ✓" : "Shown ✓", "success");
|
|
7592
7721
|
setStatus(payload && payload.message
|
|
7593
7722
|
? payload.message
|
|
7594
7723
|
: (openInSystemViewer ? "Opened PDF in the system viewer." : "Revealed PDF in the file manager."), "success");
|
|
7595
7724
|
return true;
|
|
7596
7725
|
} catch (error) {
|
|
7726
|
+
flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
|
|
7597
7727
|
setStatus((error && error.message)
|
|
7598
7728
|
? error.message
|
|
7599
7729
|
: (openInSystemViewer ? "Could not open PDF in the system viewer." : "Could not reveal PDF in the file manager."), "warning");
|
|
@@ -7605,6 +7735,31 @@
|
|
|
7605
7735
|
const available = Boolean(normalizeStudioPdfResourceQuery(studioPdfFocusResourceQuery));
|
|
7606
7736
|
if (studioPdfFocusSystemViewerBtn) studioPdfFocusSystemViewerBtn.disabled = !available;
|
|
7607
7737
|
if (studioPdfFocusRevealBtn) studioPdfFocusRevealBtn.disabled = !available;
|
|
7738
|
+
if (studioPdfFocusCopyPathBtn) studioPdfFocusCopyPathBtn.disabled = !available;
|
|
7739
|
+
}
|
|
7740
|
+
|
|
7741
|
+
async function copyStudioPdfResourcePath(resourceQuery, actionButton) {
|
|
7742
|
+
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7743
|
+
if (!query) {
|
|
7744
|
+
flashStudioPdfActionFeedback(actionButton, "Unavailable", "warning");
|
|
7745
|
+
setStatus("Could not resolve this PDF's local path.", "warning");
|
|
7746
|
+
return false;
|
|
7747
|
+
}
|
|
7748
|
+
try {
|
|
7749
|
+
const payload = await fetchStudioJson("/local-preview-link", {
|
|
7750
|
+
query: { ...query, action: "resolve" },
|
|
7751
|
+
});
|
|
7752
|
+
const path = payload && typeof payload.path === "string" ? payload.path : "";
|
|
7753
|
+
if (!path) throw new Error("Studio did not return a PDF path.");
|
|
7754
|
+
if (!(await writeTextToClipboard(path))) throw new Error("Clipboard write failed.");
|
|
7755
|
+
flashStudioPdfActionFeedback(actionButton, "Copied ✓", "success");
|
|
7756
|
+
setStatus("Copied PDF path from the computer running Pi.", "success");
|
|
7757
|
+
return true;
|
|
7758
|
+
} catch (error) {
|
|
7759
|
+
flashStudioPdfActionFeedback(actionButton, "Failed", "warning");
|
|
7760
|
+
setStatus((error && error.message) ? error.message : "Could not copy this PDF's local path.", "warning");
|
|
7761
|
+
return false;
|
|
7762
|
+
}
|
|
7608
7763
|
}
|
|
7609
7764
|
|
|
7610
7765
|
function buildRefreshedStudioPdfViewerUrl(value) {
|
|
@@ -7731,6 +7886,10 @@
|
|
|
7731
7886
|
|
|
7732
7887
|
function syncStudioPdfFocusFullscreenButton() {
|
|
7733
7888
|
if (!studioPdfFocusFullscreenBtn) return;
|
|
7889
|
+
const available = isStudioFullscreenAvailable(studioPdfFocusDialogEl);
|
|
7890
|
+
studioPdfFocusFullscreenBtn.hidden = !available;
|
|
7891
|
+
studioPdfFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
|
|
7892
|
+
if (!available) return;
|
|
7734
7893
|
const isFullscreen = Boolean(document.fullscreenElement && studioPdfFocusDialogEl && document.fullscreenElement === studioPdfFocusDialogEl);
|
|
7735
7894
|
studioPdfFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
7736
7895
|
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
@@ -7789,6 +7948,16 @@
|
|
|
7789
7948
|
studioPdfFocusFrameEl.title = String(title || "PDF focus viewer").trim() || "PDF focus viewer";
|
|
7790
7949
|
}
|
|
7791
7950
|
|
|
7951
|
+
function getStudioPdfCardResourceQuery(card) {
|
|
7952
|
+
return card && card.dataset
|
|
7953
|
+
? normalizeStudioPdfResourceQuery({
|
|
7954
|
+
path: card.dataset.studioPdfPath || "",
|
|
7955
|
+
sourcePath: card.dataset.studioPdfSourcePath || "",
|
|
7956
|
+
resourceDir: card.dataset.studioPdfResourceDir || "",
|
|
7957
|
+
})
|
|
7958
|
+
: null;
|
|
7959
|
+
}
|
|
7960
|
+
|
|
7792
7961
|
function openStudioPdfFocusFromButton(buttonEl) {
|
|
7793
7962
|
if (!buttonEl) return false;
|
|
7794
7963
|
const card = buttonEl.closest && buttonEl.closest(".studio-pdf-card");
|
|
@@ -7798,13 +7967,7 @@
|
|
|
7798
7967
|
|| String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
|
|
7799
7968
|
|| "PDF preview";
|
|
7800
7969
|
const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
|
|
7801
|
-
const resourceQuery = card
|
|
7802
|
-
? normalizeStudioPdfResourceQuery({
|
|
7803
|
-
path: card.dataset.studioPdfPath || "",
|
|
7804
|
-
sourcePath: card.dataset.studioPdfSourcePath || "",
|
|
7805
|
-
resourceDir: card.dataset.studioPdfResourceDir || "",
|
|
7806
|
-
})
|
|
7807
|
-
: null;
|
|
7970
|
+
const resourceQuery = getStudioPdfCardResourceQuery(card);
|
|
7808
7971
|
if (!viewerUrl) return false;
|
|
7809
7972
|
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
|
|
7810
7973
|
return true;
|
|
@@ -7824,6 +7987,80 @@
|
|
|
7824
7987
|
}
|
|
7825
7988
|
}
|
|
7826
7989
|
|
|
7990
|
+
function consumeStudioPreviewMediaEvent(event) {
|
|
7991
|
+
event.preventDefault();
|
|
7992
|
+
event.stopPropagation();
|
|
7993
|
+
if (typeof event.stopImmediatePropagation === "function") event.stopImmediatePropagation();
|
|
7994
|
+
}
|
|
7995
|
+
|
|
7996
|
+
function handleStudioPreviewMediaKeydown(event) {
|
|
7997
|
+
if (!event || (event.key !== "Enter" && event.key !== " ")) return;
|
|
7998
|
+
const target = event.target;
|
|
7999
|
+
const imageEl = target instanceof Element ? target.closest("img.studio-image-focus-target") : null;
|
|
8000
|
+
if (!imageEl) return;
|
|
8001
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8002
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
8003
|
+
}
|
|
8004
|
+
|
|
8005
|
+
function handleStudioPreviewMediaActivation(event) {
|
|
8006
|
+
const target = event && event.target;
|
|
8007
|
+
if (!(target instanceof Element)) return;
|
|
8008
|
+
|
|
8009
|
+
const blockedMediaButton = target.closest("button.studio-blocked-media-allow");
|
|
8010
|
+
if (blockedMediaButton) {
|
|
8011
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8012
|
+
void handleStudioBlockedMediaAllowButton(blockedMediaButton);
|
|
8013
|
+
return;
|
|
8014
|
+
}
|
|
8015
|
+
|
|
8016
|
+
const imageEl = target.closest("img.studio-image-focus-target");
|
|
8017
|
+
if (imageEl) {
|
|
8018
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8019
|
+
if (!openPreviewImageElementInFocus(imageEl)) setStatus("Could not open image focus view.", "warning");
|
|
8020
|
+
return;
|
|
8021
|
+
}
|
|
8022
|
+
|
|
8023
|
+
const pdfFigureEl = target.closest(".studio-pdf-preview-focus-target");
|
|
8024
|
+
if (pdfFigureEl && !target.closest("button, a")) {
|
|
8025
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8026
|
+
if (!openPreviewPdfFigureInFocus(pdfFigureEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
|
|
8027
|
+
return;
|
|
8028
|
+
}
|
|
8029
|
+
|
|
8030
|
+
const actionBtn = target.closest(
|
|
8031
|
+
".studio-pdf-card-system-viewer, .studio-pdf-card-reveal, .studio-pdf-card-copy-path, .studio-pdf-card-refresh, .studio-pdf-card-auto-refresh"
|
|
8032
|
+
);
|
|
8033
|
+
if (!actionBtn) return;
|
|
8034
|
+
const card = actionBtn.closest(".studio-pdf-card");
|
|
8035
|
+
if (!card) return;
|
|
8036
|
+
consumeStudioPreviewMediaEvent(event);
|
|
8037
|
+
const resourceQuery = getStudioPdfCardResourceQuery(card);
|
|
8038
|
+
if (actionBtn.classList.contains("studio-pdf-card-system-viewer")) {
|
|
8039
|
+
void runStudioPdfLocalAction("system-viewer", resourceQuery, actionBtn);
|
|
8040
|
+
return;
|
|
8041
|
+
}
|
|
8042
|
+
if (actionBtn.classList.contains("studio-pdf-card-reveal")) {
|
|
8043
|
+
void runStudioPdfLocalAction("reveal", resourceQuery, actionBtn);
|
|
8044
|
+
return;
|
|
8045
|
+
}
|
|
8046
|
+
if (actionBtn.classList.contains("studio-pdf-card-copy-path")) {
|
|
8047
|
+
void copyStudioPdfResourcePath(resourceQuery, actionBtn);
|
|
8048
|
+
return;
|
|
8049
|
+
}
|
|
8050
|
+
if (actionBtn.classList.contains("studio-pdf-card-refresh")) {
|
|
8051
|
+
const refreshed = refreshStudioPdfCard(card);
|
|
8052
|
+
flashStudioPdfActionFeedback(actionBtn, refreshed ? "Refreshed ✓" : "Failed", refreshed ? "success" : "warning");
|
|
8053
|
+
if (!refreshed) setStatus("Could not refresh this PDF preview.", "warning");
|
|
8054
|
+
return;
|
|
8055
|
+
}
|
|
8056
|
+
const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
8057
|
+
if (!state) {
|
|
8058
|
+
setStatus("Could not resolve this PDF for auto-refresh.", "warning");
|
|
8059
|
+
return;
|
|
8060
|
+
}
|
|
8061
|
+
setStudioPdfCardAutoRefresh(card, !state.enabled);
|
|
8062
|
+
}
|
|
8063
|
+
|
|
7827
8064
|
function isStudioImageFocusOpen() {
|
|
7828
8065
|
return Boolean(studioImageFocusOverlayEl && studioImageFocusOverlayEl.hidden === false);
|
|
7829
8066
|
}
|
|
@@ -7976,6 +8213,10 @@
|
|
|
7976
8213
|
|
|
7977
8214
|
function syncStudioImageFocusFullscreenButton() {
|
|
7978
8215
|
if (!studioImageFocusFullscreenBtn) return;
|
|
8216
|
+
const available = isStudioFullscreenAvailable(studioImageFocusDialogEl);
|
|
8217
|
+
studioImageFocusFullscreenBtn.hidden = !available;
|
|
8218
|
+
studioImageFocusFullscreenBtn.setAttribute("aria-hidden", available ? "false" : "true");
|
|
8219
|
+
if (!available) return;
|
|
7979
8220
|
const isFullscreen = Boolean(document.fullscreenElement && studioImageFocusDialogEl && document.fullscreenElement === studioImageFocusDialogEl);
|
|
7980
8221
|
studioImageFocusFullscreenBtn.replaceChildren(makeStudioUiRefreshIcon(isFullscreen ? "fullscreen-exit" : "fullscreen"));
|
|
7981
8222
|
const label = isFullscreen ? "Exit fullscreen" : "Fullscreen";
|
|
@@ -8212,7 +8453,86 @@
|
|
|
8212
8453
|
});
|
|
8213
8454
|
}
|
|
8214
8455
|
|
|
8215
|
-
function
|
|
8456
|
+
function openPreviewPdfFigureInFocus(previewEl) {
|
|
8457
|
+
const canvas = previewEl && typeof previewEl.querySelector === "function"
|
|
8458
|
+
? previewEl.querySelector("canvas")
|
|
8459
|
+
: null;
|
|
8460
|
+
if (!(canvas instanceof HTMLCanvasElement)) return false;
|
|
8461
|
+
try {
|
|
8462
|
+
const dataUrl = canvas.toDataURL("image/png");
|
|
8463
|
+
const title = String(previewEl.getAttribute("title") || "PDF figure preview").trim() || "PDF figure preview";
|
|
8464
|
+
return openStudioImageFocusViewer(dataUrl, title);
|
|
8465
|
+
} catch {
|
|
8466
|
+
return false;
|
|
8467
|
+
}
|
|
8468
|
+
}
|
|
8469
|
+
|
|
8470
|
+
function decoratePreviewPdfFigures(targetEl) {
|
|
8471
|
+
if (!targetEl || typeof targetEl.querySelectorAll !== "function") return;
|
|
8472
|
+
const previews = Array.from(targetEl.querySelectorAll(".studio-pdf-preview"));
|
|
8473
|
+
previews.forEach((previewEl) => {
|
|
8474
|
+
if (!(previewEl instanceof HTMLElement)) return;
|
|
8475
|
+
if (previewEl.dataset && previewEl.dataset.studioPdfFigureFocusDecorated === "1") return;
|
|
8476
|
+
if (!previewEl.querySelector("canvas")) return;
|
|
8477
|
+
previewEl.classList.add("studio-pdf-preview-focus-target");
|
|
8478
|
+
previewEl.title = "PDF figure preview (page 1). Click to enlarge.";
|
|
8479
|
+
if (previewEl.dataset) previewEl.dataset.studioPdfFigureFocusDecorated = "1";
|
|
8480
|
+
|
|
8481
|
+
previewEl.addEventListener("click", (event) => {
|
|
8482
|
+
if (event.target instanceof Element && event.target.closest("button, a")) return;
|
|
8483
|
+
event.preventDefault();
|
|
8484
|
+
if (!openPreviewPdfFigureInFocus(previewEl)) setStatus("Could not enlarge this PDF figure preview.", "warning");
|
|
8485
|
+
});
|
|
8486
|
+
});
|
|
8487
|
+
}
|
|
8488
|
+
|
|
8489
|
+
function createStudioPdfStatusCard(block, content) {
|
|
8490
|
+
const options = block && block.options ? block.options : {};
|
|
8491
|
+
const path = String(options.path || "").trim();
|
|
8492
|
+
const title = String(options.title || path || "Embedded PDF").trim();
|
|
8493
|
+
const caption = String(options.caption || "").trim();
|
|
8494
|
+
const card = document.createElement("figure");
|
|
8495
|
+
card.className = "studio-pdf-card studio-pdf-card-status";
|
|
8496
|
+
const header = document.createElement("figcaption");
|
|
8497
|
+
header.className = "studio-pdf-card-header";
|
|
8498
|
+
const titleEl = document.createElement("div");
|
|
8499
|
+
titleEl.className = "studio-pdf-card-title";
|
|
8500
|
+
titleEl.textContent = title;
|
|
8501
|
+
header.appendChild(titleEl);
|
|
8502
|
+
card.appendChild(header);
|
|
8503
|
+
if (caption) {
|
|
8504
|
+
const captionEl = document.createElement("div");
|
|
8505
|
+
captionEl.className = "studio-pdf-card-caption";
|
|
8506
|
+
captionEl.textContent = caption;
|
|
8507
|
+
card.appendChild(captionEl);
|
|
8508
|
+
}
|
|
8509
|
+
if (content instanceof Element) {
|
|
8510
|
+
card.appendChild(content);
|
|
8511
|
+
} else {
|
|
8512
|
+
const errorEl = document.createElement("div");
|
|
8513
|
+
errorEl.className = "studio-pdf-card-error";
|
|
8514
|
+
errorEl.textContent = String(content || "PDF resource unavailable.");
|
|
8515
|
+
card.appendChild(errorEl);
|
|
8516
|
+
}
|
|
8517
|
+
return card;
|
|
8518
|
+
}
|
|
8519
|
+
|
|
8520
|
+
async function createStudioPdfCard(block, useEditorResourceContext) {
|
|
8521
|
+
const options = block && block.options ? block.options : {};
|
|
8522
|
+
const path = String(options.path || "").trim();
|
|
8523
|
+
if (!path) return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8524
|
+
const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
8525
|
+
try {
|
|
8526
|
+
await fetchPreviewLocalLink("resolve", path, resourceQuery, { skipGrantPrompt: true });
|
|
8527
|
+
return createAuthorizedStudioPdfCard(block, useEditorResourceContext);
|
|
8528
|
+
} catch (error) {
|
|
8529
|
+
const notice = createStudioBlockedMediaNotice(error, "pdf", path);
|
|
8530
|
+
if (notice) return createStudioPdfStatusCard(block, notice);
|
|
8531
|
+
return createStudioPdfStatusCard(block, error && error.message ? error.message : "PDF resource unavailable.");
|
|
8532
|
+
}
|
|
8533
|
+
}
|
|
8534
|
+
|
|
8535
|
+
function createAuthorizedStudioPdfCard(block, useEditorResourceContext) {
|
|
8216
8536
|
const options = block && block.options ? block.options : {};
|
|
8217
8537
|
const path = String(options.path || "").trim();
|
|
8218
8538
|
const title = String(options.title || path || "Embedded PDF").trim();
|
|
@@ -8244,12 +8564,13 @@
|
|
|
8244
8564
|
focusBtn.type = "button";
|
|
8245
8565
|
focusBtn.className = "studio-pdf-card-action studio-pdf-card-focus";
|
|
8246
8566
|
focusBtn.title = "Open this PDF in a larger Studio overlay.";
|
|
8247
|
-
focusBtn.setAttribute("aria-label", "
|
|
8567
|
+
focusBtn.setAttribute("aria-label", "Enlarge PDF");
|
|
8248
8568
|
if (focusBtn.dataset) {
|
|
8249
8569
|
focusBtn.dataset.studioPdfViewerUrl = viewerUrl;
|
|
8250
8570
|
focusBtn.dataset.studioPdfTitle = title;
|
|
8251
8571
|
}
|
|
8252
8572
|
focusBtn.appendChild(makeStudioUiRefreshIcon("focus"));
|
|
8573
|
+
focusBtn.appendChild(document.createTextNode("Enlarge"));
|
|
8253
8574
|
focusBtn.addEventListener("click", handleStudioPdfFocusButtonClick);
|
|
8254
8575
|
titleGroup.appendChild(focusBtn);
|
|
8255
8576
|
}
|
|
@@ -8276,11 +8597,11 @@
|
|
|
8276
8597
|
systemViewerBtn.type = "button";
|
|
8277
8598
|
systemViewerBtn.className = "studio-pdf-card-action studio-pdf-card-system-viewer";
|
|
8278
8599
|
systemViewerBtn.textContent = "System viewer";
|
|
8279
|
-
systemViewerBtn.title = "Open the local PDF in the
|
|
8600
|
+
systemViewerBtn.title = "Open the local PDF in the default viewer on the computer running Pi.";
|
|
8280
8601
|
systemViewerBtn.addEventListener("click", (event) => {
|
|
8281
8602
|
event.preventDefault();
|
|
8282
8603
|
event.stopPropagation();
|
|
8283
|
-
void runStudioPdfLocalAction("system-viewer", resourceQuery);
|
|
8604
|
+
void runStudioPdfLocalAction("system-viewer", resourceQuery, systemViewerBtn);
|
|
8284
8605
|
});
|
|
8285
8606
|
actions.appendChild(systemViewerBtn);
|
|
8286
8607
|
|
|
@@ -8288,14 +8609,26 @@
|
|
|
8288
8609
|
revealBtn.type = "button";
|
|
8289
8610
|
revealBtn.className = "studio-pdf-card-action studio-pdf-card-reveal";
|
|
8290
8611
|
revealBtn.textContent = "Show in folder";
|
|
8291
|
-
revealBtn.title = "Reveal the local PDF in
|
|
8612
|
+
revealBtn.title = "Reveal the local PDF in the file manager on the computer running Pi.";
|
|
8292
8613
|
revealBtn.addEventListener("click", (event) => {
|
|
8293
8614
|
event.preventDefault();
|
|
8294
8615
|
event.stopPropagation();
|
|
8295
|
-
void runStudioPdfLocalAction("reveal", resourceQuery);
|
|
8616
|
+
void runStudioPdfLocalAction("reveal", resourceQuery, revealBtn);
|
|
8296
8617
|
});
|
|
8297
8618
|
actions.appendChild(revealBtn);
|
|
8298
8619
|
|
|
8620
|
+
const copyPathBtn = document.createElement("button");
|
|
8621
|
+
copyPathBtn.type = "button";
|
|
8622
|
+
copyPathBtn.className = "studio-pdf-card-action studio-pdf-card-copy-path";
|
|
8623
|
+
copyPathBtn.textContent = "Copy path";
|
|
8624
|
+
copyPathBtn.title = "Copy the PDF path on the computer running Pi.";
|
|
8625
|
+
copyPathBtn.addEventListener("click", (event) => {
|
|
8626
|
+
event.preventDefault();
|
|
8627
|
+
event.stopPropagation();
|
|
8628
|
+
void copyStudioPdfResourcePath(resourceQuery, copyPathBtn);
|
|
8629
|
+
});
|
|
8630
|
+
actions.appendChild(copyPathBtn);
|
|
8631
|
+
|
|
8299
8632
|
const refreshBtn = document.createElement("button");
|
|
8300
8633
|
refreshBtn.type = "button";
|
|
8301
8634
|
refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
|
|
@@ -8362,17 +8695,17 @@
|
|
|
8362
8695
|
return card;
|
|
8363
8696
|
}
|
|
8364
8697
|
|
|
8365
|
-
function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8698
|
+
async function renderStudioPdfBlocksInElement(targetEl, blocks, useEditorResourceContext) {
|
|
8366
8699
|
if (!targetEl || !Array.isArray(blocks) || blocks.length === 0) return;
|
|
8367
8700
|
const candidates = Array.from(targetEl.querySelectorAll("p, pre, div"));
|
|
8368
|
-
|
|
8701
|
+
for (const block of blocks) {
|
|
8369
8702
|
const placeholder = block && block.placeholder ? block.placeholder : "";
|
|
8370
|
-
if (!placeholder)
|
|
8703
|
+
if (!placeholder) continue;
|
|
8371
8704
|
const match = candidates.find((el) => String(el.textContent || "").trim() === placeholder);
|
|
8372
|
-
if (match
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
}
|
|
8705
|
+
if (!match || !match.parentNode) continue;
|
|
8706
|
+
const card = await createStudioPdfCard(block, useEditorResourceContext);
|
|
8707
|
+
if (match.isConnected && match.parentNode) match.replaceWith(card);
|
|
8708
|
+
}
|
|
8376
8709
|
}
|
|
8377
8710
|
|
|
8378
8711
|
function sanitizeRenderedHtml(html, markdown, options) {
|
|
@@ -8944,6 +9277,91 @@
|
|
|
8944
9277
|
targetEl.appendChild(el);
|
|
8945
9278
|
}
|
|
8946
9279
|
|
|
9280
|
+
function createStudioBlockedMediaNotice(error, mediaKind, sourceLabel) {
|
|
9281
|
+
return createStudioBlockedMediaNoticeForRequest(getStudioResourceGrantRequest(error), mediaKind, sourceLabel);
|
|
9282
|
+
}
|
|
9283
|
+
|
|
9284
|
+
function createStudioBlockedMediaNoticeForRequest(grantRequest, mediaKind, sourceLabel) {
|
|
9285
|
+
if (!grantRequest) return null;
|
|
9286
|
+
const kind = mediaKind === "pdf" ? "PDF" : "image";
|
|
9287
|
+
const notice = document.createElement("span");
|
|
9288
|
+
notice.className = "studio-blocked-media studio-blocked-media-" + (mediaKind === "pdf" ? "pdf" : "image");
|
|
9289
|
+
notice.setAttribute("role", "note");
|
|
9290
|
+
|
|
9291
|
+
const text = document.createElement("span");
|
|
9292
|
+
text.className = "studio-blocked-media-text";
|
|
9293
|
+
const title = document.createElement("strong");
|
|
9294
|
+
title.textContent = "Local " + kind + " blocked";
|
|
9295
|
+
const detail = document.createElement("span");
|
|
9296
|
+
detail.textContent = "This media is outside the locations allowed for the current Studio session.";
|
|
9297
|
+
const path = document.createElement("code");
|
|
9298
|
+
path.textContent = grantRequest.path || String(sourceLabel || "local media");
|
|
9299
|
+
path.title = "Path on the computer running Pi";
|
|
9300
|
+
text.appendChild(title);
|
|
9301
|
+
text.appendChild(detail);
|
|
9302
|
+
text.appendChild(path);
|
|
9303
|
+
notice.appendChild(text);
|
|
9304
|
+
|
|
9305
|
+
const allowButton = document.createElement("button");
|
|
9306
|
+
allowButton.type = "button";
|
|
9307
|
+
allowButton.className = "studio-blocked-media-allow";
|
|
9308
|
+
allowButton.textContent = "Allow local " + kind + "…";
|
|
9309
|
+
allowButton.title = "Choose whether to allow only this file or its containing folder for the current Studio session.";
|
|
9310
|
+
allowButton.dataset.studioBlockedMediaPath = grantRequest.path;
|
|
9311
|
+
allowButton.dataset.studioBlockedMediaDirectory = grantRequest.directoryPath;
|
|
9312
|
+
allowButton.dataset.studioBlockedMediaLabel = grantRequest.label;
|
|
9313
|
+
allowButton.dataset.studioBlockedMediaKind = mediaKind === "pdf" ? "pdf" : "image";
|
|
9314
|
+
notice.appendChild(allowButton);
|
|
9315
|
+
return notice;
|
|
9316
|
+
}
|
|
9317
|
+
|
|
9318
|
+
function replaceStudioBlockedMediaElement(element, source, error, mediaKind) {
|
|
9319
|
+
if (!element || !element.parentNode) return false;
|
|
9320
|
+
const notice = createStudioBlockedMediaNotice(error, mediaKind, source);
|
|
9321
|
+
if (!notice) return false;
|
|
9322
|
+
element.replaceWith(notice);
|
|
9323
|
+
return true;
|
|
9324
|
+
}
|
|
9325
|
+
|
|
9326
|
+
function refreshStudioPassiveMediaAfterGrant() {
|
|
9327
|
+
renderSourcePreview({ previewDelayMs: 0 });
|
|
9328
|
+
if (rightView === "preview" || rightView === "editor-preview" || rightView === "side-questions") {
|
|
9329
|
+
renderActiveResult();
|
|
9330
|
+
}
|
|
9331
|
+
if (isQuizOpen()) renderQuizOverlay({ preserveScroll: true });
|
|
9332
|
+
}
|
|
9333
|
+
|
|
9334
|
+
async function handleStudioBlockedMediaAllowButton(button) {
|
|
9335
|
+
if (!(button instanceof HTMLButtonElement) || button.dataset.studioBlockedMediaBusy === "1") return false;
|
|
9336
|
+
const request = {
|
|
9337
|
+
path: String(button.dataset.studioBlockedMediaPath || "").trim(),
|
|
9338
|
+
directoryPath: String(button.dataset.studioBlockedMediaDirectory || "").trim(),
|
|
9339
|
+
label: String(button.dataset.studioBlockedMediaLabel || "local media").trim() || "local media",
|
|
9340
|
+
};
|
|
9341
|
+
if (!request.path || !request.directoryPath) {
|
|
9342
|
+
setStatus("Could not resolve this blocked media path.", "warning");
|
|
9343
|
+
return false;
|
|
9344
|
+
}
|
|
9345
|
+
button.dataset.studioBlockedMediaBusy = "1";
|
|
9346
|
+
button.disabled = true;
|
|
9347
|
+
const baselineText = button.textContent;
|
|
9348
|
+
button.textContent = "Choosing…";
|
|
9349
|
+
try {
|
|
9350
|
+
if (!(await requestStudioResourceGrant(request))) return false;
|
|
9351
|
+
refreshStudioPassiveMediaAfterGrant();
|
|
9352
|
+
return true;
|
|
9353
|
+
} catch (error) {
|
|
9354
|
+
setStatus("Could not allow local media: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
9355
|
+
return false;
|
|
9356
|
+
} finally {
|
|
9357
|
+
if (button.isConnected) {
|
|
9358
|
+
delete button.dataset.studioBlockedMediaBusy;
|
|
9359
|
+
button.disabled = false;
|
|
9360
|
+
button.textContent = baselineText;
|
|
9361
|
+
}
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
|
|
8947
9365
|
function hasMeaningfulPreviewContent(targetEl) {
|
|
8948
9366
|
if (!targetEl || typeof targetEl.querySelector !== "function") return false;
|
|
8949
9367
|
if (targetEl.querySelector(".preview-loading")) return false;
|
|
@@ -9238,6 +9656,32 @@
|
|
|
9238
9656
|
});
|
|
9239
9657
|
}
|
|
9240
9658
|
|
|
9659
|
+
function copyResponsePaneCanvasState(sourceEl, replacementEl) {
|
|
9660
|
+
if (!sourceEl || !replacementEl || typeof sourceEl.querySelectorAll !== "function" || typeof replacementEl.querySelectorAll !== "function") {
|
|
9661
|
+
return false;
|
|
9662
|
+
}
|
|
9663
|
+
const sourceCanvases = Array.from(sourceEl.querySelectorAll("canvas"));
|
|
9664
|
+
const replacementCanvases = Array.from(replacementEl.querySelectorAll("canvas"));
|
|
9665
|
+
if (sourceCanvases.length !== replacementCanvases.length) return false;
|
|
9666
|
+
|
|
9667
|
+
for (let index = 0; index < sourceCanvases.length; index += 1) {
|
|
9668
|
+
const sourceCanvas = sourceCanvases[index];
|
|
9669
|
+
const replacementCanvas = replacementCanvases[index];
|
|
9670
|
+
if (!(sourceCanvas instanceof HTMLCanvasElement) || !(replacementCanvas instanceof HTMLCanvasElement)) return false;
|
|
9671
|
+
if (sourceCanvas.width < 1 || sourceCanvas.height < 1) continue;
|
|
9672
|
+
try {
|
|
9673
|
+
replacementCanvas.width = sourceCanvas.width;
|
|
9674
|
+
replacementCanvas.height = sourceCanvas.height;
|
|
9675
|
+
const context = replacementCanvas.getContext("2d");
|
|
9676
|
+
if (!context) return false;
|
|
9677
|
+
context.drawImage(sourceCanvas, 0, 0);
|
|
9678
|
+
} catch {
|
|
9679
|
+
return false;
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
return true;
|
|
9683
|
+
}
|
|
9684
|
+
|
|
9241
9685
|
function replaceResponsePaneWithClone() {
|
|
9242
9686
|
const currentEl = critiqueViewEl;
|
|
9243
9687
|
if (!currentEl || !currentEl.parentNode || typeof currentEl.cloneNode !== "function") {
|
|
@@ -9245,7 +9689,7 @@
|
|
|
9245
9689
|
}
|
|
9246
9690
|
|
|
9247
9691
|
const replacement = currentEl.cloneNode(true);
|
|
9248
|
-
if (!replacement || replacement.nodeType !== 1) {
|
|
9692
|
+
if (!replacement || replacement.nodeType !== 1 || !copyResponsePaneCanvasState(currentEl, replacement)) {
|
|
9249
9693
|
return currentEl;
|
|
9250
9694
|
}
|
|
9251
9695
|
|
|
@@ -10019,6 +10463,10 @@
|
|
|
10019
10463
|
|
|
10020
10464
|
function exportRightPaneFormat(format) {
|
|
10021
10465
|
closeExportPreviewMenu();
|
|
10466
|
+
if (String(format || "").startsWith("side-markdown-") && rightView !== "side-questions") {
|
|
10467
|
+
setStatus("Side-thread Markdown export is available only in Side questions.", "warning");
|
|
10468
|
+
return false;
|
|
10469
|
+
}
|
|
10022
10470
|
if (format === "side-markdown-save") {
|
|
10023
10471
|
return saveSideQuestionTranscriptMarkdown();
|
|
10024
10472
|
}
|
|
@@ -10220,6 +10668,33 @@
|
|
|
10220
10668
|
});
|
|
10221
10669
|
}
|
|
10222
10670
|
|
|
10671
|
+
async function hydrateStudioPreviewLocalMedia(targetEl, resourceContext) {
|
|
10672
|
+
const fetchResource = (resourceUrl, timeoutLabel) => fetchLocalPreviewResourceDataUrl(
|
|
10673
|
+
resourceContext,
|
|
10674
|
+
resourceUrl,
|
|
10675
|
+
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
10676
|
+
timeoutLabel,
|
|
10677
|
+
);
|
|
10678
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalImages(
|
|
10679
|
+
targetEl,
|
|
10680
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview image load"),
|
|
10681
|
+
{
|
|
10682
|
+
onError: (imageEl, resourceUrl, error) => {
|
|
10683
|
+
replaceStudioBlockedMediaElement(imageEl, resourceUrl, error, "image");
|
|
10684
|
+
},
|
|
10685
|
+
},
|
|
10686
|
+
);
|
|
10687
|
+
await previewResourceHelpers.hydrateStudioPreviewLocalPdfEmbeds(
|
|
10688
|
+
targetEl,
|
|
10689
|
+
(resourceUrl) => fetchResource(resourceUrl, "Preview PDF load"),
|
|
10690
|
+
{
|
|
10691
|
+
onError: (embedEl, resourceUrl, error) => {
|
|
10692
|
+
replaceStudioBlockedMediaElement(embedEl, resourceUrl, error, "pdf");
|
|
10693
|
+
},
|
|
10694
|
+
},
|
|
10695
|
+
);
|
|
10696
|
+
}
|
|
10697
|
+
|
|
10223
10698
|
async function applyRenderedMarkdown(targetEl, markdown, pane, nonce) {
|
|
10224
10699
|
const previewPrepared = annotationsEnabled
|
|
10225
10700
|
? prepareMarkdownForPandocPreview(markdown)
|
|
@@ -10246,19 +10721,13 @@
|
|
|
10246
10721
|
clearPreviewJumpHighlight(targetEl);
|
|
10247
10722
|
finishPreviewRender(targetEl);
|
|
10248
10723
|
targetEl.innerHTML = sanitizeRenderedHtml(renderedHtml, markdown, previewFallbackOptions);
|
|
10249
|
-
await
|
|
10250
|
-
|
|
10251
|
-
previewResourceContext,
|
|
10252
|
-
resourceUrl,
|
|
10253
|
-
RENDERED_PREVIEW_IMAGE_FETCH_TIMEOUT_MS,
|
|
10254
|
-
"Preview image load",
|
|
10255
|
-
)
|
|
10256
|
-
));
|
|
10257
|
-
renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
10724
|
+
await hydrateStudioPreviewLocalMedia(targetEl, previewResourceContext);
|
|
10725
|
+
await renderStudioPdfBlocksInElement(targetEl, pdfPrepared.blocks, previewingEditorText);
|
|
10258
10726
|
applyPreviewAnnotationPlaceholdersToElement(targetEl, previewPrepared.placeholders);
|
|
10259
10727
|
await renderAnnotationMathInElement(targetEl);
|
|
10260
10728
|
decoratePdfEmbeds(targetEl);
|
|
10261
10729
|
await renderPdfPreviewsInElement(targetEl);
|
|
10730
|
+
decoratePreviewPdfFigures(targetEl);
|
|
10262
10731
|
const annotationMode = (pane === "source" || pane === "response")
|
|
10263
10732
|
? (annotationsEnabled ? "highlight" : "hide")
|
|
10264
10733
|
: "none";
|
|
@@ -11123,71 +11592,92 @@
|
|
|
11123
11592
|
return "<select class='files-sort-select' data-files-sort aria-label='Files sort order' title='Sort file browser entries. Folders remain grouped first.'>" + options + "</select>";
|
|
11124
11593
|
}
|
|
11125
11594
|
|
|
11595
|
+
function buildFileBrowserEntryRowHtml(entry) {
|
|
11596
|
+
const type = entry && entry.type === "directory" ? "directory" : "file";
|
|
11597
|
+
const kind = (entry && entry.kind) || (type === "directory" ? "directory" : "other");
|
|
11598
|
+
const path = String((entry && entry.path) || "");
|
|
11599
|
+
const name = String((entry && entry.name) || basenameForStudioPath(path));
|
|
11600
|
+
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11601
|
+
const metaParts = [getFileBrowserKindLabel(entry)];
|
|
11602
|
+
if (type === "file") metaParts.push(formatFileBrowserSize(entry && entry.size));
|
|
11603
|
+
const time = formatFileBrowserTime(entry && entry.mtimeMs);
|
|
11604
|
+
if (time) metaParts.push(time);
|
|
11605
|
+
const newTabAction = kind === "text" || kind === "office"
|
|
11606
|
+
? "open-new"
|
|
11607
|
+
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11608
|
+
const newTabLabel = kind === "text"
|
|
11609
|
+
? "Open file-backed tab"
|
|
11610
|
+
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11611
|
+
const newTabTitle = kind === "text"
|
|
11612
|
+
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11613
|
+
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11614
|
+
const newTabButton = newTabAction
|
|
11615
|
+
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11616
|
+
: "";
|
|
11617
|
+
const openTitle = type === "directory"
|
|
11618
|
+
? "Open folder"
|
|
11619
|
+
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11620
|
+
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11621
|
+
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11622
|
+
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11623
|
+
+ "<span class='files-name'>" + escapeHtml(name) + "</span>"
|
|
11624
|
+
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11625
|
+
+ "</button>"
|
|
11626
|
+
+ "<span class='files-actions'>"
|
|
11627
|
+
+ newTabButton
|
|
11628
|
+
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(path) + "'>Copy path</button>"
|
|
11629
|
+
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(path) + "'>Reveal</button>" : "")
|
|
11630
|
+
+ "</span>"
|
|
11631
|
+
+ "</div>";
|
|
11632
|
+
}
|
|
11633
|
+
|
|
11634
|
+
function buildFileBrowserLocationSelectHtml(state) {
|
|
11635
|
+
const locations = Array.isArray(state && state.locations) ? state.locations : [];
|
|
11636
|
+
if (!locations.length) return "<span class='files-no-locations'>No allowed folders</span>";
|
|
11637
|
+
const rootDir = String((state && state.rootDir) || "");
|
|
11638
|
+
const options = locations.map((location) => {
|
|
11639
|
+
const path = String((location && location.path) || "");
|
|
11640
|
+
const label = String((location && location.label) || basenameForStudioPath(path) || path);
|
|
11641
|
+
return "<option value='" + escapeHtml(path) + "'" + (path === rootDir ? " selected" : "") + ">" + escapeHtml(label + " — " + path) + "</option>";
|
|
11642
|
+
}).join("");
|
|
11643
|
+
return "<select class='files-location-select' data-files-location aria-label='Allowed Files location' title='Choose a folder allowed for this Studio session on the computer running Pi.'>" + options + "</select>";
|
|
11644
|
+
}
|
|
11645
|
+
|
|
11126
11646
|
function buildFileBrowserPanelHtml() {
|
|
11127
11647
|
const state = fileBrowserState || {};
|
|
11128
11648
|
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
11649
|
+
const exactFiles = Array.isArray(state.exactFiles) ? state.exactFiles : [];
|
|
11129
11650
|
const currentDir = state.currentDir || "";
|
|
11130
11651
|
const rootDir = state.rootDir || "";
|
|
11131
11652
|
const relativeDir = state.relativeDir || ".";
|
|
11132
11653
|
const parentDisabled = state.parentDir ? "" : " disabled";
|
|
11133
11654
|
const rows = entries.length
|
|
11134
|
-
? entries.map((
|
|
11135
|
-
const type = entry.type === "directory" ? "directory" : "file";
|
|
11136
|
-
const kind = entry.kind || (type === "directory" ? "directory" : "other");
|
|
11137
|
-
const icon = type === "directory" ? "📁" : (kind === "pdf" ? "📄" : (kind === "image" ? "🖼️" : (kind === "text" || kind === "office" ? "📝" : "📦")));
|
|
11138
|
-
const metaParts = [];
|
|
11139
|
-
metaParts.push(getFileBrowserKindLabel(entry));
|
|
11140
|
-
if (type === "file") metaParts.push(formatFileBrowserSize(entry.size));
|
|
11141
|
-
const time = formatFileBrowserTime(entry.mtimeMs);
|
|
11142
|
-
if (time) metaParts.push(time);
|
|
11143
|
-
const newTabAction = kind === "text" || kind === "office"
|
|
11144
|
-
? "open-new"
|
|
11145
|
-
: ((kind === "pdf" || kind === "image") ? "open-preview-new" : "");
|
|
11146
|
-
const newTabLabel = kind === "text"
|
|
11147
|
-
? "Open file-backed tab"
|
|
11148
|
-
: (kind === "office" ? "Convert tab" : ((kind === "pdf" || kind === "image") ? "Preview tab" : "New tab"));
|
|
11149
|
-
const newTabTitle = kind === "text"
|
|
11150
|
-
? "Open this file-backed document in a new refreshable editor tab. Save editor and Refresh from disk will use this file."
|
|
11151
|
-
: (kind === "office" ? "Convert this document to Markdown in a new editor tab." : ((kind === "pdf" || kind === "image") ? "Open this preview in a new Studio tab." : "Open in a new Studio tab."));
|
|
11152
|
-
const textActions = newTabAction
|
|
11153
|
-
? "<button type='button' data-files-action='" + escapeHtml(newTabAction) + "' data-files-path='" + escapeHtml(entry.path) + "' title='" + escapeHtml(newTabTitle) + "'>" + escapeHtml(newTabLabel) + "</button>"
|
|
11154
|
-
: "";
|
|
11155
|
-
const openTitle = type === "directory"
|
|
11156
|
-
? "Open folder"
|
|
11157
|
-
: (kind === "text" ? "Open file-backed document in the current editor. Save editor and Refresh from disk will use this file." : (kind === "office" ? "Convert to Markdown in the current editor" : (kind === "pdf" ? "Open PDF preview" : (kind === "image" ? "Open image preview" : "Copy or reveal this file"))));
|
|
11158
|
-
return "<div class='files-row files-row-" + escapeHtml(type) + " files-kind-" + escapeHtml(kind) + "'>"
|
|
11159
|
-
+ "<button type='button' class='files-open-btn' data-files-action='" + (type === "directory" ? "open-dir" : "open") + "' data-files-path='" + escapeHtml(entry.path) + "' data-files-kind='" + escapeHtml(kind) + "' title='" + escapeHtml(openTitle) + "'>"
|
|
11160
|
-
+ "<span class='files-icon' aria-hidden='true'>" + icon + "</span>"
|
|
11161
|
-
+ "<span class='files-name'>" + escapeHtml(entry.name) + "</span>"
|
|
11162
|
-
+ "<span class='files-meta'>" + escapeHtml(metaParts.filter(Boolean).join(" · ")) + "</span>"
|
|
11163
|
-
+ "</button>"
|
|
11164
|
-
+ "<span class='files-actions'>"
|
|
11165
|
-
+ textActions
|
|
11166
|
-
+ "<button type='button' data-files-action='copy-path' data-files-path='" + escapeHtml(entry.path) + "'>Copy path</button>"
|
|
11167
|
-
+ (type === "file" ? "<button type='button' data-files-action='reveal' data-files-path='" + escapeHtml(entry.path) + "'>Reveal</button>" : "")
|
|
11168
|
-
+ "</span>"
|
|
11169
|
-
+ "</div>";
|
|
11170
|
-
}).join("")
|
|
11655
|
+
? entries.map(buildFileBrowserEntryRowHtml).join("")
|
|
11171
11656
|
: "<div class='files-empty'>" + (state.loading ? "Loading files…" : "This folder is empty.") + "</div>";
|
|
11657
|
+
const exactRows = exactFiles.map(buildFileBrowserEntryRowHtml).join("");
|
|
11172
11658
|
const notices = [];
|
|
11173
11659
|
if (state.error) notices.push("<div class='files-notice files-notice-error'>" + escapeHtml(state.error) + "</div>");
|
|
11660
|
+
if (state.grantRequiredPath) notices.push("<div class='files-notice'>Allow that folder to browse it in Files. Exact-file grants do not expose their parent folders.</div>");
|
|
11174
11661
|
if (state.omitted) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omitted)) + " item" + (state.omitted === 1 ? "" : "s") + " omitted.</div>");
|
|
11175
11662
|
if (state.omittedIgnored) notices.push("<div class='files-notice'>" + escapeHtml(String(state.omittedIgnored)) + " heavy/cache folder" + (state.omittedIgnored === 1 ? "" : "s") + " hidden.</div>");
|
|
11176
11663
|
return "<div class='files-panel'>"
|
|
11177
11664
|
+ "<div class='files-toolbar'>"
|
|
11178
11665
|
+ "<div class='files-path-group'><span class='files-label'>Files</span><span class='files-path' title='" + escapeHtml(currentDir) + "'>" + escapeHtml(relativeDir || ".") + "</span></div>"
|
|
11179
11666
|
+ "<div class='files-toolbar-actions'>"
|
|
11667
|
+
+ buildFileBrowserLocationSelectHtml(state)
|
|
11180
11668
|
+ buildFileBrowserSortSelectHtml()
|
|
11669
|
+
+ "<button type='button' data-files-action='allow-folder'>Allow folder…</button>"
|
|
11181
11670
|
+ "<button type='button' data-files-action='parent'" + parentDisabled + ">Parent</button>"
|
|
11182
11671
|
+ "<button type='button' data-files-action='refresh'>Refresh</button>"
|
|
11183
11672
|
+ (currentDir ? "<button type='button' data-files-action='copy-current' data-files-path='" + escapeHtml(currentDir) + "'>Copy path</button>" : "")
|
|
11184
11673
|
+ (currentDir ? "<button type='button' data-files-action='use-working-dir' data-files-path='" + escapeHtml(currentDir) + "'>Use as working dir</button>" : "")
|
|
11185
|
-
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager.'>Open root</button>" : "")
|
|
11674
|
+
+ (rootDir ? "<button type='button' data-files-action='open-root' data-files-path='" + escapeHtml(rootDir) + "' title='Open the Files root folder in Finder or the system file manager on the computer running Pi.'>Open root</button>" : "")
|
|
11186
11675
|
+ (rootDir ? "<button type='button' data-files-action='copy-root' data-files-path='" + escapeHtml(rootDir) + "'>Copy root</button>" : "")
|
|
11187
11676
|
+ "</div>"
|
|
11188
11677
|
+ "</div>"
|
|
11189
|
-
+ "<div class='files-subtitle'>
|
|
11678
|
+
+ "<div class='files-subtitle'>Allowed root on the computer running Pi: <span title='" + escapeHtml(rootDir) + "'>" + escapeHtml(rootDir || "none selected") + "</span></div>"
|
|
11190
11679
|
+ notices.join("")
|
|
11680
|
+
+ (exactRows ? "<section class='files-exact-section'><div class='files-section-title'>Allowed exact files</div><div class='files-list' role='list'>" + exactRows + "</div></section>" : "")
|
|
11191
11681
|
+ "<div class='files-list' role='list'>" + rows + "</div>"
|
|
11192
11682
|
+ "</div>";
|
|
11193
11683
|
}
|
|
@@ -11202,6 +11692,9 @@
|
|
|
11202
11692
|
relativeDir: "",
|
|
11203
11693
|
parentDir: null,
|
|
11204
11694
|
entries: [],
|
|
11695
|
+
exactFiles: [],
|
|
11696
|
+
locations: [],
|
|
11697
|
+
grantRequiredPath: "",
|
|
11205
11698
|
omitted: 0,
|
|
11206
11699
|
omittedIgnored: 0,
|
|
11207
11700
|
sort: fileBrowserSortMode,
|
|
@@ -11223,12 +11716,17 @@
|
|
|
11223
11716
|
async function loadFileBrowserDirectory(dir, options) {
|
|
11224
11717
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11225
11718
|
const contextKey = getFileBrowserContextKey();
|
|
11719
|
+
const config = options && typeof options === "object" ? options : {};
|
|
11720
|
+
const requestedRoot = Object.prototype.hasOwnProperty.call(config, "root")
|
|
11721
|
+
? normalizeStudioResourceDirValue(config.root || "")
|
|
11722
|
+
: normalizeStudioResourceDirValue(fileBrowserState.rootDir || "");
|
|
11226
11723
|
const nonce = ++fileBrowserLoadNonce;
|
|
11227
11724
|
fileBrowserState = {
|
|
11228
11725
|
...fileBrowserState,
|
|
11229
11726
|
contextKey,
|
|
11230
11727
|
loading: true,
|
|
11231
11728
|
error: "",
|
|
11729
|
+
grantRequiredPath: "",
|
|
11232
11730
|
};
|
|
11233
11731
|
if (rightView === "files") {
|
|
11234
11732
|
finishPreviewRender(critiqueViewEl);
|
|
@@ -11237,6 +11735,7 @@
|
|
|
11237
11735
|
try {
|
|
11238
11736
|
const query = {};
|
|
11239
11737
|
if (dir) query.dir = String(dir);
|
|
11738
|
+
if (requestedRoot) query.root = requestedRoot;
|
|
11240
11739
|
if (context.sourcePath) query.sourcePath = context.sourcePath;
|
|
11241
11740
|
if (context.resourceDir) query.resourceDir = context.resourceDir;
|
|
11242
11741
|
query.sort = normalizeFileBrowserSortMode(fileBrowserSortMode);
|
|
@@ -11249,6 +11748,9 @@
|
|
|
11249
11748
|
relativeDir: typeof payload.relativeDir === "string" ? payload.relativeDir : ".",
|
|
11250
11749
|
parentDir: typeof payload.parentDir === "string" ? payload.parentDir : null,
|
|
11251
11750
|
entries: Array.isArray(payload.entries) ? payload.entries : [],
|
|
11751
|
+
exactFiles: Array.isArray(payload.exactFiles) ? payload.exactFiles : [],
|
|
11752
|
+
locations: Array.isArray(payload.locations) ? payload.locations : [],
|
|
11753
|
+
grantRequiredPath: "",
|
|
11252
11754
|
omitted: Number(payload.omitted) || 0,
|
|
11253
11755
|
omittedIgnored: Number(payload.omittedIgnored) || 0,
|
|
11254
11756
|
sort: normalizeFileBrowserSortMode(payload.sort || fileBrowserSortMode),
|
|
@@ -11262,11 +11764,18 @@
|
|
|
11262
11764
|
critiqueViewEl.innerHTML = buildFileBrowserPanelHtml();
|
|
11263
11765
|
scheduleResponsePaneRepaintNudge();
|
|
11264
11766
|
}
|
|
11265
|
-
if (
|
|
11767
|
+
if (config.user) setStatus("Loaded file list.", "success");
|
|
11266
11768
|
} catch (error) {
|
|
11267
11769
|
if (nonce !== fileBrowserLoadNonce) return;
|
|
11770
|
+
const errorPayload = error && error.studioPayload && typeof error.studioPayload === "object" ? error.studioPayload : null;
|
|
11771
|
+
const grantRequiredPath = errorPayload && errorPayload.code === "studio-resource-directory-grant-required" && typeof errorPayload.directoryPath === "string"
|
|
11772
|
+
? normalizeStudioResourceDirValue(errorPayload.directoryPath)
|
|
11773
|
+
: "";
|
|
11268
11774
|
fileBrowserState = {
|
|
11269
11775
|
...fileBrowserState,
|
|
11776
|
+
exactFiles: errorPayload && Array.isArray(errorPayload.exactFiles) ? errorPayload.exactFiles : fileBrowserState.exactFiles,
|
|
11777
|
+
locations: errorPayload && Array.isArray(errorPayload.locations) ? errorPayload.locations : fileBrowserState.locations,
|
|
11778
|
+
grantRequiredPath,
|
|
11270
11779
|
loading: false,
|
|
11271
11780
|
error: (error && error.message) ? error.message : String(error || "Could not load files."),
|
|
11272
11781
|
loaded: true,
|
|
@@ -11314,7 +11823,7 @@
|
|
|
11314
11823
|
return;
|
|
11315
11824
|
}
|
|
11316
11825
|
if (kind === "pdf") {
|
|
11317
|
-
openPreviewPdfLink(path, path, context);
|
|
11826
|
+
await openPreviewPdfLink(path, path, context);
|
|
11318
11827
|
return;
|
|
11319
11828
|
}
|
|
11320
11829
|
if (kind === "image") {
|
|
@@ -11324,6 +11833,34 @@
|
|
|
11324
11833
|
setStatus("No Studio preview for this file type. Use Copy path or Reveal.", "warning");
|
|
11325
11834
|
}
|
|
11326
11835
|
|
|
11836
|
+
async function allowFileBrowserFolder() {
|
|
11837
|
+
const context = getHtmlPreviewResourceContextOptions();
|
|
11838
|
+
const suggested = normalizeStudioResourceDirValue(
|
|
11839
|
+
fileBrowserState.grantRequiredPath
|
|
11840
|
+
|| fileBrowserState.currentDir
|
|
11841
|
+
|| fileBrowserState.rootDir
|
|
11842
|
+
|| context.resourceDir
|
|
11843
|
+
|| (context.sourcePath ? dirnameForDisplayPath(context.sourcePath) : "")
|
|
11844
|
+
|| "./",
|
|
11845
|
+
);
|
|
11846
|
+
const path = await requestStudioTextInput(
|
|
11847
|
+
"Allow a folder on the computer running Pi for this Studio session:",
|
|
11848
|
+
suggested,
|
|
11849
|
+
{ title: "Allow folder", confirmLabel: "Allow folder", inputLabel: "Folder path" },
|
|
11850
|
+
);
|
|
11851
|
+
if (!path) return;
|
|
11852
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
11853
|
+
method: "POST",
|
|
11854
|
+
body: JSON.stringify({ grantKind: "directory", path }),
|
|
11855
|
+
});
|
|
11856
|
+
const grantedPath = payload && payload.grant && payload.grant.kind === "directory" && typeof payload.grant.path === "string"
|
|
11857
|
+
? payload.grant.path
|
|
11858
|
+
: "";
|
|
11859
|
+
if (!grantedPath) throw new Error("Studio did not return the allowed folder.");
|
|
11860
|
+
await loadFileBrowserDirectory("", { root: grantedPath });
|
|
11861
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed folder for this Studio session.", "success");
|
|
11862
|
+
}
|
|
11863
|
+
|
|
11327
11864
|
function setFileBrowserCurrentDirectoryAsWorkingDir(path) {
|
|
11328
11865
|
const nextDir = normalizeStudioResourceDirValue(path || fileBrowserState.currentDir || "");
|
|
11329
11866
|
if (!nextDir) {
|
|
@@ -11345,6 +11882,7 @@
|
|
|
11345
11882
|
}
|
|
11346
11883
|
const context = getHtmlPreviewResourceContextOptions();
|
|
11347
11884
|
const body = { dir: targetDir };
|
|
11885
|
+
if (fileBrowserState.rootDir) body.root = fileBrowserState.rootDir;
|
|
11348
11886
|
if (context.sourcePath) body.sourcePath = context.sourcePath;
|
|
11349
11887
|
if (context.resourceDir) body.resourceDir = context.resourceDir;
|
|
11350
11888
|
const payload = await fetchStudioJson("/file-browser-open", {
|
|
@@ -11357,6 +11895,14 @@
|
|
|
11357
11895
|
async function handleFilesPaneChange(event) {
|
|
11358
11896
|
if (rightView !== "files") return;
|
|
11359
11897
|
const target = event.target;
|
|
11898
|
+
const locationSelect = target instanceof Element ? target.closest("[data-files-location]") : null;
|
|
11899
|
+
if (locationSelect && "value" in locationSelect) {
|
|
11900
|
+
const nextRoot = normalizeStudioResourceDirValue(locationSelect.value || "");
|
|
11901
|
+
if (nextRoot && nextRoot !== fileBrowserState.rootDir) {
|
|
11902
|
+
await loadFileBrowserDirectory("", { root: nextRoot, user: true });
|
|
11903
|
+
}
|
|
11904
|
+
return;
|
|
11905
|
+
}
|
|
11360
11906
|
const sortSelect = target instanceof Element ? target.closest("[data-files-sort]") : null;
|
|
11361
11907
|
if (!sortSelect || !("value" in sortSelect)) return;
|
|
11362
11908
|
const nextSort = writeFileBrowserSortMode(sortSelect.value);
|
|
@@ -11375,6 +11921,10 @@
|
|
|
11375
11921
|
const path = actionEl.getAttribute("data-files-path") || "";
|
|
11376
11922
|
const kind = actionEl.getAttribute("data-files-kind") || getPreviewLocalLinkKind(path);
|
|
11377
11923
|
try {
|
|
11924
|
+
if (action === "allow-folder") {
|
|
11925
|
+
await allowFileBrowserFolder();
|
|
11926
|
+
return;
|
|
11927
|
+
}
|
|
11378
11928
|
if (action === "parent") {
|
|
11379
11929
|
if (fileBrowserState.parentDir) await loadFileBrowserDirectory(fileBrowserState.parentDir, { user: true });
|
|
11380
11930
|
return;
|
|
@@ -11392,14 +11942,6 @@
|
|
|
11392
11942
|
return;
|
|
11393
11943
|
}
|
|
11394
11944
|
if (action === "open-new") {
|
|
11395
|
-
if (kind === "text" && isLikelyAbsoluteStudioPath(path)) {
|
|
11396
|
-
openFileBackedStudioEditorTab(path, {
|
|
11397
|
-
label: basenameForStudioPath(path),
|
|
11398
|
-
resourceDir: fileBrowserState.rootDir || getCurrentResourceDirValue() || dirnameForDisplayPath(path),
|
|
11399
|
-
});
|
|
11400
|
-
setStatus("Opening file-backed document in a new editor.");
|
|
11401
|
-
return;
|
|
11402
|
-
}
|
|
11403
11945
|
await openPreviewDocumentInNewEditor(path, getFileBrowserLocalLinkContext());
|
|
11404
11946
|
return;
|
|
11405
11947
|
}
|
|
@@ -12113,6 +12655,33 @@
|
|
|
12113
12655
|
};
|
|
12114
12656
|
}
|
|
12115
12657
|
|
|
12658
|
+
async function ensureSideQuestionContextRootAuthorized(context) {
|
|
12659
|
+
if (!context || context.gatherScope === "none") return { contextRoot: "" };
|
|
12660
|
+
const body = JSON.stringify({
|
|
12661
|
+
gatherScope: context.gatherScope,
|
|
12662
|
+
sourcePath: context.sourcePath,
|
|
12663
|
+
resourceDir: context.resourceDir,
|
|
12664
|
+
contextPath: context.contextPath,
|
|
12665
|
+
});
|
|
12666
|
+
const check = () => fetchStudioJson("/side-question-context-root", { method: "POST", body });
|
|
12667
|
+
try {
|
|
12668
|
+
return await check();
|
|
12669
|
+
} catch (error) {
|
|
12670
|
+
const request = getStudioDirectoryGrantRequest(error);
|
|
12671
|
+
if (!request) throw error;
|
|
12672
|
+
const allowed = await requestStudioDirectoryGrant(request, {
|
|
12673
|
+
message: "Side questions can map, search, and read supported files beneath this folder through read-only context tools. Allow that access for this Studio session?",
|
|
12674
|
+
cancelStatus: "Side-question folder access cancelled.",
|
|
12675
|
+
});
|
|
12676
|
+
if (!allowed) {
|
|
12677
|
+
const cancelled = new Error("Side-question folder access cancelled.");
|
|
12678
|
+
cancelled.studioCancelled = true;
|
|
12679
|
+
throw cancelled;
|
|
12680
|
+
}
|
|
12681
|
+
return check();
|
|
12682
|
+
}
|
|
12683
|
+
}
|
|
12684
|
+
|
|
12116
12685
|
async function renderSideQuestionMarkdownToHtml(markdown) {
|
|
12117
12686
|
const source = String(markdown || "");
|
|
12118
12687
|
if (sideQuestionMarkdownRenderCache.has(source)) return sideQuestionMarkdownRenderCache.get(source);
|
|
@@ -12137,10 +12706,15 @@
|
|
|
12137
12706
|
const html = await renderSideQuestionMarkdownToHtml(markdown);
|
|
12138
12707
|
if (nonce !== sideQuestionPreviewRenderNonce || rightView !== "side-questions" || !critiqueViewEl.contains(target)) return;
|
|
12139
12708
|
target.innerHTML = html;
|
|
12709
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
12140
12710
|
await renderAnnotationMathInElement(target);
|
|
12711
|
+
decoratePdfEmbeds(target);
|
|
12712
|
+
await renderPdfPreviewsInElement(target);
|
|
12713
|
+
decoratePreviewPdfFigures(target);
|
|
12141
12714
|
await renderMermaidInElement(target);
|
|
12142
12715
|
await renderMathFallbackInElement(target);
|
|
12143
12716
|
decorateCopyablePreviewBlocks(target);
|
|
12717
|
+
decoratePreviewImages(target);
|
|
12144
12718
|
} catch (error) {
|
|
12145
12719
|
console.error("Side-question markdown preview failed:", error);
|
|
12146
12720
|
target.classList.add("side-question-markdown-failed");
|
|
@@ -12199,6 +12773,7 @@
|
|
|
12199
12773
|
], sideQuestionUi.thinking) + "</select></label>"
|
|
12200
12774
|
+ "</div>"
|
|
12201
12775
|
+ "<details class='side-question-context-rule'><summary id='sideQuestionContextRule'>Automatic: selection → heading block at cursor → nearby text</summary><p>A heading block starts at the nearest Markdown/LaTeX heading above the cursor and ends before the next heading of the same or higher level. With no heading, Studio uses the surrounding text block or a nearby excerpt.</p></details>"
|
|
12776
|
+
+ (scope === "none" ? "" : "<p class='side-question-context-access-note'>Related-file access is limited to folders allowed for this Studio session. Studio asks before starting if this folder is not already allowed.</p>")
|
|
12202
12777
|
+ (scope === "custom" ? "<label class='side-question-path-label'>Folder path<input data-side-question-field='customPath' type='text' value='" + escapeHtml(sideQuestionUi.customPath) + "' placeholder='Folder on the computer running Pi'></label>" : "")
|
|
12203
12778
|
+ "<div class='side-question-checks'>"
|
|
12204
12779
|
+ "<label><input data-side-question-field='includeConversation' type='checkbox'" + (sideQuestionUi.includeConversation ? " checked" : "") + "> Include the current main conversation snapshot</label>"
|
|
@@ -12209,7 +12784,7 @@
|
|
|
12209
12784
|
+ "<dl class='side-question-context-summary'><div><dt>Starting text</dt><dd>" + escapeHtml(summary.attachmentText) + "</dd></div><div><dt>Related files</dt><dd>" + escapeHtml(summary.relatedFilesText) + "</dd></div>" + (summary.gitContextText ? "<div><dt>Git context</dt><dd>" + escapeHtml(summary.gitContextText) + "</dd></div>" : "") + "</dl>"
|
|
12210
12785
|
+ "<label class='side-question-composer-label'>Question<textarea data-side-question-field='draft' rows='4' title='Enter adds a new line. Cmd/Ctrl+Enter asks the side question.' placeholder='Ask about the starting text or anything you want checked…'>" + escapeHtml(sideQuestionUi.draft) + "</textarea></label>"
|
|
12211
12786
|
+ (sideQuestionState && sideQuestionState.error ? "<div class='side-question-error'>" + escapeHtml(sideQuestionState.error) + "</div>" : "")
|
|
12212
|
-
+ "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (!isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question") + "</button></div>"
|
|
12787
|
+
+ "<div class='side-question-actions'><button type='button' class='side-question-primary' data-side-question-action='ask' aria-keyshortcuts='Meta+Enter Control+Enter' title='Ask side question (Cmd/Ctrl+Enter)'" + (sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || (sideQuestionState && sideQuestionState.status === "running") || !sideQuestionUi.draft.trim() || (scope === "custom" && !sideQuestionUi.customPath.trim()) ? " disabled" : "") + ">" + (sideQuestionContextGrantPending ? "Checking related-file access…" : (sideQuestionState && sideQuestionState.status === "running" ? "Preparing side thread…" : "Ask side question")) + "</button></div>"
|
|
12213
12788
|
+ "</div>";
|
|
12214
12789
|
}
|
|
12215
12790
|
|
|
@@ -12280,7 +12855,8 @@
|
|
|
12280
12855
|
}
|
|
12281
12856
|
}
|
|
12282
12857
|
|
|
12283
|
-
function submitSideQuestion() {
|
|
12858
|
+
async function submitSideQuestion() {
|
|
12859
|
+
if (sideQuestionContextGrantPending) return;
|
|
12284
12860
|
const question = String(sideQuestionUi.draft || "").trim();
|
|
12285
12861
|
if (!question) {
|
|
12286
12862
|
setStatus("Enter a side question first.", "warning");
|
|
@@ -12299,11 +12875,31 @@
|
|
|
12299
12875
|
if (sideQuestionState && sideQuestionState.threadId) {
|
|
12300
12876
|
message.threadId = sideQuestionState.threadId;
|
|
12301
12877
|
} else {
|
|
12302
|
-
|
|
12303
|
-
if (
|
|
12878
|
+
const context = buildSideQuestionContextPayload();
|
|
12879
|
+
if (context.gatherScope === "custom" && !String(context.contextPath || "").trim()) {
|
|
12304
12880
|
setStatus("Choose a custom context path first.", "warning");
|
|
12305
12881
|
return;
|
|
12306
12882
|
}
|
|
12883
|
+
if (context.gatherScope !== "none") {
|
|
12884
|
+
sideQuestionContextGrantPending = true;
|
|
12885
|
+
if (rightView === "side-questions") renderSideQuestionView();
|
|
12886
|
+
setStatus("Checking side-question folder access…", "warning");
|
|
12887
|
+
try {
|
|
12888
|
+
await ensureSideQuestionContextRootAuthorized(context);
|
|
12889
|
+
} catch (error) {
|
|
12890
|
+
if (!(error && error.studioCancelled)) {
|
|
12891
|
+
const messageText = error && error.message ? error.message : String(error || "Could not authorize side-question context.");
|
|
12892
|
+
if (!sideQuestionState) sideQuestionState = normalizeSideQuestionState(null);
|
|
12893
|
+
sideQuestionState.error = messageText;
|
|
12894
|
+
setStatus("Could not use related files: " + messageText, "warning");
|
|
12895
|
+
}
|
|
12896
|
+
return;
|
|
12897
|
+
} finally {
|
|
12898
|
+
sideQuestionContextGrantPending = false;
|
|
12899
|
+
if (rightView === "side-questions" && (!sideQuestionState || !sideQuestionState.threadId)) renderSideQuestionView();
|
|
12900
|
+
}
|
|
12901
|
+
}
|
|
12902
|
+
message.context = context;
|
|
12307
12903
|
}
|
|
12308
12904
|
if (!sendMessage(message)) return;
|
|
12309
12905
|
sideQuestionUi.draft = "";
|
|
@@ -12419,7 +13015,7 @@
|
|
|
12419
13015
|
event.preventDefault();
|
|
12420
13016
|
const action = target.getAttribute("data-side-question-action");
|
|
12421
13017
|
if (action === "ask") {
|
|
12422
|
-
submitSideQuestion();
|
|
13018
|
+
await submitSideQuestion();
|
|
12423
13019
|
} else if (action === "stop") {
|
|
12424
13020
|
if (sideQuestionState && sideQuestionState.threadId && sideQuestionState.requestId) {
|
|
12425
13021
|
sendMessage({ type: "side_question_cancel_request", threadId: sideQuestionState.threadId, requestId: sideQuestionState.requestId });
|
|
@@ -12472,7 +13068,7 @@
|
|
|
12472
13068
|
if (field === "draft") sideQuestionUi.draft = target.value;
|
|
12473
13069
|
if (field === "customPath") sideQuestionUi.customPath = target.value;
|
|
12474
13070
|
const askButton = critiqueViewEl.querySelector("[data-side-question-action='ask']");
|
|
12475
|
-
if (askButton) askButton.disabled = !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
13071
|
+
if (askButton) askButton.disabled = sideQuestionContextGrantPending || !isSideQuestionConnectionReady() || !sideQuestionUi.draft.trim() || (getSideQuestionGatherScope() === "custom" && !sideQuestionUi.customPath.trim());
|
|
12476
13072
|
}
|
|
12477
13073
|
|
|
12478
13074
|
function handleSideQuestionKeydown(event) {
|
|
@@ -12486,7 +13082,7 @@
|
|
|
12486
13082
|
if (!submitShortcut) return;
|
|
12487
13083
|
event.preventDefault();
|
|
12488
13084
|
event.stopPropagation();
|
|
12489
|
-
if (!sideQuestionState || sideQuestionState.status !== "running") submitSideQuestion();
|
|
13085
|
+
if (!sideQuestionState || sideQuestionState.status !== "running") void submitSideQuestion();
|
|
12490
13086
|
}
|
|
12491
13087
|
|
|
12492
13088
|
async function handleSideQuestionChange(event) {
|
|
@@ -14305,6 +14901,11 @@
|
|
|
14305
14901
|
return launch.fail(message || "Studio could not prepare this tab. Return to the originating Studio page for details.");
|
|
14306
14902
|
}
|
|
14307
14903
|
|
|
14904
|
+
function cancelPendingStudioTab(launch, message) {
|
|
14905
|
+
if (!launch || typeof launch.cancel !== "function") return false;
|
|
14906
|
+
return launch.cancel(message || "Studio tab launch was cancelled.");
|
|
14907
|
+
}
|
|
14908
|
+
|
|
14308
14909
|
function abandonAllStudioTabLaunches(message) {
|
|
14309
14910
|
const launches = Array.from(activeStudioTabLaunches);
|
|
14310
14911
|
activeStudioTabLaunches.clear();
|
|
@@ -14383,7 +14984,10 @@
|
|
|
14383
14984
|
const message = payload && typeof payload.error === "string"
|
|
14384
14985
|
? payload.error
|
|
14385
14986
|
: (response.status + " " + response.statusText).trim();
|
|
14386
|
-
|
|
14987
|
+
const requestError = new Error(message || (method + " " + pathname + " failed."));
|
|
14988
|
+
requestError.studioPayload = payload && typeof payload === "object" ? payload : null;
|
|
14989
|
+
requestError.studioStatus = response.status;
|
|
14990
|
+
throw requestError;
|
|
14387
14991
|
}
|
|
14388
14992
|
return payload;
|
|
14389
14993
|
}
|
|
@@ -14416,6 +15020,7 @@
|
|
|
14416
15020
|
]);
|
|
14417
15021
|
let previewLinkMenuEl = null;
|
|
14418
15022
|
let activePreviewLinkContext = null;
|
|
15023
|
+
let previewLinkMenuRequestId = 0;
|
|
14419
15024
|
|
|
14420
15025
|
function stripPreviewLocalLinkUrlSuffix(href) {
|
|
14421
15026
|
const raw = String(href || "").trim();
|
|
@@ -14482,7 +15087,7 @@
|
|
|
14482
15087
|
if (!raw || raw.charAt(0) === "#") return false;
|
|
14483
15088
|
if (/^\/\//.test(raw)) return false;
|
|
14484
15089
|
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
14485
|
-
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
15090
|
+
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|import-file-copy|resource-grants|local-preview-link|reveal-local-resource|open-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
14486
15091
|
return true;
|
|
14487
15092
|
}
|
|
14488
15093
|
|
|
@@ -14552,19 +15157,35 @@
|
|
|
14552
15157
|
menu.style.top = y + "px";
|
|
14553
15158
|
}
|
|
14554
15159
|
|
|
14555
|
-
function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
15160
|
+
async function showPreviewLinkMenu(anchor, event, contextOverride) {
|
|
14556
15161
|
const href = String(anchor && anchor.getAttribute ? anchor.getAttribute("href") || "" : (contextOverride && contextOverride.href ? contextOverride.href : "")).trim();
|
|
14557
15162
|
if (!isStudioLocalPreviewHref(href)) return false;
|
|
14558
15163
|
const kind = getPreviewLocalLinkKind(href);
|
|
14559
|
-
const menu = ensurePreviewLinkMenu();
|
|
14560
|
-
menu.innerHTML = "";
|
|
14561
15164
|
const linkContext = getEffectivePreviewLinkContext(contextOverride);
|
|
14562
|
-
|
|
15165
|
+
const nextContext = {
|
|
14563
15166
|
href,
|
|
14564
15167
|
title: String((contextOverride && contextOverride.title) || (anchor && anchor.textContent) || href || "local link").trim() || href,
|
|
14565
15168
|
sourcePath: linkContext.sourcePath,
|
|
14566
15169
|
resourceDir: linkContext.resourceDir,
|
|
14567
15170
|
};
|
|
15171
|
+
const menuPoint = {
|
|
15172
|
+
clientX: event && event.clientX,
|
|
15173
|
+
clientY: event && event.clientY,
|
|
15174
|
+
};
|
|
15175
|
+
closePreviewLinkMenu();
|
|
15176
|
+
const menuRequestId = ++previewLinkMenuRequestId;
|
|
15177
|
+
try {
|
|
15178
|
+
await fetchPreviewLocalLink("resolve", href, nextContext);
|
|
15179
|
+
} catch (error) {
|
|
15180
|
+
if (!(error && error.studioCancelled)) {
|
|
15181
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not inspect this local resource."), "warning");
|
|
15182
|
+
}
|
|
15183
|
+
return false;
|
|
15184
|
+
}
|
|
15185
|
+
if (menuRequestId !== previewLinkMenuRequestId) return false;
|
|
15186
|
+
const menu = ensurePreviewLinkMenu();
|
|
15187
|
+
menu.innerHTML = "";
|
|
15188
|
+
activePreviewLinkContext = nextContext;
|
|
14568
15189
|
if (kind === "pdf") {
|
|
14569
15190
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
14570
15191
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
@@ -14581,7 +15202,7 @@
|
|
|
14581
15202
|
}
|
|
14582
15203
|
appendPreviewLinkMenuButton(menu, "Reveal in file manager", "reveal");
|
|
14583
15204
|
appendPreviewLinkMenuButton(menu, "Copy path", "copy-path");
|
|
14584
|
-
positionPreviewLinkMenu(menu,
|
|
15205
|
+
positionPreviewLinkMenu(menu, menuPoint.clientX, menuPoint.clientY);
|
|
14585
15206
|
const firstButton = menu.querySelector("button");
|
|
14586
15207
|
if (firstButton && typeof firstButton.focus === "function") {
|
|
14587
15208
|
window.setTimeout(() => firstButton.focus({ preventScroll: true }), 0);
|
|
@@ -14589,10 +15210,102 @@
|
|
|
14589
15210
|
return true;
|
|
14590
15211
|
}
|
|
14591
15212
|
|
|
14592
|
-
|
|
14593
|
-
|
|
15213
|
+
function getStudioResourceGrantRequest(error) {
|
|
15214
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15215
|
+
? error.studioPayload
|
|
15216
|
+
: null;
|
|
15217
|
+
if (!payload || payload.code !== "studio-resource-grant-required") return null;
|
|
15218
|
+
const path = typeof payload.path === "string" ? payload.path.trim() : "";
|
|
15219
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15220
|
+
if (!path || !directoryPath) return null;
|
|
15221
|
+
return {
|
|
15222
|
+
path,
|
|
15223
|
+
directoryPath,
|
|
15224
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(path),
|
|
15225
|
+
};
|
|
15226
|
+
}
|
|
15227
|
+
|
|
15228
|
+
function getStudioDirectoryGrantRequest(error) {
|
|
15229
|
+
const payload = error && error.studioPayload && typeof error.studioPayload === "object"
|
|
15230
|
+
? error.studioPayload
|
|
15231
|
+
: null;
|
|
15232
|
+
if (!payload || payload.code !== "studio-resource-directory-grant-required") return null;
|
|
15233
|
+
const directoryPath = typeof payload.directoryPath === "string" ? payload.directoryPath.trim() : "";
|
|
15234
|
+
if (!directoryPath) return null;
|
|
15235
|
+
return {
|
|
15236
|
+
directoryPath,
|
|
15237
|
+
label: typeof payload.label === "string" && payload.label.trim() ? payload.label.trim() : basenameForStudioPath(directoryPath),
|
|
15238
|
+
};
|
|
15239
|
+
}
|
|
15240
|
+
|
|
15241
|
+
async function requestStudioDirectoryGrant(request, options) {
|
|
15242
|
+
if (!request || !request.directoryPath) return false;
|
|
15243
|
+
const config = options && typeof options === "object" ? options : {};
|
|
15244
|
+
const choice = await openStudioDecision({
|
|
15245
|
+
mode: "confirm",
|
|
15246
|
+
title: "Allow " + (request.label || "this folder") + "?",
|
|
15247
|
+
message: (config.message || "Allow read-only access beneath this folder for the current Studio session?")
|
|
15248
|
+
+ "\n\nFolder on the computer running Pi:\n" + request.directoryPath,
|
|
15249
|
+
cancelLabel: "Cancel",
|
|
15250
|
+
confirmLabel: "Allow this folder for this Studio session",
|
|
15251
|
+
});
|
|
15252
|
+
if (choice !== true) {
|
|
15253
|
+
setStatus(config.cancelStatus || "Folder access cancelled.", "warning");
|
|
15254
|
+
return false;
|
|
15255
|
+
}
|
|
15256
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15257
|
+
method: "POST",
|
|
15258
|
+
body: JSON.stringify({ grantKind: "directory", path: request.directoryPath }),
|
|
15259
|
+
});
|
|
15260
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15261
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed this folder for the current Studio session.", "success");
|
|
15262
|
+
return true;
|
|
15263
|
+
}
|
|
15264
|
+
|
|
15265
|
+
async function requestStudioResourceGrant(request) {
|
|
15266
|
+
if (!request) return false;
|
|
15267
|
+
const choice = await openStudioDecision({
|
|
15268
|
+
mode: "confirm",
|
|
15269
|
+
title: "Allow " + request.label + "?",
|
|
15270
|
+
message: "This local resource is outside the locations currently available to Studio.\n\n"
|
|
15271
|
+
+ "File on the computer running Pi:\n" + request.path + "\n\n"
|
|
15272
|
+
+ "Allow only this file, or allow its containing folder for this Studio session?",
|
|
15273
|
+
cancelLabel: "Cancel",
|
|
15274
|
+
secondaryLabel: "Allow this folder for this Studio session",
|
|
15275
|
+
secondaryValue: "directory",
|
|
15276
|
+
confirmLabel: "Allow this file",
|
|
15277
|
+
});
|
|
15278
|
+
const grantKind = choice === true ? "file" : choice === "directory" ? "directory" : "";
|
|
15279
|
+
if (!grantKind) {
|
|
15280
|
+
setStatus("Local resource access cancelled.", "warning");
|
|
15281
|
+
return false;
|
|
15282
|
+
}
|
|
15283
|
+
const grantPath = grantKind === "directory" ? request.directoryPath : request.path;
|
|
15284
|
+
const payload = await fetchStudioJson("/resource-grants", {
|
|
15285
|
+
method: "POST",
|
|
15286
|
+
body: JSON.stringify({ grantKind, path: grantPath }),
|
|
15287
|
+
});
|
|
15288
|
+
fileBrowserState = { ...fileBrowserState, contextKey: "", loaded: false };
|
|
15289
|
+
setStatus(typeof payload.message === "string" ? payload.message : "Allowed local resource for this Studio session.", "success");
|
|
15290
|
+
return true;
|
|
15291
|
+
}
|
|
15292
|
+
|
|
15293
|
+
async function fetchPreviewLocalLink(action, href, contextOverride, options) {
|
|
15294
|
+
const request = () => fetchStudioJson("/local-preview-link", {
|
|
14594
15295
|
query: { ...getPreviewLinkResourceQuery(href, contextOverride), action },
|
|
14595
15296
|
});
|
|
15297
|
+
try {
|
|
15298
|
+
return await request();
|
|
15299
|
+
} catch (error) {
|
|
15300
|
+
const grantRequest = getStudioResourceGrantRequest(error);
|
|
15301
|
+
if (!grantRequest || (options && options.skipGrantPrompt === true)) throw error;
|
|
15302
|
+
if (!(await requestStudioResourceGrant(grantRequest))) {
|
|
15303
|
+
const cancelled = new Error("Local resource access cancelled.");
|
|
15304
|
+
cancelled.studioCancelled = true;
|
|
15305
|
+
throw cancelled;
|
|
15306
|
+
}
|
|
15307
|
+
return request();
|
|
15308
|
+
}
|
|
14596
15309
|
}
|
|
14597
15310
|
|
|
14598
15311
|
function getPreviewPdfViewerUrl(href, contextOverride) {
|
|
@@ -14603,16 +15316,16 @@
|
|
|
14603
15316
|
return resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
14604
15317
|
}
|
|
14605
15318
|
|
|
14606
|
-
function openPreviewPdfLink(href, title, contextOverride) {
|
|
15319
|
+
async function openPreviewPdfLink(href, title, contextOverride) {
|
|
15320
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14607
15321
|
const viewerUrl = getPreviewPdfViewerUrl(href, contextOverride);
|
|
14608
15322
|
if (!viewerUrl) {
|
|
14609
15323
|
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
14610
15324
|
return false;
|
|
14611
15325
|
}
|
|
14612
|
-
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
14613
15326
|
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
14614
15327
|
const resourceQuery = buildStudioPdfResourceQuery({
|
|
14615
|
-
path:
|
|
15328
|
+
path: stripPreviewLocalLinkUrlSuffix(href),
|
|
14616
15329
|
sourcePath: context.sourcePath || "",
|
|
14617
15330
|
resourceDir: context.resourceDir || "",
|
|
14618
15331
|
}, true);
|
|
@@ -14621,6 +15334,7 @@
|
|
|
14621
15334
|
}
|
|
14622
15335
|
|
|
14623
15336
|
async function openPreviewImageLink(href, title, contextOverride) {
|
|
15337
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14624
15338
|
const payload = await fetchStudioJson("/html-preview-resource", {
|
|
14625
15339
|
query: getPreviewLinkResourceQuery(href, contextOverride),
|
|
14626
15340
|
});
|
|
@@ -14722,7 +15436,11 @@
|
|
|
14722
15436
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14723
15437
|
setStatus(payload && payload.converted ? "Opening converted document in a new editor." : "Opening file-backed document in a new editor.");
|
|
14724
15438
|
} catch (error) {
|
|
14725
|
-
|
|
15439
|
+
if (error && error.studioCancelled) {
|
|
15440
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15441
|
+
} else {
|
|
15442
|
+
failPendingStudioTab(launch, "Studio could not prepare this document tab. Return to the originating Studio page for details.");
|
|
15443
|
+
}
|
|
14726
15444
|
throw error;
|
|
14727
15445
|
}
|
|
14728
15446
|
}
|
|
@@ -14737,7 +15455,11 @@
|
|
|
14737
15455
|
navigatePendingStudioTab(launch, relativeUrl);
|
|
14738
15456
|
setStatus("Opening preview in a new Studio tab.");
|
|
14739
15457
|
} catch (error) {
|
|
14740
|
-
|
|
15458
|
+
if (error && error.studioCancelled) {
|
|
15459
|
+
cancelPendingStudioTab(launch, "Local resource access was cancelled.");
|
|
15460
|
+
} else {
|
|
15461
|
+
failPendingStudioTab(launch, "Studio could not prepare this preview tab. Return to the originating Studio page for details.");
|
|
15462
|
+
}
|
|
14741
15463
|
throw error;
|
|
14742
15464
|
}
|
|
14743
15465
|
}
|
|
@@ -14752,6 +15474,7 @@
|
|
|
14752
15474
|
}
|
|
14753
15475
|
|
|
14754
15476
|
async function revealPreviewLocalLink(href, contextOverride) {
|
|
15477
|
+
await fetchPreviewLocalLink("resolve", href, contextOverride);
|
|
14755
15478
|
const query = getPreviewLinkResourceQuery(href, contextOverride);
|
|
14756
15479
|
const payload = await fetchStudioJson("/reveal-local-resource", {
|
|
14757
15480
|
method: "POST",
|
|
@@ -14765,10 +15488,11 @@
|
|
|
14765
15488
|
if (!href) return;
|
|
14766
15489
|
try {
|
|
14767
15490
|
if (action === "open-pdf") {
|
|
14768
|
-
openPreviewPdfLink(href, context.title || href, context);
|
|
15491
|
+
await openPreviewPdfLink(href, context.title || href, context);
|
|
14769
15492
|
return;
|
|
14770
15493
|
}
|
|
14771
15494
|
if (action === "open-system") {
|
|
15495
|
+
await fetchPreviewLocalLink("resolve", href, context);
|
|
14772
15496
|
await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
|
|
14773
15497
|
return;
|
|
14774
15498
|
}
|
|
@@ -14810,7 +15534,9 @@
|
|
|
14810
15534
|
closePreviewLinkMenu();
|
|
14811
15535
|
const title = String(anchor.textContent || href).trim() || href;
|
|
14812
15536
|
if (kind === "pdf") {
|
|
14813
|
-
openPreviewPdfLink(href, title)
|
|
15537
|
+
void openPreviewPdfLink(href, title).catch((error) => {
|
|
15538
|
+
setStatus((error && error.message) ? error.message : String(error || "Could not open linked PDF."), "warning");
|
|
15539
|
+
});
|
|
14814
15540
|
return;
|
|
14815
15541
|
}
|
|
14816
15542
|
if (kind === "image") {
|
|
@@ -14820,9 +15546,7 @@
|
|
|
14820
15546
|
return;
|
|
14821
15547
|
}
|
|
14822
15548
|
if (kind === "text" || kind === "office") {
|
|
14823
|
-
void
|
|
14824
|
-
setStatus((error && error.message) ? error.message : String(error || "Could not open linked file."), "warning");
|
|
14825
|
-
});
|
|
15549
|
+
void showPreviewLinkMenu(anchor, event);
|
|
14826
15550
|
return;
|
|
14827
15551
|
}
|
|
14828
15552
|
setStatus("Right-click this local link for file actions.", "warning");
|
|
@@ -14833,7 +15557,7 @@
|
|
|
14833
15557
|
if (!anchor) return;
|
|
14834
15558
|
event.preventDefault();
|
|
14835
15559
|
event.stopPropagation();
|
|
14836
|
-
showPreviewLinkMenu(anchor, event);
|
|
15560
|
+
void showPreviewLinkMenu(anchor, event);
|
|
14837
15561
|
}
|
|
14838
15562
|
|
|
14839
15563
|
function makeRequestId() {
|
|
@@ -15012,12 +15736,15 @@
|
|
|
15012
15736
|
const html = await renderQuizMarkdownToHtml(markdown);
|
|
15013
15737
|
if (nonce !== quizPreviewRenderNonce || !quizDialogEl || !quizDialogEl.contains(target)) return;
|
|
15014
15738
|
target.innerHTML = html;
|
|
15739
|
+
await hydrateStudioPreviewLocalMedia(target, getHtmlPreviewResourceContextOptions());
|
|
15015
15740
|
await renderAnnotationMathInElement(target);
|
|
15016
15741
|
decoratePdfEmbeds(target);
|
|
15017
15742
|
await renderPdfPreviewsInElement(target);
|
|
15743
|
+
decoratePreviewPdfFigures(target);
|
|
15018
15744
|
await renderMermaidInElement(target);
|
|
15019
15745
|
await renderMathFallbackInElement(target);
|
|
15020
15746
|
decorateCopyablePreviewBlocks(target);
|
|
15747
|
+
decoratePreviewImages(target);
|
|
15021
15748
|
if (preserveScroll) restoreQuizScrollTopSoon(scrollTop);
|
|
15022
15749
|
} catch (error) {
|
|
15023
15750
|
console.error("Quiz markdown preview render failed:", error);
|
|
@@ -23552,6 +24279,9 @@
|
|
|
23552
24279
|
});
|
|
23553
24280
|
}
|
|
23554
24281
|
|
|
24282
|
+
document.addEventListener("click", handleStudioPreviewMediaActivation, true);
|
|
24283
|
+
document.addEventListener("keydown", handleStudioPreviewMediaKeydown, true);
|
|
24284
|
+
|
|
23555
24285
|
document.addEventListener("click", (event) => {
|
|
23556
24286
|
const target = event.target;
|
|
23557
24287
|
const focusBtn = target instanceof Element ? target.closest(".studio-pdf-card-focus") : null;
|