new functions and fixes

This commit is contained in:
Mateusz Gruszczyński
2026-08-06 09:08:02 +02:00
parent 1e80fb2bcb
commit 7989a873e7
11 changed files with 191 additions and 42 deletions
+23 -1
View File
@@ -2510,6 +2510,17 @@ dialog::backdrop {
min-width: 74px;
}
.file-delete-notice {
margin: 0;
padding: 10px 12px;
border: 1px solid var(--border-strong);
border-radius: 9px;
background: var(--surface-2);
color: var(--text-secondary);
font-size: .8rem;
line-height: 1.45;
}
@media (max-width: 760px) {
.file-actions {
flex-wrap: wrap;
@@ -2567,13 +2578,24 @@ dialog::backdrop {
text-align: left;
}
.video-choice-actions strong {
color: inherit;
}
.video-choice-actions span {
color: var(--muted);
font-size: .76rem;
font-weight: 500;
line-height: 1.4;
}
.video-choice-actions > .action-button--primary span {
color: var(--on-accent);
}
.video-choice-actions > .action-button--secondary span {
color: var(--text-secondary);
}
@media (max-width: 600px) {
.video-choice-actions {
grid-template-columns: minmax(0, 1fr);
+12 -2
View File
@@ -9,6 +9,9 @@
function clamp(value, min, max) { return Math.min(max, Math.max(min, value)); }
function stem(name) { return name.replace(/\.[^.]+$/, "") || "image"; }
function imageNeedsProcessing(width, height, aspect, maxSize) {
return aspect !== "original" || (maxSize > 0 && Math.max(width, height) > maxSize);
}
export async function prepareImageFile(file) {
if (!file.type.startsWith("image/")) return file;
@@ -61,8 +64,15 @@ export async function prepareImageFile(file) {
const result = new Promise(resolve => {
dialog.addEventListener("close", () => { URL.revokeObjectURL(url); dialog.remove(); resolve(accepted); }, { once: true });
dialog.querySelector("[data-apply]").addEventListener("click", async () => {
const box = cropBox(), maxSize = Number(sizeSelect.value), out = document.createElement("canvas");
if (aspectSelect.value === "original") {
const box = cropBox(), maxSize = Number(sizeSelect.value);
const wholeImage = aspectSelect.value === "original";
if (!imageNeedsProcessing(image.naturalWidth, image.naturalHeight, aspectSelect.value, maxSize)) {
accepted = file;
dialog.close();
return;
}
const out = document.createElement("canvas");
if (wholeImage) {
const ratio = Math.min(1, maxSize ? maxSize / Math.max(image.naturalWidth, image.naturalHeight) : 1);
out.width = Math.max(1, Math.round(image.naturalWidth * ratio)); out.height = Math.max(1, Math.round(image.naturalHeight * ratio));
out.getContext("2d").drawImage(image, 0, 0, out.width, out.height);
+5 -1
View File
@@ -102,7 +102,11 @@ export function createWorkspaceNoteAdapter() {
}),
configureView(info) {
const button = document.querySelector("#delete-note");
if (button) button.hidden = info.note_protected;
if (!button) return;
button.hidden = !info.can_delete;
button.title = info.note_protected
? "Delete protected note (owner only)"
: "Delete note";
},
async deleteNote(info, accessToken) {
if (!await askConfirm(`Delete note “${info.title}”? This cannot be undone.`, {
+8
View File
@@ -1354,6 +1354,14 @@ export function startNoteEditor(adapter) {
editor, toast, getAccessToken: () => accessToken,
getUploadMaxSize: () => Number(info?.upload_max_size_bytes) || 0,
canDelete: () => Boolean(info?.can_delete_files),
getDeleteRestrictionMessage: () => {
if (info?.note_protected && !info?.can_delete_files) {
return "This note is protected. Only its owner can delete files.";
}
if (info?.access_level !== "write") return "Read-write access is required to delete files.";
if (!info?.can_delete_files) return "Only the note owner can delete files.";
return "";
},
canUpload: () => Boolean(info?.can_upload_files),
canEdit: canEditDocument,
endpoints: adapter.fileEndpoints,
+21 -3
View File
@@ -79,6 +79,17 @@ function safeAttachmentUrl(value) {
: safePublicUrl(raw, { allowMailto: false });
}
function downloadAttachmentUrl(value) {
const safe = safeAttachmentUrl(value);
try {
const url = new URL(safe, location.origin);
if (/^\/f\/[^/]+\/[^/]+$/.test(url.pathname)) url.searchParams.set("download", "1");
return url.href;
} catch {
return safe;
}
}
function createVideoInsertDialog() {
const dialog = document.createElement("dialog");
dialog.className = "app-dialog video-insert-dialog";
@@ -102,7 +113,7 @@ function createVideoInsertDialog() {
return dialog;
}
export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxSize = () => 0, canDelete, canUpload, canEdit = () => true, toast, onFilesChanged = () => { } }) {
export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxSize = () => 0, canDelete, canUpload, canEdit = () => true, getDeleteRestrictionMessage = () => "", toast, onFilesChanged = () => { } }) {
const dialog = document.querySelector("#files-dialog");
const list = document.querySelector("#files-list");
const input = document.querySelector("#file-input");
@@ -163,7 +174,11 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
const files = await api(endpoints.list, { method: "PUT", body: JSON.stringify({ access_token: getAccessToken() || null }) });
const totalSize = files.reduce((sum, file) => sum + (Number(file.size_bytes) || 0), 0);
footer.textContent = `${files.length} ${files.length === 1 ? "file" : "files"} · ${formatBytes(totalSize)}`;
list.innerHTML = files.length ? files.map(file => `
const restrictionMessage = getDeleteRestrictionMessage();
const restrictionNotice = restrictionMessage
? `<p class="file-delete-notice">${escapeHtml(restrictionMessage)}</p>`
: "";
const fileRows = files.length ? files.map(file => `
<div class="file-row" data-file-row="${file.id}">
<div class="file-row-main">
<div class="file-name">${escapeHtml(file.filename)}</div>
@@ -172,6 +187,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
<div class="file-actions">${fileActionButtons(file)}</div>
<div class="file-code" hidden><textarea readonly aria-label="Generated file code"></textarea><button class="action-button action-button--primary compact-button" data-copy-generated>Copy</button></div>
</div>`).join("") : '<p class="dialog-copy">No files uploaded.</p>';
list.innerHTML = restrictionNotice + fileRows;
onFilesChanged(files);
if (open && !dialog.open) dialog.showModal();
} catch (error) {
@@ -314,7 +330,9 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
const output = panel.querySelector("textarea");
const safeUrl = safeAttachmentUrl(showButton.dataset.url);
const absolute = new URL(safeUrl, location.origin).href;
let text = absolute;
let text = showButton.dataset.showFileCode === "link" && isVideo(showButton.dataset.mime)
? downloadAttachmentUrl(showButton.dataset.url)
: absolute;
if (showButton.dataset.showFileCode === "alias") {
text = aliasCode(showButton.dataset.name, showButton.dataset.name, showButton.dataset.mime);
} else if (showButton.dataset.showFileCode === "markdown") {
+6 -2
View File
@@ -165,9 +165,13 @@ function setNotesView(view) {
});
}
function deleteButton(note, inline = false) {
const disabled = note.protected;
const disabled = !note.can_delete;
const classes = `note-delete-button${inline ? " note-delete-button--inline" : ""}`;
const reason = disabled ? "Protected notes cannot be deleted" : `Delete ${note.title}`;
const reason = !disabled
? `Delete ${note.title}`
: note.protected
? "Protected note: only its owner can delete it"
: "Read-write access is required to delete this note";
return `<button class="${classes}" data-delete-note="${escapeHtml(note.slug)}" data-note-title="${escapeHtml(note.title)}" ${disabled ? "disabled" : ""} title="${escapeHtml(reason)}">Delete</button>`;
}
function renderNotes(notes = notesCache) {