mirror of
https://github.com/zadam/trilium.git
synced 2026-03-21 19:31:41 +01:00
Compare commits
46 Commits
autocomple
...
feature/vi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d63e2a00f | ||
|
|
5a16aa416f | ||
|
|
df2a53e010 | ||
|
|
4f08389f80 | ||
|
|
ff0fb4bcfd | ||
|
|
5f410faaa9 | ||
|
|
25bd9e8abd | ||
|
|
301a1b2288 | ||
|
|
00c4933344 | ||
|
|
cd9b46e1c7 | ||
|
|
b356b355ca | ||
|
|
8834899012 | ||
|
|
55dea474e9 | ||
|
|
bc74455a64 | ||
|
|
2d0b28367f | ||
|
|
7d8a3e2811 | ||
|
|
79e5d9595a | ||
|
|
0310626025 | ||
|
|
fefbb40c03 | ||
|
|
12f89078b8 | ||
|
|
8d873c5869 | ||
|
|
27f4ac1d03 | ||
|
|
d533360903 | ||
|
|
49f5dc1c26 | ||
|
|
16419ed4ac | ||
|
|
1595d1b5c9 | ||
|
|
50eb11997c | ||
|
|
d974dfbc31 | ||
|
|
e9b63e50d4 | ||
|
|
b6efb7c9ab | ||
|
|
f84479b1c2 | ||
|
|
8597fa560b | ||
|
|
b29ab93fd5 | ||
|
|
225cdaff46 | ||
|
|
61dfba8c32 | ||
|
|
4fee91d219 | ||
|
|
12e4f76a8b | ||
|
|
ec30598397 | ||
|
|
fdd2cc77e6 | ||
|
|
40ab2bc798 | ||
|
|
bc907ee6ad | ||
|
|
e9987b40e6 | ||
|
|
1990a990c3 | ||
|
|
d1159d3af9 | ||
|
|
2b94d96930 | ||
|
|
5b5222b846 |
@@ -35,14 +35,14 @@
|
||||
"@triliumnext/highlightjs": "workspace:*",
|
||||
"@triliumnext/share-theme": "workspace:*",
|
||||
"@triliumnext/split.js": "workspace:*",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.17.0",
|
||||
"@univerjs/preset-sheets-core": "0.17.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.17.0",
|
||||
"@univerjs/preset-sheets-filter": "0.17.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.17.0",
|
||||
"@univerjs/preset-sheets-note": "0.17.0",
|
||||
"@univerjs/preset-sheets-sort": "0.17.0",
|
||||
"@univerjs/presets": "0.17.0",
|
||||
"@univerjs/preset-sheets-conditional-formatting": "0.18.0",
|
||||
"@univerjs/preset-sheets-core": "0.18.0",
|
||||
"@univerjs/preset-sheets-data-validation": "0.18.0",
|
||||
"@univerjs/preset-sheets-filter": "0.18.0",
|
||||
"@univerjs/preset-sheets-find-replace": "0.18.0",
|
||||
"@univerjs/preset-sheets-note": "0.18.0",
|
||||
"@univerjs/preset-sheets-sort": "0.18.0",
|
||||
"@univerjs/presets": "0.18.0",
|
||||
"@zumer/snapdom": "2.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
"bootstrap": "5.3.8",
|
||||
@@ -58,8 +58,7 @@
|
||||
"jquery": "4.0.0",
|
||||
"jquery.fancytree": "2.38.5",
|
||||
"jsplumb": "2.15.6",
|
||||
"katex": "0.16.38",
|
||||
"knockout": "3.5.2",
|
||||
"katex": "0.16.39",
|
||||
"leaflet": "1.9.4",
|
||||
"leaflet-gpx": "2.2.0",
|
||||
"mark.js": "8.11.1",
|
||||
@@ -92,4 +91,4 @@
|
||||
"script-loader": "0.7.2",
|
||||
"vite-plugin-static-copy": "3.3.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import FAttachment from "../entities/fattachment.js";
|
||||
import FNote from "../entities/fnote.js";
|
||||
import imageContextMenuService from "../menus/image_context_menu.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import { renderReactWidget, renderReactWidgetAtElement } from "../widgets/react/react_utils";
|
||||
import renderText from "./content_renderer_text.js";
|
||||
import renderDoc from "./doc_renderer.js";
|
||||
import { loadElkIfNeeded, postprocessMermaidSvg } from "./mermaid.js";
|
||||
@@ -212,15 +213,16 @@ async function renderFile(entity: FNote | FAttachment, type: string, $renderedCo
|
||||
|
||||
$content.append($audioPreview);
|
||||
} else if (type === "video") {
|
||||
const $videoPreview = $("<video controls></video>")
|
||||
.attr("src", openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`))
|
||||
.attr("type", entity.mime)
|
||||
.css("width", "100%");
|
||||
const url = openService.getUrlForDownload(`api/${entityType}/${entityId}/open-partial`);
|
||||
const mime = entity.mime;
|
||||
|
||||
$content.append($videoPreview);
|
||||
const VideoPreviewContent = (await import("../widgets/type_widgets/file/Video")).VideoPreviewContent;
|
||||
const $viewer = renderReactWidget(null, h(VideoPreviewContent, { url, mime }));
|
||||
|
||||
$content.append($viewer);
|
||||
}
|
||||
|
||||
if (entityType === "notes" && "noteId" in entity) {
|
||||
if (entityType === "notes" && "noteId" in entity && type !== "video") {
|
||||
// TODO: we should make this available also for attachments, but there's a problem with "Open externally" support
|
||||
// in attachment list
|
||||
const $downloadButton = $(`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type LabelType = "text" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
|
||||
export type LabelType = "text" | "textarea" | "number" | "boolean" | "date" | "datetime" | "time" | "url" | "color";
|
||||
type Multiplicity = "single" | "multi";
|
||||
|
||||
export interface DefinitionObject {
|
||||
@@ -17,7 +17,7 @@ function parse(value: string) {
|
||||
for (const token of tokens) {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
|
||||
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url", "color"].includes(token)) {
|
||||
defObj.labelType = token as LabelType;
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token as Multiplicity;
|
||||
|
||||
@@ -1,68 +1,107 @@
|
||||
import "jquery";
|
||||
|
||||
import ko from "knockout";
|
||||
|
||||
import utils from "./services/utils.js";
|
||||
|
||||
// TriliumNextTODO: properly make use of below types
|
||||
// type SetupModelSetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
// type SetupModelStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop";
|
||||
type SetupStep = "sync-in-progress" | "setup-type" | "new-document-in-progress" | "sync-from-desktop" | "sync-from-server";
|
||||
type SetupType = "new-document" | "sync-from-desktop" | "sync-from-server" | "";
|
||||
|
||||
class SetupModel {
|
||||
syncInProgress: boolean;
|
||||
step: ko.Observable<string>;
|
||||
setupType: ko.Observable<string>;
|
||||
setupNewDocument: ko.Observable<boolean>;
|
||||
setupSyncFromDesktop: ko.Observable<boolean>;
|
||||
setupSyncFromServer: ko.Observable<boolean>;
|
||||
syncServerHost: ko.Observable<string | undefined>;
|
||||
syncProxy: ko.Observable<string | undefined>;
|
||||
password: ko.Observable<string | undefined>;
|
||||
class SetupController {
|
||||
private step: SetupStep;
|
||||
private setupType: SetupType = "";
|
||||
private syncPollIntervalId: number | null = null;
|
||||
private rootNode: HTMLElement;
|
||||
private setupTypeForm: HTMLFormElement;
|
||||
private syncFromServerForm: HTMLFormElement;
|
||||
private setupTypeNextButton: HTMLButtonElement;
|
||||
private setupTypeInputs: HTMLInputElement[];
|
||||
private syncServerHostInput: HTMLInputElement;
|
||||
private syncProxyInput: HTMLInputElement;
|
||||
private passwordInput: HTMLInputElement;
|
||||
private sections: Record<SetupStep, HTMLElement>;
|
||||
|
||||
constructor(syncInProgress: boolean) {
|
||||
this.syncInProgress = syncInProgress;
|
||||
this.step = ko.observable(syncInProgress ? "sync-in-progress" : "setup-type");
|
||||
this.setupType = ko.observable("");
|
||||
this.setupNewDocument = ko.observable(false);
|
||||
this.setupSyncFromDesktop = ko.observable(false);
|
||||
this.setupSyncFromServer = ko.observable(false);
|
||||
this.syncServerHost = ko.observable();
|
||||
this.syncProxy = ko.observable();
|
||||
this.password = ko.observable();
|
||||
constructor(rootNode: HTMLElement, syncInProgress: boolean) {
|
||||
this.rootNode = rootNode;
|
||||
this.step = syncInProgress ? "sync-in-progress" : "setup-type";
|
||||
this.setupTypeForm = mustGetElement("setup-type-form", HTMLFormElement);
|
||||
this.syncFromServerForm = mustGetElement("sync-from-server-form", HTMLFormElement);
|
||||
this.setupTypeNextButton = mustGetElement("setup-type-next", HTMLButtonElement);
|
||||
this.setupTypeInputs = Array.from(document.querySelectorAll<HTMLInputElement>("input[name='setup-type']"));
|
||||
this.syncServerHostInput = mustGetElement("sync-server-host", HTMLInputElement);
|
||||
this.syncProxyInput = mustGetElement("sync-proxy", HTMLInputElement);
|
||||
this.passwordInput = mustGetElement("password", HTMLInputElement);
|
||||
this.sections = {
|
||||
"setup-type": mustGetElement("setup-type-section", HTMLElement),
|
||||
"new-document-in-progress": mustGetElement("new-document-in-progress-section", HTMLElement),
|
||||
"sync-from-desktop": mustGetElement("sync-from-desktop-section", HTMLElement),
|
||||
"sync-from-server": mustGetElement("sync-from-server-section", HTMLElement),
|
||||
"sync-in-progress": mustGetElement("sync-in-progress-section", HTMLElement)
|
||||
};
|
||||
}
|
||||
|
||||
if (this.syncInProgress) {
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
init() {
|
||||
this.setupTypeForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void this.selectSetupType();
|
||||
});
|
||||
|
||||
this.syncFromServerForm.addEventListener("submit", (event) => {
|
||||
event.preventDefault();
|
||||
void this.finish();
|
||||
});
|
||||
|
||||
for (const input of this.setupTypeInputs) {
|
||||
input.addEventListener("change", () => {
|
||||
this.setupType = input.value as SetupType;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
|
||||
for (const backButton of document.querySelectorAll<HTMLElement>("[data-action='back']")) {
|
||||
backButton.addEventListener("click", () => {
|
||||
this.back();
|
||||
});
|
||||
}
|
||||
|
||||
const serverAddress = `${location.protocol}//${location.host}`;
|
||||
$("#current-host").html(serverAddress);
|
||||
|
||||
if (this.step === "sync-in-progress") {
|
||||
this.startSyncPolling();
|
||||
}
|
||||
|
||||
this.render();
|
||||
this.rootNode.style.display = "";
|
||||
}
|
||||
|
||||
// this is called in setup.ejs
|
||||
setupTypeSelected() {
|
||||
return !!this.setupType();
|
||||
}
|
||||
private async selectSetupType() {
|
||||
if (this.setupType === "new-document") {
|
||||
this.setStep("new-document-in-progress");
|
||||
|
||||
selectSetupType() {
|
||||
if (this.setupType() === "new-document") {
|
||||
this.step("new-document-in-progress");
|
||||
await $.post("api/setup/new-document");
|
||||
window.location.replace("./setup");
|
||||
return;
|
||||
}
|
||||
|
||||
$.post("api/setup/new-document").then(() => {
|
||||
window.location.replace("./setup");
|
||||
});
|
||||
} else {
|
||||
this.step(this.setupType());
|
||||
if (this.setupType) {
|
||||
this.setStep(this.setupType);
|
||||
}
|
||||
}
|
||||
|
||||
back() {
|
||||
this.step("setup-type");
|
||||
this.setupType("");
|
||||
private back() {
|
||||
this.setStep("setup-type");
|
||||
this.setupType = "";
|
||||
|
||||
for (const input of this.setupTypeInputs) {
|
||||
input.checked = false;
|
||||
}
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
async finish() {
|
||||
const syncServerHost = this.syncServerHost();
|
||||
const syncProxy = this.syncProxy();
|
||||
const password = this.password();
|
||||
private async finish() {
|
||||
const syncServerHost = this.syncServerHostInput.value.trim();
|
||||
const syncProxy = this.syncProxyInput.value.trim();
|
||||
const password = this.passwordInput.value;
|
||||
|
||||
if (!syncServerHost) {
|
||||
showAlert("Trilium server address can't be empty");
|
||||
@@ -82,15 +121,38 @@ class SetupModel {
|
||||
});
|
||||
|
||||
if (resp.result === "success") {
|
||||
this.step("sync-in-progress");
|
||||
|
||||
setInterval(checkOutstandingSyncs, 1000);
|
||||
|
||||
hideAlert();
|
||||
this.setStep("sync-in-progress");
|
||||
this.startSyncPolling();
|
||||
} else {
|
||||
showAlert(`Sync setup failed: ${resp.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
private setStep(step: SetupStep) {
|
||||
this.step = step;
|
||||
this.render();
|
||||
}
|
||||
|
||||
private render() {
|
||||
for (const [step, section] of Object.entries(this.sections) as [SetupStep, HTMLElement][]) {
|
||||
section.style.display = step === this.step ? "" : "none";
|
||||
}
|
||||
|
||||
this.setupTypeNextButton.disabled = !this.setupType;
|
||||
}
|
||||
|
||||
private getSelectedSetupType(): SetupType {
|
||||
return (this.setupTypeInputs.find((input) => input.checked)?.value ?? "") as SetupType;
|
||||
}
|
||||
|
||||
private startSyncPolling() {
|
||||
if (this.syncPollIntervalId !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.syncPollIntervalId = window.setInterval(checkOutstandingSyncs, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
async function checkOutstandingSyncs() {
|
||||
@@ -124,9 +186,19 @@ function getSyncInProgress() {
|
||||
return !!parseInt(el.content);
|
||||
}
|
||||
|
||||
function mustGetElement<T extends typeof HTMLElement>(id: string, ctor: T): InstanceType<T> {
|
||||
const element = document.getElementById(id);
|
||||
|
||||
if (!element || !(element instanceof ctor)) {
|
||||
throw new Error(`Expected element #${id}`);
|
||||
}
|
||||
|
||||
return element as InstanceType<T>;
|
||||
}
|
||||
|
||||
addEventListener("DOMContentLoaded", (event) => {
|
||||
const rootNode = document.getElementById("setup-dialog");
|
||||
if (!rootNode) return;
|
||||
ko.applyBindings(new SetupModel(getSyncInProgress()), rootNode);
|
||||
$("#setup-dialog").show();
|
||||
if (!rootNode || !(rootNode instanceof HTMLElement)) return;
|
||||
|
||||
new SetupController(rootNode, getSyncInProgress()).init();
|
||||
});
|
||||
|
||||
@@ -675,10 +675,11 @@ li.dropdown-item a.dropdown-item-button:focus-visible {
|
||||
div.alert {
|
||||
margin-bottom: 8px;
|
||||
background: var(--alert-bar-background) !important;
|
||||
color: var(--main-text-color);
|
||||
border-radius: 8px;
|
||||
font-size: .85em;
|
||||
}
|
||||
|
||||
div.alert p + p {
|
||||
margin-block: 1em 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1069,7 +1069,6 @@
|
||||
"rename_note": "اعادة تسمية الملاحظة",
|
||||
"remove_relation": "حذف العلاقة",
|
||||
"default_new_note_title": "ملاحظة جديدة",
|
||||
"open_in_new_tab": "فتح في تبويب جديد",
|
||||
"enter_new_title": "ادخل عنوان ملاحظة جديدة:",
|
||||
"note_not_found": "الملاحظة {{noteId}} غير موجودة!",
|
||||
"cannot_match_transform": "تعذر مطابقة التحويل: {{transform}}"
|
||||
|
||||
@@ -1047,7 +1047,6 @@
|
||||
"unprotecting-title": "解除保护状态"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "在新标签页中打开",
|
||||
"remove_note": "删除笔记",
|
||||
"edit_title": "编辑标题",
|
||||
"rename_note": "重命名笔记",
|
||||
|
||||
@@ -1046,7 +1046,6 @@
|
||||
"unprotecting-title": "Ungeschützt-Status"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "In neuem Tab öffnen",
|
||||
"remove_note": "Notiz entfernen",
|
||||
"edit_title": "Titel bearbeiten",
|
||||
"rename_note": "Notiz umbenennen",
|
||||
|
||||
@@ -343,6 +343,7 @@
|
||||
"label_type_title": "Type of the label will help Trilium to choose suitable interface to enter the label value.",
|
||||
"label_type": "Type",
|
||||
"text": "Text",
|
||||
"textarea": "Multi-line Text",
|
||||
"number": "Number",
|
||||
"boolean": "Boolean",
|
||||
"date": "Date",
|
||||
@@ -1041,6 +1042,7 @@
|
||||
"pause": "Pause (Space)",
|
||||
"back-10s": "Back 10s (Left arrow key)",
|
||||
"forward-30s": "Forward 30s",
|
||||
"volume": "Volume",
|
||||
"mute": "Mute (M)",
|
||||
"unmute": "Unmute (M)",
|
||||
"playback-speed": "Playback speed",
|
||||
@@ -1053,7 +1055,8 @@
|
||||
"exit-fullscreen": "Exit fullscreen",
|
||||
"unsupported-format": "Media preview is not available for this file format:\n{{mime}}",
|
||||
"zoom-to-fit": "Zoom to fill",
|
||||
"zoom-reset": "Reset zoom to fill"
|
||||
"zoom-reset": "Reset zoom to fill",
|
||||
"more-options": "More options"
|
||||
},
|
||||
"protected_session": {
|
||||
"enter_password_instruction": "Showing protected note requires entering your password:",
|
||||
@@ -1068,7 +1071,6 @@
|
||||
"unprotecting-title": "Unprotecting status"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Open in new tab",
|
||||
"remove_note": "Remove note",
|
||||
"edit_title": "Edit title",
|
||||
"rename_note": "Rename note",
|
||||
|
||||
@@ -1051,7 +1051,6 @@
|
||||
"unprotecting-title": "Estado de desprotección"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir en nueva pestaña",
|
||||
"remove_note": "Quitar nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Cambiar nombre de nota",
|
||||
|
||||
@@ -1036,7 +1036,6 @@
|
||||
"unprotecting-title": "Statut de la non-protection"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Ouvrir dans un nouvel onglet",
|
||||
"remove_note": "Supprimer la note",
|
||||
"edit_title": "Modifier le titre",
|
||||
"rename_note": "Renommer la note",
|
||||
|
||||
@@ -1055,7 +1055,6 @@
|
||||
"unprotecting-title": "Stádas díchosanta"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Oscail i gcluaisín nua",
|
||||
"remove_note": "Bain nóta",
|
||||
"edit_title": "Cuir an teideal in eagar",
|
||||
"rename_note": "Athainmnigh an nóta",
|
||||
|
||||
@@ -1049,7 +1049,6 @@
|
||||
"unprotecting-title": "अन-प्रोटेक्ट स्टेटस"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "नए टैब में खोलें",
|
||||
"remove_note": "नोट हटाएं",
|
||||
"edit_title": "टाइटल एडिट करें",
|
||||
"rename_note": "नोट का नाम बदलें",
|
||||
|
||||
@@ -1424,7 +1424,6 @@
|
||||
"unprotecting-title": "Stato non protetto"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Apri in una nuova scheda",
|
||||
"remove_note": "Rimuovi nota",
|
||||
"edit_title": "Modifica titolo",
|
||||
"rename_note": "Rinomina nota",
|
||||
|
||||
@@ -1537,7 +1537,6 @@
|
||||
"url_placeholder": "http://web サイト..."
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "新しいタブで開く",
|
||||
"remove_note": "ノートを削除",
|
||||
"edit_title": "タイトルを編集",
|
||||
"rename_note": "ノート名を変更",
|
||||
|
||||
@@ -1275,7 +1275,6 @@
|
||||
"unprotecting-title": "Status zdejmowania ochrony"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Otwórz w nowej karcie",
|
||||
"remove_note": "Usuń notatkę",
|
||||
"edit_title": "Edytuj tytuł",
|
||||
"rename_note": "Zmień nazwę notatki",
|
||||
|
||||
@@ -1047,7 +1047,6 @@
|
||||
"unprotecting-title": "Estado da remoção de proteção"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir em nova guia",
|
||||
"remove_note": "Remover nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Renomear nota",
|
||||
|
||||
@@ -1111,7 +1111,6 @@
|
||||
"start_session_button": "Iniciar sessão protegida"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Abrir em nova aba",
|
||||
"remove_note": "Remover nota",
|
||||
"edit_title": "Editar título",
|
||||
"rename_note": "Renomear nota",
|
||||
|
||||
@@ -1054,7 +1054,6 @@
|
||||
"enter_title_of_new_note": "Introduceți titlul noii notițe",
|
||||
"note_already_in_diagram": "Notița „{{title}}” deja se află pe diagramă.",
|
||||
"note_not_found": "Notița „{{noteId}}” nu a putut fi găsită!",
|
||||
"open_in_new_tab": "Deschide într-un tab nou",
|
||||
"remove_note": "Șterge notița",
|
||||
"remove_relation": "Șterge relația",
|
||||
"rename_note": "Redenumește notița",
|
||||
|
||||
@@ -1625,7 +1625,6 @@
|
||||
"rename_note": "Переименовать заметку",
|
||||
"remove_relation": "Удалить отношение",
|
||||
"default_new_note_title": "новая заметка",
|
||||
"open_in_new_tab": "Открыть в новой вкладке",
|
||||
"confirm_remove_relation": "Вы уверены, что хотите удалить связь?",
|
||||
"enter_new_title": "Введите новое название заметки:",
|
||||
"note_not_found": "Заметка {{noteId}} не найдена!",
|
||||
|
||||
@@ -1046,7 +1046,6 @@
|
||||
"unprotecting-title": "解除保護狀態"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "在新分頁中打開",
|
||||
"remove_note": "刪除筆記",
|
||||
"edit_title": "編輯標題",
|
||||
"rename_note": "重新命名筆記",
|
||||
|
||||
@@ -1151,7 +1151,6 @@
|
||||
"unprotecting-title": "Статус зняття захисту"
|
||||
},
|
||||
"relation_map": {
|
||||
"open_in_new_tab": "Відкрити в новій вкладці",
|
||||
"remove_note": "Видалити нотатку",
|
||||
"edit_title": "Редагувати заголовок",
|
||||
"rename_note": "Перейменувати нотатку",
|
||||
|
||||
@@ -2,7 +2,7 @@ import "./PromotedAttributes.css";
|
||||
|
||||
import { UpdateAttributeResponse } from "@triliumnext/commons";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChild, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { ComponentChild, createElement, HTMLInputTypeAttribute, InputHTMLAttributes, MouseEventHandler, TargetedEvent, TargetedInputEvent } from "preact";
|
||||
import { Dispatch, StateUpdater, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
|
||||
import NoteContext from "../components/note_context";
|
||||
@@ -36,7 +36,7 @@ interface CellProps {
|
||||
setCellToFocus(cell: Cell): void;
|
||||
}
|
||||
|
||||
type OnChangeEventData = TargetedEvent<HTMLInputElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
|
||||
type OnChangeEventData = TargetedEvent<HTMLInputElement | HTMLTextAreaElement, Event> | InputEvent | JQuery.TriggeredEvent<HTMLInputElement, undefined, HTMLInputElement, HTMLInputElement>;
|
||||
type OnChangeListener = (e: OnChangeEventData) => void | Promise<void>;
|
||||
|
||||
export default function PromotedAttributes() {
|
||||
@@ -171,8 +171,9 @@ function PromotedAttributeCell(props: CellProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute> = {
|
||||
const LABEL_MAPPINGS: Record<LabelType, HTMLInputTypeAttribute | undefined> = {
|
||||
text: "text",
|
||||
textarea: undefined,
|
||||
number: "number",
|
||||
boolean: "checkbox",
|
||||
date: "date",
|
||||
@@ -226,20 +227,21 @@ function LabelInput(props: CellProps & { inputId: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const inputNode = <input
|
||||
className="form-control promoted-attribute-input"
|
||||
tabIndex={200 + definitionAttr.position}
|
||||
id={inputId}
|
||||
type={LABEL_MAPPINGS[definition.labelType ?? "text"]}
|
||||
value={valueDraft}
|
||||
checked={definition.labelType === "boolean" ? valueAttr.value === "true" : undefined}
|
||||
placeholder={t("promoted_attributes.unset-field-placeholder")}
|
||||
data-attribute-id={valueAttr.attributeId}
|
||||
data-attribute-type={valueAttr.type}
|
||||
data-attribute-name={valueAttr.name}
|
||||
onBlur={onChangeListener}
|
||||
{...extraInputProps}
|
||||
/>;
|
||||
|
||||
const inputNode = createElement(definition.labelType === "textarea" ? "textarea" : "input", {
|
||||
className: "form-control promoted-attribute-input",
|
||||
tabIndex: 200 + definitionAttr.position,
|
||||
id: inputId,
|
||||
type: LABEL_MAPPINGS[definition.labelType ?? "text"],
|
||||
value: valueDraft,
|
||||
checked: definition.labelType === "boolean" ? valueAttr.value === "true" : undefined,
|
||||
placeholder: t("promoted_attributes.unset-field-placeholder"),
|
||||
"data-attribute-id": valueAttr.attributeId,
|
||||
"data-attribute-type": valueAttr.type,
|
||||
"data-attribute-name": valueAttr.name,
|
||||
onBlur: onChangeListener,
|
||||
...extraInputProps
|
||||
});
|
||||
|
||||
if (definition.labelType === "boolean") {
|
||||
return <>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import server from "../../services/server.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import attributeAutocompleteService from "../../services/attribute_autocomplete.js";
|
||||
import type { Attribute } from "../../services/attribute_parser.js";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
|
||||
import froca from "../../services/froca.js";
|
||||
import { t } from "../../services/i18n.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import noteAutocompleteService from "../../services/note_autocomplete.js";
|
||||
import promotedAttributeDefinitionParser from "../../services/promoted_attribute_definition_parser.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
import server from "../../services/server.js";
|
||||
import shortcutService from "../../services/shortcuts.js";
|
||||
import SpacedUpdate from "../../services/spaced_update.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import shortcutService from "../../services/shortcuts.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type { Attribute } from "../../services/attribute_parser.js";
|
||||
import { focusSavedElement, saveFocusedElement } from "../../services/focus.js";
|
||||
import { isExperimentalFeatureEnabled } from "../../services/experimental_features.js";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
const TPL = /*html*/`
|
||||
<div class="attr-detail tn-tool-dialog">
|
||||
@@ -29,6 +29,7 @@ const TPL = /*html*/`
|
||||
max-height: 600px;
|
||||
overflow: auto;
|
||||
box-shadow: 10px 10px 93px -25px black;
|
||||
contain: none;
|
||||
}
|
||||
|
||||
.attr-help td {
|
||||
@@ -137,6 +138,7 @@ const TPL = /*html*/`
|
||||
<td>
|
||||
<select class="attr-input-label-type form-control">
|
||||
<option value="text">${t("attribute_detail.text")}</option>
|
||||
<option value="textarea">${t("attribute_detail.textarea")}</option>
|
||||
<option value="number">${t("attribute_detail.number")}</option>
|
||||
<option value="boolean">${t("attribute_detail.boolean")}</option>
|
||||
<option value="date">${t("attribute_detail.date")}</option>
|
||||
@@ -342,6 +344,7 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
private $relatedNotesList!: JQuery<HTMLElement>;
|
||||
private $relatedNotesMoreNotes!: JQuery<HTMLElement>;
|
||||
private $attrHelp!: JQuery<HTMLElement>;
|
||||
private $statusBar?: JQuery<HTMLElement>;
|
||||
|
||||
private relatedNotesSpacedUpdate!: SpacedUpdate;
|
||||
private attribute!: Attribute;
|
||||
@@ -576,17 +579,24 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
|
||||
if (isNewLayout) {
|
||||
if (!this.$statusBar) {
|
||||
this.$statusBar = $(document.body).find(".component.status-bar");
|
||||
}
|
||||
|
||||
const statusBarHeight = this.$statusBar.outerHeight() ?? 0;
|
||||
const maxHeight = document.body.clientHeight - statusBarHeight;
|
||||
this.$widget
|
||||
.css("left", offset.left + (typeof detPosition.left === "number" ? detPosition.left : 0))
|
||||
.css("top", "unset")
|
||||
.css("bottom", 70)
|
||||
.css("max-height", "80vh");
|
||||
.css("bottom", statusBarHeight ?? 0)
|
||||
.css("max-height", maxHeight);
|
||||
} else {
|
||||
this.$widget
|
||||
.css("left", detPosition.left)
|
||||
.css("right", detPosition.right)
|
||||
.css("top", y - offset.top + 70)
|
||||
.css("max-height", outerHeight + y > height - 50 ? height - y - 50 : 10000);
|
||||
}
|
||||
|
||||
if (focus === "name") {
|
||||
@@ -694,14 +704,14 @@ export default class AttributeDetailWidget extends NoteContextAwareWidget {
|
||||
return "label-definition";
|
||||
} else if (attribute.name.startsWith("relation:")) {
|
||||
return "relation-definition";
|
||||
} else {
|
||||
return "label";
|
||||
}
|
||||
return "label";
|
||||
|
||||
} else if (attribute.type === "relation") {
|
||||
return "relation";
|
||||
} else {
|
||||
this.$title.text("");
|
||||
}
|
||||
this.$title.text("");
|
||||
|
||||
}
|
||||
|
||||
updateAttributeInEditor() {
|
||||
|
||||
@@ -12,6 +12,11 @@
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
|
||||
body.mobile & {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.note-list-bottom-pager {
|
||||
@@ -269,8 +274,9 @@
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
body.mobile & {
|
||||
flex-basis: 150px;
|
||||
body.mobile &.mobile-full-width {
|
||||
grid-column-start: 1;
|
||||
grid-column-end: 3;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -364,23 +370,19 @@
|
||||
mask-repeat: no-repeat;
|
||||
mask-size: 100% 100%;
|
||||
}
|
||||
|
||||
|
||||
.ck-content p {
|
||||
margin-bottom: 0.5em;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.ck-content figure.image {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.ck-content .table {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
overflow-x: scroll;
|
||||
--scrollbar-thickness: 0;
|
||||
scrollbar-width: none;
|
||||
|
||||
|
||||
table {
|
||||
width: max-content;
|
||||
table-layout: auto;
|
||||
@@ -435,4 +437,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* #endregion */
|
||||
/* #endregion */
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import "./ListOrGridView.css";
|
||||
import { Card, CardFrame, CardSection } from "../../react/Card";
|
||||
|
||||
import { clsx } from "clsx";
|
||||
import { ComponentChildren, TargetedMouseEvent } from "preact";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
|
||||
import FNote from "../../../entities/fnote";
|
||||
import linkContextMenuService from "../../../menus/link_context_menu";
|
||||
import attribute_renderer from "../../../services/attribute_renderer";
|
||||
import content_renderer from "../../../services/content_renderer";
|
||||
import { t } from "../../../services/i18n";
|
||||
import link from "../../../services/link";
|
||||
import CollectionProperties from "../../note_bars/CollectionProperties";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import { Card, CardFrame, CardSection } from "../../react/Card";
|
||||
import { useImperativeSearchHighlighlighting, useNoteLabel, useNoteLabelBoolean, useNoteProperty } from "../../react/hooks";
|
||||
import Icon from "../../react/Icon";
|
||||
import NoteLink from "../../react/NoteLink";
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { Pager, usePagination, PaginationContext } from "../Pagination";
|
||||
import { Pager, PaginationContext,usePagination } from "../Pagination";
|
||||
import { filterChildNotes, useFilteredNoteIds } from "./utils";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import { clsx } from "clsx";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import linkContextMenuService from "../../../menus/link_context_menu";
|
||||
import { ComponentChildren, TargetedMouseEvent } from "preact";
|
||||
|
||||
const contentSizeObserver = new ResizeObserver(onContentResized);
|
||||
|
||||
@@ -53,13 +53,13 @@ export function GridView({ note, noteIds: unfilteredNoteIds, highlightedTokens }
|
||||
<div className={clsx("note-list-container use-tn-links", {"search-results": (noteType === "search")})}>
|
||||
{pageNotes?.map(childNote => (
|
||||
<GridNoteCard key={childNote.noteId}
|
||||
note={childNote}
|
||||
parentNote={note}
|
||||
highlightedTokens={highlightedTokens}
|
||||
includeArchived={includeArchived} />
|
||||
note={childNote}
|
||||
parentNote={note}
|
||||
highlightedTokens={highlightedTokens}
|
||||
includeArchived={includeArchived} />
|
||||
))}
|
||||
</div>
|
||||
</NoteList>
|
||||
</NoteList>;
|
||||
}
|
||||
|
||||
interface NoteListProps {
|
||||
@@ -82,13 +82,13 @@ function NoteList(props: NoteListProps) {
|
||||
|
||||
{props.noteIds.length > 0 && <div className="note-list-wrapper">
|
||||
{!hasCollectionProperties && <Pager {...props.pagination} />}
|
||||
|
||||
|
||||
{props.children}
|
||||
|
||||
<Pager className="note-list-bottom-pager" {...props.pagination} />
|
||||
</div>}
|
||||
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expandDepth, includeArchived }: {
|
||||
@@ -106,25 +106,25 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
// Reset expand state if switching to another note, or if user manually toggled expansion state.
|
||||
useEffect(() => setExpanded(currentLevel <= expandDepth), [ note, currentLevel, expandDepth ]);
|
||||
|
||||
let subSections: JSX.Element | undefined = undefined;
|
||||
let subSections: JSX.Element | undefined;
|
||||
if (isExpanded) {
|
||||
subSections = <>
|
||||
<CardSection className="note-content-preview">
|
||||
<NoteContent note={note}
|
||||
highlightedTokens={highlightedTokens}
|
||||
noChildrenList
|
||||
includeArchivedNotes={includeArchived} />
|
||||
highlightedTokens={highlightedTokens}
|
||||
noChildrenList
|
||||
includeArchivedNotes={includeArchived} />
|
||||
</CardSection>
|
||||
|
||||
<NoteChildren note={note}
|
||||
parentNote={parentNote}
|
||||
highlightedTokens={highlightedTokens}
|
||||
currentLevel={currentLevel}
|
||||
expandDepth={expandDepth}
|
||||
includeArchived={includeArchived} />
|
||||
</>
|
||||
parentNote={parentNote}
|
||||
highlightedTokens={highlightedTokens}
|
||||
currentLevel={currentLevel}
|
||||
expandDepth={expandDepth}
|
||||
includeArchived={includeArchived} />
|
||||
</>;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<CardSection
|
||||
className={clsx("nested-note-list-item", "no-tooltip-preview", note.getColorClass(), {
|
||||
@@ -137,14 +137,14 @@ function ListNoteCard({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
data-note-id={note.noteId}
|
||||
>
|
||||
<h5>
|
||||
<span className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
|
||||
onClick={() => setExpanded(!isExpanded)}/>
|
||||
<span className={`note-expander ${isExpanded ? "bx bx-chevron-down" : "bx bx-chevron-right"}`}
|
||||
onClick={() => setExpanded(!isExpanded)}/>
|
||||
<Icon className="note-icon" icon={note.getIcon()} />
|
||||
<NoteLink className="note-book-title"
|
||||
notePath={notePath}
|
||||
noPreview
|
||||
showNotePath={parentNote.type === "search"}
|
||||
highlightedTokens={highlightedTokens} />
|
||||
notePath={notePath}
|
||||
noPreview
|
||||
showNotePath={parentNote.type === "search"}
|
||||
highlightedTokens={highlightedTokens} />
|
||||
<NoteAttributes note={note} />
|
||||
<NoteMenuButton notePath={notePath} />
|
||||
</h5>
|
||||
@@ -164,27 +164,28 @@ function GridNoteCard(props: GridNoteCardProps) {
|
||||
|
||||
return (
|
||||
<CardFrame className={clsx("note-book-card", "no-tooltip-preview", "block-link", props.note.getColorClass(), {
|
||||
"archived": props.note.isArchived
|
||||
})}
|
||||
data-href={`#${notePath}`}
|
||||
data-note-id={props.note.noteId}
|
||||
onClick={(e) => link.goToLink(e)}
|
||||
"archived": props.note.isArchived,
|
||||
"mobile-full-width": props.note.type === "file"
|
||||
})}
|
||||
data-href={`#${notePath}`}
|
||||
data-note-id={props.note.noteId}
|
||||
onClick={(e) => link.goToLink(e)}
|
||||
>
|
||||
<h5 className={clsx("note-book-header")}>
|
||||
<Icon className="note-icon" icon={props.note.getIcon()} />
|
||||
<NoteLink className="note-book-title"
|
||||
notePath={notePath}
|
||||
noPreview
|
||||
showNotePath={props.parentNote.type === "search"}
|
||||
highlightedTokens={props.highlightedTokens}
|
||||
notePath={notePath}
|
||||
noPreview
|
||||
showNotePath={props.parentNote.type === "search"}
|
||||
highlightedTokens={props.highlightedTokens}
|
||||
/>
|
||||
{!props.note.isOptions() && <NoteMenuButton notePath={notePath} />}
|
||||
|
||||
|
||||
</h5>
|
||||
<NoteContent note={props.note}
|
||||
trim
|
||||
highlightedTokens={props.highlightedTokens}
|
||||
includeArchivedNotes={props.includeArchived}
|
||||
trim
|
||||
highlightedTokens={props.highlightedTokens}
|
||||
includeArchivedNotes={props.includeArchived}
|
||||
/>
|
||||
</CardFrame>
|
||||
);
|
||||
@@ -222,7 +223,7 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
|
||||
|
||||
return () => {
|
||||
contentSizeObserver.unobserve(contentElement);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -281,13 +282,13 @@ function NoteChildren({ note, parentNote, highlightedTokens, currentLevel, expan
|
||||
function NoteMenuButton(props: {notePath: string}) {
|
||||
const openMenu = useCallback((e: TargetedMouseEvent<HTMLElement>) => {
|
||||
linkContextMenuService.openContextMenu(props.notePath, e);
|
||||
e.stopPropagation()
|
||||
e.stopPropagation();
|
||||
}, [props.notePath]);
|
||||
|
||||
return <ActionButton className="note-book-item-menu"
|
||||
icon="bx bx-dots-vertical-rounded" text=""
|
||||
onClick={openMenu}
|
||||
/>
|
||||
icon="bx bx-dots-vertical-rounded" text=""
|
||||
onClick={openMenu}
|
||||
/>;
|
||||
}
|
||||
|
||||
function getNotePath(parentNote: FNote, childNote: FNote) {
|
||||
@@ -315,7 +316,7 @@ function useExpansionDepth(note: FNote) {
|
||||
|
||||
function onContentResized(entries: ResizeObserverEntry[], observer: ResizeObserver): void {
|
||||
for (const contentElement of entries) {
|
||||
const isOverflowing = ((contentElement.target.scrollHeight > contentElement.target.clientHeight))
|
||||
const isOverflowing = ((contentElement.target.scrollHeight > contentElement.target.clientHeight));
|
||||
contentElement.target.classList.toggle("note-book-content-overflowing", isOverflowing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ const labelTypeMappings: Record<ColumnType, Partial<ColumnDefinition>> = {
|
||||
text: {
|
||||
editor: "input"
|
||||
},
|
||||
textarea: {
|
||||
editor: "textarea",
|
||||
formatter: "textarea",
|
||||
editorParams: {
|
||||
shiftEnterSubmit: true
|
||||
}
|
||||
},
|
||||
boolean: {
|
||||
formatter: "tickCross",
|
||||
editor: "tickCross"
|
||||
|
||||
@@ -75,3 +75,9 @@
|
||||
font-size: 1.5em;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.tabulator .tabulator-editable {
|
||||
textarea {
|
||||
padding: 7px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
|
||||
import FAttribute from "../../entities/fattribute";
|
||||
@@ -74,7 +75,7 @@ export default function InheritedAttributesTab({ note, componentId, emptyListStr
|
||||
)}
|
||||
</div>
|
||||
|
||||
{attributeDetailWidgetEl}
|
||||
{createPortal(attributeDetailWidgetEl, document.body)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AttributeEditor as CKEditorAttributeEditor, MentionFeed, ModelElement, ModelNode, ModelPosition } from "@triliumnext/ckeditor5";
|
||||
import { AttributeType } from "@triliumnext/commons";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { MutableRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "preact/hooks";
|
||||
|
||||
import type { CommandData, FilteredCommandNames } from "../../../components/app_context";
|
||||
@@ -336,7 +337,8 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
let matchedAttr: Attribute | null = null;
|
||||
|
||||
for (const attr of parsedAttrs) {
|
||||
if (attr.startIndex && clickIndex > attr.startIndex && attr.endIndex && clickIndex <= attr.endIndex) {
|
||||
if (attr.startIndex !== undefined && clickIndex > attr.startIndex &&
|
||||
attr.endIndex !== undefined && clickIndex <= attr.endIndex) {
|
||||
matchedAttr = attr;
|
||||
break;
|
||||
}
|
||||
@@ -407,7 +409,7 @@ export default function AttributeEditor({ api, note, componentId, notePath, ntxI
|
||||
)}
|
||||
</div>}
|
||||
|
||||
{attributeDetailWidgetEl}
|
||||
{createPortal(attributeDetailWidgetEl, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,13 +50,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media-volume-row {
|
||||
.media-volume-dropdown-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25em;
|
||||
padding: 0.5em;
|
||||
|
||||
.volume-mute-btn {
|
||||
padding: 0.25em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.media-volume-slider {
|
||||
width: 80px;
|
||||
width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,30 +102,47 @@ export function VolumeControl({ mediaRef }: { mediaRef: RefObject<HTMLVideoEleme
|
||||
}
|
||||
};
|
||||
|
||||
const toggleMute = () => {
|
||||
const toggleMute = (e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
const media = mediaRef.current;
|
||||
if (!media) return;
|
||||
media.muted = !media.muted;
|
||||
setMuted(media.muted);
|
||||
};
|
||||
|
||||
const volumeIcon = muted || volume === 0
|
||||
? "bx bx-volume-mute"
|
||||
: volume < 0.5
|
||||
? "bx bx-volume-low"
|
||||
: "bx bx-volume-full";
|
||||
|
||||
return (
|
||||
<div class="media-volume-row">
|
||||
<ActionButton
|
||||
icon={muted || volume === 0 ? "bx bx-volume-mute" : volume < 0.5 ? "bx bx-volume-low" : "bx bx-volume-full"}
|
||||
text={muted ? t("media.unmute") : t("media.mute")}
|
||||
onClick={toggleMute}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
class="media-volume-slider"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={muted ? 0 : volume}
|
||||
onInput={onVolumeChange}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
iconAction
|
||||
hideToggleArrow
|
||||
buttonClassName="volume-dropdown"
|
||||
text={<Icon icon={volumeIcon} />}
|
||||
title={t("media.volume")}
|
||||
>
|
||||
<li class="media-volume-dropdown-content">
|
||||
<button
|
||||
class="dropdown-item volume-mute-btn"
|
||||
onClick={toggleMute}
|
||||
title={muted ? t("media.unmute") : t("media.mute")}
|
||||
>
|
||||
<Icon icon={volumeIcon} />
|
||||
</button>
|
||||
<input
|
||||
type="range"
|
||||
class="media-volume-slider"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.05}
|
||||
value={muted ? 0 : volume}
|
||||
onInput={onVolumeChange}
|
||||
/>
|
||||
</li>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
.note-detail-file > .video-preview-wrapper {
|
||||
.video-preview-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
background-color: black;
|
||||
background-color: black;
|
||||
|
||||
.video-preview {
|
||||
background-color: black;
|
||||
|
||||
@@ -7,19 +7,29 @@ import FNote from "../../../entities/fnote";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { getUrlForDownload } from "../../../services/open";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import Dropdown from "../../react/Dropdown";
|
||||
import { FormListHeader, FormListItem } from "../../react/FormList";
|
||||
import Icon from "../../react/Icon";
|
||||
import NoItems from "../../react/NoItems";
|
||||
import { LoopButton, PlaybackSpeed, PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
|
||||
import { PlayPauseButton, SeekBar, SkipButton, VolumeControl } from "./MediaPlayer";
|
||||
|
||||
const AUTO_HIDE_DELAY = 3000;
|
||||
|
||||
export default function VideoPreview({ note }: { note: FNote }) {
|
||||
return <VideoPreviewContent
|
||||
url={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
|
||||
mime={note.mime}
|
||||
/>;
|
||||
}
|
||||
|
||||
export function VideoPreviewContent({ url, mime }: { url: string, mime: string }) {
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const [playing, setPlaying] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const { visible: controlsVisible, onMouseMove, flash: flashControls } = useAutoHideControls(videoRef, playing);
|
||||
|
||||
useEffect(() => setError(false), [note.noteId]);
|
||||
useEffect(() => setError(false), [ url ]);
|
||||
const onError = useCallback(() => setError(true), []);
|
||||
|
||||
const togglePlayback = useCallback(() => {
|
||||
@@ -33,6 +43,7 @@ export default function VideoPreview({ note }: { note: FNote }) {
|
||||
}, []);
|
||||
|
||||
const onVideoClick = useCallback((e: MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if ((e.target as HTMLElement).closest(".media-preview-controls")) return;
|
||||
togglePlayback();
|
||||
}, [togglePlayback]);
|
||||
@@ -40,7 +51,7 @@ export default function VideoPreview({ note }: { note: FNote }) {
|
||||
const onKeyDown = useKeyboardShortcuts(videoRef, wrapperRef, togglePlayback, flashControls);
|
||||
|
||||
if (error) {
|
||||
return <NoItems icon="bx bx-video-off" text={t("media.unsupported-format", { mime: note.mime.replace("/", "-") })} />;
|
||||
return <NoItems icon="bx bx-video-off" text={t("media.unsupported-format", { mime: mime.replace("/", "-") })} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -48,8 +59,8 @@ export default function VideoPreview({ note }: { note: FNote }) {
|
||||
<video
|
||||
ref={videoRef}
|
||||
class="video-preview"
|
||||
src={getUrlForDownload(`api/notes/${note.noteId}/open-partial`)}
|
||||
datatype={note?.mime}
|
||||
src={url}
|
||||
datatype={mime}
|
||||
onPlay={() => setPlaying(true)}
|
||||
onPause={() => setPlaying(false)}
|
||||
onError={onError}
|
||||
@@ -59,19 +70,17 @@ export default function VideoPreview({ note }: { note: FNote }) {
|
||||
<SeekBar mediaRef={videoRef} />
|
||||
<div class="media-buttons-row">
|
||||
<div className="left">
|
||||
<PlaybackSpeed mediaRef={videoRef} />
|
||||
<RotateButton videoRef={videoRef} />
|
||||
<OverflowMenu videoRef={videoRef} />
|
||||
</div>
|
||||
<div className="center">
|
||||
<div className="spacer" />
|
||||
<SkipButton mediaRef={videoRef} seconds={-10} icon="bx bx-rewind" text={t("media.back-10s")} />
|
||||
<PlayPauseButton playing={playing} togglePlayback={togglePlayback} />
|
||||
<SkipButton mediaRef={videoRef} seconds={30} icon="bx bx-fast-forward" text={t("media.forward-30s")} />
|
||||
<LoopButton mediaRef={videoRef} />
|
||||
<div className="spacer" />
|
||||
</div>
|
||||
<div className="right">
|
||||
<VolumeControl mediaRef={videoRef} />
|
||||
<ZoomToFitButton videoRef={videoRef} />
|
||||
<PictureInPictureButton videoRef={videoRef} />
|
||||
<FullscreenButton targetRef={wrapperRef} />
|
||||
</div>
|
||||
@@ -171,8 +180,49 @@ function useAutoHideControls(videoRef: RefObject<HTMLVideoElement>, playing: boo
|
||||
return { visible, onMouseMove, flash: onMouseMove };
|
||||
}
|
||||
|
||||
function RotateButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
|
||||
const PLAYBACK_SPEEDS = [0.5, 1, 1.25, 1.5, 2];
|
||||
|
||||
function OverflowMenu({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
|
||||
const [speed, setSpeed] = useState(() => videoRef.current?.playbackRate ?? 1);
|
||||
const [loop, setLoop] = useState(() => videoRef.current?.loop ?? false);
|
||||
const [rotation, setRotation] = useState(0);
|
||||
const [fitted, setFitted] = useState(false);
|
||||
|
||||
// Sync playback rate
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
|
||||
setSpeed(video.playbackRate);
|
||||
const onRateChange = () => setSpeed(video.playbackRate);
|
||||
video.addEventListener("ratechange", onRateChange);
|
||||
return () => video.removeEventListener("ratechange", onRateChange);
|
||||
}, [videoRef]);
|
||||
|
||||
// Sync loop state
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
setLoop(video.loop);
|
||||
|
||||
const observer = new MutationObserver(() => setLoop(video.loop));
|
||||
observer.observe(video, { attributes: true, attributeFilter: ["loop"] });
|
||||
return () => observer.disconnect();
|
||||
}, [videoRef]);
|
||||
|
||||
const selectSpeed = (rate: number) => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.playbackRate = rate;
|
||||
setSpeed(rate);
|
||||
};
|
||||
|
||||
const toggleLoop = () => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
video.loop = !video.loop;
|
||||
setLoop(video.loop);
|
||||
};
|
||||
|
||||
const rotate = () => {
|
||||
const video = videoRef.current;
|
||||
@@ -182,7 +232,6 @@ function RotateButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
|
||||
|
||||
const isSideways = next === 90 || next === 270;
|
||||
if (isSideways) {
|
||||
// Scale down so the rotated video fits within its container.
|
||||
const container = video.parentElement;
|
||||
if (container) {
|
||||
const ratio = container.clientWidth / container.clientHeight;
|
||||
@@ -195,19 +244,7 @@ function RotateButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
icon="bx bx-rotate-right"
|
||||
text={t("media.rotate")}
|
||||
onClick={rotate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function ZoomToFitButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }) {
|
||||
const [fitted, setFitted] = useState(false);
|
||||
|
||||
const toggle = () => {
|
||||
const toggleFit = () => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const next = !fitted;
|
||||
@@ -216,12 +253,50 @@ function ZoomToFitButton({ videoRef }: { videoRef: RefObject<HTMLVideoElement> }
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className={fitted ? "active" : ""}
|
||||
icon={fitted ? "bx bx-collapse" : "bx bx-expand"}
|
||||
text={fitted ? t("media.zoom-reset") : t("media.zoom-to-fit")}
|
||||
onClick={toggle}
|
||||
/>
|
||||
<Dropdown
|
||||
iconAction
|
||||
hideToggleArrow
|
||||
noSelectButtonStyle
|
||||
noDropdownListStyle
|
||||
mobileBackdrop
|
||||
buttonClassName="overflow-menu-dropdown"
|
||||
dropdownContainerClassName="mobile-bottom-menu"
|
||||
text={<Icon icon="bx bx-dots-horizontal-rounded" />}
|
||||
title={t("media.more-options")}
|
||||
>
|
||||
<FormListHeader text={t("media.playback-speed")} />
|
||||
{PLAYBACK_SPEEDS.map((rate) => (
|
||||
<FormListItem
|
||||
key={rate}
|
||||
icon={rate === speed ? "bx bx-check" : "bx bx-empty"}
|
||||
active={rate === speed}
|
||||
onClick={() => selectSpeed(rate)}
|
||||
>
|
||||
{rate}x
|
||||
</FormListItem>
|
||||
))}
|
||||
<li class="dropdown-divider" />
|
||||
<FormListItem
|
||||
icon="bx bx-rotate-right"
|
||||
onClick={rotate}
|
||||
>
|
||||
{t("media.rotate")}
|
||||
</FormListItem>
|
||||
<FormListItem
|
||||
icon={loop ? "bx bx-check" : "bx bx-repeat"}
|
||||
active={loop}
|
||||
onClick={toggleLoop}
|
||||
>
|
||||
{loop ? t("media.disable-loop") : t("media.loop")}
|
||||
</FormListItem>
|
||||
<FormListItem
|
||||
icon={fitted ? "bx bx-collapse" : "bx bx-expand"}
|
||||
active={fitted}
|
||||
onClick={toggleFit}
|
||||
>
|
||||
{fitted ? t("media.zoom-reset") : t("media.zoom-to-fit")}
|
||||
</FormListItem>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { Connection } from "jsplumb";
|
||||
import { RefObject } from "preact";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import contextMenu from "../../../menus/context_menu";
|
||||
import link_context_menu from "../../../menus/link_context_menu";
|
||||
import dialog from "../../../services/dialog";
|
||||
import { t } from "../../../services/i18n";
|
||||
import server from "../../../services/server";
|
||||
import RelationMapApi from "./api";
|
||||
import { Connection } from "jsplumb";
|
||||
|
||||
export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapApiRef: RefObject<RelationMapApi>) {
|
||||
return (e: MouseEvent) => {
|
||||
@@ -17,22 +19,8 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [
|
||||
{
|
||||
title: t("relation_map.open_in_new_tab"),
|
||||
uiIcon: "bx bx-empty",
|
||||
handler: () => appContext.tabManager.openTabWithNoteWithHoisting(note.noteId)
|
||||
},
|
||||
{
|
||||
title: t("relation_map.remove_note"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: async () => {
|
||||
if (!note) return;
|
||||
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
|
||||
if (typeof result !== "object" || !result.confirmed) return;
|
||||
|
||||
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
|
||||
}
|
||||
},
|
||||
...link_context_menu.getItems(e),
|
||||
{ kind: "separator" },
|
||||
{
|
||||
title: t("relation_map.edit_title"),
|
||||
uiIcon: "bx bx-pencil",
|
||||
@@ -49,10 +37,26 @@ export function buildNoteContextMenuHandler(note: FNote | null | undefined, mapA
|
||||
|
||||
await server.put(`notes/${note.noteId}/title`, { title });
|
||||
}
|
||||
}
|
||||
},
|
||||
{ kind: "separator" },
|
||||
|
||||
{
|
||||
title: t("relation_map.remove_note"),
|
||||
uiIcon: "bx bx-trash",
|
||||
handler: async () => {
|
||||
if (!note) return;
|
||||
const result = await dialog.confirmDeleteNoteBoxWithNote(note.title);
|
||||
if (typeof result !== "object" || !result.confirmed) return;
|
||||
|
||||
mapApiRef.current?.removeItem(note.noteId, result.isDeleteNoteChecked);
|
||||
}
|
||||
},
|
||||
],
|
||||
selectMenuItemHandler() {}
|
||||
})
|
||||
selectMenuItemHandler({ command }) {
|
||||
// Pass the events to the link context menu
|
||||
link_context_menu.handleLinkContextMenuItem(command, e, note.noteId);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"main": "./src/main.ts",
|
||||
"scripts": {
|
||||
"dev": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"dev-alt": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_DATA_DIR=data2 TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"start-no-dir": "cross-env NODE_ENV=development TRILIUM_ENV=dev TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"edit-integration-db": "cross-env NODE_ENV=development TRILIUM_PORT=8086 TRILIUM_ENV=dev TRILIUM_DATA_DIR=spec/db TRILIUM_INTEGRATION_TEST=edit TRILIUM_RESOURCE_DIR=src tsx watch --ignore '../client/node_modules/.vite-temp' ./src/main.ts",
|
||||
"build": "tsx scripts/build.ts",
|
||||
@@ -125,7 +126,7 @@
|
||||
"tmp": "0.2.5",
|
||||
"turnish": "1.8.0",
|
||||
"unescape": "1.0.1",
|
||||
"vite": "8.0.0",
|
||||
"vite": "8.0.1",
|
||||
"ws": "8.19.0",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.1"
|
||||
|
||||
@@ -58,35 +58,35 @@
|
||||
<div class="alert alert-warning" id="alert" style="display: none;">
|
||||
</div>
|
||||
|
||||
<div id="setup-type" data-bind="visible: step() == 'setup-type'" style="margin-top: 20px;">
|
||||
<form data-bind="submit: selectSetupType">
|
||||
<div id="setup-type-section" style="margin-top: 20px;">
|
||||
<form id="setup-type-form">
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="new-document" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="new-document">
|
||||
<%= t("setup.new-document") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="sync-from-desktop" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="sync-from-desktop">
|
||||
<%= t("setup.sync-from-desktop") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="radio" style="margin-bottom: 15px;">
|
||||
<label class="tn-radio">
|
||||
<input type="radio" name="setup-type" value="sync-from-server" data-bind="checked: setupType">
|
||||
<input type="radio" name="setup-type" value="sync-from-server">
|
||||
<%= t("setup.sync-from-server") %>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" data-bind="disable: !setupTypeSelected()" class="btn btn-primary"><%= t("setup.next") %></button>
|
||||
<button type="submit" id="setup-type-next" class="btn btn-primary" disabled><%= t("setup.next") %></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'new-document-in-progress'">
|
||||
<div id="new-document-in-progress-section">
|
||||
<h2><%= t("setup.init-in-progress") %></h2>
|
||||
|
||||
<div style="display: flex; justify-content: flex-start; margin-top: 20px;">
|
||||
@@ -103,7 +103,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-desktop'">
|
||||
<div id="sync-from-desktop-section">
|
||||
<h2><%= t("setup_sync-from-desktop.heading") %></h2>
|
||||
|
||||
<p><%= t("setup_sync-from-desktop.description") %></p>
|
||||
@@ -117,11 +117,11 @@
|
||||
<li><%- t("setup_sync-from-desktop.step6", { link: `<a href="/">${t("setup_sync-from-desktop.step6-here")}</a>` }) %></li>
|
||||
</ol>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-secondary">Back</button>
|
||||
<button type="button" data-action="back" class="btn btn-secondary">Back</button>
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-from-server'">
|
||||
<form data-bind="submit: finish">
|
||||
<div id="sync-from-server-section">
|
||||
<form id="sync-from-server-form">
|
||||
|
||||
<h2><%= t("setup_sync-from-server.heading") %></h2>
|
||||
|
||||
@@ -129,27 +129,27 @@
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sync-server-host"><%= t("setup_sync-from-server.server-host") %></label>
|
||||
<input type="text" id="syncServerHost" class="form-control" data-bind="value: syncServerHost" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
|
||||
<input type="text" id="sync-server-host" class="form-control" placeholder="<%= t("setup_sync-from-server.server-host-placeholder") %>">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="sync-proxy"><%= t("setup_sync-from-server.proxy-server") %></label>
|
||||
<input type="text" id="sync-proxy" class="form-control" data-bind="value: syncProxy" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
|
||||
<input type="text" id="sync-proxy" class="form-control" placeholder="<%= t("setup_sync-from-server.proxy-server-placeholder") %>">
|
||||
|
||||
<p><strong><%= t("setup_sync-from-server.note") %></strong> <%= t("setup_sync-from-server.proxy-instruction") %></p>
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 8px;">
|
||||
<label for="password"><%= t("setup_sync-from-server.password") %></label>
|
||||
<input type="password" id="password" class="form-control" data-bind="value: password" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
|
||||
<input type="password" id="password" class="form-control" placeholder="<%= t("setup_sync-from-server.password-placeholder") %>">
|
||||
</div>
|
||||
|
||||
<button type="button" data-bind="click: back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
|
||||
<button type="button" data-action="back" class="btn btn-secondary"><%= t("setup_sync-from-server.back") %></button>
|
||||
|
||||
<button type="submit" class="btn btn-primary"><%= t("setup_sync-from-server.finish-setup") %></button>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<div data-bind="visible: step() == 'sync-in-progress'">
|
||||
<div id="sync-in-progress-section">
|
||||
<h2><%= t("setup_sync-in-progress.heading") %></h2>
|
||||
|
||||
<div class="alert alert-success"><%= t("setup_sync-in-progress.successful") %></div>
|
||||
|
||||
@@ -7,7 +7,7 @@ function parse(value: string): DefinitionObject {
|
||||
for (const token of tokens) {
|
||||
if (token === "promoted") {
|
||||
defObj.isPromoted = true;
|
||||
} else if (["text", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
} else if (["text", "textarea", "number", "boolean", "date", "datetime", "time", "url"].includes(token)) {
|
||||
defObj.labelType = token;
|
||||
} else if (["single", "multi"].includes(token)) {
|
||||
defObj.multiplicity = token;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import safeCompare from "safe-compare";
|
||||
|
||||
import type { Request, Response, Router } from "express";
|
||||
import type { NextFunction, Request, Response, Router } from "express";
|
||||
|
||||
import shaca from "./shaca/shaca.js";
|
||||
import shacaLoader from "./shaca/shaca_loader.js";
|
||||
@@ -10,6 +10,16 @@ import type SNote from "./shaca/entities/snote.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import { getDefaultTemplatePath, renderNoteContent } from "./content_renderer.js";
|
||||
import utils from "../services/utils.js";
|
||||
import { isShareDbReady } from "./sql.js";
|
||||
|
||||
function assertShareDbReady(_req: Request, res: Response, next: NextFunction) {
|
||||
if (!isShareDbReady()) {
|
||||
res.status(503).send("The application is still initializing. Please try again in a moment.");
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
function addNoIndexHeader(note: SNote, res: Response) {
|
||||
if (note.isLabelTruthy("shareDisallowRobotIndexing")) {
|
||||
@@ -115,6 +125,8 @@ function render404(res: Response) {
|
||||
}
|
||||
|
||||
function register(router: Router) {
|
||||
// Guard: if the share DB is not yet initialized, return 503 for all /share routes.
|
||||
router.use("/share", assertShareDbReady);
|
||||
|
||||
function renderNote(note: SNote, req: Request, res: Response) {
|
||||
if (!note) {
|
||||
|
||||
@@ -5,12 +5,14 @@ import dataDir from "../services/data_dir.js";
|
||||
import sql_init from "../services/sql_init.js";
|
||||
|
||||
let dbConnection!: Database.Database;
|
||||
let dbConnectionReady = false;
|
||||
|
||||
sql_init.dbReady.then(() => {
|
||||
dbConnection = new Database(dataDir.DOCUMENT_PATH, {
|
||||
readonly: true,
|
||||
nativeBinding: process.env.BETTERSQLITE3_NATIVE_PATH || undefined
|
||||
});
|
||||
dbConnectionReady = true;
|
||||
|
||||
[`exit`, `SIGINT`, `SIGUSR1`, `SIGUSR2`, `SIGTERM`].forEach((eventType) => {
|
||||
process.on(eventType, () => {
|
||||
@@ -23,18 +25,31 @@ sql_init.dbReady.then(() => {
|
||||
});
|
||||
});
|
||||
|
||||
function assertDbReady(): void {
|
||||
if (!dbConnectionReady) {
|
||||
throw new Error("Share database connection is not yet ready. The application may still be initializing.");
|
||||
}
|
||||
}
|
||||
|
||||
function getRawRows<T>(query: string, params = []): T[] {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).raw().all(params) as T[];
|
||||
}
|
||||
|
||||
function getRow<T>(query: string, params: string[] = []): T {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).get(params) as T;
|
||||
}
|
||||
|
||||
function getColumn<T>(query: string, params: string[] = []): T[] {
|
||||
assertDbReady();
|
||||
return dbConnection.prepare(query).pluck().all(params) as T[];
|
||||
}
|
||||
|
||||
export function isShareDbReady(): boolean {
|
||||
return dbConnectionReady;
|
||||
}
|
||||
|
||||
export default {
|
||||
getRawRows,
|
||||
getRow,
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"eslint-config-preact": "2.0.0",
|
||||
"typescript": "5.9.3",
|
||||
"user-agent-data-types": "0.4.2",
|
||||
"vite": "8.0.0",
|
||||
"vite": "8.0.1",
|
||||
"vitest": "4.1.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"server:build": "pnpm run --filter server build",
|
||||
"server:coverage": "pnpm run --filter server test --coverage",
|
||||
"server:start": "pnpm run --filter server dev",
|
||||
"server:start-alt": "pnpm run --filter server dev-alt",
|
||||
"server:start-prod": "pnpm run --filter server start-prod",
|
||||
"desktop:start": "pnpm run --filter desktop dev",
|
||||
"desktop:build": "pnpm run --filter desktop build",
|
||||
@@ -61,7 +62,7 @@
|
||||
"eslint": "10.0.3",
|
||||
"eslint-config-preact": "2.0.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-plugin-playwright": "2.10.0",
|
||||
"eslint-plugin-playwright": "2.10.1",
|
||||
"eslint-plugin-simple-import-sort": "12.1.1",
|
||||
"happy-dom": "20.8.4",
|
||||
"http-server": "14.1.1",
|
||||
@@ -75,7 +76,7 @@
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.57.1",
|
||||
"upath": "2.0.1",
|
||||
"vite": "8.0.0",
|
||||
"vite": "8.0.1",
|
||||
"vite-plugin-dts": "4.5.4",
|
||||
"vitest": "4.1.0"
|
||||
},
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
|
||||
@@ -22,8 +22,9 @@
|
||||
flex-direction: column;
|
||||
padding: var(--ck-spacing-standard);
|
||||
box-sizing: border-box;
|
||||
max-width: 80vw;
|
||||
max-height: 80vh;
|
||||
min-width: 400px;
|
||||
max-width: 60vw;
|
||||
max-height: 350px;
|
||||
overflow: visible;
|
||||
user-select: text;
|
||||
}
|
||||
@@ -63,8 +64,8 @@
|
||||
border-radius: var(--ck-border-radius);
|
||||
background: var(--ck-color-input-background) !important;
|
||||
transition: border-color 120ms ease;
|
||||
overflow: visible !important;
|
||||
clip-path: none !important;
|
||||
overflow: auto;
|
||||
clip-path: none;
|
||||
}
|
||||
.ck.ck-math-input .ck-mathlive-container:focus-within {
|
||||
border-color: var(--ck-color-focus-border);
|
||||
@@ -159,16 +160,12 @@
|
||||
.ck.ck-math-preview {
|
||||
width: 100%;
|
||||
min-height: 40px;
|
||||
max-height: none !important;
|
||||
height: auto !important;
|
||||
padding: var(--ck-spacing-small);
|
||||
background: transparent !important;
|
||||
border: none !important;
|
||||
display: block;
|
||||
text-align: left;
|
||||
overflow-x: auto !important;
|
||||
overflow-y: visible !important;
|
||||
flex-shrink: 0;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* Center equation when in display mode */
|
||||
@@ -213,8 +210,7 @@
|
||||
.ck.ck-balloon-panel .ck-balloon-panel__content,
|
||||
.ck.ck-math-form,
|
||||
.ck-math-view,
|
||||
.ck.ck-math-input,
|
||||
.ck.ck-math-input .ck-mathlive-container {
|
||||
.ck.ck-math-input {
|
||||
overflow: visible !important;
|
||||
clip-path: none !important;
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"ckeditor5-metadata.json"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.0.0",
|
||||
"@ckeditor/ckeditor5-dev-build-tools": "55.2.0",
|
||||
"@ckeditor/ckeditor5-inspector": ">=4.1.0",
|
||||
"@ckeditor/ckeditor5-package-tools": "5.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.57.1",
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"ckeditor5-premium-features": "47.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@smithy/middleware-retry": "4.4.43",
|
||||
"@smithy/middleware-retry": "4.4.44",
|
||||
"@types/jquery": "4.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"fuse.js": "7.1.0",
|
||||
"katex": "0.16.38",
|
||||
"katex": "0.16.39",
|
||||
"mermaid": "11.13.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
2683
pnpm-lock.yaml
generated
2683
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user