mirror of
https://github.com/TriliumNext/Notes.git
synced 2025-07-27 18:12:29 +08:00
Merge remote-tracking branch 'origin/develop' into feature/task_list
This commit is contained in:
commit
a3fbf15902
63
package-lock.json
generated
63
package-lock.json
generated
@ -23,7 +23,7 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/swagger-ui-express": "4.1.7",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
@ -31,6 +31,7 @@
|
||||
"better-sqlite3": "11.8.1",
|
||||
"bootstrap": "5.3.3",
|
||||
"boxicons": "2.1.4",
|
||||
"chardet": "2.0.0",
|
||||
"cheerio": "1.0.0",
|
||||
"chokidar": "4.0.3",
|
||||
"cls-hooked": "4.2.2",
|
||||
@ -97,6 +98,7 @@
|
||||
"source-map-support": "0.5.21",
|
||||
"split.js": "1.6.5",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
"striptags": "3.2.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tmp": "0.2.3",
|
||||
@ -104,7 +106,7 @@
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4",
|
||||
"ws": "8.18.0",
|
||||
"ws": "8.18.1",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0"
|
||||
},
|
||||
@ -143,7 +145,7 @@
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.13.4",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
@ -170,7 +172,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.19.3",
|
||||
"typedoc": "0.27.7",
|
||||
"typedoc": "0.27.8",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.6",
|
||||
"webpack": "5.98.0",
|
||||
@ -4271,9 +4273,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
|
||||
"integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
|
||||
"version": "22.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
|
||||
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.20.0"
|
||||
@ -4432,9 +4434,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/swagger-ui-express": {
|
||||
"version": "4.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz",
|
||||
"integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==",
|
||||
"version": "4.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz",
|
||||
"integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/express": "*",
|
||||
"@types/serve-static": "*"
|
||||
@ -6175,6 +6178,12 @@
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chardet": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-2.0.0.tgz",
|
||||
"integrity": "sha512-xVgPpulCooDjY6zH4m9YW3jbkaBe3FKIAvF5sj5t7aBNsVl2ljIE+xwJ4iNgiDZHFQvNIpjdKdVOQvvk5ZfxbQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/check-error": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
|
||||
@ -11889,6 +11898,16 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/load-json-file/node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/loader-runner": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
|
||||
@ -15917,13 +15936,15 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strip-bom": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
|
||||
"integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
|
||||
"dev": true,
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-5.0.0.tgz",
|
||||
"integrity": "sha512-p+byADHF7SzEcVnLvc/r3uognM1hUhObuHXxJcgLCfD194XAkaLbjq3Wzb0N5G2tgIjH0dgT708Z51QxMeu60A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-eof": {
|
||||
@ -16840,9 +16861,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typedoc": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.7.tgz",
|
||||
"integrity": "sha512-K/JaUPX18+61W3VXek1cWC5gwmuLvYTOXJzBvD9W7jFvbPnefRnCHQCEPw7MSNrP/Hj7JJrhZtDDLKdcYm6ucg==",
|
||||
"version": "0.27.8",
|
||||
"resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.27.8.tgz",
|
||||
"integrity": "sha512-q0/2TUunNEDmWkn23ULKGXieK8cgGuAmBUXC/HcZ/rgzMI9Yr4Nq3in1K1vT1NZ9zx6M78yTk3kmIPbwJgK5KA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@ -18300,9 +18321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
|
18
package.json
18
package.json
@ -26,7 +26,6 @@
|
||||
"server:start-test": "npm run server:switch && rimraf ./data-test && cross-env TRILIUM_DATA_DIR=./data-test TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev TRILIUM_PORT=9999 nodemon src/main.ts",
|
||||
"server:qstart": "npm run server:switch && npm run server:start",
|
||||
"server:switch": "rimraf ./node_modules/better-sqlite3 && npm install",
|
||||
|
||||
"electron:start": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev electron ./electron-main.ts --inspect=5858 .",
|
||||
"electron:start-no-dir": "cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_ENV=dev electron --inspect=5858 .",
|
||||
"electron:start-nix": "electron-rebuild --version 33.3.1 && cross-env NODE_OPTIONS=\"--import tsx\" TRILIUM_DATA_DIR=./data TRILIUM_SYNC_SERVER_HOST=http://tsyncserver:4000 TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./electron-main.ts --inspect=5858 .\"",
|
||||
@ -37,30 +36,23 @@
|
||||
"electron:start-prod-nix-no-dir": "electron-rebuild --version 33.3.1 && npm run build:prepare-dist && cross-env TRILIUM_ENV=dev nix-shell -p electron_33 --run \"electron ./dist/electron-main.js --inspect=5858 .\"",
|
||||
"electron:qstart": "npm run electron:switch && npm run electron:start",
|
||||
"electron:switch": "electron-rebuild",
|
||||
|
||||
"electron-forge:start": "npm run build:prepare-dist && electron-forge start",
|
||||
"electron-forge:make": "npm run build:prepare-dist && electron-forge make",
|
||||
"electron-forge:package": "npm run build:prepare-dist && electron-forge package",
|
||||
|
||||
"docs:build-backend": "rimraf ./docs/backend_api && typedoc ./docs/backend_api src/becca/entities/*.ts src/services/backend_script_api.ts src/services/sql.ts",
|
||||
"docs:build-frontend": "rimraf ./docs/frontend_api && jsdoc -c jsdoc-conf.json -d ./docs/frontend_api src/public/app/entities/*.js src/public/app/services/frontend_script_api.js src/public/app/widgets/basic_widget.js src/public/app/widgets/note_context_aware_widget.js src/public/app/widgets/right_panel_widget.js",
|
||||
"docs:build": "npm run docs:build-backend && npm run docs:build-frontend",
|
||||
|
||||
"build:webpack": "tsx node_modules/webpack/bin/webpack.js -c webpack.config.ts",
|
||||
"build:prepare-dist": "npm run build:webpack && rimraf ./dist && tsc && tsx ./bin/copy-dist.ts",
|
||||
|
||||
"test": "cross-env TRILIUM_DATA_DIR=./integration-tests/db TRILIUM_INTEGRATION_TEST=memory vitest",
|
||||
"test:coverage": "cross-env TRILIUM_DATA_DIR=./integration-tests/db vitest --coverage",
|
||||
"test:playwright": "playwright test",
|
||||
|
||||
"test:integration-edit-db": "cross-env TRILIUM_INTEGRATION_TEST=edit TRILIUM_PORT=8081 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"test:integration-mem-db": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
"test:integration-mem-db-dev": "cross-env TRILIUM_INTEGRATION_TEST=memory TRILIUM_PORT=8082 TRILIUM_ENV=dev TRILIUM_DATA_DIR=./integration-tests/db nodemon src/main.ts",
|
||||
|
||||
"dev:watch-dist": "tsx ./bin/watch-dist.ts",
|
||||
"dev:prettier-check": "prettier . --check",
|
||||
"dev:prettier-fix": "prettier . --write",
|
||||
|
||||
"chore:update-build-info": "tsx bin/update-build-info.ts",
|
||||
"chore:ci-update-nightly-version": "tsx ./bin/update-nightly-version.ts",
|
||||
"chore:generate-document": "cross-env nodemon ./bin/generate_document.ts 1000",
|
||||
@ -81,7 +73,7 @@
|
||||
"@types/js-yaml": "4.0.9",
|
||||
"@types/leaflet": "1.9.16",
|
||||
"@types/react-dom": "18.3.5",
|
||||
"@types/swagger-ui-express": "4.1.7",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"archiver": "7.0.1",
|
||||
"async-mutex": "0.5.0",
|
||||
"autocomplete.js": "0.38.1",
|
||||
@ -89,6 +81,7 @@
|
||||
"better-sqlite3": "11.8.1",
|
||||
"bootstrap": "5.3.3",
|
||||
"boxicons": "2.1.4",
|
||||
"chardet": "2.0.0",
|
||||
"cheerio": "1.0.0",
|
||||
"chokidar": "4.0.3",
|
||||
"cls-hooked": "4.2.2",
|
||||
@ -155,6 +148,7 @@
|
||||
"source-map-support": "0.5.21",
|
||||
"split.js": "1.6.5",
|
||||
"stream-throttle": "0.1.3",
|
||||
"strip-bom": "5.0.0",
|
||||
"striptags": "3.2.0",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"tmp": "0.2.3",
|
||||
@ -162,7 +156,7 @@
|
||||
"turndown": "7.2.0",
|
||||
"unescape": "1.0.1",
|
||||
"vanilla-js-wheel-zoom": "9.0.4",
|
||||
"ws": "8.18.0",
|
||||
"ws": "8.18.1",
|
||||
"xml2js": "0.6.2",
|
||||
"yauzl": "3.2.0"
|
||||
},
|
||||
@ -198,7 +192,7 @@
|
||||
"@types/leaflet-gpx": "1.3.7",
|
||||
"@types/mime-types": "2.1.4",
|
||||
"@types/multer": "1.4.12",
|
||||
"@types/node": "22.13.4",
|
||||
"@types/node": "22.13.5",
|
||||
"@types/react": "18.3.18",
|
||||
"@types/safe-compare": "1.1.2",
|
||||
"@types/sanitize-html": "2.13.0",
|
||||
@ -225,7 +219,7 @@
|
||||
"swagger-jsdoc": "6.2.8",
|
||||
"tslib": "2.8.1",
|
||||
"tsx": "4.19.3",
|
||||
"typedoc": "0.27.7",
|
||||
"typedoc": "0.27.8",
|
||||
"typescript": "5.7.3",
|
||||
"vitest": "3.0.6",
|
||||
"webpack": "5.98.0",
|
||||
|
@ -40,6 +40,10 @@ export default class NoteListRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
get isFullHeight() {
|
||||
return this.viewMode?.isFullHeight;
|
||||
}
|
||||
|
||||
async renderList() {
|
||||
if (!this.viewMode) {
|
||||
return null;
|
||||
|
@ -30,6 +30,27 @@ function parseDate(str: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// Source: https://stackoverflow.com/a/30465299/4898894
|
||||
function getMonthsInDateRange(startDate: string, endDate: string) {
|
||||
const start = startDate.split('-');
|
||||
const end = endDate.split('-');
|
||||
const startYear = parseInt(start[0]);
|
||||
const endYear = parseInt(end[0]);
|
||||
const dates = [];
|
||||
|
||||
for (let i = startYear; i <= endYear; i++) {
|
||||
const endMonth = i != endYear ? 11 : parseInt(end[1]) - 1;
|
||||
const startMon = i === startYear ? parseInt(start[1])-1 : 0;
|
||||
|
||||
for(let j = startMon; j <= endMonth; j = j > 12 ? j % 12 || 11 : j+1) {
|
||||
const month = j+1;
|
||||
const displayMonth = month < 10 ? '0'+month : month;
|
||||
dates.push([i, displayMonth].join('-'));
|
||||
}
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
function padNum(num: number) {
|
||||
return `${num <= 9 ? "0" : ""}${num}`;
|
||||
}
|
||||
@ -621,6 +642,7 @@ export default {
|
||||
reloadFrontendApp,
|
||||
reloadTray,
|
||||
parseDate,
|
||||
getMonthsInDateRange,
|
||||
formatDateISO,
|
||||
formatDateTime,
|
||||
formatTimeInterval,
|
||||
|
@ -15,6 +15,11 @@ const TPL = `
|
||||
.note-list-widget .note-list {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.note-list-widget.full-height,
|
||||
.note-list-widget.full-height .note-list-widget-content {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="note-list-widget-content">
|
||||
@ -68,6 +73,7 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
|
||||
async renderNoteList(note: FNote) {
|
||||
const noteListRenderer = new NoteListRenderer(this.$content, note, note.getChildNoteIds());
|
||||
this.$widget.toggleClass("full-height", noteListRenderer.isFullHeight);
|
||||
await noteListRenderer.renderList();
|
||||
this.viewMode = noteListRenderer.viewMode;
|
||||
}
|
||||
@ -111,8 +117,10 @@ export default class NoteListWidget extends NoteContextAwareWidget {
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
|
||||
if (this.viewMode) {
|
||||
this.viewMode.entitiesReloadedEvents(e);
|
||||
// Inform the view mode of changes and refresh if needed.
|
||||
if (this.viewMode && this.viewMode.onEntitiesReloaded(e)) {
|
||||
this.refresh();
|
||||
this.checkRenderStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ const TPL = `
|
||||
flex-shrink: 0.4;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.promoted-attributes-container {
|
||||
margin: 0 1.5em;
|
||||
overflow: auto;
|
||||
@ -39,7 +39,7 @@ const TPL = `
|
||||
display: table-cell;
|
||||
padding: 1px 0;
|
||||
}
|
||||
|
||||
|
||||
.promoted-attribute-cell div.input-group {
|
||||
margin-left: 10px;
|
||||
display: flex;
|
||||
@ -54,9 +54,9 @@ const TPL = `
|
||||
flex-grow: 0;
|
||||
width: unset;
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
||||
|
||||
<div class="promoted-attributes-container"></div>
|
||||
</div>`;
|
||||
|
||||
@ -155,6 +155,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
.attr("data-attribute-type", valueAttr.type)
|
||||
.attr("data-attribute-name", valueAttr.name)
|
||||
.prop("value", valueAttr.value)
|
||||
.prop("placeholder", t("promoted_attributes.unset-field-placeholder"))
|
||||
.addClass("form-control")
|
||||
.addClass("promoted-attribute-input")
|
||||
.on("change", (event) => this.promotedAttributeChanged(event));
|
||||
@ -226,6 +227,9 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
} else if (definition.labelType === "boolean") {
|
||||
$input.prop("type", "checkbox");
|
||||
|
||||
$input.wrap($(`<label class="tn-checkbox"></label>`));
|
||||
$wrapper.find(".input-group").removeClass("input-group");
|
||||
|
||||
if (valueAttr.value === "true") {
|
||||
$input.prop("checked", "checked");
|
||||
}
|
||||
@ -272,7 +276,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
|
||||
if (definition.multiplicity === "multi") {
|
||||
const $addButton = $("<span>")
|
||||
.addClass("bx bx-plus pointer")
|
||||
.addClass("bx bx-plus pointer tn-tool-button")
|
||||
.prop("title", t("promoted_attributes.add_new_attribute"))
|
||||
.on("click", async () => {
|
||||
const $new = await this.createPromotedAttributeCell(
|
||||
@ -292,7 +296,7 @@ export default class PromotedAttributesWidget extends NoteContextAwareWidget {
|
||||
});
|
||||
|
||||
const $removeButton = $("<span>")
|
||||
.addClass("bx bx-trash pointer")
|
||||
.addClass("bx bx-trash pointer tn-tool-button")
|
||||
.prop("title", t("promoted_attributes.remove_this_attribute"))
|
||||
.on("click", async () => {
|
||||
const attributeId = $input.attr("data-attribute-id");
|
||||
|
@ -14,7 +14,8 @@ import CodeAutoReadOnlySizeOptions from "./options/code_notes/code_auto_read_onl
|
||||
import CodeMimeTypesOptions from "./options/code_notes/code_mime_types.js";
|
||||
import ImageOptions from "./options/images/images.js";
|
||||
import SpellcheckOptions from "./options/spellcheck.js";
|
||||
import PasswordOptions from "./options/password.js";
|
||||
import PasswordOptions from "./options/password/password.js";
|
||||
import ProtectedSessionTimeoutOptions from "./options/password/protected_session_timeout.js"
|
||||
import EtapiOptions from "./options/etapi.js";
|
||||
import BackupOptions from "./options/backup.js";
|
||||
import SyncOptions from "./options/sync.js";
|
||||
@ -35,6 +36,7 @@ import RibbonOptions from "./options/appearance/ribbon.js";
|
||||
import LocalizationOptions from "./options/appearance/i18n.js";
|
||||
import CodeBlockOptions from "./options/appearance/code_block.js";
|
||||
import EditorOptions from "./options/text_notes/editor.js";
|
||||
import ShareSettingsOptions from "./options/other/share_settings.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type NoteContextAwareWidget from "../note_context_aware_widget.js";
|
||||
|
||||
@ -64,7 +66,7 @@ const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
|
||||
_optionsCodeNotes: [VimKeyBindingsOptions, WrapLinesOptions, CodeAutoReadOnlySizeOptions, CodeMimeTypesOptions],
|
||||
_optionsImages: [ImageOptions],
|
||||
_optionsSpellcheck: [SpellcheckOptions],
|
||||
_optionsPassword: [PasswordOptions],
|
||||
_optionsPassword: [PasswordOptions, ProtectedSessionTimeoutOptions],
|
||||
_optionsEtapi: [EtapiOptions],
|
||||
_optionsBackup: [BackupOptions],
|
||||
_optionsSync: [SyncOptions],
|
||||
@ -76,14 +78,14 @@ const CONTENT_WIDGETS: Record<string, (typeof NoteContextAwareWidget)[]> = {
|
||||
RevisionsSnapshotIntervalOptions,
|
||||
RevisionSnapshotsLimitOptions,
|
||||
NetworkConnectionsOptions,
|
||||
HtmlImportTagsOptions
|
||||
HtmlImportTagsOptions,
|
||||
ShareSettingsOptions
|
||||
],
|
||||
_optionsAdvanced: [DatabaseIntegrityCheckOptions, DatabaseAnonymizationOptions, AdvancedSyncOptions, VacuumDatabaseOptions],
|
||||
_backendLog: [BackendLogWidget]
|
||||
};
|
||||
|
||||
export default class ContentWidgetTypeWidget extends TypeWidget {
|
||||
|
||||
private $content!: JQuery<HTMLElement>;
|
||||
|
||||
static getType() {
|
||||
|
@ -223,7 +223,9 @@ export default class EditableTextTypeWidget extends AbstractTextTypeWidget {
|
||||
}
|
||||
|
||||
$classicToolbarWidget.empty();
|
||||
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
|
||||
if ($classicToolbarWidget.length) {
|
||||
$classicToolbarWidget[0].appendChild(editor.ui.view.toolbar.element);
|
||||
}
|
||||
|
||||
if (utils.isMobile()) {
|
||||
$classicToolbarWidget.addClass("visible");
|
||||
|
@ -1,30 +1,31 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap } from "../../../../../../services/options_interface.js";
|
||||
import TimeSelector from "../time_selector.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("revisions_snapshot_interval.note_revisions_snapshot_interval_title")}</h4>
|
||||
|
||||
<p class="use-tn-links">${t("revisions_snapshot_interval.note_revisions_snapshot_description")}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label>${t("revisions_snapshot_interval.snapshot_time_interval_label")}</label>
|
||||
<input class="revision-snapshot-time-interval-in-seconds form-control options-number-input" type="number" min="10">
|
||||
</div>
|
||||
<div id="time-selector-placeholder"></div>
|
||||
</div>`;
|
||||
|
||||
export default class RevisionsSnapshotIntervalOptions extends OptionsWidget {
|
||||
export default class RevisionsSnapshotIntervalOptions extends TimeSelector {
|
||||
|
||||
private $revisionsTimeInterval!: JQuery<HTMLElement>;
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: "revision-snapshot-time-interval",
|
||||
widgetLabelId: "revisions_snapshot_interval.snapshot_time_interval_label",
|
||||
optionValueId: "revisionSnapshotTimeInterval",
|
||||
optionTimeScaleId: "revisionSnapshotTimeIntervalTimeScale",
|
||||
minimumSeconds: 10
|
||||
});
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
const $timeSelector = this.$widget;
|
||||
// inject TimeSelector widget template
|
||||
this.$widget = $(TPL);
|
||||
this.$revisionsTimeInterval = this.$widget.find(".revision-snapshot-time-interval-in-seconds");
|
||||
this.$revisionsTimeInterval.on("change", () => this.updateOption("revisionSnapshotTimeInterval", this.$revisionsTimeInterval.val()));
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
this.$revisionsTimeInterval.val(options.revisionSnapshotTimeInterval);
|
||||
this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,97 @@
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import options from "../../../../services/options.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import type { OptionMap, OptionNames } from "../../../../../../services/options_interface.js";
|
||||
import searchService from "../../../../services/search.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("share.title")}</h4>
|
||||
|
||||
<label class="tn-checkbox">
|
||||
<input class="form-check-input" type="checkbox" name="redirectBareDomain" value="true">
|
||||
${t("share.redirect_bare_domain")}
|
||||
</label>
|
||||
<p class="form-text">${t("share.redirect_bare_domain_description")}</p>
|
||||
|
||||
<label class="tn-checkbox">
|
||||
<input class="form-check-input" type="checkbox" name="showLoginInShareTheme" value="true">
|
||||
${t("share.show_login_link")}
|
||||
</label>
|
||||
<p class="form-text">${t("share.show_login_link_description")}</p>
|
||||
</div>`;
|
||||
|
||||
export default class ShareSettingsOptions extends OptionsWidget {
|
||||
private $shareRootCheck!: JQuery<HTMLElement>;
|
||||
private $shareRootStatus!: JQuery<HTMLElement>;
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.contentSized();
|
||||
|
||||
this.$shareRootCheck = this.$widget.find('.share-root-check');
|
||||
this.$shareRootStatus = this.$widget.find('.share-root-status');
|
||||
|
||||
// Add change handlers for both checkboxes
|
||||
this.$widget.find('input[type="checkbox"]').on("change", (e: JQuery.ChangeEvent) => {
|
||||
this.save();
|
||||
|
||||
// Show/hide share root status section based on redirectBareDomain checkbox
|
||||
const target = e.target as HTMLInputElement;
|
||||
if (target.name === 'redirectBareDomain') {
|
||||
this.$shareRootCheck.toggle(target.checked);
|
||||
if (target.checked) {
|
||||
this.checkShareRoot();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Add click handler for check share root button
|
||||
this.$widget.find('.check-share-root').on("click", () => this.checkShareRoot());
|
||||
}
|
||||
|
||||
async optionsLoaded(options: OptionMap) {
|
||||
const redirectBareDomain = options.redirectBareDomain === "true";
|
||||
this.$widget.find('input[name="redirectBareDomain"]').prop("checked", redirectBareDomain);
|
||||
this.$shareRootCheck.toggle(redirectBareDomain);
|
||||
if (redirectBareDomain) {
|
||||
await this.checkShareRoot();
|
||||
}
|
||||
|
||||
this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked", options.showLoginInShareTheme === "true");
|
||||
}
|
||||
|
||||
async checkShareRoot() {
|
||||
const $button = this.$widget.find('.check-share-root');
|
||||
$button.prop('disabled', true);
|
||||
|
||||
try {
|
||||
const shareRootNotes = await searchService.searchForNotes("#shareRoot");
|
||||
const sharedShareRootNote = shareRootNotes.find(note => note.isShared());
|
||||
|
||||
if (sharedShareRootNote) {
|
||||
this.$shareRootStatus
|
||||
.removeClass('text-danger')
|
||||
.addClass('text-success')
|
||||
.text(t("share.share_root_found", {noteTitle: sharedShareRootNote.title}));
|
||||
} else {
|
||||
this.$shareRootStatus
|
||||
.removeClass('text-success')
|
||||
.addClass('text-danger')
|
||||
.text(shareRootNotes.length > 0
|
||||
? t("share.share_root_not_shared", {noteTitle: shareRootNotes[0].title})
|
||||
: t("share.share_root_not_found"));
|
||||
}
|
||||
} finally {
|
||||
$button.prop('disabled', false);
|
||||
}
|
||||
}
|
||||
|
||||
async save() {
|
||||
const redirectBareDomain = this.$widget.find('input[name="redirectBareDomain"]').prop("checked");
|
||||
await this.updateOption<"redirectBareDomain">("redirectBareDomain", redirectBareDomain.toString());
|
||||
|
||||
const showLoginInShareTheme = this.$widget.find('input[name="showLoginInShareTheme"]').prop("checked");
|
||||
await this.updateOption<"showLoginInShareTheme">("showLoginInShareTheme", showLoginInShareTheme.toString());
|
||||
}
|
||||
}
|
@ -1,9 +1,9 @@
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import server from "../../../services/server.js";
|
||||
import protectedSessionHolder from "../../../services/protected_session_holder.js";
|
||||
import toastService from "../../../services/toast.js";
|
||||
import OptionsWidget from "./options_widget.js";
|
||||
import type { OptionMap } from "../../../../../services/options_interface.js";
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import server from "../../../../services/server.js";
|
||||
import protectedSessionHolder from "../../../../services/protected_session_holder.js";
|
||||
import toastService from "../../../../services/toast.js";
|
||||
import OptionsWidget from "../options_widget.js";
|
||||
import type { OptionMap } from "../../../../../../services/options_interface.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
@ -32,17 +32,7 @@ const TPL = `
|
||||
<button class="save-password-button btn btn-primary">${t("password.change_password")}</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="options-section">
|
||||
<h4>${t("password.protected_session_timeout")}</h4>
|
||||
|
||||
<p>${t("password.protected_session_timeout_description")} <a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html" class="external">${t("password.wiki")}</a> ${t("password.for_more_info")}</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="protected-session-timeout-in-seconds">${t("password.protected_session_timeout_label")}</label>
|
||||
<input id="protected-session-timeout-in-seconds" class="protected-session-timeout-in-seconds form-control options-number-input" type="number" min="60">
|
||||
</div>
|
||||
</div>`;
|
||||
`;
|
||||
|
||||
// TODO: Deduplicate
|
||||
interface ChangePasswordResponse {
|
@ -0,0 +1,30 @@
|
||||
import { t } from "../../../../services/i18n.js";
|
||||
import TimeSelector from "../time_selector.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="options-section">
|
||||
<h4>${t("password.protected_session_timeout")}</h4>
|
||||
|
||||
<p>${t("password.protected_session_timeout_description")} <a class="tn-link" href="https://triliumnext.github.io/Docs/Wiki/protected-notes.html" class="external">${t("password.wiki")}</a> ${t("password.for_more_info")}</p>
|
||||
<div id="time-selector-placeholder"></div>
|
||||
</div>`;
|
||||
|
||||
export default class ProtectedSessionTimeoutOptions extends TimeSelector {
|
||||
constructor() {
|
||||
super({
|
||||
widgetId: "protected-session-timeout",
|
||||
widgetLabelId: "password.protected_session_timeout_label",
|
||||
optionValueId: "protectedSessionTimeout",
|
||||
optionTimeScaleId: "protectedSessionTimeoutTimeScale",
|
||||
minimumSeconds: 60
|
||||
});
|
||||
super.doRender();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
const $timeSelector = this.$widget;
|
||||
// inject TimeSelector widget template
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.find("#time-selector-placeholder").replaceWith($timeSelector)
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventSourceInput, PluginDef } from "@fullcalendar/core";
|
||||
import type { Calendar, DateSelectArg, EventChangeArg, EventDropArg, EventInput, EventSourceFunc, EventSourceFuncArg, EventSourceInput, PluginDef } from "@fullcalendar/core";
|
||||
import froca from "../../services/froca.js";
|
||||
import ViewMode, { type ViewModeArgs } from "./view_mode.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
@ -9,6 +9,9 @@ import options from "../../services/options.js";
|
||||
import dialogService from "../../services/dialog.js";
|
||||
import attributes from "../../services/attributes.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import date_notes from "../../services/date_notes.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
|
||||
const TPL = `
|
||||
<div class="calendar-view">
|
||||
@ -66,6 +69,7 @@ export default class CalendarView extends ViewMode {
|
||||
private noteIds: string[];
|
||||
private parentNote: FNote;
|
||||
private calendar?: Calendar;
|
||||
private isCalendarRoot: boolean;
|
||||
|
||||
constructor(args: ViewModeArgs) {
|
||||
super(args);
|
||||
@ -74,31 +78,66 @@ export default class CalendarView extends ViewMode {
|
||||
this.$calendarContainer = this.$root.find(".calendar-container");
|
||||
this.noteIds = args.noteIds;
|
||||
this.parentNote = args.parentNote;
|
||||
console.log(args);
|
||||
this.isCalendarRoot = false;
|
||||
args.$parent.append(this.$root);
|
||||
}
|
||||
|
||||
get isFullHeight(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
async renderList(): Promise<JQuery<HTMLElement> | undefined> {
|
||||
const isEditable = true;
|
||||
this.isCalendarRoot = this.parentNote.hasLabel("calendarRoot") || this.parentNote.hasLabel("workspaceCalendarRoot");
|
||||
const isEditable = !this.isCalendarRoot;
|
||||
|
||||
const { Calendar } = await import("@fullcalendar/core");
|
||||
const plugins: PluginDef[] = [];
|
||||
plugins.push((await import("@fullcalendar/daygrid")).default);
|
||||
|
||||
if (isEditable) {
|
||||
if (isEditable || this.isCalendarRoot) {
|
||||
plugins.push((await import("@fullcalendar/interaction")).default);
|
||||
}
|
||||
|
||||
let eventBuilder: EventSourceFunc;
|
||||
if (!this.isCalendarRoot) {
|
||||
eventBuilder = async () => await this.#buildEvents(this.noteIds)
|
||||
} else {
|
||||
eventBuilder = async (e: EventSourceFuncArg) => await this.#buildEventsForCalendar(e);
|
||||
}
|
||||
|
||||
const calendar = new Calendar(this.$calendarContainer[0], {
|
||||
plugins,
|
||||
initialView: "dayGridMonth",
|
||||
events: async () => await CalendarView.#buildEvents(this.noteIds),
|
||||
events: eventBuilder,
|
||||
editable: isEditable,
|
||||
selectable: isEditable,
|
||||
select: (e) => this.#onCalendarSelection(e),
|
||||
eventChange: (e) => this.#onEventMoved(e),
|
||||
firstDay: options.getInt("firstDayOfWeek") ?? 0,
|
||||
locale: await CalendarView.#getLocale()
|
||||
weekends: !this.parentNote.hasAttribute("label", "calendar:hideWeekends"),
|
||||
weekNumbers: this.parentNote.hasAttribute("label", "calendar:weekNumbers"),
|
||||
locale: await CalendarView.#getLocale(),
|
||||
height: "100%",
|
||||
eventContent: (e => {
|
||||
let html = "";
|
||||
|
||||
const iconClass = e.event.extendedProps.iconClass;
|
||||
if (iconClass) {
|
||||
html += `<span class="${iconClass}"></span> `;
|
||||
}
|
||||
|
||||
html += utils.escapeHtml(e.event.title);
|
||||
return { html };
|
||||
}),
|
||||
dateClick: async (e) => {
|
||||
if (!this.isCalendarRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
const note = await date_notes.getDayNote(e.dateStr);
|
||||
if (note) {
|
||||
appContext.tabManager.getActiveContext().setNote(note.noteId);
|
||||
}
|
||||
}
|
||||
});
|
||||
calendar.render();
|
||||
this.calendar = calendar;
|
||||
@ -174,47 +213,113 @@ export default class CalendarView extends ViewMode {
|
||||
CalendarView.#setAttribute(note, "label", "endDate", endDate);
|
||||
}
|
||||
|
||||
entitiesReloadedEvents({ loadResults }: EventData<"entitiesReloaded">): void {
|
||||
onEntitiesReloaded({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// Refresh note IDs if they got changed.
|
||||
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId == this.parentNote.noteId)) {
|
||||
if (loadResults.getBranchRows().some((branch) => branch.parentNoteId === this.parentNote.noteId)) {
|
||||
this.noteIds = this.parentNote.getChildNoteIds();
|
||||
}
|
||||
|
||||
// Refresh calendar on attribute change.
|
||||
if (loadResults.getAttributeRows().some((attribute) => attribute.noteId === this.parentNote.noteId && attribute.name?.startsWith("calendar:"))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Refresh dataset on subnote change.
|
||||
if (this.calendar && loadResults.getAttributeRows().some((a) => this.noteIds.includes(a.noteId ?? ""))) {
|
||||
this.calendar.refetchEvents();
|
||||
}
|
||||
}
|
||||
|
||||
static async #buildEvents(noteIds: string[]) {
|
||||
async #buildEventsForCalendar(e: EventSourceFuncArg) {
|
||||
const events = [];
|
||||
|
||||
// Gather all the required date note IDs.
|
||||
const dateRange = utils.getMonthsInDateRange(e.startStr, e.endStr);
|
||||
let allDateNoteIds: string[] = [];
|
||||
for (const month of dateRange) {
|
||||
// TODO: Deduplicate get type.
|
||||
const dateNotesForMonth = await server.get<Record<string, string>>(`special-notes/notes-for-month/${month}`);
|
||||
const dateNoteIds = Object.values(dateNotesForMonth);
|
||||
allDateNoteIds = [ ...allDateNoteIds, ...dateNoteIds ];
|
||||
}
|
||||
|
||||
// Request all the date notes.
|
||||
const dateNotes = await froca.getNotes(allDateNoteIds);
|
||||
const childNoteToDateMapping: Record<string, string> = {};
|
||||
for (const dateNote of dateNotes) {
|
||||
const startDate = dateNote.getLabelValue("dateNote");
|
||||
if (!startDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
events.push(await CalendarView.#buildEvent(dateNote, startDate));
|
||||
|
||||
if (dateNote.hasChildren()) {
|
||||
const childNoteIds = dateNote.getChildNoteIds();
|
||||
for (const childNoteId of childNoteIds) {
|
||||
childNoteToDateMapping[childNoteId] = startDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Request all child notes of date notes in a single run.
|
||||
const childNoteIds = Object.keys(childNoteToDateMapping);
|
||||
const childNotes = await froca.getNotes(childNoteIds);
|
||||
for (const childNote of childNotes) {
|
||||
const startDate = childNoteToDateMapping[childNote.noteId];
|
||||
const event = await CalendarView.#buildEvent(childNote, startDate);
|
||||
events.push(event);
|
||||
}
|
||||
|
||||
return events.flat();
|
||||
}
|
||||
|
||||
async #buildEvents(noteIds: string[]) {
|
||||
const notes = await froca.getNotes(noteIds);
|
||||
const events: EventSourceInput = [];
|
||||
|
||||
for (const note of notes) {
|
||||
const startDate = note.getAttributeValue("label", "startDate");
|
||||
const customTitle = note.getAttributeValue("label", "calendar:title");
|
||||
let startDate = note.getLabelValue("startDate");
|
||||
|
||||
if (note.hasChildren()) {
|
||||
const childrenEventData = await this.#buildEvents(note.getChildNoteIds());
|
||||
if (childrenEventData.length > 0) {
|
||||
events.push(childrenEventData);
|
||||
}
|
||||
}
|
||||
|
||||
if (!startDate) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const titles = await CalendarView.#parseCustomTitle(customTitle, note);
|
||||
for (const title of titles) {
|
||||
const eventData: typeof events[0] = {
|
||||
title: title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}`,
|
||||
noteId: note.noteId
|
||||
};
|
||||
|
||||
const endDate = CalendarView.#offsetDate(note.getAttributeValue("label", "endDate") ?? startDate, 1);
|
||||
if (endDate) {
|
||||
eventData.end = CalendarView.#formatDateToLocalISO(endDate);
|
||||
}
|
||||
|
||||
events.push(eventData);
|
||||
}
|
||||
const endDate = note.getAttributeValue("label", "endDate");
|
||||
events.push(await CalendarView.#buildEvent(note, startDate, endDate));
|
||||
}
|
||||
|
||||
return events.flat();
|
||||
}
|
||||
|
||||
static async #buildEvent(note: FNote, startDate: string, endDate?: string | null) {
|
||||
const customTitle = note.getLabelValue("calendar:title");
|
||||
const titles = await CalendarView.#parseCustomTitle(customTitle, note);
|
||||
const color = note.getLabelValue("calendar:color") ?? note.getLabelValue("color");
|
||||
const events: EventInput[] = [];
|
||||
for (const title of titles) {
|
||||
const eventData: EventInput = {
|
||||
title: title,
|
||||
start: startDate,
|
||||
url: `#${note.noteId}`,
|
||||
noteId: note.noteId,
|
||||
color: color ?? undefined,
|
||||
iconClass: note.getLabelValue("iconClass")
|
||||
};
|
||||
|
||||
const endDateOffset = CalendarView.#offsetDate(endDate ?? startDate, 1);
|
||||
if (endDateOffset) {
|
||||
eventData.end = CalendarView.#formatDateToLocalISO(endDateOffset);
|
||||
}
|
||||
events.push(eventData);
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
|
@ -17,8 +17,19 @@ export default abstract class ViewMode {
|
||||
|
||||
abstract renderList(): Promise<JQuery<HTMLElement> | undefined>;
|
||||
|
||||
entitiesReloadedEvents(e: EventData<"entitiesReloaded">) {
|
||||
/**
|
||||
* Called whenever an "entitiesReloaded" event has been received by the parent component.
|
||||
*
|
||||
* @param e the event data.
|
||||
* @return {@code true} if the view should be re-rendered, a falsy value otherwise.
|
||||
*/
|
||||
onEntitiesReloaded(e: EventData<"entitiesReloaded">): boolean | void {
|
||||
// Do nothing by default.
|
||||
}
|
||||
|
||||
get isFullHeight() {
|
||||
// Override to change its value.
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -144,6 +144,9 @@
|
||||
|
||||
--alert-bar-background: #6b6b6b3b;
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #000000b3;
|
||||
|
||||
--right-pane-item-hover-background: #ffffff26;
|
||||
--right-pane-item-hover-color: white;
|
||||
|
||||
|
@ -138,6 +138,9 @@
|
||||
|
||||
--alert-bar-background: #32637b29;
|
||||
|
||||
--promoted-attribute-card-background-color: var(--card-background-color);
|
||||
--promoted-attribute-card-shadow-color: #00000033;
|
||||
|
||||
--new-tab-button-background: #d8d8d8;
|
||||
--new-tab-button-color: #3a3a3a;
|
||||
--new-tab-button-shadow: 2px 2px 4px rgba(0, 0, 0, 0.2);
|
||||
|
@ -140,6 +140,8 @@ input:not([type]),
|
||||
input[type="text"],
|
||||
input[type="number"],
|
||||
input[type="password"],
|
||||
input[type="date"],
|
||||
input[type="time"],
|
||||
input[type="datetime-local"],
|
||||
textarea.form-control,
|
||||
textarea,
|
||||
@ -156,6 +158,8 @@ input:not([type]):hover,
|
||||
input[type="text"]:hover,
|
||||
input[type="number"]:hover,
|
||||
input[type="password"]:hover,
|
||||
input[type="date"]:hover,
|
||||
input[type="time"]:hover,
|
||||
input[type="datetime-local"]:hover,
|
||||
textarea.form-control:hover,
|
||||
textarea:hover,
|
||||
@ -168,6 +172,8 @@ input:not([type]):focus,
|
||||
input[type="text"]:focus,
|
||||
input[type="number"]:focus,
|
||||
input[type="password"]:focus,
|
||||
input[type="date"]:focus,
|
||||
input[type="time"]:focus,
|
||||
input[type="datetime-local"]:focus,
|
||||
textarea.form-control:focus,
|
||||
textarea:focus,
|
||||
@ -193,6 +199,11 @@ input::selection,
|
||||
color: var(--input-selection-text-color);
|
||||
}
|
||||
|
||||
.form-text {
|
||||
color: var(--main-text-color);
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Input groups */
|
||||
|
||||
.input-group {
|
||||
@ -270,17 +281,20 @@ input::selection,
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.input-group .input-group-text:not(button):not(a) {
|
||||
.input-group .input-group-text {
|
||||
/* Background color hijack */
|
||||
--accented-background-color: transparent;
|
||||
|
||||
border: none;
|
||||
font-style: italic;
|
||||
color: var(--input-placeholder-color);
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.input-group .input-group-text:not(button):not(a):not(.bx) {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Combo box-like dropdown buttons */
|
||||
|
||||
.select-button.dropdown-toggle::after {
|
||||
|
@ -1836,3 +1836,110 @@ body.background-effects.zen #root-widget {
|
||||
animation: alert-show 300ms ease-in;
|
||||
border-bottom: 2px solid #0000001c !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Promoted attributes
|
||||
*/
|
||||
|
||||
/* The promoted attributes section */
|
||||
div.promoted-attributes-container {
|
||||
display: flex;
|
||||
margin-right: 10%;
|
||||
padding: 6px 0;
|
||||
gap: 8px;
|
||||
align-items: stretch;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
div.promoted-attributes-container,
|
||||
div.promoted-attributes-container input {
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
/* A promoted attribute card */
|
||||
div.promoted-attribute-cell {
|
||||
--pa-card-padding-left: 16px;
|
||||
--pa-card-padding-right: 2px;
|
||||
--input-background-color: transparent;
|
||||
|
||||
box-shadow: 1px 1px 2px var(--promoted-attribute-card-shadow-color);
|
||||
|
||||
display: inline-flex;
|
||||
margin: 0;
|
||||
border-radius: 8px;
|
||||
padding: 2px var(--pa-card-padding-right) 2px var(--pa-card-padding-left);
|
||||
background: var(--promoted-attribute-card-background-color);
|
||||
overflow-y: visible;
|
||||
}
|
||||
|
||||
@container (max-width: 400px) {
|
||||
/* Narrow promoted attributes section */
|
||||
div.promoted-attribute-cell {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* A promoted attribute card (boolean attribute) */
|
||||
div.promoted-attribute-cell:has(input[type="checkbox"]):not(:has(.multiplicity > span)) {
|
||||
/* Checbox attribute, without multiplicity */
|
||||
padding-right: var(--pa-card-padding-left);
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell > * {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell > label {
|
||||
font-weight: normal;
|
||||
white-space: nowrap;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell:not(:has(input[type="checkbox"])) > label::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell div.input-group {
|
||||
min-height: auto;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell input {
|
||||
padding: 2px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell input[type="text"] {
|
||||
width: 10em !important;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell input[type="number"] {
|
||||
width: 6em !important;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell span.open-external-link-button {
|
||||
display: flex;
|
||||
padding: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
div.promoted-attribute-cell .tn-checkbox {
|
||||
--box-label-gap: 0;
|
||||
|
||||
height: 1cap;
|
||||
}
|
||||
|
||||
/* The <div> containing the checkbox for a promoted boolean attribute */
|
||||
div.promoted-attribute-cell div:has(input[type="checkbox"]) {
|
||||
order: -1; /* Relocate the checkbox before the label */
|
||||
margin-right: 1.5em;
|
||||
}
|
||||
|
||||
/* The element containing the "new attribute" and "remove this attribute button" */
|
||||
div.promoted-attribute-cell .multiplicity:has(span) {
|
||||
--icon-button-size: 24px;
|
||||
|
||||
margin-left: 8px;
|
||||
margin-right: calc(var(--pa-card-padding-left) - var(--pa-card-padding-right));
|
||||
font-size: 0; /* Prevent whitespaces creating a gap between buttons */
|
||||
}
|
@ -1120,7 +1120,7 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "附件清理超时",
|
||||
"attachment_auto_deletion_description": "如果附件在一段时间后不再被笔记引用,它们将自动被删除(并被清理)。",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_attachments_after": "",
|
||||
"manual_erasing_description": "您还可以手动触发清理(而不考虑上述定义的超时时间):",
|
||||
"erase_unused_attachments_now": "立即清理未使用的附件笔记",
|
||||
"unused_attachments_erased": "未使用的附件已被删除。"
|
||||
@ -1132,15 +1132,15 @@
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "笔记清理超时",
|
||||
"note_erasure_description": "被删除的笔记(以及属性、历史版本等)最初仅被标记为“删除”,可以从“最近修改”对话框中恢复它们。经过一段时间后,已删除的笔记会被“清理”,这意味着它们的内容将无法恢复。此设置允许您配置从删除到清除笔记之间的时间长度。",
|
||||
"erase_notes_after": "Erase notes after:",
|
||||
"erase_notes_after": "",
|
||||
"manual_erasing_description": "您还可以手动触发清理(不考虑上述定义的超时):",
|
||||
"erase_deleted_notes_now": "立即清理已删除的笔记",
|
||||
"deleted_notes_erased": "已删除的笔记已被清理。"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "笔记修改快照间隔",
|
||||
"note_revisions_snapshot_description": "笔记修改快照时间间隔是指经过多少秒后会为笔记创建新的修改历史。更多信息请参见<a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"snapshot_time_interval_label": "笔记修改快照时间间隔(单位:秒)"
|
||||
"note_revisions_snapshot_interval_title": "",
|
||||
"note_revisions_snapshot_description": "",
|
||||
"snapshot_time_interval_label": ""
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "笔记历史快照限制",
|
||||
@ -1259,7 +1259,7 @@
|
||||
"protected_session_timeout_description": "保护会话超时是一个时间段,超时后保护会话会从浏览器内存中清除。这是从最后一次与保护笔记的交互开始计时的。更多信息请见",
|
||||
"wiki": "维基",
|
||||
"for_more_info": "更多信息。",
|
||||
"protected_session_timeout_label": "保护会话超时(秒)",
|
||||
"protected_session_timeout_label": "",
|
||||
"reset_confirmation": "重置密码将永久丧失对所有现受保护笔记的访问。您真的要重置密码吗?",
|
||||
"reset_success_message": "密码已重置。请设置新密码",
|
||||
"change_password_heading": "更改密码",
|
||||
|
@ -1166,8 +1166,8 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Snapshot-Intervall für Notizrevisionen",
|
||||
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit in Sekunden, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">Wiki</a>.",
|
||||
"snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot (in Sekunden)"
|
||||
"note_revisions_snapshot_description": "Das Snapshot-Zeitintervall für Notizrevisionen ist die Zeit, nach der eine neue Notizrevision erstellt wird. Weitere Informationen findest du im <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">Wiki</a>.",
|
||||
"snapshot_time_interval_label": "Zeitintervall für Notiz-Revisions-Snapshot:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Limit für Notizrevision-Snapshots",
|
||||
@ -1286,7 +1286,7 @@
|
||||
"protected_session_timeout_description": "Das Zeitlimit für geschützte Sitzungen ist ein Zeitraum, nach dem die geschützte Sitzung aus dem Speicher des Browsers gelöscht wird. Dies wird ab der letzten Interaktion mit geschützten Notizen gemessen. Sehen",
|
||||
"wiki": "Wiki",
|
||||
"for_more_info": "für weitere Informationen.",
|
||||
"protected_session_timeout_label": "Zeitüberschreitung der geschützten Sitzung (in Sekunden)",
|
||||
"protected_session_timeout_label": "Zeitüberschreitung der geschützten Sitzung:",
|
||||
"reset_confirmation": "Durch das Zurücksetzen des Passworts verlierst du für immer den Zugriff auf alle Ihre bestehenden geschützten Notizen. Möchtest du das Passwort wirklich zurücksetzen?",
|
||||
"reset_success_message": "Das Passwort wurde zurückgesetzt. Bitte lege ein neues Passwort fest",
|
||||
"change_password_heading": "Kennwort ändern",
|
||||
|
@ -824,6 +824,7 @@
|
||||
},
|
||||
"promoted_attributes": {
|
||||
"promoted_attributes": "Promoted Attributes",
|
||||
"unset-field-placeholder": "not set",
|
||||
"url_placeholder": "http://website...",
|
||||
"open_external_link": "Open external link",
|
||||
"unknown_label_type": "Unknown label type '{{type}}'",
|
||||
@ -1166,8 +1167,8 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Note Revision Snapshot Interval",
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time in seconds after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval (in seconds):"
|
||||
"note_revisions_snapshot_description": "The Note revision snapshot interval is the time after which a new note revision will be created for the note. See <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> for more info.",
|
||||
"snapshot_time_interval_label": "Note revision snapshot time interval:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Note Revision Snapshot Limit",
|
||||
@ -1286,7 +1287,7 @@
|
||||
"protected_session_timeout_description": "Protected session timeout is a time period after which the protected session is wiped from the browser's memory. This is measured from the last interaction with protected notes. See",
|
||||
"wiki": "wiki",
|
||||
"for_more_info": "for more info.",
|
||||
"protected_session_timeout_label": "Protected session timeout (in seconds)",
|
||||
"protected_session_timeout_label": "Protected session timeout:",
|
||||
"reset_confirmation": "By resetting the password you will forever lose access to all your existing protected notes. Do you really want to reset the password?",
|
||||
"reset_success_message": "Password has been reset. Please set new password",
|
||||
"change_password_heading": "Change Password",
|
||||
@ -1657,6 +1658,17 @@
|
||||
"hours": "Hours",
|
||||
"days": "Days"
|
||||
},
|
||||
"share": {
|
||||
"title": "Share Settings",
|
||||
"redirect_bare_domain": "Redirect bare domain to Share page",
|
||||
"redirect_bare_domain_description": "Redirect anonymous users to the Share page instead of showing Login",
|
||||
"show_login_link": "Show Login link in Share theme",
|
||||
"show_login_link_description": "Add a login link to the Share page footer",
|
||||
"check_share_root": "Check Share Root Status",
|
||||
"share_root_found": "Share root note '{{noteTitle}}' is ready",
|
||||
"share_root_not_found": "No note with #shareRoot label found",
|
||||
"share_root_not_shared": "Note '{{noteTitle}}' has #shareRoot label but is not Shared"
|
||||
},
|
||||
"time_selector": {
|
||||
"invalid_input": "The entered time value is not a valid number.",
|
||||
"minimum_input": "The entered time value needs to be at least {{minimumSeconds}} seconds."
|
||||
|
@ -1163,8 +1163,8 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Intervalo de instantáneas de revisiones de notas",
|
||||
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo en segundos después de lo cual se creará una nueva revisión para la nota. Ver <a href=\"https://triliumnext.github.io/docs/wiki/note-revisions.html\" class=\"external\"> wiki </a> para obtener más información.",
|
||||
"snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas (en segundos)"
|
||||
"note_revisions_snapshot_description": "El intervalo de tiempo de la instantánea de revisión de nota es el tiempo después de lo cual se creará una nueva revisión para la nota. Ver <a href=\"https://triliumnext.github.io/docs/wiki/note-revisions.html\" class=\"external\"> wiki </a> para obtener más información.",
|
||||
"snapshot_time_interval_label": "Intervalo de tiempo de la instantánea de revisión de notas:"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Límite de respaldos de revisiones de nota",
|
||||
@ -1283,7 +1283,7 @@
|
||||
"protected_session_timeout_description": "El tiempo de espera de la sesión protegida es el período de tiempo después del cual la sesión protegida se borra de la memoria del navegador. Esto se mide desde la última interacción con notas protegidas. Ver",
|
||||
"wiki": "wiki",
|
||||
"for_more_info": "para más información.",
|
||||
"protected_session_timeout_label": "Tiempo de espera de sesión protegida (en segundos)",
|
||||
"protected_session_timeout_label": "Tiempo de espera de sesión protegida:",
|
||||
"reset_confirmation": "Al restablecer la contraseña, perderá para siempre el acceso a todas sus notas protegidas existentes. ¿Realmente quieres restablecer la contraseña?",
|
||||
"reset_success_message": "La contraseña ha sido restablecida. Por favor establezca una nueva contraseña",
|
||||
"change_password_heading": "Cambiar contraseña",
|
||||
|
@ -1108,8 +1108,8 @@
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "Intervalle d'enregistrement automatique des versions des notes",
|
||||
"note_revisions_snapshot_description": "L'intervalle d'enregistrement automatique des versions de note est le temps en secondes après lequel une nouvelle version de note est créée pour une note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
|
||||
"snapshot_time_interval_label": "Intervalle de temps entre deux enregistrements de version de note (en secondes) :"
|
||||
"note_revisions_snapshot_description": "L'intervalle d'enregistrement automatique des versions de note est le temps après lequel une nouvelle version de note est créée pour une note. Consultez le <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a> pour plus d'informations.",
|
||||
"snapshot_time_interval_label": "Intervalle de temps entre deux enregistrements de version de note :"
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "Limite des enregistrements de version de note",
|
||||
@ -1226,7 +1226,7 @@
|
||||
"protected_session_timeout_description": "Le délai d'expiration de la session protégée est une période de temps après laquelle la session protégée est effacée de la mémoire du navigateur. Il est mesuré à partir de la dernière interaction avec des notes protégées. Voir",
|
||||
"wiki": "wiki",
|
||||
"for_more_info": "pour plus d'informations.",
|
||||
"protected_session_timeout_label": "Délai d'expiration de la session protégée (en secondes)",
|
||||
"protected_session_timeout_label": "Délai d'expiration de la session protégée :",
|
||||
"reset_confirmation": "En réinitialisant le mot de passe, vous perdrez à jamais l'accès à toutes vos notes protégées existantes. Voulez-vous vraiment réinitialiser le mot de passe ?",
|
||||
"reset_success_message": "Le mot de passe a été réinitialisé. Veuillez définir un nouveau mot de passe",
|
||||
"change_password_heading": "Changer le mot de passe",
|
||||
|
@ -923,7 +923,7 @@
|
||||
"password_mismatch": "Noile parole nu coincid.",
|
||||
"protected_session_timeout": "Timpul de expirare a sesiunii protejate",
|
||||
"protected_session_timeout_description": "Timpul de expirare a sesiunii protejate este o perioadă de timp după care sesiunea protejată este ștearsă din memoria navigatorului. Aceasta este măsurată de la timpul ultimei interacțiuni cu notițele protejate. Vezi",
|
||||
"protected_session_timeout_label": "Timpul de expirare a sesiunii protejate (în secunde)",
|
||||
"protected_session_timeout_label": "Timpul de expirare a sesiunii protejate:",
|
||||
"reset_confirmation": "Prin resetarea parolei se va pierde pentru totdeauna accesul la notițele protejate existente. Sigur doriți resetarea parolei?",
|
||||
"reset_link": "click aici pentru a o reseta.",
|
||||
"reset_success_message": "Parola a fost resetată. Setați o nouă parolă",
|
||||
@ -944,7 +944,8 @@
|
||||
"remove_this_attribute": "Elimină acest atribut",
|
||||
"unknown_attribute_type": "Tip de atribut necunoscut „{{type}}”",
|
||||
"unknown_label_type": "Tip de etichetă necunoscut „{{type}}”",
|
||||
"url_placeholder": "http://siteweb..."
|
||||
"url_placeholder": "http://siteweb...",
|
||||
"unset-field-placeholder": "nesetat"
|
||||
},
|
||||
"prompt": {
|
||||
"defaultTitle": "Aviz",
|
||||
@ -1068,9 +1069,9 @@
|
||||
"note_revisions": "Revizii ale notiței"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul în secunde după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_description": "Intervalul de salvare a reviziilor este timpul după care se crează o nouă revizie a unei notițe. Vedeți <a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki-ul</a> pentru mai multe informații.",
|
||||
"note_revisions_snapshot_interval_title": "Intervalul de salvare a reviziilor",
|
||||
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor (în secunde)"
|
||||
"snapshot_time_interval_label": "Intervalul de salvare a reviziilor:"
|
||||
},
|
||||
"ribbon": {
|
||||
"edited_notes_message": "Tab-ul panglicii „Notițe editate” se va deschide automat pentru notițele zilnice",
|
||||
|
@ -1098,7 +1098,7 @@
|
||||
"attachment_erasure_timeout": {
|
||||
"attachment_erasure_timeout": "附件清理超時",
|
||||
"attachment_auto_deletion_description": "如果附件在一段時間後不再被筆記引用,它們將自動被刪除(並被清理)。",
|
||||
"erase_attachments_after": "Erase unused attachments after:",
|
||||
"erase_attachments_after": "",
|
||||
"manual_erasing_description": "您還可以手動觸發清理(而不考慮上述定義的超時時間):",
|
||||
"erase_unused_attachments_now": "立即清理未使用的附件筆記",
|
||||
"unused_attachments_erased": "未使用的附件已被刪除。"
|
||||
@ -1110,15 +1110,15 @@
|
||||
"note_erasure_timeout": {
|
||||
"note_erasure_timeout_title": "筆記清理超時",
|
||||
"note_erasure_description": "被刪除的筆記(以及屬性、歷史版本等)最初僅被標記為「刪除」,可以從「最近修改」對話框中恢復它們。經過一段時間後,已刪除的筆記會被「清理」,這意味著它們的內容將無法恢復。此設定允許您設定從刪除到清除筆記之間的時間長度。",
|
||||
"erase_notes_after": "Erase notes after:",
|
||||
"erase_notes_after": "",
|
||||
"manual_erasing_description": "您還可以手動觸發清理(不考慮上述定義的超時):",
|
||||
"erase_deleted_notes_now": "立即清理已刪除的筆記",
|
||||
"deleted_notes_erased": "已刪除的筆記已被清理。"
|
||||
},
|
||||
"revisions_snapshot_interval": {
|
||||
"note_revisions_snapshot_interval_title": "筆記修改快照間隔",
|
||||
"note_revisions_snapshot_description": "筆記修改快照時間間隔是指經過多少秒後會為筆記新增新的修改歷史。更多資訊請參見<a href=\"https://triliumnext.github.io/Docs/Wiki/note-revisions.html\" class=\"external\">wiki</a>。",
|
||||
"snapshot_time_interval_label": "筆記修改快照時間間隔(單位:秒)"
|
||||
"note_revisions_snapshot_interval_title": "",
|
||||
"note_revisions_snapshot_description": "",
|
||||
"snapshot_time_interval_label": ""
|
||||
},
|
||||
"revisions_snapshot_limit": {
|
||||
"note_revisions_snapshot_limit_title": "筆記歷史快照限制",
|
||||
@ -1237,7 +1237,7 @@
|
||||
"protected_session_timeout_description": "保護會話超時是一個時間段,超時後保護會話會從瀏覽器內存中清除。這是從最後一次與保護筆記的交互開始計時的。更多資訊請見",
|
||||
"wiki": "維基",
|
||||
"for_more_info": "更多資訊。",
|
||||
"protected_session_timeout_label": "保護會話超時(秒)",
|
||||
"protected_session_timeout_label": "",
|
||||
"reset_confirmation": "重置密碼將永久喪失對所有現受保護筆記的訪問。您真的要重置密碼嗎?",
|
||||
"reset_success_message": "密碼已重置。請設定新密碼",
|
||||
"change_password_heading": "更改密碼",
|
||||
|
@ -14,7 +14,9 @@ const ALLOWED_OPTIONS = new Set([
|
||||
"eraseEntitiesAfterTimeInSeconds",
|
||||
"eraseEntitiesAfterTimeScale",
|
||||
"protectedSessionTimeout",
|
||||
"protectedSessionTimeoutTimeScale",
|
||||
"revisionSnapshotTimeInterval",
|
||||
"revisionSnapshotTimeIntervalTimeScale",
|
||||
"revisionSnapshotNumberLimit",
|
||||
"zoomFactor",
|
||||
"theme",
|
||||
@ -73,7 +75,9 @@ const ALLOWED_OPTIONS = new Set([
|
||||
"textNoteEditorMultilineToolbar",
|
||||
"layoutOrientation",
|
||||
"backgroundEffects",
|
||||
"allowedHtmlTags" // Allow configuring HTML import tags
|
||||
"allowedHtmlTags",
|
||||
"redirectBareDomain",
|
||||
"showLoginInShareTheme"
|
||||
]);
|
||||
|
||||
function getOptions() {
|
||||
|
@ -7,6 +7,8 @@ import { isElectron } from "./utils.js";
|
||||
import passwordEncryptionService from "./encryption/password_encryption.js";
|
||||
import config from "./config.js";
|
||||
import passwordService from "./encryption/password.js";
|
||||
import options from "./options.js";
|
||||
import attributes from "./attributes.js";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
|
||||
const noAuthentication = config.General && config.General.noAuthentication === true;
|
||||
@ -15,7 +17,16 @@ function checkAuth(req: Request, res: Response, next: NextFunction) {
|
||||
if (!sqlInit.isDbInitialized()) {
|
||||
res.redirect("setup");
|
||||
} else if (!req.session.loggedIn && !isElectron && !noAuthentication) {
|
||||
res.redirect("login");
|
||||
const redirectToShare = options.getOptionBool("redirectBareDomain");
|
||||
if (redirectToShare) {
|
||||
// Check if any note has the #shareRoot label
|
||||
const shareRootNotes = attributes.getNotesWithLabel("shareRoot");
|
||||
if (shareRootNotes.length === 0) {
|
||||
res.status(404).json({ message: "Share root not found. Please set up a note with #shareRoot label first." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
res.redirect(redirectToShare ? "share" : "login");
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
|
53
src/services/html_sanitizer.spec.ts
Normal file
53
src/services/html_sanitizer.spec.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import html_sanitizer from "./html_sanitizer.js";
|
||||
import { trimIndentation } from "../../spec/support/utils.js";
|
||||
|
||||
describe("sanitize", () => {
|
||||
it("filters out position inline CSS", () => {
|
||||
const dirty = `<div style="z-index:999999999;margin:0px;left:250px;height:100px;display:table;background:none;position:fixed;top:250px;"></div>`;
|
||||
const clean = `<div></div>`;
|
||||
expect(html_sanitizer.sanitize(dirty)).toBe(clean);
|
||||
});
|
||||
|
||||
it("keeps inline styles defined in CKEDitor", () => {
|
||||
const dirty = trimIndentation`\
|
||||
<p>
|
||||
<span style="color:hsl(0, 0%, 90%);">
|
||||
Hi
|
||||
</span>
|
||||
|
||||
<span style="background-color:hsl(30, 75%, 60%);">
|
||||
there
|
||||
</span>
|
||||
</p>
|
||||
<figure class="table" style="float:left;height:800px;width:600px;">
|
||||
<table style="background-color:hsl(0, 0%, 90%);border-color:hsl(0, 0%, 0%);border-style:dotted;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border:2px groove hsl(60, 75%, 60%);"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>`;
|
||||
const clean = trimIndentation`\
|
||||
<p>
|
||||
<span style="color:hsl(0, 0%, 90%)">
|
||||
Hi
|
||||
</span>
|
||||
|
||||
<span style="background-color:hsl(30, 75%, 60%)">
|
||||
there
|
||||
</span>
|
||||
</p>
|
||||
<figure class="table" style="float:left;height:800px;width:600px">
|
||||
<table style="background-color:hsl(0, 0%, 90%);border-color:hsl(0, 0%, 0%);border-style:dotted">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="border:2px groove hsl(60, 75%, 60%)"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</figure>`;
|
||||
expect(html_sanitizer.sanitize(dirty)) .toBe(clean);
|
||||
});
|
||||
});
|
@ -141,6 +141,9 @@ function sanitize(dirtyHtml: string) {
|
||||
allowedTags = DEFAULT_ALLOWED_TAGS;
|
||||
}
|
||||
|
||||
const colorRegex = [/^#(0x)?[0-9a-f]+$/i, /^rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$/, /^hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\s*\)$/];
|
||||
const sizeRegex = [/^\d+(?:px|em|%)$/];
|
||||
|
||||
// to minimize document changes, compress H
|
||||
return sanitizeHtml(dirtyHtml, {
|
||||
allowedTags,
|
||||
@ -148,6 +151,24 @@ function sanitize(dirtyHtml: string) {
|
||||
"*": ["class", "style", "title", "src", "href", "hash", "disabled", "align", "alt", "center", "data-*"],
|
||||
input: ["type", "checked"]
|
||||
},
|
||||
allowedStyles: {
|
||||
"*": {
|
||||
"color": colorRegex,
|
||||
"background-color": colorRegex
|
||||
},
|
||||
"figure": {
|
||||
"float": [ /^\s*(left|right|none)\s*$/ ],
|
||||
"width": sizeRegex,
|
||||
"height": sizeRegex
|
||||
},
|
||||
"table": {
|
||||
"border-color": colorRegex,
|
||||
"border-style": [ /^\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*$/ ]
|
||||
},
|
||||
"td": {
|
||||
"border": [ /^\s*\d+(?:px|em|%)\s*(none|hidden|dotted|dashed|solid|double|groove|ridge|inset|outset)\s*(#(0x)?[0-9a-fA-F]+|rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)|hsl\(\s*(\d{1,3})\s*,\s*(\d{1,3})%\s*,\s*(\d{1,3})%\))\s*$/ ]
|
||||
}
|
||||
},
|
||||
allowedSchemes: ALLOWED_PROTOCOLS,
|
||||
nonTextTags: ["head"],
|
||||
transformTags
|
||||
|
BIN
src/services/import/samples/IREN Reports Q2 FY25 Results.htm
Normal file
BIN
src/services/import/samples/IREN Reports Q2 FY25 Results.htm
Normal file
Binary file not shown.
Binary file not shown.
BIN
src/services/import/samples/UTF-16LE Code Note.json
Normal file
BIN
src/services/import/samples/UTF-16LE Code Note.json
Normal file
Binary file not shown.
BIN
src/services/import/samples/UTF-16LE Text Note.md
Normal file
BIN
src/services/import/samples/UTF-16LE Text Note.md
Normal file
Binary file not shown.
BIN
src/services/import/samples/UTF-16LE Text Note.txt
Normal file
BIN
src/services/import/samples/UTF-16LE Text Note.txt
Normal file
Binary file not shown.
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@ -10,40 +10,72 @@ import cls from "../cls.js";
|
||||
import sql_init from "../sql_init.js";
|
||||
import { initializeTranslations } from "../i18n.js";
|
||||
import single from "./single.js";
|
||||
import stripBom from "strip-bom";
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe("processNoteContent", () => {
|
||||
it("treats single MDX as Markdown", async () => {
|
||||
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", "Text Note.mdx"));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "import", {
|
||||
textImportedAsText: true
|
||||
});
|
||||
async function testImport(fileName: string, mimetype: string) {
|
||||
const buffer = fs.readFileSync(path.join(scriptDir, "samples", fileName));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "import", {
|
||||
textImportedAsText: true,
|
||||
codeImportedAsCode: true
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
initializeTranslations();
|
||||
sql_init.initializeDb();
|
||||
await sql_init.dbReady;
|
||||
return new Promise<{ buffer: Buffer, importedNote: BNote }>((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
const rootNote = becca.getNote("root");
|
||||
if (!rootNote) {
|
||||
reject("Missing root note.");
|
||||
}
|
||||
|
||||
const rootNote = becca.getNote("root");
|
||||
if (!rootNote) {
|
||||
reject("Missing root note.");
|
||||
}
|
||||
|
||||
const importedNote = single.importSingleFile(taskContext, {
|
||||
originalname: "Text Note.mdx",
|
||||
mimetype: "text/mdx",
|
||||
buffer: mdxSample
|
||||
}, rootNote as BNote);
|
||||
try {
|
||||
expect(importedNote.mime).toBe("text/html");
|
||||
expect(importedNote.type).toBe("text");
|
||||
expect(importedNote.title).toBe("Text Note");
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
resolve();
|
||||
const importedNote = single.importSingleFile(taskContext, {
|
||||
originalname: fileName,
|
||||
mimetype,
|
||||
buffer: buffer
|
||||
}, rootNote as BNote);
|
||||
resolve({
|
||||
buffer,
|
||||
importedNote
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("processNoteContent", () => {
|
||||
beforeAll(async () => {
|
||||
initializeTranslations();
|
||||
sql_init.initializeDb();
|
||||
await sql_init.dbReady;
|
||||
});
|
||||
|
||||
it("treats single MDX as Markdown", async () => {
|
||||
const { importedNote } = await testImport("Text Note.mdx", "text/mdx");
|
||||
expect(importedNote.mime).toBe("text/html");
|
||||
expect(importedNote.type).toBe("text");
|
||||
expect(importedNote.title).toBe("Text Note");
|
||||
});
|
||||
|
||||
it("supports HTML note with UTF-16 (w/ BOM) from Microsoft Outlook", async () => {
|
||||
const { importedNote } = await testImport("IREN Reports Q2 FY25 Results.htm", "text/html");
|
||||
expect(importedNote.mime).toBe("text/html");
|
||||
expect(importedNote.title).toBe("IREN Reports Q2 FY25 Results");
|
||||
expect(importedNote.getContent().toString().substring(0, 5)).toEqual("<html");
|
||||
});
|
||||
|
||||
it("supports code note with UTF-16", async () => {
|
||||
const { importedNote, buffer } = await testImport("UTF-16LE Code Note.json", "application/json");
|
||||
expect(importedNote.mime).toBe("application/json");
|
||||
expect(importedNote.getContent().toString()).toStrictEqual(stripBom(buffer.toString("utf-16le")));
|
||||
});
|
||||
|
||||
it("supports plain text note with UTF-16", async () => {
|
||||
const { importedNote } = await testImport("UTF-16LE Text Note.txt", "text/plain");
|
||||
expect(importedNote.mime).toBe("text/html");
|
||||
expect(importedNote.getContent().toString()).toBe("<p>Plain text goes here.<br></p>");
|
||||
});
|
||||
|
||||
it("supports markdown note with UTF-16", async () => {
|
||||
const { importedNote } = await testImport("UTF-16LE Text Note.md", "text/markdown");
|
||||
expect(importedNote.mime).toBe("text/html");
|
||||
expect(importedNote.getContent().toString()).toBe("<h2>Hello world</h2>\n<p>Plain text goes here.</p>\n");
|
||||
});
|
||||
})
|
||||
|
@ -8,7 +8,7 @@ import imageService from "../../services/image.js";
|
||||
import protectedSessionService from "../protected_session.js";
|
||||
import markdownService from "./markdown.js";
|
||||
import mimeService from "./mime.js";
|
||||
import { getNoteTitle } from "../../services/utils.js";
|
||||
import { getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import importUtils from "./utils.js";
|
||||
import htmlSanitizer from "../html_sanitizer.js";
|
||||
import type { File } from "./common.js";
|
||||
@ -69,7 +69,7 @@ function importFile(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
|
||||
function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
||||
const content = file.buffer.toString("utf-8");
|
||||
const content = processStringOrBuffer(file.buffer);
|
||||
const detectedMime = mimeService.getMime(file.originalname) || file.mimetype;
|
||||
const mime = mimeService.normalizeMimeType(detectedMime);
|
||||
|
||||
@ -89,7 +89,7 @@ function importCodeNote(taskContext: TaskContext, file: File, parentNote: BNote)
|
||||
|
||||
function importPlainText(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
||||
const plainTextContent = file.buffer.toString("utf-8");
|
||||
const plainTextContent = processStringOrBuffer(file.buffer);
|
||||
const htmlContent = convertTextToHtml(plainTextContent);
|
||||
|
||||
const { note } = noteService.createNewNote({
|
||||
@ -125,7 +125,7 @@ function convertTextToHtml(text: string) {
|
||||
function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
const title = getNoteTitle(file.originalname, !!taskContext.data?.replaceUnderscoresWithSpaces);
|
||||
|
||||
const markdownContent = file.buffer.toString("utf-8");
|
||||
const markdownContent = processStringOrBuffer(file.buffer);
|
||||
let htmlContent = markdownService.renderToHtml(markdownContent, title);
|
||||
|
||||
if (taskContext.data?.safeImport) {
|
||||
@ -147,7 +147,7 @@ function importMarkdown(taskContext: TaskContext, file: File, parentNote: BNote)
|
||||
}
|
||||
|
||||
function importHtml(taskContext: TaskContext, file: File, parentNote: BNote) {
|
||||
let content = file.buffer.toString("utf-8");
|
||||
let content = processStringOrBuffer(file.buffer);
|
||||
|
||||
// Try to get title from HTML first, fall back to filename
|
||||
// We do this before sanitization since that turns all <h1>s into <h2>
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
@ -12,35 +12,46 @@ import sql_init from "../sql_init.js";
|
||||
import { initializeTranslations } from "../i18n.js";
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe("processNoteContent", () => {
|
||||
it("treats single MDX as Markdown in ZIP as text note", async () => {
|
||||
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", "mdx.zip"));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "import", {
|
||||
textImportedAsText: true
|
||||
});
|
||||
async function testImport(fileName: string) {
|
||||
const mdxSample = fs.readFileSync(path.join(scriptDir, "samples", fileName));
|
||||
const taskContext = TaskContext.getInstance("import-mdx", "import", {
|
||||
textImportedAsText: true
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
initializeTranslations();
|
||||
sql_init.initializeDb();
|
||||
await sql_init.dbReady;
|
||||
return new Promise<{ importedNote: BNote; rootNote: BNote }>((resolve, reject) => {
|
||||
cls.init(async () => {
|
||||
const rootNote = becca.getNote("root");
|
||||
if (!rootNote) {
|
||||
expect(rootNote).toBeTruthy();
|
||||
return;
|
||||
}
|
||||
|
||||
const rootNote = becca.getNote("root");
|
||||
if (!rootNote) {
|
||||
expect(rootNote).toBeTruthy();
|
||||
return;
|
||||
}
|
||||
|
||||
const importedNote = await zip.importZip(taskContext, mdxSample, rootNote as BNote);
|
||||
try {
|
||||
expect(importedNote.mime).toBe("text/mdx");
|
||||
expect(importedNote.type).toBe("text");
|
||||
expect(importedNote.title).toBe("Text Note");
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
resolve();
|
||||
const importedNote = await zip.importZip(taskContext, mdxSample, rootNote as BNote);
|
||||
resolve({
|
||||
importedNote,
|
||||
rootNote
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("processNoteContent", () => {
|
||||
beforeAll(async () => {
|
||||
initializeTranslations();
|
||||
sql_init.initializeDb();
|
||||
await sql_init.dbReady;
|
||||
});
|
||||
|
||||
it("treats single MDX as Markdown in ZIP as text note", async () => {
|
||||
const { importedNote } = await testImport("mdx.zip");
|
||||
expect(importedNote.mime).toBe("text/mdx");
|
||||
expect(importedNote.type).toBe("text");
|
||||
expect(importedNote.title).toBe("Text Note");
|
||||
});
|
||||
|
||||
it("can import email from Microsoft Outlook with UTF-16 with BOM", async () => {
|
||||
const { rootNote, importedNote } = await testImport("IREN.Reports.Q2.FY25.Results_files.zip");
|
||||
const htmlNote = rootNote.children.find((ch) => ch.title === "IREN Reports Q2 FY25 Results");
|
||||
expect(htmlNote?.getContent().toString().substring(0, 4)).toEqual("<div");
|
||||
});
|
||||
})
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import BAttribute from "../../becca/entities/battribute.js";
|
||||
import { removeTextFileExtension, newEntityId, getNoteTitle } from "../../services/utils.js";
|
||||
import { removeTextFileExtension, newEntityId, getNoteTitle, processStringOrBuffer } from "../../services/utils.js";
|
||||
import log from "../../services/log.js";
|
||||
import noteService from "../../services/notes.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
@ -457,7 +457,7 @@ async function importZip(taskContext: TaskContext, fileBuffer: Buffer, importRoo
|
||||
}
|
||||
|
||||
if (type !== "file" && type !== "image") {
|
||||
content = content.toString("utf-8");
|
||||
content = processStringOrBuffer(content);
|
||||
}
|
||||
|
||||
const noteTitle = getNoteTitle(filePath, taskContext.data?.replaceUnderscoresWithSpaces || false, noteMeta);
|
||||
|
@ -75,8 +75,10 @@ async function initNotSyncedOptions(initialized: boolean, opts: NotSyncedOpts =
|
||||
*/
|
||||
const defaultOptions: DefaultOption[] = [
|
||||
{ name: "revisionSnapshotTimeInterval", value: "600", isSynced: true },
|
||||
{ name: "revisionSnapshotTimeIntervalTimeScale", value: "60", isSynced: true }, // default to Minutes
|
||||
{ name: "revisionSnapshotNumberLimit", value: "-1", isSynced: true },
|
||||
{ name: "protectedSessionTimeout", value: "600", isSynced: true },
|
||||
{ name: "protectedSessionTimeoutTimeScale", value: "60", isSynced: true },
|
||||
{ name: "zoomFactor", value: isWindows ? "0.9" : "1.0", isSynced: false },
|
||||
{ name: "overrideThemeFonts", value: "false", isSynced: false },
|
||||
{ name: "mainFontFamily", value: "theme", isSynced: false },
|
||||
@ -253,7 +255,11 @@ const defaultOptions: DefaultOption[] = [
|
||||
"tt"
|
||||
]),
|
||||
isSynced: true
|
||||
}
|
||||
},
|
||||
|
||||
// Share settings
|
||||
{ name: "redirectBareDomain", value: "false", isSynced: true },
|
||||
{ name: "showLoginInShareTheme", value: "false", isSynced: true }
|
||||
];
|
||||
|
||||
/**
|
||||
|
@ -49,8 +49,10 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
||||
lastSyncedPull: number;
|
||||
lastSyncedPush: number;
|
||||
revisionSnapshotTimeInterval: number;
|
||||
revisionSnapshotTimeIntervalTimeScale: number;
|
||||
revisionSnapshotNumberLimit: number;
|
||||
protectedSessionTimeout: number;
|
||||
protectedSessionTimeoutTimeScale: number;
|
||||
zoomFactor: number;
|
||||
mainFontSize: number;
|
||||
treeFontSize: number;
|
||||
@ -95,6 +97,10 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
||||
codeBlockWordWrap: boolean;
|
||||
textNoteEditorMultilineToolbar: boolean;
|
||||
backgroundEffects: boolean;
|
||||
// Share settings
|
||||
redirectBareDomain: boolean;
|
||||
showLoginInShareTheme: boolean;
|
||||
|
||||
}
|
||||
|
||||
export type OptionNames = keyof OptionDefinitions;
|
||||
|
@ -1,5 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import chardet from "chardet";
|
||||
import stripBom from "strip-bom";
|
||||
import crypto from "crypto";
|
||||
import { generator } from "rand-token";
|
||||
import unescape from "unescape";
|
||||
@ -330,6 +332,36 @@ function compareVersions(v1: string, v2: string): number {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* For buffers, they are scanned for a supported encoding and decoded (UTF-8, UTF-16). In some cases, the BOM is also stripped.
|
||||
*
|
||||
* For strings, they are returned immediately without any transformation.
|
||||
*
|
||||
* For nullish values, an empty string is returned.
|
||||
*
|
||||
* @param data the string or buffer to process.
|
||||
* @returns the string representation of the buffer, or the same string is it's a string.
|
||||
*/
|
||||
export function processStringOrBuffer(data: string | Buffer | null) {
|
||||
if (!data) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!Buffer.isBuffer(data)) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const detectedEncoding = chardet.detect(data);
|
||||
console.log("Detected as ", detectedEncoding);
|
||||
switch (detectedEncoding) {
|
||||
case "UTF-16LE":
|
||||
return stripBom(data.toString("utf-16le"));
|
||||
case "UTF-8":
|
||||
default:
|
||||
return data.toString("utf-8");
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
compareVersions,
|
||||
crash,
|
||||
|
@ -16,6 +16,7 @@ import type SNote from "./shaca/entities/snote.js";
|
||||
import type SBranch from "./shaca/entities/sbranch.js";
|
||||
import type SAttachment from "./shaca/entities/sattachment.js";
|
||||
import utils from "../services/utils.js";
|
||||
import options from "../services/options.js";
|
||||
|
||||
function getSharedSubTreeRoot(note: SNote): { note?: SNote; branch?: SBranch } {
|
||||
if (note.noteId === shareRoot.SHARE_ROOT_NOTE_ID) {
|
||||
@ -151,7 +152,8 @@ function register(router: Router) {
|
||||
|
||||
const { header, content, isEmpty } = contentRenderer.getContent(note);
|
||||
const subRoot = getSharedSubTreeRoot(note);
|
||||
const opts = { note, header, content, isEmpty, subRoot, assetPath, appPath };
|
||||
const showLoginInShareTheme = options.getOption("showLoginInShareTheme");
|
||||
const opts = { note, header, content, isEmpty, subRoot, assetPath, appPath, showLoginInShareTheme };
|
||||
let useDefaultView = true;
|
||||
|
||||
// Check if the user has their own template
|
||||
|
@ -88,5 +88,10 @@
|
||||
</nav>
|
||||
<% } %>
|
||||
</div>
|
||||
<footer>
|
||||
<% if (showLoginInShareTheme === 'true') { %>
|
||||
<p><a href="/login" class="login-link">Login</a></p>
|
||||
<% } %>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
Loading…
x
Reference in New Issue
Block a user