pi-studio 0.9.47 → 0.9.49
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 +19 -0
- package/README.md +10 -4
- package/client/studio-client.js +847 -82
- package/client/studio-preview-resource-helpers.js +52 -0
- package/client/studio.css +144 -17
- package/index.ts +348 -89
- package/package.json +1 -1
- package/shared/studio-latex-pandoc-compat.js +122 -0
- package/shared/studio-local-preview-path.js +46 -0
package/client/studio-client.js
CHANGED
|
@@ -85,6 +85,7 @@
|
|
|
85
85
|
const showMeResponseBtn = document.getElementById("showMeResponseBtn");
|
|
86
86
|
const quizBtn = document.getElementById("quizBtn");
|
|
87
87
|
const lensSelect = document.getElementById("lensSelect");
|
|
88
|
+
const importFileBtn = document.getElementById("importFileBtn");
|
|
88
89
|
const fileInput = document.getElementById("fileInput");
|
|
89
90
|
const resourceDirBtn = document.getElementById("resourceDirBtn");
|
|
90
91
|
const resourceDirLabel = document.getElementById("resourceDirLabel");
|
|
@@ -226,7 +227,10 @@
|
|
|
226
227
|
if (
|
|
227
228
|
!previewResourceHelpers
|
|
228
229
|
|| typeof previewResourceHelpers.areStudioPreviewResourceContextsEqual !== "function"
|
|
230
|
+
|| typeof previewResourceHelpers.buildStudioPdfVersionSignature !== "function"
|
|
231
|
+
|| typeof previewResourceHelpers.createStudioPdfVersionObservationState !== "function"
|
|
229
232
|
|| typeof previewResourceHelpers.hydrateStudioPreviewLocalImages !== "function"
|
|
233
|
+
|| typeof previewResourceHelpers.observeStudioPdfVersion !== "function"
|
|
230
234
|
) {
|
|
231
235
|
throw new Error("Studio preview resource helpers failed to load.");
|
|
232
236
|
}
|
|
@@ -268,10 +272,19 @@
|
|
|
268
272
|
let studioPdfFocusFrameEl = null;
|
|
269
273
|
let studioPdfFocusTitleEl = null;
|
|
270
274
|
let studioPdfFocusOpenLinkEl = null;
|
|
275
|
+
let studioPdfFocusSystemViewerBtn = null;
|
|
276
|
+
let studioPdfFocusRevealBtn = null;
|
|
277
|
+
let studioPdfFocusAutoRefreshBtn = null;
|
|
271
278
|
let studioPdfFocusFullscreenBtn = null;
|
|
272
279
|
let studioPdfFocusCloseBtn = null;
|
|
273
280
|
let studioPdfFocusLastFocusedEl = null;
|
|
274
281
|
let studioPdfFocusMovedFrameState = null;
|
|
282
|
+
let studioPdfFocusResourceQuery = null;
|
|
283
|
+
let studioPdfFocusSourceCard = null;
|
|
284
|
+
let studioPdfFocusStandaloneAutoRefreshState = null;
|
|
285
|
+
const studioPdfCardAutoRefreshStates = new WeakMap();
|
|
286
|
+
const STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS = 1_000;
|
|
287
|
+
const STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS = 2;
|
|
275
288
|
let studioHtmlFocusOverlayEl = null;
|
|
276
289
|
let studioHtmlFocusShellEl = null;
|
|
277
290
|
let studioHtmlFocusFullscreenBtn = null;
|
|
@@ -289,6 +302,16 @@
|
|
|
289
302
|
let studioImageFocusLastFocusedEl = null;
|
|
290
303
|
let studioImageFocusZoomMode = "fit";
|
|
291
304
|
let studioImageFocusZoom = 1;
|
|
305
|
+
let studioDecisionOverlayEl = null;
|
|
306
|
+
let studioDecisionDialogEl = null;
|
|
307
|
+
let studioDecisionTitleEl = null;
|
|
308
|
+
let studioDecisionMessageEl = null;
|
|
309
|
+
let studioDecisionInputEl = null;
|
|
310
|
+
let studioDecisionCancelBtn = null;
|
|
311
|
+
let studioDecisionSecondaryBtn = null;
|
|
312
|
+
let studioDecisionConfirmBtn = null;
|
|
313
|
+
let studioDecisionState = null;
|
|
314
|
+
let studioImportDecisionOpen = false;
|
|
292
315
|
let pendingRequestId = null;
|
|
293
316
|
let pendingKind = null;
|
|
294
317
|
let stickyStudioKind = null;
|
|
@@ -2595,7 +2618,7 @@
|
|
|
2595
2618
|
event.preventDefault();
|
|
2596
2619
|
event.stopPropagation();
|
|
2597
2620
|
closeStudioUiRefreshMenus();
|
|
2598
|
-
resetEditorOrigin();
|
|
2621
|
+
void resetEditorOrigin();
|
|
2599
2622
|
});
|
|
2600
2623
|
sourceOpenCurrentFileTabBtn = makeStudioUiRefreshElement("button", "source-open-file-tab-btn", "Open current file in new editor tab");
|
|
2601
2624
|
sourceOpenCurrentFileTabBtn.type = "button";
|
|
@@ -3625,6 +3648,207 @@
|
|
|
3625
3648
|
});
|
|
3626
3649
|
}
|
|
3627
3650
|
|
|
3651
|
+
function finishStudioDecision(value, restoreFocus) {
|
|
3652
|
+
const state = studioDecisionState;
|
|
3653
|
+
if (!state) return;
|
|
3654
|
+
studioDecisionState = null;
|
|
3655
|
+
if (studioDecisionOverlayEl) studioDecisionOverlayEl.hidden = true;
|
|
3656
|
+
if (document.body) document.body.classList.remove("studio-decision-open");
|
|
3657
|
+
const returnFocusEl = state.returnFocusEl;
|
|
3658
|
+
state.resolve(value);
|
|
3659
|
+
if (restoreFocus !== false && returnFocusEl && typeof returnFocusEl.focus === "function") {
|
|
3660
|
+
window.setTimeout(() => {
|
|
3661
|
+
if (returnFocusEl.isConnected) returnFocusEl.focus({ preventScroll: true });
|
|
3662
|
+
}, 0);
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
function getStudioDecisionFocusableElements() {
|
|
3667
|
+
return [studioDecisionInputEl, studioDecisionCancelBtn, studioDecisionSecondaryBtn, studioDecisionConfirmBtn]
|
|
3668
|
+
.filter((element) => element && !element.hidden && !element.disabled);
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
function ensureStudioDecisionDialog() {
|
|
3672
|
+
if (studioDecisionOverlayEl) return studioDecisionOverlayEl;
|
|
3673
|
+
|
|
3674
|
+
const overlay = document.createElement("div");
|
|
3675
|
+
overlay.className = "studio-decision-overlay";
|
|
3676
|
+
overlay.hidden = true;
|
|
3677
|
+
|
|
3678
|
+
const dialog = document.createElement("div");
|
|
3679
|
+
dialog.className = "studio-decision-dialog";
|
|
3680
|
+
dialog.setAttribute("role", "alertdialog");
|
|
3681
|
+
dialog.setAttribute("aria-modal", "true");
|
|
3682
|
+
dialog.setAttribute("aria-labelledby", "studioDecisionTitle");
|
|
3683
|
+
dialog.setAttribute("aria-describedby", "studioDecisionMessage");
|
|
3684
|
+
|
|
3685
|
+
const title = document.createElement("h2");
|
|
3686
|
+
title.id = "studioDecisionTitle";
|
|
3687
|
+
title.className = "studio-decision-title";
|
|
3688
|
+
dialog.appendChild(title);
|
|
3689
|
+
|
|
3690
|
+
const message = document.createElement("div");
|
|
3691
|
+
message.id = "studioDecisionMessage";
|
|
3692
|
+
message.className = "studio-decision-message";
|
|
3693
|
+
dialog.appendChild(message);
|
|
3694
|
+
|
|
3695
|
+
const input = document.createElement("input");
|
|
3696
|
+
input.type = "text";
|
|
3697
|
+
input.className = "studio-decision-input";
|
|
3698
|
+
input.hidden = true;
|
|
3699
|
+
input.autocomplete = "off";
|
|
3700
|
+
input.spellcheck = false;
|
|
3701
|
+
input.setAttribute("autocapitalize", "off");
|
|
3702
|
+
input.setAttribute("aria-label", "Value");
|
|
3703
|
+
dialog.appendChild(input);
|
|
3704
|
+
|
|
3705
|
+
const actions = document.createElement("div");
|
|
3706
|
+
actions.className = "studio-decision-actions";
|
|
3707
|
+
|
|
3708
|
+
const cancelBtn = document.createElement("button");
|
|
3709
|
+
cancelBtn.type = "button";
|
|
3710
|
+
cancelBtn.className = "studio-decision-cancel";
|
|
3711
|
+
cancelBtn.textContent = "Cancel";
|
|
3712
|
+
cancelBtn.addEventListener("click", () => finishStudioDecision(null));
|
|
3713
|
+
actions.appendChild(cancelBtn);
|
|
3714
|
+
|
|
3715
|
+
const secondaryBtn = document.createElement("button");
|
|
3716
|
+
secondaryBtn.type = "button";
|
|
3717
|
+
secondaryBtn.className = "studio-decision-secondary";
|
|
3718
|
+
secondaryBtn.hidden = true;
|
|
3719
|
+
secondaryBtn.addEventListener("click", () => {
|
|
3720
|
+
const handler = studioDecisionState && studioDecisionState.onSecondary;
|
|
3721
|
+
if (typeof handler !== "function") return;
|
|
3722
|
+
try {
|
|
3723
|
+
const result = handler();
|
|
3724
|
+
if (result && typeof result.catch === "function") {
|
|
3725
|
+
result.catch((error) => {
|
|
3726
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3729
|
+
} catch (error) {
|
|
3730
|
+
setStatus("Action failed: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
3731
|
+
}
|
|
3732
|
+
});
|
|
3733
|
+
actions.appendChild(secondaryBtn);
|
|
3734
|
+
|
|
3735
|
+
const confirmBtn = document.createElement("button");
|
|
3736
|
+
confirmBtn.type = "button";
|
|
3737
|
+
confirmBtn.className = "studio-decision-confirm";
|
|
3738
|
+
confirmBtn.textContent = "Confirm";
|
|
3739
|
+
confirmBtn.addEventListener("click", () => {
|
|
3740
|
+
if (!studioDecisionState) return;
|
|
3741
|
+
finishStudioDecision(studioDecisionState.mode === "prompt" ? input.value : true);
|
|
3742
|
+
});
|
|
3743
|
+
actions.appendChild(confirmBtn);
|
|
3744
|
+
|
|
3745
|
+
dialog.appendChild(actions);
|
|
3746
|
+
overlay.appendChild(dialog);
|
|
3747
|
+
document.body.appendChild(overlay);
|
|
3748
|
+
|
|
3749
|
+
overlay.addEventListener("click", (event) => {
|
|
3750
|
+
if (event.target === overlay) finishStudioDecision(null);
|
|
3751
|
+
});
|
|
3752
|
+
dialog.addEventListener("keydown", (event) => {
|
|
3753
|
+
event.stopPropagation();
|
|
3754
|
+
if (event.key === "Escape") {
|
|
3755
|
+
event.preventDefault();
|
|
3756
|
+
finishStudioDecision(null);
|
|
3757
|
+
return;
|
|
3758
|
+
}
|
|
3759
|
+
if (event.key === "Enter" && event.target === input) {
|
|
3760
|
+
event.preventDefault();
|
|
3761
|
+
confirmBtn.click();
|
|
3762
|
+
return;
|
|
3763
|
+
}
|
|
3764
|
+
if (event.key !== "Tab") return;
|
|
3765
|
+
const focusable = getStudioDecisionFocusableElements();
|
|
3766
|
+
if (focusable.length < 2) return;
|
|
3767
|
+
const currentIndex = focusable.indexOf(document.activeElement);
|
|
3768
|
+
const nextIndex = event.shiftKey
|
|
3769
|
+
? (currentIndex <= 0 ? focusable.length - 1 : currentIndex - 1)
|
|
3770
|
+
: (currentIndex < 0 || currentIndex === focusable.length - 1 ? 0 : currentIndex + 1);
|
|
3771
|
+
event.preventDefault();
|
|
3772
|
+
focusable[nextIndex].focus();
|
|
3773
|
+
});
|
|
3774
|
+
|
|
3775
|
+
studioDecisionOverlayEl = overlay;
|
|
3776
|
+
studioDecisionDialogEl = dialog;
|
|
3777
|
+
studioDecisionTitleEl = title;
|
|
3778
|
+
studioDecisionMessageEl = message;
|
|
3779
|
+
studioDecisionInputEl = input;
|
|
3780
|
+
studioDecisionCancelBtn = cancelBtn;
|
|
3781
|
+
studioDecisionSecondaryBtn = secondaryBtn;
|
|
3782
|
+
studioDecisionConfirmBtn = confirmBtn;
|
|
3783
|
+
return overlay;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
function openStudioDecision(options) {
|
|
3787
|
+
const settings = options && typeof options === "object" ? options : {};
|
|
3788
|
+
const mode = settings.mode === "prompt" ? "prompt" : "confirm";
|
|
3789
|
+
ensureStudioDecisionDialog();
|
|
3790
|
+
if (studioDecisionState) finishStudioDecision(null, false);
|
|
3791
|
+
const returnFocusEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
3792
|
+
|
|
3793
|
+
const secondaryLabel = String(settings.secondaryLabel || "").trim();
|
|
3794
|
+
studioDecisionTitleEl.textContent = String(settings.title || (mode === "prompt" ? "Enter a value" : "Confirm action"));
|
|
3795
|
+
studioDecisionMessageEl.textContent = String(settings.message || "");
|
|
3796
|
+
studioDecisionInputEl.hidden = mode !== "prompt";
|
|
3797
|
+
studioDecisionInputEl.value = mode === "prompt" ? String(settings.defaultValue || "") : "";
|
|
3798
|
+
studioDecisionInputEl.placeholder = mode === "prompt" ? String(settings.placeholder || "") : "";
|
|
3799
|
+
studioDecisionInputEl.setAttribute("aria-label", String(settings.inputLabel || "Value"));
|
|
3800
|
+
studioDecisionCancelBtn.textContent = String(settings.cancelLabel || "Cancel");
|
|
3801
|
+
studioDecisionSecondaryBtn.hidden = !secondaryLabel;
|
|
3802
|
+
studioDecisionSecondaryBtn.disabled = settings.secondaryDisabled === true;
|
|
3803
|
+
studioDecisionSecondaryBtn.textContent = secondaryLabel;
|
|
3804
|
+
studioDecisionConfirmBtn.textContent = String(settings.confirmLabel || (mode === "prompt" ? "Continue" : "Confirm"));
|
|
3805
|
+
studioDecisionConfirmBtn.classList.toggle("is-destructive", settings.destructive === true);
|
|
3806
|
+
studioDecisionDialogEl.classList.toggle("is-destructive", settings.destructive === true);
|
|
3807
|
+
studioDecisionOverlayEl.hidden = false;
|
|
3808
|
+
if (document.body) document.body.classList.add("studio-decision-open");
|
|
3809
|
+
|
|
3810
|
+
return new Promise((resolve) => {
|
|
3811
|
+
const decisionState = {
|
|
3812
|
+
mode,
|
|
3813
|
+
resolve,
|
|
3814
|
+
returnFocusEl,
|
|
3815
|
+
onSecondary: typeof settings.onSecondary === "function" ? settings.onSecondary : null,
|
|
3816
|
+
};
|
|
3817
|
+
studioDecisionState = decisionState;
|
|
3818
|
+
const schedule = typeof window.requestAnimationFrame === "function"
|
|
3819
|
+
? window.requestAnimationFrame.bind(window)
|
|
3820
|
+
: (callback) => window.setTimeout(callback, 16);
|
|
3821
|
+
schedule(() => {
|
|
3822
|
+
if (studioDecisionState !== decisionState) return;
|
|
3823
|
+
if (mode === "prompt") {
|
|
3824
|
+
studioDecisionInputEl.focus();
|
|
3825
|
+
studioDecisionInputEl.select();
|
|
3826
|
+
} else {
|
|
3827
|
+
studioDecisionCancelBtn.focus();
|
|
3828
|
+
}
|
|
3829
|
+
});
|
|
3830
|
+
});
|
|
3831
|
+
}
|
|
3832
|
+
|
|
3833
|
+
async function requestStudioConfirmation(message, options) {
|
|
3834
|
+
const value = await openStudioDecision({
|
|
3835
|
+
...(options && typeof options === "object" ? options : {}),
|
|
3836
|
+
mode: "confirm",
|
|
3837
|
+
message: String(message || ""),
|
|
3838
|
+
});
|
|
3839
|
+
return value === true;
|
|
3840
|
+
}
|
|
3841
|
+
|
|
3842
|
+
async function requestStudioTextInput(message, defaultValue, options) {
|
|
3843
|
+
const value = await openStudioDecision({
|
|
3844
|
+
...(options && typeof options === "object" ? options : {}),
|
|
3845
|
+
mode: "prompt",
|
|
3846
|
+
message: String(message || ""),
|
|
3847
|
+
defaultValue: String(defaultValue || ""),
|
|
3848
|
+
});
|
|
3849
|
+
return typeof value === "string" ? value : null;
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3628
3852
|
renderStatus();
|
|
3629
3853
|
|
|
3630
3854
|
window.addEventListener("focus", () => {
|
|
@@ -3714,14 +3938,17 @@
|
|
|
3714
3938
|
}
|
|
3715
3939
|
}
|
|
3716
3940
|
|
|
3717
|
-
function resetEditorOrigin() {
|
|
3941
|
+
async function resetEditorOrigin() {
|
|
3718
3942
|
const descriptor = getCurrentStudioDocumentDescriptor();
|
|
3719
3943
|
const message = descriptor.fileBacked
|
|
3720
3944
|
? ("Reset editor origin and detach the current text from\n\n" + descriptor.label + "\n\ninto a new draft? The file on disk will not be changed, and the current scratchpad/review notes will carry into the new draft.")
|
|
3721
3945
|
: ("Reset editor origin and start a new independent draft? The current editor text, scratchpad, and review notes will carry into the new draft.");
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3946
|
+
const confirmed = await requestStudioConfirmation(message, {
|
|
3947
|
+
title: descriptor.fileBacked ? "Detach editor from file?" : "Reset editor origin?",
|
|
3948
|
+
confirmLabel: descriptor.fileBacked ? "Detach" : "Reset origin",
|
|
3949
|
+
destructive: true,
|
|
3950
|
+
});
|
|
3951
|
+
if (!confirmed) return;
|
|
3725
3952
|
const nextLabel = String(sourceTextEl.value || "").trim() ? "draft" : "blank";
|
|
3726
3953
|
setSourceState({
|
|
3727
3954
|
source: "blank",
|
|
@@ -4271,6 +4498,15 @@
|
|
|
4271
4498
|
|
|
4272
4499
|
if (handleStudioImageFocusShortcut(event)) return;
|
|
4273
4500
|
|
|
4501
|
+
const otherModalOwnsEvent = scratchpadOwnsEvent
|
|
4502
|
+
|| reviewNotesOwnsEvent
|
|
4503
|
+
|| outlineOwnsEvent
|
|
4504
|
+
|| shortcutsOwnsEvent
|
|
4505
|
+
|| htmlFocusOwnsEvent
|
|
4506
|
+
|| imageFocusOwnsEvent
|
|
4507
|
+
|| quizOwnsEvent;
|
|
4508
|
+
if (!otherModalOwnsEvent && handleStudioPdfRefreshShortcut(event)) return;
|
|
4509
|
+
|
|
4274
4510
|
if (isScratchpadOpen() && plainEscape) {
|
|
4275
4511
|
event.preventDefault();
|
|
4276
4512
|
closeScratchpad();
|
|
@@ -6633,7 +6869,7 @@
|
|
|
6633
6869
|
}
|
|
6634
6870
|
|
|
6635
6871
|
function parseStudioPdfBlockOptions(body) {
|
|
6636
|
-
const options = { path: "", title: "", caption: "", page: "", height: "" };
|
|
6872
|
+
const options = { path: "", title: "", caption: "", page: "", height: "", watch: "" };
|
|
6637
6873
|
String(body || "").split(/\r?\n/).forEach((line) => {
|
|
6638
6874
|
const raw = String(line || "").trim();
|
|
6639
6875
|
if (!raw || raw.startsWith("#")) return;
|
|
@@ -6646,6 +6882,7 @@
|
|
|
6646
6882
|
else if (key === "caption") options.caption = value;
|
|
6647
6883
|
else if (key === "page") options.page = value;
|
|
6648
6884
|
else if (key === "height") options.height = value;
|
|
6885
|
+
else if (key === "watch" || key === "auto-refresh" || key === "autorefresh") options.watch = value;
|
|
6649
6886
|
return;
|
|
6650
6887
|
}
|
|
6651
6888
|
if (!options.path) options.path = stripMatchingQuotes(raw);
|
|
@@ -6677,6 +6914,10 @@
|
|
|
6677
6914
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
6678
6915
|
}
|
|
6679
6916
|
|
|
6917
|
+
function normalizeStudioPdfAutoRefresh(value) {
|
|
6918
|
+
return /^(?:1|true|yes|on|watch)$/i.test(String(value || "").trim());
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6680
6921
|
function isStudioPdfFocusOpen() {
|
|
6681
6922
|
return Boolean(studioPdfFocusOverlayEl && studioPdfFocusOverlayEl.hidden === false);
|
|
6682
6923
|
}
|
|
@@ -6723,18 +6964,49 @@
|
|
|
6723
6964
|
openLink.className = "studio-pdf-focus-link";
|
|
6724
6965
|
openLink.target = "_blank";
|
|
6725
6966
|
openLink.rel = "noopener noreferrer";
|
|
6726
|
-
openLink.textContent = "
|
|
6967
|
+
openLink.textContent = "Browser tab";
|
|
6968
|
+
openLink.title = "Open this PDF in a browser tab.";
|
|
6727
6969
|
actions.appendChild(openLink);
|
|
6728
6970
|
|
|
6971
|
+
const systemViewerBtn = document.createElement("button");
|
|
6972
|
+
systemViewerBtn.type = "button";
|
|
6973
|
+
systemViewerBtn.className = "studio-pdf-focus-btn studio-pdf-focus-system-viewer";
|
|
6974
|
+
systemViewerBtn.textContent = "System viewer";
|
|
6975
|
+
systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
|
|
6976
|
+
systemViewerBtn.addEventListener("click", () => {
|
|
6977
|
+
void runStudioPdfLocalAction("system-viewer", studioPdfFocusResourceQuery);
|
|
6978
|
+
});
|
|
6979
|
+
actions.appendChild(systemViewerBtn);
|
|
6980
|
+
|
|
6981
|
+
const revealBtn = document.createElement("button");
|
|
6982
|
+
revealBtn.type = "button";
|
|
6983
|
+
revealBtn.className = "studio-pdf-focus-btn studio-pdf-focus-reveal";
|
|
6984
|
+
revealBtn.textContent = "Show in folder";
|
|
6985
|
+
revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
|
|
6986
|
+
revealBtn.addEventListener("click", () => {
|
|
6987
|
+
void runStudioPdfLocalAction("reveal", studioPdfFocusResourceQuery);
|
|
6988
|
+
});
|
|
6989
|
+
actions.appendChild(revealBtn);
|
|
6990
|
+
|
|
6729
6991
|
const refreshBtn = document.createElement("button");
|
|
6730
6992
|
refreshBtn.type = "button";
|
|
6731
6993
|
refreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-refresh";
|
|
6732
6994
|
refreshBtn.textContent = "Refresh";
|
|
6733
|
-
refreshBtn.title = "Reload this PDF preview from disk.";
|
|
6995
|
+
refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
|
|
6734
6996
|
refreshBtn.setAttribute("aria-label", "Refresh PDF preview from disk");
|
|
6735
6997
|
refreshBtn.addEventListener("click", () => refreshStudioPdfFocusViewer());
|
|
6736
6998
|
actions.appendChild(refreshBtn);
|
|
6737
6999
|
|
|
7000
|
+
const autoRefreshBtn = document.createElement("button");
|
|
7001
|
+
autoRefreshBtn.type = "button";
|
|
7002
|
+
autoRefreshBtn.className = "studio-pdf-focus-btn studio-pdf-focus-auto-refresh";
|
|
7003
|
+
autoRefreshBtn.textContent = "Auto-refresh";
|
|
7004
|
+
autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
7005
|
+
autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
|
|
7006
|
+
autoRefreshBtn.setAttribute("aria-pressed", "false");
|
|
7007
|
+
autoRefreshBtn.addEventListener("click", () => toggleStudioPdfFocusAutoRefresh());
|
|
7008
|
+
actions.appendChild(autoRefreshBtn);
|
|
7009
|
+
|
|
6738
7010
|
const fullscreenBtn = document.createElement("button");
|
|
6739
7011
|
fullscreenBtn.type = "button";
|
|
6740
7012
|
fullscreenBtn.className = "studio-pdf-focus-btn studio-pdf-focus-fullscreen";
|
|
@@ -6789,16 +7061,23 @@
|
|
|
6789
7061
|
studioPdfFocusFrameEl = frame;
|
|
6790
7062
|
studioPdfFocusTitleEl = titleEl;
|
|
6791
7063
|
studioPdfFocusOpenLinkEl = openLink;
|
|
7064
|
+
studioPdfFocusSystemViewerBtn = systemViewerBtn;
|
|
7065
|
+
studioPdfFocusRevealBtn = revealBtn;
|
|
7066
|
+
studioPdfFocusAutoRefreshBtn = autoRefreshBtn;
|
|
6792
7067
|
studioPdfFocusFullscreenBtn = fullscreenBtn;
|
|
6793
7068
|
studioPdfFocusCloseBtn = closeBtn;
|
|
6794
7069
|
syncStudioPdfFocusFullscreenButton();
|
|
7070
|
+
syncStudioPdfFocusResourceActions();
|
|
6795
7071
|
return overlay;
|
|
6796
7072
|
}
|
|
6797
7073
|
|
|
6798
|
-
function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame) {
|
|
7074
|
+
function openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, sourceCard) {
|
|
6799
7075
|
const src = String(viewerUrl || "").trim();
|
|
6800
7076
|
if (!src) return;
|
|
6801
7077
|
ensureStudioPdfFocusViewer();
|
|
7078
|
+
studioPdfFocusResourceQuery = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7079
|
+
setStudioPdfFocusAutoRefreshSource(sourceCard, studioPdfFocusResourceQuery);
|
|
7080
|
+
syncStudioPdfFocusResourceActions();
|
|
6802
7081
|
studioPdfFocusLastFocusedEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
6803
7082
|
if (studioPdfFocusTitleEl) studioPdfFocusTitleEl.textContent = String(title || "PDF preview").trim() || "PDF preview";
|
|
6804
7083
|
if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = src;
|
|
@@ -6827,6 +7106,9 @@
|
|
|
6827
7106
|
restoreStudioPdfFocusMovedFrame();
|
|
6828
7107
|
if (studioPdfFocusFrameEl) studioPdfFocusFrameEl.src = "about:blank";
|
|
6829
7108
|
if (document.body) document.body.classList.remove("studio-pdf-focus-open");
|
|
7109
|
+
clearStudioPdfFocusAutoRefreshSource();
|
|
7110
|
+
studioPdfFocusResourceQuery = null;
|
|
7111
|
+
syncStudioPdfFocusResourceActions();
|
|
6830
7112
|
syncStudioPdfFocusFullscreenButton();
|
|
6831
7113
|
const focusTarget = studioPdfFocusLastFocusedEl;
|
|
6832
7114
|
studioPdfFocusLastFocusedEl = null;
|
|
@@ -6836,24 +7118,274 @@
|
|
|
6836
7118
|
return true;
|
|
6837
7119
|
}
|
|
6838
7120
|
|
|
6839
|
-
function
|
|
6840
|
-
const token = getToken();
|
|
6841
|
-
if (!token) return "";
|
|
7121
|
+
function buildStudioPdfResourceQuery(options, useEditorResourceContext) {
|
|
6842
7122
|
const pdfPath = String(options && options.path ? options.path : "").trim();
|
|
6843
|
-
if (!pdfPath) return
|
|
7123
|
+
if (!pdfPath) return null;
|
|
6844
7124
|
const explicitSourcePath = options && typeof options.sourcePath === "string" ? options.sourcePath.trim() : "";
|
|
6845
7125
|
const explicitResourceDir = options && typeof options.resourceDir === "string" ? normalizeStudioResourceDirValue(options.resourceDir) : "";
|
|
6846
7126
|
const effectivePath = getEffectiveSavePath();
|
|
6847
7127
|
const sourcePath = explicitSourcePath || (useEditorResourceContext ? (effectivePath || sourceState.path || "") : "");
|
|
6848
7128
|
const resourceDir = explicitResourceDir || getCurrentResourceDirValue();
|
|
6849
|
-
const
|
|
6850
|
-
if (sourcePath)
|
|
6851
|
-
|
|
7129
|
+
const query = { path: pdfPath };
|
|
7130
|
+
if (sourcePath) query.sourcePath = sourcePath;
|
|
7131
|
+
if (resourceDir) query.resourceDir = resourceDir;
|
|
7132
|
+
return query;
|
|
7133
|
+
}
|
|
7134
|
+
|
|
7135
|
+
function buildStudioPdfResourceUrl(options, useEditorResourceContext) {
|
|
7136
|
+
const token = getToken();
|
|
7137
|
+
const query = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
7138
|
+
if (!token || !query) return "";
|
|
7139
|
+
return "/pdf-resource?" + new URLSearchParams({ token, ...query }).toString();
|
|
7140
|
+
}
|
|
7141
|
+
|
|
7142
|
+
function normalizeStudioPdfResourceQuery(value) {
|
|
7143
|
+
if (!value || typeof value !== "object") return null;
|
|
7144
|
+
const path = typeof value.path === "string" ? value.path.trim() : "";
|
|
7145
|
+
if (!path) return null;
|
|
7146
|
+
const query = { path };
|
|
7147
|
+
if (typeof value.sourcePath === "string" && value.sourcePath.trim()) query.sourcePath = value.sourcePath.trim();
|
|
7148
|
+
if (typeof value.resourceDir === "string" && value.resourceDir.trim()) query.resourceDir = value.resourceDir.trim();
|
|
7149
|
+
return query;
|
|
7150
|
+
}
|
|
7151
|
+
|
|
7152
|
+
function syncStudioPdfAutoRefreshButton(button, enabled, available) {
|
|
7153
|
+
if (!button) return;
|
|
7154
|
+
const active = Boolean(enabled && available);
|
|
7155
|
+
button.disabled = !available;
|
|
7156
|
+
button.setAttribute("aria-pressed", active ? "true" : "false");
|
|
7157
|
+
button.setAttribute("aria-label", active ? "Disable PDF auto-refresh" : "Enable PDF auto-refresh");
|
|
7158
|
+
button.textContent = "Auto-refresh";
|
|
7159
|
+
button.title = active
|
|
7160
|
+
? "Auto-refresh is on. Studio checks this PDF while the tab is visible and reloads it after a changed file is stable on disk."
|
|
7161
|
+
: "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
7162
|
+
}
|
|
7163
|
+
|
|
7164
|
+
function createStudioPdfAutoRefreshState(resourceQuery, hooks) {
|
|
7165
|
+
return {
|
|
7166
|
+
resourceQuery: normalizeStudioPdfResourceQuery(resourceQuery),
|
|
7167
|
+
enabled: false,
|
|
7168
|
+
timer: null,
|
|
7169
|
+
inFlight: false,
|
|
7170
|
+
generation: 0,
|
|
7171
|
+
observation: previewResourceHelpers.createStudioPdfVersionObservationState(),
|
|
7172
|
+
isAlive: hooks && typeof hooks.isAlive === "function" ? hooks.isAlive : () => false,
|
|
7173
|
+
refresh: hooks && typeof hooks.refresh === "function" ? hooks.refresh : () => false,
|
|
7174
|
+
sync: hooks && typeof hooks.sync === "function" ? hooks.sync : () => {},
|
|
7175
|
+
};
|
|
7176
|
+
}
|
|
7177
|
+
|
|
7178
|
+
function resetStudioPdfAutoRefreshObservation(state) {
|
|
7179
|
+
if (!state) return;
|
|
7180
|
+
state.observation = previewResourceHelpers.createStudioPdfVersionObservationState();
|
|
7181
|
+
}
|
|
7182
|
+
|
|
7183
|
+
function scheduleStudioPdfAutoRefreshPoll(state, delayMs) {
|
|
7184
|
+
if (!state || !state.enabled) return;
|
|
7185
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7186
|
+
state.timer = window.setTimeout(() => {
|
|
7187
|
+
state.timer = null;
|
|
7188
|
+
void pollStudioPdfAutoRefreshState(state);
|
|
7189
|
+
}, Math.max(0, Number(delayMs) || 0));
|
|
7190
|
+
}
|
|
7191
|
+
|
|
7192
|
+
function stopStudioPdfAutoRefreshState(state) {
|
|
7193
|
+
if (!state) return;
|
|
7194
|
+
state.enabled = false;
|
|
7195
|
+
state.inFlight = false;
|
|
7196
|
+
state.generation += 1;
|
|
7197
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7198
|
+
state.timer = null;
|
|
7199
|
+
resetStudioPdfAutoRefreshObservation(state);
|
|
7200
|
+
state.sync();
|
|
7201
|
+
}
|
|
7202
|
+
|
|
7203
|
+
function setStudioPdfAutoRefreshEnabled(state, enabled, options) {
|
|
7204
|
+
if (!state) return false;
|
|
7205
|
+
const nextEnabled = Boolean(enabled && state.resourceQuery);
|
|
7206
|
+
if (state.enabled === nextEnabled) {
|
|
7207
|
+
state.sync();
|
|
7208
|
+
return nextEnabled;
|
|
7209
|
+
}
|
|
7210
|
+
if (state.timer) window.clearTimeout(state.timer);
|
|
7211
|
+
state.timer = null;
|
|
7212
|
+
state.enabled = nextEnabled;
|
|
7213
|
+
state.inFlight = false;
|
|
7214
|
+
state.generation += 1;
|
|
7215
|
+
resetStudioPdfAutoRefreshObservation(state);
|
|
7216
|
+
state.sync();
|
|
7217
|
+
if (nextEnabled) {
|
|
7218
|
+
if (!options || !options.silent) state.refresh();
|
|
7219
|
+
scheduleStudioPdfAutoRefreshPoll(state, 0);
|
|
7220
|
+
}
|
|
7221
|
+
if (!options || !options.silent) {
|
|
7222
|
+
setStatus(nextEnabled ? "PDF auto-refresh on." : "PDF auto-refresh off.", "success");
|
|
7223
|
+
}
|
|
7224
|
+
return nextEnabled;
|
|
7225
|
+
}
|
|
7226
|
+
|
|
7227
|
+
async function fetchStudioPdfVersionSignature(resourceQuery) {
|
|
7228
|
+
const url = buildStudioPdfResourceUrl(resourceQuery, false);
|
|
7229
|
+
if (!url) throw new Error("Could not resolve this PDF for auto-refresh.");
|
|
7230
|
+
const response = await fetchWithTimeout(url, {
|
|
7231
|
+
method: "HEAD",
|
|
7232
|
+
cache: "no-store",
|
|
7233
|
+
}, 4_000, "PDF auto-refresh check");
|
|
7234
|
+
if (!response.ok) throw new Error("PDF auto-refresh check failed with HTTP " + response.status + ".");
|
|
7235
|
+
return previewResourceHelpers.buildStudioPdfVersionSignature(response.headers);
|
|
7236
|
+
}
|
|
7237
|
+
|
|
7238
|
+
async function pollStudioPdfAutoRefreshState(state) {
|
|
7239
|
+
if (!state || !state.enabled || state.inFlight) return;
|
|
7240
|
+
if (!state.isAlive()) {
|
|
7241
|
+
stopStudioPdfAutoRefreshState(state);
|
|
7242
|
+
return;
|
|
7243
|
+
}
|
|
7244
|
+
if (document.hidden || document.visibilityState === "hidden") {
|
|
7245
|
+
scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
|
|
7246
|
+
return;
|
|
6852
7247
|
}
|
|
6853
|
-
|
|
6854
|
-
|
|
7248
|
+
const generation = state.generation;
|
|
7249
|
+
state.inFlight = true;
|
|
7250
|
+
try {
|
|
7251
|
+
const signature = await fetchStudioPdfVersionSignature(state.resourceQuery);
|
|
7252
|
+
if (!state.enabled || state.generation !== generation) return;
|
|
7253
|
+
if (document.hidden || document.visibilityState === "hidden") return;
|
|
7254
|
+
if (!state.isAlive()) {
|
|
7255
|
+
stopStudioPdfAutoRefreshState(state);
|
|
7256
|
+
return;
|
|
7257
|
+
}
|
|
7258
|
+
const observed = previewResourceHelpers.observeStudioPdfVersion(
|
|
7259
|
+
state.observation,
|
|
7260
|
+
signature,
|
|
7261
|
+
STUDIO_PDF_AUTO_REFRESH_STABLE_OBSERVATIONS,
|
|
7262
|
+
);
|
|
7263
|
+
state.observation = observed.state;
|
|
7264
|
+
if (observed.changed) state.refresh();
|
|
7265
|
+
} catch {
|
|
7266
|
+
// LaTeX tools may briefly replace or lock the output PDF. Retry quietly.
|
|
7267
|
+
} finally {
|
|
7268
|
+
if (state.generation !== generation) return;
|
|
7269
|
+
state.inFlight = false;
|
|
7270
|
+
if (state.enabled) scheduleStudioPdfAutoRefreshPoll(state, STUDIO_PDF_AUTO_REFRESH_INTERVAL_MS);
|
|
6855
7271
|
}
|
|
6856
|
-
|
|
7272
|
+
}
|
|
7273
|
+
|
|
7274
|
+
function syncStudioPdfCardAutoRefreshButton(card) {
|
|
7275
|
+
if (!card) return;
|
|
7276
|
+
const state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7277
|
+
const button = typeof card.querySelector === "function" ? card.querySelector(".studio-pdf-card-auto-refresh") : null;
|
|
7278
|
+
syncStudioPdfAutoRefreshButton(button, state && state.enabled, Boolean(state && state.resourceQuery));
|
|
7279
|
+
}
|
|
7280
|
+
|
|
7281
|
+
function ensureStudioPdfCardAutoRefreshState(card, resourceQuery) {
|
|
7282
|
+
if (!card) return null;
|
|
7283
|
+
let state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7284
|
+
if (state) return state;
|
|
7285
|
+
state = createStudioPdfAutoRefreshState(resourceQuery, {
|
|
7286
|
+
isAlive: () => Boolean(card.isConnected || (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card)),
|
|
7287
|
+
refresh: () => {
|
|
7288
|
+
if (isStudioPdfFocusOpen() && studioPdfFocusSourceCard === card) {
|
|
7289
|
+
return refreshStudioPdfFocusViewer({ automatic: true });
|
|
7290
|
+
}
|
|
7291
|
+
return refreshStudioPdfCard(card, { automatic: true });
|
|
7292
|
+
},
|
|
7293
|
+
sync: () => {
|
|
7294
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
7295
|
+
if (studioPdfFocusSourceCard === card) syncStudioPdfFocusAutoRefreshButton();
|
|
7296
|
+
},
|
|
7297
|
+
});
|
|
7298
|
+
studioPdfCardAutoRefreshStates.set(card, state);
|
|
7299
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
7300
|
+
return state;
|
|
7301
|
+
}
|
|
7302
|
+
|
|
7303
|
+
function setStudioPdfCardAutoRefresh(card, enabled, options) {
|
|
7304
|
+
const state = studioPdfCardAutoRefreshStates.get(card) || null;
|
|
7305
|
+
return setStudioPdfAutoRefreshEnabled(state, enabled, options);
|
|
7306
|
+
}
|
|
7307
|
+
|
|
7308
|
+
function getStudioPdfFocusAutoRefreshState() {
|
|
7309
|
+
if (studioPdfFocusSourceCard) return studioPdfCardAutoRefreshStates.get(studioPdfFocusSourceCard) || null;
|
|
7310
|
+
return studioPdfFocusStandaloneAutoRefreshState;
|
|
7311
|
+
}
|
|
7312
|
+
|
|
7313
|
+
function syncStudioPdfFocusAutoRefreshButton() {
|
|
7314
|
+
const state = getStudioPdfFocusAutoRefreshState();
|
|
7315
|
+
syncStudioPdfAutoRefreshButton(
|
|
7316
|
+
studioPdfFocusAutoRefreshBtn,
|
|
7317
|
+
state && state.enabled,
|
|
7318
|
+
Boolean(state && state.resourceQuery),
|
|
7319
|
+
);
|
|
7320
|
+
}
|
|
7321
|
+
|
|
7322
|
+
function clearStudioPdfFocusAutoRefreshSource() {
|
|
7323
|
+
if (studioPdfFocusStandaloneAutoRefreshState) stopStudioPdfAutoRefreshState(studioPdfFocusStandaloneAutoRefreshState);
|
|
7324
|
+
studioPdfFocusStandaloneAutoRefreshState = null;
|
|
7325
|
+
studioPdfFocusSourceCard = null;
|
|
7326
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7327
|
+
}
|
|
7328
|
+
|
|
7329
|
+
function setStudioPdfFocusAutoRefreshSource(sourceCard, resourceQuery) {
|
|
7330
|
+
clearStudioPdfFocusAutoRefreshSource();
|
|
7331
|
+
const card = sourceCard instanceof Element ? sourceCard : null;
|
|
7332
|
+
if (card) {
|
|
7333
|
+
studioPdfFocusSourceCard = card;
|
|
7334
|
+
ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
7335
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7336
|
+
return;
|
|
7337
|
+
}
|
|
7338
|
+
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7339
|
+
if (!query) {
|
|
7340
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7341
|
+
return;
|
|
7342
|
+
}
|
|
7343
|
+
studioPdfFocusStandaloneAutoRefreshState = createStudioPdfAutoRefreshState(query, {
|
|
7344
|
+
isAlive: () => Boolean(isStudioPdfFocusOpen() && !studioPdfFocusSourceCard),
|
|
7345
|
+
refresh: () => refreshStudioPdfFocusViewer({ automatic: true }),
|
|
7346
|
+
sync: () => syncStudioPdfFocusAutoRefreshButton(),
|
|
7347
|
+
});
|
|
7348
|
+
syncStudioPdfFocusAutoRefreshButton();
|
|
7349
|
+
}
|
|
7350
|
+
|
|
7351
|
+
function toggleStudioPdfFocusAutoRefresh() {
|
|
7352
|
+
const state = getStudioPdfFocusAutoRefreshState();
|
|
7353
|
+
if (!state) {
|
|
7354
|
+
setStatus("Could not resolve this PDF for auto-refresh.", "warning");
|
|
7355
|
+
return false;
|
|
7356
|
+
}
|
|
7357
|
+
return setStudioPdfAutoRefreshEnabled(state, !state.enabled);
|
|
7358
|
+
}
|
|
7359
|
+
|
|
7360
|
+
async function runStudioPdfLocalAction(action, resourceQuery) {
|
|
7361
|
+
const query = normalizeStudioPdfResourceQuery(resourceQuery);
|
|
7362
|
+
if (!query) {
|
|
7363
|
+
setStatus("Could not resolve this PDF's local path.", "warning");
|
|
7364
|
+
return false;
|
|
7365
|
+
}
|
|
7366
|
+
const openInSystemViewer = action === "system-viewer";
|
|
7367
|
+
const endpoint = openInSystemViewer ? "/open-local-resource" : "/reveal-local-resource";
|
|
7368
|
+
try {
|
|
7369
|
+
const payload = await fetchStudioJson(endpoint, {
|
|
7370
|
+
method: "POST",
|
|
7371
|
+
body: JSON.stringify(query),
|
|
7372
|
+
});
|
|
7373
|
+
setStatus(payload && payload.message
|
|
7374
|
+
? payload.message
|
|
7375
|
+
: (openInSystemViewer ? "Opened PDF in the system viewer." : "Revealed PDF in the file manager."), "success");
|
|
7376
|
+
return true;
|
|
7377
|
+
} catch (error) {
|
|
7378
|
+
setStatus((error && error.message)
|
|
7379
|
+
? error.message
|
|
7380
|
+
: (openInSystemViewer ? "Could not open PDF in the system viewer." : "Could not reveal PDF in the file manager."), "warning");
|
|
7381
|
+
return false;
|
|
7382
|
+
}
|
|
7383
|
+
}
|
|
7384
|
+
|
|
7385
|
+
function syncStudioPdfFocusResourceActions() {
|
|
7386
|
+
const available = Boolean(normalizeStudioPdfResourceQuery(studioPdfFocusResourceQuery));
|
|
7387
|
+
if (studioPdfFocusSystemViewerBtn) studioPdfFocusSystemViewerBtn.disabled = !available;
|
|
7388
|
+
if (studioPdfFocusRevealBtn) studioPdfFocusRevealBtn.disabled = !available;
|
|
6857
7389
|
}
|
|
6858
7390
|
|
|
6859
7391
|
function buildRefreshedStudioPdfViewerUrl(value) {
|
|
@@ -6886,7 +7418,7 @@
|
|
|
6886
7418
|
if (focusBtn && focusBtn.dataset) focusBtn.dataset.studioPdfViewerUrl = nextUrl;
|
|
6887
7419
|
}
|
|
6888
7420
|
|
|
6889
|
-
function refreshStudioPdfCard(card) {
|
|
7421
|
+
function refreshStudioPdfCard(card, options) {
|
|
6890
7422
|
if (!card) return false;
|
|
6891
7423
|
const frame = typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
|
|
6892
7424
|
const currentUrl = String(card.dataset && card.dataset.studioPdfViewerUrl ? card.dataset.studioPdfViewerUrl : "").trim()
|
|
@@ -6894,7 +7426,14 @@
|
|
|
6894
7426
|
const nextUrl = buildRefreshedStudioPdfViewerUrl(currentUrl);
|
|
6895
7427
|
if (!nextUrl) return false;
|
|
6896
7428
|
syncStudioPdfCardViewerUrl(card, nextUrl);
|
|
6897
|
-
|
|
7429
|
+
if (!options || !options.automatic) {
|
|
7430
|
+
resetStudioPdfAutoRefreshObservation(studioPdfCardAutoRefreshStates.get(card) || null);
|
|
7431
|
+
}
|
|
7432
|
+
if (!options || !options.silent) {
|
|
7433
|
+
setStatus(options && options.automatic
|
|
7434
|
+
? "PDF changed on disk; refreshed preview."
|
|
7435
|
+
: "Refreshed PDF preview from disk.", "success");
|
|
7436
|
+
}
|
|
6898
7437
|
return true;
|
|
6899
7438
|
}
|
|
6900
7439
|
|
|
@@ -6905,7 +7444,7 @@
|
|
|
6905
7444
|
return studioPdfFocusFrameEl;
|
|
6906
7445
|
}
|
|
6907
7446
|
|
|
6908
|
-
function refreshStudioPdfFocusViewer() {
|
|
7447
|
+
function refreshStudioPdfFocusViewer(options) {
|
|
6909
7448
|
const frame = getStudioPdfFocusActiveFrame();
|
|
6910
7449
|
const currentUrl = String(frame && frame.src ? frame.src : "").trim()
|
|
6911
7450
|
|| String(studioPdfFocusOpenLinkEl && studioPdfFocusOpenLinkEl.href ? studioPdfFocusOpenLinkEl.href : "").trim();
|
|
@@ -6916,7 +7455,58 @@
|
|
|
6916
7455
|
}
|
|
6917
7456
|
if (frame) frame.src = nextUrl;
|
|
6918
7457
|
if (studioPdfFocusOpenLinkEl) studioPdfFocusOpenLinkEl.href = nextUrl;
|
|
6919
|
-
|
|
7458
|
+
if (studioPdfFocusSourceCard) syncStudioPdfCardViewerUrl(studioPdfFocusSourceCard, nextUrl);
|
|
7459
|
+
if (!options || !options.automatic) {
|
|
7460
|
+
resetStudioPdfAutoRefreshObservation(getStudioPdfFocusAutoRefreshState());
|
|
7461
|
+
}
|
|
7462
|
+
if (!options || !options.silent) {
|
|
7463
|
+
setStatus(options && options.automatic
|
|
7464
|
+
? "PDF changed on disk; refreshed preview."
|
|
7465
|
+
: "Refreshed PDF preview from disk.", "success");
|
|
7466
|
+
}
|
|
7467
|
+
return true;
|
|
7468
|
+
}
|
|
7469
|
+
|
|
7470
|
+
function getVisibleStudioPdfCards() {
|
|
7471
|
+
return Array.from(document.querySelectorAll(".studio-pdf-card")).filter((card) => {
|
|
7472
|
+
if (!card || !card.isConnected || card.hidden) return false;
|
|
7473
|
+
if (typeof card.getClientRects === "function" && card.getClientRects().length === 0) return false;
|
|
7474
|
+
try {
|
|
7475
|
+
const style = window.getComputedStyle(card);
|
|
7476
|
+
return style.display !== "none" && style.visibility !== "hidden";
|
|
7477
|
+
} catch {
|
|
7478
|
+
return true;
|
|
7479
|
+
}
|
|
7480
|
+
});
|
|
7481
|
+
}
|
|
7482
|
+
|
|
7483
|
+
function refreshVisibleStudioPdfPreviews() {
|
|
7484
|
+
if (isStudioPdfFocusOpen()) return refreshStudioPdfFocusViewer();
|
|
7485
|
+
const cards = getVisibleStudioPdfCards();
|
|
7486
|
+
let refreshed = 0;
|
|
7487
|
+
cards.forEach((card) => {
|
|
7488
|
+
if (refreshStudioPdfCard(card, { silent: true })) refreshed += 1;
|
|
7489
|
+
});
|
|
7490
|
+
if (refreshed > 0) {
|
|
7491
|
+
setStatus(refreshed === 1
|
|
7492
|
+
? "Refreshed PDF preview from disk."
|
|
7493
|
+
: ("Refreshed " + refreshed + " PDF previews from disk."), "success");
|
|
7494
|
+
return true;
|
|
7495
|
+
}
|
|
7496
|
+
return false;
|
|
7497
|
+
}
|
|
7498
|
+
|
|
7499
|
+
function handleStudioPdfRefreshShortcut(event) {
|
|
7500
|
+
if (!event) return false;
|
|
7501
|
+
const key = typeof event.key === "string" ? event.key.toLowerCase() : "";
|
|
7502
|
+
const code = typeof event.code === "string" ? event.code : "";
|
|
7503
|
+
const matches = (key === "r" || code === "KeyR")
|
|
7504
|
+
&& (event.metaKey || event.ctrlKey)
|
|
7505
|
+
&& event.altKey
|
|
7506
|
+
&& !event.shiftKey;
|
|
7507
|
+
if (!matches) return false;
|
|
7508
|
+
if (!refreshVisibleStudioPdfPreviews()) return false;
|
|
7509
|
+
event.preventDefault();
|
|
6920
7510
|
return true;
|
|
6921
7511
|
}
|
|
6922
7512
|
|
|
@@ -6989,8 +7579,15 @@
|
|
|
6989
7579
|
|| String(card && card.dataset ? (card.dataset.studioPdfTitle || "") : "").trim()
|
|
6990
7580
|
|| "PDF preview";
|
|
6991
7581
|
const sourceFrame = card && typeof card.querySelector === "function" ? card.querySelector("iframe.studio-pdf-frame") : null;
|
|
7582
|
+
const resourceQuery = card && card.dataset
|
|
7583
|
+
? normalizeStudioPdfResourceQuery({
|
|
7584
|
+
path: card.dataset.studioPdfPath || "",
|
|
7585
|
+
sourcePath: card.dataset.studioPdfSourcePath || "",
|
|
7586
|
+
resourceDir: card.dataset.studioPdfResourceDir || "",
|
|
7587
|
+
})
|
|
7588
|
+
: null;
|
|
6992
7589
|
if (!viewerUrl) return false;
|
|
6993
|
-
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame);
|
|
7590
|
+
openStudioPdfFocusViewer(viewerUrl, title, sourceFrame, resourceQuery, card);
|
|
6994
7591
|
return true;
|
|
6995
7592
|
}
|
|
6996
7593
|
|
|
@@ -7403,6 +8000,8 @@
|
|
|
7403
8000
|
const caption = String(options.caption || "").trim();
|
|
7404
8001
|
const height = normalizeStudioPdfHeight(options.height);
|
|
7405
8002
|
const page = normalizeStudioPdfPage(options.page);
|
|
8003
|
+
const autoRefreshRequested = normalizeStudioPdfAutoRefresh(options.watch);
|
|
8004
|
+
const resourceQuery = buildStudioPdfResourceQuery(options, useEditorResourceContext);
|
|
7406
8005
|
const resourceUrl = buildStudioPdfResourceUrl(options, useEditorResourceContext);
|
|
7407
8006
|
const viewerUrl = resourceUrl && page ? resourceUrl + "#page=" + encodeURIComponent(String(page)) : resourceUrl;
|
|
7408
8007
|
|
|
@@ -7411,6 +8010,9 @@
|
|
|
7411
8010
|
if (card.dataset) {
|
|
7412
8011
|
card.dataset.studioPdfViewerUrl = viewerUrl || "";
|
|
7413
8012
|
card.dataset.studioPdfTitle = title;
|
|
8013
|
+
card.dataset.studioPdfPath = resourceQuery && resourceQuery.path ? resourceQuery.path : "";
|
|
8014
|
+
card.dataset.studioPdfSourcePath = resourceQuery && resourceQuery.sourcePath ? resourceQuery.sourcePath : "";
|
|
8015
|
+
card.dataset.studioPdfResourceDir = resourceQuery && resourceQuery.resourceDir ? resourceQuery.resourceDir : "";
|
|
7414
8016
|
}
|
|
7415
8017
|
|
|
7416
8018
|
const header = document.createElement("figcaption");
|
|
@@ -7447,14 +8049,39 @@
|
|
|
7447
8049
|
openLink.href = viewerUrl;
|
|
7448
8050
|
openLink.target = "_blank";
|
|
7449
8051
|
openLink.rel = "noopener noreferrer";
|
|
7450
|
-
openLink.textContent = "
|
|
8052
|
+
openLink.textContent = "Browser tab";
|
|
8053
|
+
openLink.title = "Open this PDF in a browser tab.";
|
|
7451
8054
|
actions.appendChild(openLink);
|
|
7452
8055
|
|
|
8056
|
+
const systemViewerBtn = document.createElement("button");
|
|
8057
|
+
systemViewerBtn.type = "button";
|
|
8058
|
+
systemViewerBtn.className = "studio-pdf-card-action studio-pdf-card-system-viewer";
|
|
8059
|
+
systemViewerBtn.textContent = "System viewer";
|
|
8060
|
+
systemViewerBtn.title = "Open the local PDF in the operating system's default PDF viewer.";
|
|
8061
|
+
systemViewerBtn.addEventListener("click", (event) => {
|
|
8062
|
+
event.preventDefault();
|
|
8063
|
+
event.stopPropagation();
|
|
8064
|
+
void runStudioPdfLocalAction("system-viewer", resourceQuery);
|
|
8065
|
+
});
|
|
8066
|
+
actions.appendChild(systemViewerBtn);
|
|
8067
|
+
|
|
8068
|
+
const revealBtn = document.createElement("button");
|
|
8069
|
+
revealBtn.type = "button";
|
|
8070
|
+
revealBtn.className = "studio-pdf-card-action studio-pdf-card-reveal";
|
|
8071
|
+
revealBtn.textContent = "Show in folder";
|
|
8072
|
+
revealBtn.title = "Reveal the local PDF in Finder or the system file manager.";
|
|
8073
|
+
revealBtn.addEventListener("click", (event) => {
|
|
8074
|
+
event.preventDefault();
|
|
8075
|
+
event.stopPropagation();
|
|
8076
|
+
void runStudioPdfLocalAction("reveal", resourceQuery);
|
|
8077
|
+
});
|
|
8078
|
+
actions.appendChild(revealBtn);
|
|
8079
|
+
|
|
7453
8080
|
const refreshBtn = document.createElement("button");
|
|
7454
8081
|
refreshBtn.type = "button";
|
|
7455
8082
|
refreshBtn.className = "studio-pdf-card-action studio-pdf-card-refresh";
|
|
7456
8083
|
refreshBtn.textContent = "Refresh";
|
|
7457
|
-
refreshBtn.title = "Reload this PDF preview from disk.";
|
|
8084
|
+
refreshBtn.title = "Reload this PDF preview from disk. Shortcut: Cmd/Ctrl+Alt+R.";
|
|
7458
8085
|
refreshBtn.addEventListener("click", (event) => {
|
|
7459
8086
|
event.preventDefault();
|
|
7460
8087
|
event.stopPropagation();
|
|
@@ -7462,6 +8089,25 @@
|
|
|
7462
8089
|
});
|
|
7463
8090
|
actions.appendChild(refreshBtn);
|
|
7464
8091
|
|
|
8092
|
+
const autoRefreshBtn = document.createElement("button");
|
|
8093
|
+
autoRefreshBtn.type = "button";
|
|
8094
|
+
autoRefreshBtn.className = "studio-pdf-card-action studio-pdf-card-auto-refresh";
|
|
8095
|
+
autoRefreshBtn.textContent = "Auto-refresh";
|
|
8096
|
+
autoRefreshBtn.title = "Watch this local PDF and reload it after a changed file is stable on disk.";
|
|
8097
|
+
autoRefreshBtn.setAttribute("aria-label", "Enable PDF auto-refresh");
|
|
8098
|
+
autoRefreshBtn.setAttribute("aria-pressed", "false");
|
|
8099
|
+
autoRefreshBtn.addEventListener("click", (event) => {
|
|
8100
|
+
event.preventDefault();
|
|
8101
|
+
event.stopPropagation();
|
|
8102
|
+
const state = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
8103
|
+
if (!state) {
|
|
8104
|
+
setStatus("Could not resolve this PDF for auto-refresh.", "warning");
|
|
8105
|
+
return;
|
|
8106
|
+
}
|
|
8107
|
+
setStudioPdfCardAutoRefresh(card, !state.enabled);
|
|
8108
|
+
});
|
|
8109
|
+
actions.appendChild(autoRefreshBtn);
|
|
8110
|
+
|
|
7465
8111
|
header.appendChild(actions);
|
|
7466
8112
|
}
|
|
7467
8113
|
card.appendChild(header);
|
|
@@ -7488,6 +8134,12 @@
|
|
|
7488
8134
|
iframe.loading = "lazy";
|
|
7489
8135
|
iframe.style.height = height + "px";
|
|
7490
8136
|
card.appendChild(iframe);
|
|
8137
|
+
const autoRefreshState = ensureStudioPdfCardAutoRefreshState(card, resourceQuery);
|
|
8138
|
+
if (autoRefreshRequested) {
|
|
8139
|
+
setStudioPdfAutoRefreshEnabled(autoRefreshState, true, { silent: true });
|
|
8140
|
+
} else {
|
|
8141
|
+
syncStudioPdfCardAutoRefreshButton(card);
|
|
8142
|
+
}
|
|
7491
8143
|
return card;
|
|
7492
8144
|
}
|
|
7493
8145
|
|
|
@@ -8777,7 +9429,13 @@
|
|
|
8777
9429
|
} else {
|
|
8778
9430
|
failPendingStudioTab(studioLaunch, "Studio did not return a preview-tab URL for this PDF export.");
|
|
8779
9431
|
const viewerUrl = getStudioPdfViewerUrlForExportPayload(payload);
|
|
8780
|
-
|
|
9432
|
+
const resourceQuery = exportPath
|
|
9433
|
+
? buildStudioPdfResourceQuery({
|
|
9434
|
+
path: exportPath,
|
|
9435
|
+
resourceDir: exportPath.split(/[\\/]/).slice(0, -1).join("/"),
|
|
9436
|
+
}, false)
|
|
9437
|
+
: null;
|
|
9438
|
+
if (viewerUrl) openStudioPdfFocusViewer(viewerUrl, downloadName, null, resourceQuery);
|
|
8781
9439
|
}
|
|
8782
9440
|
if (writeError) {
|
|
8783
9441
|
setStatus(openedStudio
|
|
@@ -11399,6 +12057,7 @@
|
|
|
11399
12057
|
const canRefreshFromDisk = hasRefreshableFilePath();
|
|
11400
12058
|
|
|
11401
12059
|
fileInput.disabled = uiBusy;
|
|
12060
|
+
if (importFileBtn) importFileBtn.disabled = uiBusy;
|
|
11402
12061
|
if (sourceBadgeEl) sourceBadgeEl.disabled = uiBusy;
|
|
11403
12062
|
if (sourceResetOriginBtn) sourceResetOriginBtn.disabled = uiBusy;
|
|
11404
12063
|
if (sourceOpenCurrentFileTabBtn) {
|
|
@@ -11714,12 +12373,15 @@
|
|
|
11714
12373
|
return true;
|
|
11715
12374
|
}
|
|
11716
12375
|
|
|
11717
|
-
function clearStudioWorkspace() {
|
|
12376
|
+
async function clearStudioWorkspace() {
|
|
11718
12377
|
if (uiBusy) {
|
|
11719
12378
|
setStatus("Studio is busy.", "warning");
|
|
11720
12379
|
return;
|
|
11721
12380
|
}
|
|
11722
|
-
const confirmed =
|
|
12381
|
+
const confirmed = await requestStudioConfirmation(
|
|
12382
|
+
"Reset the editor to a fresh blank draft in this browser tab? Saved files and responses are not changed.",
|
|
12383
|
+
{ title: "Reset editor?", confirmLabel: "Reset editor", destructive: true },
|
|
12384
|
+
);
|
|
11723
12385
|
if (!confirmed) return;
|
|
11724
12386
|
const preservedResponseState = {
|
|
11725
12387
|
responseHistory: Array.isArray(responseHistory) ? responseHistory.slice() : [],
|
|
@@ -12863,7 +13525,7 @@
|
|
|
12863
13525
|
if (!raw || raw.charAt(0) === "#") return false;
|
|
12864
13526
|
if (/^\/\//.test(raw)) return false;
|
|
12865
13527
|
if (/^(?:https?|mailto|tel|data|blob|javascript|about):/i.test(raw)) return false;
|
|
12866
|
-
if (/^\/(?:pdf-resource|html-preview-resource|export-pdf|export-html|render-preview|render-math|local-preview-link|reveal-local-resource)(?:[?#/]|$)/i.test(raw)) return false;
|
|
13528
|
+
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;
|
|
12867
13529
|
return true;
|
|
12868
13530
|
}
|
|
12869
13531
|
|
|
@@ -12949,6 +13611,7 @@
|
|
|
12949
13611
|
if (kind === "pdf") {
|
|
12950
13612
|
appendPreviewLinkMenuButton(menu, "Open PDF preview", "open-pdf");
|
|
12951
13613
|
appendPreviewLinkMenuButton(menu, "Open in new Studio tab", "open-preview-new");
|
|
13614
|
+
appendPreviewLinkMenuButton(menu, "Open in system viewer", "open-system");
|
|
12952
13615
|
} else if (kind === "text") {
|
|
12953
13616
|
appendPreviewLinkMenuButton(menu, "Open file tab", "open-new");
|
|
12954
13617
|
appendPreviewLinkMenuButton(menu, "Open here", "open-here");
|
|
@@ -12989,7 +13652,14 @@
|
|
|
12989
13652
|
setStatus("Could not resolve this PDF link. Open the source file or set a working directory first.", "warning");
|
|
12990
13653
|
return false;
|
|
12991
13654
|
}
|
|
12992
|
-
|
|
13655
|
+
const cleanPath = stripPreviewLocalLinkUrlSuffix(href);
|
|
13656
|
+
const context = contextOverride && typeof contextOverride === "object" ? contextOverride : {};
|
|
13657
|
+
const resourceQuery = buildStudioPdfResourceQuery({
|
|
13658
|
+
path: cleanPath,
|
|
13659
|
+
sourcePath: context.sourcePath || "",
|
|
13660
|
+
resourceDir: context.resourceDir || "",
|
|
13661
|
+
}, true);
|
|
13662
|
+
openStudioPdfFocusViewer(viewerUrl, title || href, null, resourceQuery);
|
|
12993
13663
|
return true;
|
|
12994
13664
|
}
|
|
12995
13665
|
|
|
@@ -13022,16 +13692,17 @@
|
|
|
13022
13692
|
}
|
|
13023
13693
|
}
|
|
13024
13694
|
|
|
13025
|
-
function confirmPreviewOfficeConversion(href, destination) {
|
|
13695
|
+
async function confirmPreviewOfficeConversion(href, destination) {
|
|
13026
13696
|
if (getPreviewLocalLinkKind(href) !== "office") return true;
|
|
13027
13697
|
const label = getPreviewOfficeConversionLabel(href);
|
|
13028
13698
|
const target = destination === "here"
|
|
13029
13699
|
? "replace the current editor contents with an editable Markdown copy"
|
|
13030
13700
|
: "open an editable Markdown copy in a new Studio tab";
|
|
13031
|
-
const confirmed =
|
|
13701
|
+
const confirmed = await requestStudioConfirmation(
|
|
13032
13702
|
"Convert " + label + " to Markdown?\n\n"
|
|
13033
13703
|
+ "Studio will use Pandoc to " + target + ". Some layout or formatting may change. "
|
|
13034
|
-
+ "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it."
|
|
13704
|
+
+ "The original DOCX/ODT file will not be overwritten, and edits will not round-trip back to it.",
|
|
13705
|
+
{ title: "Convert document?", confirmLabel: "Convert" },
|
|
13035
13706
|
);
|
|
13036
13707
|
if (!confirmed) setStatus("Document conversion cancelled.", "warning");
|
|
13037
13708
|
return confirmed;
|
|
@@ -13043,13 +13714,17 @@
|
|
|
13043
13714
|
}
|
|
13044
13715
|
|
|
13045
13716
|
async function openPreviewDocumentHere(href, contextOverride, options) {
|
|
13046
|
-
if (!confirmPreviewOfficeConversion(href, "here")) return;
|
|
13717
|
+
if (!(await confirmPreviewOfficeConversion(href, "here"))) return;
|
|
13047
13718
|
if (editorHasPotentialUnsavedContent()) {
|
|
13048
13719
|
const kind = getPreviewLocalLinkKind(href);
|
|
13049
13720
|
const prompt = kind === "office"
|
|
13050
13721
|
? "Replace the current editor contents with this converted Markdown copy? Unsaved editor changes may be lost."
|
|
13051
13722
|
: "Open this file-backed document in the current editor?\n\nThis will replace the current editor contents and attach the editor to the file on disk, so Save editor and Refresh from disk use that file. Unsaved editor changes may be lost.";
|
|
13052
|
-
const confirmed =
|
|
13723
|
+
const confirmed = await requestStudioConfirmation(prompt, {
|
|
13724
|
+
title: "Replace editor contents?",
|
|
13725
|
+
confirmLabel: "Replace",
|
|
13726
|
+
destructive: true,
|
|
13727
|
+
});
|
|
13053
13728
|
if (!confirmed) return;
|
|
13054
13729
|
}
|
|
13055
13730
|
const payload = await fetchPreviewLocalLink("document", href, contextOverride);
|
|
@@ -13080,7 +13755,7 @@
|
|
|
13080
13755
|
}
|
|
13081
13756
|
|
|
13082
13757
|
async function openPreviewDocumentInNewEditor(href, contextOverride) {
|
|
13083
|
-
if (!confirmPreviewOfficeConversion(href, "new")) return;
|
|
13758
|
+
if (!(await confirmPreviewOfficeConversion(href, "new"))) return;
|
|
13084
13759
|
let launch = null;
|
|
13085
13760
|
try {
|
|
13086
13761
|
launch = openPendingStudioTab("document");
|
|
@@ -13136,6 +13811,10 @@
|
|
|
13136
13811
|
openPreviewPdfLink(href, context.title || href, context);
|
|
13137
13812
|
return;
|
|
13138
13813
|
}
|
|
13814
|
+
if (action === "open-system") {
|
|
13815
|
+
await runStudioPdfLocalAction("system-viewer", getPreviewLinkResourceQuery(href, context));
|
|
13816
|
+
return;
|
|
13817
|
+
}
|
|
13139
13818
|
if (action === "open-new") {
|
|
13140
13819
|
await openPreviewDocumentInNewEditor(href, context);
|
|
13141
13820
|
return;
|
|
@@ -14824,7 +15503,10 @@
|
|
|
14824
15503
|
return;
|
|
14825
15504
|
}
|
|
14826
15505
|
if (String(scratchpadText || "").trim() && String(scratchpadText || "") !== String(text || "")) {
|
|
14827
|
-
const confirmed =
|
|
15506
|
+
const confirmed = await requestStudioConfirmation(
|
|
15507
|
+
"Replace the current scratchpad with this recent scratchpad? Current scratchpad text will remain saved under its current document/draft identity, but this panel will show the loaded text for the current document.",
|
|
15508
|
+
{ title: "Replace scratchpad?", confirmLabel: "Replace", destructive: true },
|
|
15509
|
+
);
|
|
14828
15510
|
if (!confirmed) return;
|
|
14829
15511
|
}
|
|
14830
15512
|
setScratchpadText(text);
|
|
@@ -18646,7 +19328,7 @@
|
|
|
18646
19328
|
deleteBtn.textContent = "Delete";
|
|
18647
19329
|
deleteBtn.title = "Delete this local comment.";
|
|
18648
19330
|
deleteBtn.addEventListener("click", () => {
|
|
18649
|
-
deleteReviewNote(note.id);
|
|
19331
|
+
void deleteReviewNote(note.id);
|
|
18650
19332
|
});
|
|
18651
19333
|
actions.appendChild(deleteBtn);
|
|
18652
19334
|
|
|
@@ -19010,24 +19692,29 @@
|
|
|
19010
19692
|
});
|
|
19011
19693
|
}
|
|
19012
19694
|
|
|
19013
|
-
function deleteReviewNote(noteId) {
|
|
19695
|
+
async function deleteReviewNote(noteId) {
|
|
19014
19696
|
const note = reviewNotes.find((entry) => entry && entry.id === noteId);
|
|
19015
19697
|
if (!note) return;
|
|
19016
|
-
const confirmed =
|
|
19698
|
+
const confirmed = await requestStudioConfirmation("Delete this local comment?", {
|
|
19699
|
+
title: "Delete comment?",
|
|
19700
|
+
confirmLabel: "Delete",
|
|
19701
|
+
destructive: true,
|
|
19702
|
+
});
|
|
19017
19703
|
if (!confirmed) return;
|
|
19018
19704
|
setReviewNotes(reviewNotes.filter((entry) => entry && entry.id !== noteId));
|
|
19019
19705
|
setStatus("Deleted local comment.", "success");
|
|
19020
19706
|
}
|
|
19021
19707
|
|
|
19022
|
-
function deleteAllReviewNotes() {
|
|
19708
|
+
async function deleteAllReviewNotes() {
|
|
19023
19709
|
if (!reviewNotes.length) {
|
|
19024
19710
|
setStatus("No local comments to delete.", "warning");
|
|
19025
19711
|
return;
|
|
19026
19712
|
}
|
|
19027
19713
|
const count = reviewNotes.length;
|
|
19028
|
-
const confirmed =
|
|
19714
|
+
const confirmed = await requestStudioConfirmation(
|
|
19029
19715
|
"Delete all " + count + " local comment" + (count === 1 ? "" : "s") + " for this document?\n\n"
|
|
19030
19716
|
+ "Existing inline [an: ...] annotations in the editor text will not be removed.",
|
|
19717
|
+
{ title: "Delete all comments?", confirmLabel: "Delete all", destructive: true },
|
|
19031
19718
|
);
|
|
19032
19719
|
if (!confirmed) return;
|
|
19033
19720
|
setReviewNotes([]);
|
|
@@ -21238,7 +21925,7 @@
|
|
|
21238
21925
|
});
|
|
21239
21926
|
}
|
|
21240
21927
|
|
|
21241
|
-
function loadSelectedResponseIntoEditor(options) {
|
|
21928
|
+
async function loadSelectedResponseIntoEditor(options) {
|
|
21242
21929
|
if (!latestResponseMarkdown.trim()) {
|
|
21243
21930
|
setStatus("No response available yet.", "warning");
|
|
21244
21931
|
return false;
|
|
@@ -21249,12 +21936,15 @@
|
|
|
21249
21936
|
&& sourceState.source === "last-response"
|
|
21250
21937
|
&& Boolean(currentEditorText.trim())
|
|
21251
21938
|
&& normalizeForCompare(currentEditorText) !== latestResponseNormalized;
|
|
21252
|
-
if (
|
|
21253
|
-
|
|
21254
|
-
|
|
21255
|
-
|
|
21256
|
-
|
|
21257
|
-
|
|
21939
|
+
if (replacingEditedResponse) {
|
|
21940
|
+
const confirmed = await requestStudioConfirmation(
|
|
21941
|
+
"Replace your edited response with a fresh copy? Existing edits and annotations will be lost.",
|
|
21942
|
+
{ title: "Replace edited response?", confirmLabel: "Replace", destructive: true },
|
|
21943
|
+
);
|
|
21944
|
+
if (!confirmed) {
|
|
21945
|
+
setStatus("Kept the current editor text.");
|
|
21946
|
+
return false;
|
|
21947
|
+
}
|
|
21258
21948
|
}
|
|
21259
21949
|
setEditorText(latestResponseMarkdown, { preserveScroll: false, preserveSelection: false });
|
|
21260
21950
|
setSourceState({ source: "last-response", label: "last model response", path: null });
|
|
@@ -21272,11 +21962,11 @@
|
|
|
21272
21962
|
}
|
|
21273
21963
|
|
|
21274
21964
|
loadResponseBtn.addEventListener("click", () => {
|
|
21275
|
-
loadSelectedResponseIntoEditor();
|
|
21965
|
+
void loadSelectedResponseIntoEditor();
|
|
21276
21966
|
});
|
|
21277
21967
|
|
|
21278
21968
|
annotateResponseBtn.addEventListener("click", () => {
|
|
21279
|
-
loadSelectedResponseIntoEditor({ annotate: true });
|
|
21969
|
+
void loadSelectedResponseIntoEditor({ annotate: true });
|
|
21280
21970
|
});
|
|
21281
21971
|
|
|
21282
21972
|
loadCritiqueNotesBtn.addEventListener("click", () => {
|
|
@@ -21411,7 +22101,7 @@
|
|
|
21411
22101
|
setFooterThemeMenuOpen(false);
|
|
21412
22102
|
});
|
|
21413
22103
|
|
|
21414
|
-
saveAsBtn.addEventListener("click", () => {
|
|
22104
|
+
saveAsBtn.addEventListener("click", async () => {
|
|
21415
22105
|
const content = sourceTextEl.value;
|
|
21416
22106
|
if (!content.trim()) {
|
|
21417
22107
|
setStatus("Editor is empty. Nothing to save.", "warning");
|
|
@@ -21421,7 +22111,10 @@
|
|
|
21421
22111
|
var suggestedName = sourceState.label ? stripImportedFileLabel(sourceState.label) : "draft.md";
|
|
21422
22112
|
var suggestedDir = getCurrentResourceDirValue() ? getCurrentResourceDirValue().replace(/\/$/, "") + "/" : "./";
|
|
21423
22113
|
const suggested = sourceState.path || (suggestedDir + suggestedName);
|
|
21424
|
-
const path =
|
|
22114
|
+
const path = await requestStudioTextInput("Save editor content as:", suggested, {
|
|
22115
|
+
title: "Save editor as",
|
|
22116
|
+
confirmLabel: "Save",
|
|
22117
|
+
});
|
|
21425
22118
|
if (!path) return;
|
|
21426
22119
|
|
|
21427
22120
|
const requestId = beginUiAction("save_as");
|
|
@@ -21441,16 +22134,19 @@
|
|
|
21441
22134
|
}
|
|
21442
22135
|
});
|
|
21443
22136
|
|
|
21444
|
-
saveOverBtn.addEventListener("click", () => {
|
|
22137
|
+
saveOverBtn.addEventListener("click", async () => {
|
|
21445
22138
|
var effectivePath = getEffectiveSavePath();
|
|
21446
22139
|
if (!effectivePath) {
|
|
21447
22140
|
setStatus("Save editor requires a file path. Open via /studio <path>, set a working dir, or use Save editor as…", "warning");
|
|
21448
22141
|
return;
|
|
21449
22142
|
}
|
|
21450
22143
|
|
|
21451
|
-
|
|
21452
|
-
|
|
21453
|
-
|
|
22144
|
+
const confirmed = await requestStudioConfirmation("Overwrite " + effectivePath + "?", {
|
|
22145
|
+
title: "Overwrite file?",
|
|
22146
|
+
confirmLabel: "Overwrite",
|
|
22147
|
+
destructive: true,
|
|
22148
|
+
});
|
|
22149
|
+
if (!confirmed) return;
|
|
21454
22150
|
|
|
21455
22151
|
const requestId = beginUiAction("save_over");
|
|
21456
22152
|
if (!requestId) return;
|
|
@@ -21471,14 +22167,17 @@
|
|
|
21471
22167
|
});
|
|
21472
22168
|
|
|
21473
22169
|
if (refreshFromDiskBtn) {
|
|
21474
|
-
refreshFromDiskBtn.addEventListener("click", () => {
|
|
22170
|
+
refreshFromDiskBtn.addEventListener("click", async () => {
|
|
21475
22171
|
if (!hasRefreshableFilePath()) {
|
|
21476
22172
|
setStatus("Refresh from disk needs a file path. Use Files → Open here, Files → Open file tab, or /studio-editor-only <path> for a refreshable editor tab.", "warning");
|
|
21477
22173
|
return;
|
|
21478
22174
|
}
|
|
21479
22175
|
|
|
21480
22176
|
if (editorDiffersFromFileBackedBaseline()) {
|
|
21481
|
-
const confirmed =
|
|
22177
|
+
const confirmed = await requestStudioConfirmation(
|
|
22178
|
+
"Replace current editor contents with the latest version from disk?",
|
|
22179
|
+
{ title: "Refresh from disk?", confirmLabel: "Replace", destructive: true },
|
|
22180
|
+
);
|
|
21482
22181
|
if (!confirmed) return;
|
|
21483
22182
|
}
|
|
21484
22183
|
|
|
@@ -21501,7 +22200,7 @@
|
|
|
21501
22200
|
|
|
21502
22201
|
if (clearWorkspaceBtn) {
|
|
21503
22202
|
clearWorkspaceBtn.addEventListener("click", () => {
|
|
21504
|
-
clearStudioWorkspace();
|
|
22203
|
+
void clearStudioWorkspace();
|
|
21505
22204
|
});
|
|
21506
22205
|
}
|
|
21507
22206
|
|
|
@@ -21764,7 +22463,7 @@
|
|
|
21764
22463
|
|
|
21765
22464
|
if (reviewNotesDeleteAllBtn) {
|
|
21766
22465
|
reviewNotesDeleteAllBtn.addEventListener("click", () => {
|
|
21767
|
-
deleteAllReviewNotes();
|
|
22466
|
+
void deleteAllReviewNotes();
|
|
21768
22467
|
});
|
|
21769
22468
|
}
|
|
21770
22469
|
|
|
@@ -21966,9 +22665,13 @@
|
|
|
21966
22665
|
}
|
|
21967
22666
|
|
|
21968
22667
|
if (scratchpadClearBtn) {
|
|
21969
|
-
scratchpadClearBtn.addEventListener("click", () => {
|
|
22668
|
+
scratchpadClearBtn.addEventListener("click", async () => {
|
|
21970
22669
|
if (!String(scratchpadText || "").length) return;
|
|
21971
|
-
const confirmed =
|
|
22670
|
+
const confirmed = await requestStudioConfirmation("Clear scratchpad text?", {
|
|
22671
|
+
title: "Clear scratchpad?",
|
|
22672
|
+
confirmLabel: "Clear",
|
|
22673
|
+
destructive: true,
|
|
22674
|
+
});
|
|
21972
22675
|
if (!confirmed) return;
|
|
21973
22676
|
setScratchpadText("");
|
|
21974
22677
|
if (scratchpadTextEl) scratchpadTextEl.focus();
|
|
@@ -21977,7 +22680,7 @@
|
|
|
21977
22680
|
}
|
|
21978
22681
|
|
|
21979
22682
|
if (saveAnnotatedBtn) {
|
|
21980
|
-
saveAnnotatedBtn.addEventListener("click", () => {
|
|
22683
|
+
saveAnnotatedBtn.addEventListener("click", async () => {
|
|
21981
22684
|
const content = sourceTextEl.value;
|
|
21982
22685
|
if (!content.trim()) {
|
|
21983
22686
|
setStatus("Editor is empty. Nothing to save.", "warning");
|
|
@@ -21985,7 +22688,10 @@
|
|
|
21985
22688
|
}
|
|
21986
22689
|
|
|
21987
22690
|
const suggested = buildAnnotatedSaveSuggestion();
|
|
21988
|
-
const path =
|
|
22691
|
+
const path = await requestStudioTextInput("Save annotated editor content as:", suggested, {
|
|
22692
|
+
title: "Save annotated editor as",
|
|
22693
|
+
confirmLabel: "Save",
|
|
22694
|
+
});
|
|
21989
22695
|
if (!path) return;
|
|
21990
22696
|
|
|
21991
22697
|
const requestId = beginUiAction("save_as");
|
|
@@ -22007,14 +22713,17 @@
|
|
|
22007
22713
|
}
|
|
22008
22714
|
|
|
22009
22715
|
if (stripAnnotationsBtn) {
|
|
22010
|
-
stripAnnotationsBtn.addEventListener("click", () => {
|
|
22716
|
+
stripAnnotationsBtn.addEventListener("click", async () => {
|
|
22011
22717
|
const content = sourceTextEl.value;
|
|
22012
22718
|
if (!hasAnnotationMarkers(content)) {
|
|
22013
22719
|
setStatus("No [an: ...] markers found in editor.", "warning");
|
|
22014
22720
|
return;
|
|
22015
22721
|
}
|
|
22016
22722
|
|
|
22017
|
-
const confirmed =
|
|
22723
|
+
const confirmed = await requestStudioConfirmation(
|
|
22724
|
+
"Remove all [an: ...] markers from editor text? This cannot be undone.",
|
|
22725
|
+
{ title: "Remove all annotations?", confirmLabel: "Remove", destructive: true },
|
|
22726
|
+
);
|
|
22018
22727
|
if (!confirmed) return;
|
|
22019
22728
|
|
|
22020
22729
|
const strippedContent = stripAnnotationMarkers(content);
|
|
@@ -22049,7 +22758,7 @@
|
|
|
22049
22758
|
}
|
|
22050
22759
|
if (sourceBadgeEl) {
|
|
22051
22760
|
sourceBadgeEl.addEventListener("click", () => {
|
|
22052
|
-
if (!studioUiRefreshEnabled) resetEditorOrigin();
|
|
22761
|
+
if (!studioUiRefreshEnabled) void resetEditorOrigin();
|
|
22053
22762
|
});
|
|
22054
22763
|
}
|
|
22055
22764
|
if (resourceDirBtn) {
|
|
@@ -22092,6 +22801,72 @@
|
|
|
22092
22801
|
});
|
|
22093
22802
|
}
|
|
22094
22803
|
|
|
22804
|
+
function applyImportedFileCopy(text, filename) {
|
|
22805
|
+
const name = String(filename || "imported file").trim() || "imported file";
|
|
22806
|
+
setEditorText(String(text || ""), { preserveScroll: false, preserveSelection: false });
|
|
22807
|
+
setSourceState({
|
|
22808
|
+
source: "upload",
|
|
22809
|
+
label: "imported copy: " + name,
|
|
22810
|
+
path: null,
|
|
22811
|
+
});
|
|
22812
|
+
refreshResponseUi();
|
|
22813
|
+
const detectedLang = detectLanguageFromName(name);
|
|
22814
|
+
if (detectedLang) setEditorLanguage(detectedLang);
|
|
22815
|
+
setStatus("Imported file copy: " + name + ".", "success");
|
|
22816
|
+
}
|
|
22817
|
+
|
|
22818
|
+
function chooseStudioFileCopyWithBrowser() {
|
|
22819
|
+
fileInput.value = "";
|
|
22820
|
+
setStatus("If no file picker appeared, enter the file path and select Import from path.");
|
|
22821
|
+
try {
|
|
22822
|
+
fileInput.click();
|
|
22823
|
+
} catch {
|
|
22824
|
+
setStatus("This browser could not open its file picker. Enter the file path and select Import from path.", "warning");
|
|
22825
|
+
}
|
|
22826
|
+
}
|
|
22827
|
+
|
|
22828
|
+
async function openStudioFileCopyDialog() {
|
|
22829
|
+
const resourceDir = getCurrentResourceDirValue();
|
|
22830
|
+
const suggestedPath = resourceDir ? resourceDir.replace(/[\\/]$/, "") + "/" : "./";
|
|
22831
|
+
let path = null;
|
|
22832
|
+
studioImportDecisionOpen = true;
|
|
22833
|
+
try {
|
|
22834
|
+
path = await requestStudioTextInput(
|
|
22835
|
+
"Enter the path to a file you want to import, or use Browse to open your browser’s file picker.",
|
|
22836
|
+
suggestedPath,
|
|
22837
|
+
{
|
|
22838
|
+
title: "Import file copy",
|
|
22839
|
+
confirmLabel: "Import from path",
|
|
22840
|
+
secondaryLabel: "Browse…",
|
|
22841
|
+
onSecondary: chooseStudioFileCopyWithBrowser,
|
|
22842
|
+
inputLabel: "File path on computer running Pi",
|
|
22843
|
+
placeholder: "/path/to/file.md",
|
|
22844
|
+
},
|
|
22845
|
+
);
|
|
22846
|
+
} finally {
|
|
22847
|
+
studioImportDecisionOpen = false;
|
|
22848
|
+
}
|
|
22849
|
+
if (!path) return;
|
|
22850
|
+
try {
|
|
22851
|
+
const payload = await fetchStudioJson("/import-file-copy", {
|
|
22852
|
+
method: "POST",
|
|
22853
|
+
body: JSON.stringify({ path }),
|
|
22854
|
+
});
|
|
22855
|
+
if (typeof payload.text !== "string") throw new Error("Studio did not return file text.");
|
|
22856
|
+
applyImportedFileCopy(payload.text, typeof payload.filename === "string" ? payload.filename : path);
|
|
22857
|
+
} catch (error) {
|
|
22858
|
+
setStatus("Could not import file copy: " + (error && error.message ? error.message : String(error || "unknown error")), "warning");
|
|
22859
|
+
}
|
|
22860
|
+
}
|
|
22861
|
+
|
|
22862
|
+
if (importFileBtn) {
|
|
22863
|
+
importFileBtn.addEventListener("click", (event) => {
|
|
22864
|
+
event.preventDefault();
|
|
22865
|
+
event.stopPropagation();
|
|
22866
|
+
void openStudioFileCopyDialog();
|
|
22867
|
+
});
|
|
22868
|
+
}
|
|
22869
|
+
|
|
22095
22870
|
fileInput.addEventListener("change", () => {
|
|
22096
22871
|
const file = fileInput.files && fileInput.files[0];
|
|
22097
22872
|
if (!file) return;
|
|
@@ -22103,21 +22878,11 @@
|
|
|
22103
22878
|
const reader = new FileReader();
|
|
22104
22879
|
reader.onload = () => {
|
|
22105
22880
|
const text = typeof reader.result === "string" ? reader.result : "";
|
|
22106
|
-
|
|
22107
|
-
|
|
22108
|
-
source: "upload",
|
|
22109
|
-
label: "imported copy: " + file.name,
|
|
22110
|
-
path: null,
|
|
22111
|
-
});
|
|
22112
|
-
refreshResponseUi();
|
|
22113
|
-
const detectedLang = detectLanguageFromName(file.name);
|
|
22114
|
-
if (detectedLang) {
|
|
22115
|
-
setEditorLanguage(detectedLang);
|
|
22116
|
-
}
|
|
22117
|
-
setStatus("Imported file copy: " + file.name + ".", "success");
|
|
22881
|
+
if (studioImportDecisionOpen) finishStudioDecision(null);
|
|
22882
|
+
applyImportedFileCopy(text, file.name);
|
|
22118
22883
|
};
|
|
22119
22884
|
reader.onerror = () => {
|
|
22120
|
-
setStatus("Failed to read file.", "error");
|
|
22885
|
+
setStatus("Failed to read file. Enter its path in the import dialog or choose another file.", "error");
|
|
22121
22886
|
};
|
|
22122
22887
|
reader.readAsText(file);
|
|
22123
22888
|
});
|