feat: add profile language preferences and refine toast, dropdown and history UI

This commit is contained in:
Mateusz Gruszczyński
2026-09-04 23:48:09 +02:00
parent f036240d5d
commit bf13587a71
42 changed files with 3971 additions and 357 deletions
+17 -14
View File
@@ -7,6 +7,8 @@
* See LICENSE file in repository root for details.
*/
import { formatDateTime, formatNumber, tp } from "@rustpad/i18n";
import { api, uploadWithProgress } from "@rustpad/api";
import { copyText } from "@rustpad/clipboard";
import { prepareImageFile } from "@rustpad/image-upload";
@@ -20,14 +22,14 @@ function escapeHtml(value) {
function formatBytes(value) {
const bytes = Number(value) || 0;
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes < 1024) return `${formatNumber(bytes)} B`;
if (bytes < 1024 * 1024) return `${formatNumber(bytes / 1024, { maximumFractionDigits: 1 })} KB`;
return `${formatNumber(bytes / (1024 * 1024), { maximumFractionDigits: 1 })} MB`;
}
function formatDate(value) {
const date = new Date(value);
return Number.isNaN(date.getTime()) ? "" : date.toLocaleString("pl-PL");
return Number.isNaN(date.getTime()) ? "" : formatDateTime(date);
}
function isVideo(mimeType) {
@@ -114,6 +116,7 @@ function createVideoInsertDialog() {
}
export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxSize = () => 0, canDelete, canUpload, canEdit = () => true, getDeleteRestrictionMessage = () => "", toast, onFilesChanged = () => { } }) {
const notify = (type, message, options = {}) => toast(message, { ...options, type });
const dialog = document.querySelector("#files-dialog");
const list = document.querySelector("#files-list");
const input = document.querySelector("#file-input");
@@ -173,7 +176,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
try {
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)}`;
footer.textContent = tp("files.summary", files.length, { size: formatBytes(totalSize) }, `${files.length} ${files.length === 1 ? "file" : "files"} · ${formatBytes(totalSize)}`);
const restrictionMessage = getDeleteRestrictionMessage();
const restrictionNotice = restrictionMessage
? `<p class="file-delete-notice">${escapeHtml(restrictionMessage)}</p>`
@@ -191,7 +194,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
onFilesChanged(files);
if (open && !dialog.open) dialog.showModal();
} catch (error) {
if (open) toast(error.message);
if (open) notify("danger", error.message, { title: "Could not load files" });
}
}
@@ -258,7 +261,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
function requestUpload() {
if (!canUpload() || !canEdit()) {
toast("You need read-write access and upload permission to upload files.");
notify("warning", "Read-write access and file uploads are required to upload files.", { title: "Upload unavailable" });
return;
}
input.click();
@@ -276,7 +279,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
try {
file = await prepareImageFile(file);
} catch (error) {
toast(error.message);
notify("danger", error.message, { title: "Could not prepare image" });
return;
}
if (!file) return;
@@ -293,7 +296,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
if (!files.length) return;
event.preventDefault();
if (!canUpload() || !canEdit()) {
toast("You need read-write access and upload permission to paste files.");
notify("warning", "Read-write access and file uploads are required to paste files.", { title: "Paste upload unavailable" });
return;
}
@@ -333,7 +336,7 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
const text = aliasCode(addButton.dataset.name, addButton.dataset.name, addButton.dataset.mime, mode);
if (mode === "player") insertVideoPlayer(text);
else insertAttachmentText(text);
toast("Added to note");
notify("success", "The file reference was inserted into the note.", { title: "Added to note" });
return;
}
@@ -366,9 +369,9 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
if (copyButton) {
try {
await copyText(copyButton.closest(".file-code").querySelector("textarea").value);
toast("Copied");
notify("success", "Generated file code copied to the clipboard.", { title: "Copied" });
} catch (error) {
toast(error.message);
notify("danger", error.message, { title: "Could not copy file code" });
}
return;
}
@@ -378,10 +381,10 @@ export function bindNoteFiles({ editor, endpoints, getAccessToken, getUploadMaxS
if (!await askConfirm(`Delete file "${deleteButton.dataset.fileName}" permanently?`, { title: "Delete file", confirmText: "Delete", danger: true })) return;
try {
await api(endpoints.remove(deleteButton.dataset.deleteFile), { method: "DELETE", headers: {}, body: JSON.stringify({ access_token: getAccessToken() || null }) });
toast("File deleted");
notify("success", "The file was permanently deleted.", { title: "File deleted" });
await loadFiles();
} catch (error) {
toast(error.message);
notify("danger", error.message, { title: "Could not delete file" });
}
});