Merge branch 'develop' into feature/MFA

This commit is contained in:
Jin
2025-03-26 03:56:53 +01:00
51 changed files with 618 additions and 559 deletions

View File

@@ -206,7 +206,8 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
doRender() {}
toggleInt(show: boolean | null | undefined) {
this.$widget.toggleClass("hidden-int", !show);
this.$widget.toggleClass("hidden-int", !show)
.toggleClass("visible", !!show);
}
isHiddenInt() {
@@ -214,7 +215,8 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
}
toggleExt(show: boolean) {
this.$widget.toggleClass("hidden-ext", !show);
this.$widget.toggleClass("hidden-ext", !show)
.toggleClass("visible", !!show);
}
isHiddenExt() {

View File

@@ -13,7 +13,7 @@ export default class ShowTocWidgetButton extends OnClickButtonWidget {
constructor() {
super();
this.icon("bx-spreadsheet bx-rotate-180")
this.icon("bx-tn-toc")
.title(t("show_toc_widget_button.show_toc"))
.titlePlacement("bottom")
.onClick(() => {

View File

@@ -3,6 +3,14 @@ import { t } from "../../services/i18n.js";
import type FNote from "../../entities/fnote.js";
import type BasicWidget from "../basic_widget.js";
/*
* Note:
*
* For floating button widgets that require content to overflow, the has-overflow CSS class should
* be applied to the root element of the widget. Additionally, this root element may need to
* properly handle rounded corners, as defined by the --border-radius CSS variable.
*/
const TPL = `
<div class="floating-buttons no-print">
<style>
@@ -39,10 +47,18 @@ const TPL = `
top: 70px;
}
.type-canvas .floating-buttons-children > * {
--border-radius: 0; /* Overridden by themes */
}
.floating-buttons-children > *:not(.hidden-int):not(.no-content-hidden) {
margin: 2px;
}
.floating-buttons-children > *:not(.has-overflow) {
overflow: hidden;
}
.floating-buttons-children > button, .floating-buttons-children .floating-button {
font-size: 150%;
padding: 5px 10px 4px 10px;

View File

@@ -11,7 +11,7 @@ const TPL = `
export default class PngExportButton extends NoteContextAwareWidget {
isEnabled() {
return super.isEnabled() && ["mermaid"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
return super.isEnabled() && ["mermaid", "mindMap"].includes(this.note?.type ?? "") && this.note?.isContentAvailable() && this.noteContext?.viewScope?.viewMode === "default";
}
doRender() {

View File

@@ -5,7 +5,7 @@ const TPL = `
<button type="button"
class="export-svg-button"
title="${t("svg_export_button.button_title")}">
<span class="bx bx-export"></span>
<span class="bx bxs-file-image"></span>
</button>
`;

View File

@@ -10,7 +10,7 @@ import froca from "../../services/froca.js";
import type FNote from "../../entities/fnote.js";
const TPL = `
<div class="backlinks-widget">
<div class="backlinks-widget has-overflow">
<style>
.backlinks-widget {
position: relative;
@@ -61,7 +61,7 @@ const TPL = `
<span class="backlinks-count"></span>
</div>
<div class="backlinks-items" style="display: none;"></div>
<div class="backlinks-items dropdown-menu" style="display: none;"></div>
</div>
`;
@@ -121,7 +121,8 @@ export default class BacklinksWidget extends NoteContextAwareWidget {
}
toggle(show: boolean) {
this.$widget.toggleClass("hidden-no-content", !show);
this.$widget.toggleClass("hidden-no-content", !show)
.toggleClass("visible", !!show);
}
clearItems() {

View File

@@ -5,6 +5,7 @@ import server from "../services/server.js";
import type FNote from "../entities/fnote.js";
import type { EventData } from "../components/app_context.js";
import type { Icon } from "./icon_list.js";
import { Dropdown } from "bootstrap";
const TPL = `
<div class="note-icon-widget dropdown">
@@ -88,6 +89,7 @@ interface IconToCountCache {
export default class NoteIconWidget extends NoteContextAwareWidget {
private dropdown!: bootstrap.Dropdown;
private $icon!: JQuery<HTMLElement>;
private $iconList!: JQuery<HTMLElement>;
private $iconCategory!: JQuery<HTMLElement>;
@@ -96,6 +98,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
doRender() {
this.$widget = $(TPL);
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0]);
this.$icon = this.$widget.find("button.note-icon");
this.$iconList = this.$widget.find(".icon-list");
this.$iconList.on("click", "span", async (e) => {
@@ -130,6 +134,8 @@ export default class NoteIconWidget extends NoteContextAwareWidget {
async refreshWithNote(note: FNote) {
this.$icon.removeClass().addClass(`${note.getIcon()} note-icon`);
this.$icon.prop("disabled", !!(this.noteContext?.viewScope?.viewMode !== "default"));
this.dropdown.hide();
}
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {

View File

@@ -165,8 +165,7 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
}
cleanup(): void {
this.splitInstance?.destroy();
this.splitInstance = undefined;
this.#destroyResizer();
}
async doRefresh(note: FNote | null | undefined) {
@@ -189,17 +188,19 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
// Vertical vs horizontal layout
const layoutOrientation = (!utils.isMobile() ? options.get("splitEditorOrientation") ?? "horizontal" : "vertical");
if (this.layoutOrientation === layoutOrientation && this.isReadOnly === isReadOnly) {
return;
if (this.layoutOrientation !== layoutOrientation || this.isReadOnly !== isReadOnly) {
this.$widget
.toggleClass("split-horizontal", !isReadOnly && layoutOrientation === "horizontal")
.toggleClass("split-vertical", !isReadOnly && layoutOrientation === "vertical")
.toggleClass("split-read-only", isReadOnly);
this.layoutOrientation = layoutOrientation as ("horizontal" | "vertical");
this.isReadOnly = isReadOnly;
this.#destroyResizer();
}
this.$widget
.toggleClass("split-horizontal", !isReadOnly && layoutOrientation === "horizontal")
.toggleClass("split-vertical", !isReadOnly && layoutOrientation === "vertical")
.toggleClass("split-read-only", isReadOnly);
this.layoutOrientation = layoutOrientation as ("horizontal" | "vertical");
this.isReadOnly = isReadOnly;
this.#setupResizer();
if (!this.splitInstance) {
this.#setupResizer();
}
}
#setupResizer() {
@@ -226,6 +227,11 @@ export default abstract class AbstractSplitTypeWidget extends TypeWidget {
}
}
#destroyResizer() {
this.splitInstance?.destroy();
this.splitInstance = undefined;
}
/**
* Called upon when the split between the preview and content pane is initialized. Can be used to add additional listeners if needed.
*/

View File

@@ -276,4 +276,14 @@ export default class MindMapWidget extends TypeWidget {
const svg = await this.renderSvg();
utils.downloadSvg(this.note.title, svg);
}
async exportPngEvent({ ntxId }: EventData<"exportPng">) {
if (!this.isNoteContext(ntxId) || this.note?.type !== "mindMap") {
return;
}
const svg = await this.renderSvg();
utils.downloadSvgAsPng(this.note.title, svg);
}
}

View File

@@ -61,11 +61,7 @@ export default class ElectronIntegrationOptions extends OptionsWidget {
this.$backgroundEffects.on("change", () => this.updateCheckboxOption("backgroundEffects", this.$backgroundEffects));
const restartAppButton = this.$widget.find(".restart-app-button");
restartAppButton.on("click", () => {
const app = utils.dynamicRequire("@electron/remote").app;
app.relaunch();
app.exit();
});
restartAppButton.on("click", utils.restartDesktopApp);
}
isEnabled() {

View File

@@ -3,20 +3,26 @@ import server from "../../../../services/server.js";
import utils from "../../../../services/utils.js";
import { getAvailableLocales, t } from "../../../../services/i18n.js";
import type { OptionMap } from "../../../../../../services/options_interface.js";
import type { Locale } from "../../../../../../services/i18n.js";
const TPL = `
<div class="options-section">
<h4>${t("i18n.title")}</h4>
<div class="form-group row">
<div class="col-6">
<div class="locale-options-container">
<div class="option-row">
<label for="locale-select">${t("i18n.language")}</label>
<select id="locale-select" class="locale-select form-select"></select>
</div>
<div class="col-6">
<div class="option-row electron-only">
<label for="formatting-locale-select">${t("i18n.formatting-locale")}</label>
<select id="formatting-locale-select" class="formatting-locale-select form-select"></select>
</div>
<div class="option-row">
<label id="first-day-of-week-label">${t("i18n.first-day-of-the-week")}</label>
<div role="group" aria-labelledby="first-day-of-week-label" style="margin-top: .33em;">
<div role="group" aria-labelledby="first-day-of-week-label">
<label class="tn-radio">
<input name="first-day-of-week" type="radio" value="0" />
${t("i18n.sunday")}
@@ -28,13 +34,44 @@ const TPL = `
</label>
</div>
</div>
<div class="option-row centered">
<button class="btn btn-secondary btn-micro restart-app-button">${t("electron_integration.restart-app-button")}</button>
</div>
</div>
<style>
.locale-options-container .option-row {
border-bottom: 1px solid var(--main-border-color);
display: flex;
align-items: center;
padding: 0.5em 0;
}
.locale-options-container .option-row > label {
width: 40%;
margin-bottom: 0 !important;
}
.locale-options-container .option-row > select {
width: 60%;
}
.locale-options-container .option-row:last-of-type {
border-bottom: unset;
}
.locale-options-container .option-row.centered {
justify-content: center;
}
</style>
</div>
`;
export default class LocalizationOptions extends OptionsWidget {
private $localeSelect!: JQuery<HTMLElement>;
private $formattingLocaleSelect!: JQuery<HTMLElement>;
doRender() {
this.$widget = $(TPL);
@@ -43,24 +80,44 @@ export default class LocalizationOptions extends OptionsWidget {
this.$localeSelect.on("change", async () => {
const newLocale = this.$localeSelect.val();
await server.put(`options/locale/${newLocale}`);
utils.reloadFrontendApp("locale change");
});
this.$formattingLocaleSelect = this.$widget.find(".formatting-locale-select");
this.$formattingLocaleSelect.on("change", async () => {
const newLocale = this.$formattingLocaleSelect.val();
await server.put(`options/formattingLocale/${newLocale}`);
});
this.$widget.find(`input[name="first-day-of-week"]`).on("change", () => {
const firstDayOfWeek = String(this.$widget.find(`input[name="first-day-of-week"]:checked`).val());
this.updateOption("firstDayOfWeek", firstDayOfWeek);
});
this.$widget.find(".restart-app-button").on("click", utils.restartDesktopApp);
}
async optionsLoaded(options: OptionMap) {
const availableLocales = getAvailableLocales().filter(l => !l.contentOnly);
this.$localeSelect.empty();
const allLocales = getAvailableLocales();
for (const locale of availableLocales) {
this.$localeSelect.append($("<option>").attr("value", locale.id).text(locale.name));
function buildLocaleItem(locale: Locale, value: string) {
return $("<option>")
.attr("value", value)
.text(locale.name)
}
// Build list of UI locales.
this.$localeSelect.empty();
for (const locale of allLocales.filter(l => !l.contentOnly)) {
this.$localeSelect.append(buildLocaleItem(locale, locale.id));
}
this.$localeSelect.val(options.locale);
// Build list of Electron locales.
this.$formattingLocaleSelect.empty();
for (const locale of allLocales.filter(l => l.electronLocale)) {
this.$formattingLocaleSelect.append(buildLocaleItem(locale, locale.electronLocale as string));
}
this.$formattingLocaleSelect.val(options.formattingLocale);
this.$widget.find(`input[name="first-day-of-week"][value="${options.firstDayOfWeek}"]`)
.prop("checked", "true");
}