From a8158117e20f056dc9b3579ee53f87f1726fc81d Mon Sep 17 00:00:00 2001 From: Danila Miadzvedz Date: Mon, 24 Nov 2025 08:06:09 +0100 Subject: [PATCH] Template creation and management modals (#25) --- .test.autosave.xopp | Bin 0 -> 949 bytes package.json | 4 + src/CreateXoppModalManager.ts | 9 +- src/TemplateCreationModalManager.ts | 144 ++++++++++++++++++++++ src/TemplateEditingModalManager.ts | 154 ++++++++++++++++++++++++ src/XoppSettingsTab.ts | 80 ++++++++----- src/modals/TemplateCreationModal.ts | 177 ++++++++++++++++++++++++++++ src/modals/TemplateEditingModal.ts | 161 +++++++++++++++++++++++++ src/modals/XoppFileNameModal.ts | 1 + styles.css | 6 + 10 files changed, 708 insertions(+), 28 deletions(-) create mode 100644 .test.autosave.xopp create mode 100644 src/TemplateCreationModalManager.ts create mode 100644 src/TemplateEditingModalManager.ts create mode 100644 src/modals/TemplateCreationModal.ts create mode 100644 src/modals/TemplateEditingModal.ts diff --git a/.test.autosave.xopp b/.test.autosave.xopp new file mode 100644 index 0000000000000000000000000000000000000000..da335acfa5802b39b5b6cae759a5511d400d2f12 GIT binary patch literal 949 zcmV;m14{fKiwFP!000006LnPCvZ6>3eebUzm`7s-WS`L6Q3!ZJP()nd4FLf~;1p=b#NLi^(e)Wj-ezUrOg z$*grtPMMT%4DR#mejq!|R<7!#Aw{|h2Gc4@QtNe9pp0)g zC)p1Vpm>UbEHk@V7lu$=NAP8aBFhy^9GZH-9p~7X2f)Eb+SRtg>AD0U;w~UnapJU+P(S4uX2U$j?H#pqQD*UYUL049;M$@h4wqWI&(si?ysi+`#t_gg=@|HIdLcqpS zIGjcUAgweYr9r-Y*4ox~&24pc$!RKmsZ@72nrIT!tTa!0$N(C43V=6u-mRQ%W$N)0 z`&!4j@^R=Vy`8yW*!TJQ0lFkR5Lc0)K(+Ih;7 zFX^SAy?P*=Z>kv&AXc-o+ksbKPx|e&ImMiY26balo2xwj8^)&HP~P2iUAH}xo3lA608CiaN}*(@pN4Ju z;coYu5Zyko3cTxa94{7g;@6~&Gv~(<)}|p!7P!DrQ$Y}U4#$!xjT5xMS&^O2c$O8P zLw@mtdkl*@4>8o2%&W4&Y)cEX|`?s@KVElvOxv4 0 ? templatePath diff --git a/src/TemplateCreationModalManager.ts b/src/TemplateCreationModalManager.ts new file mode 100644 index 0000000..d4b3099 --- /dev/null +++ b/src/TemplateCreationModalManager.ts @@ -0,0 +1,144 @@ +import XoppPlugin from "main"; +import { App, normalizePath, Notice } from "obsidian"; +import type { TemplateSpec } from "src/modals/TemplateCreationModal"; +import * as pako from "pako"; + +const MM_TO_PT = 72 / 25.4; + +export const PAGE_PRESETS = { + A3: { widthMm: 297, heightMm: 420 }, + A4: { widthMm: 210, heightMm: 297 }, + A5: { widthMm: 148, heightMm: 210 }, + "US Legal": { widthMm: 215.9, heightMm: 355.6 }, + "US Letter": { widthMm: 215.9, heightMm: 279.4 }, + "16x9": { widthMm: 180, heightMm: 320 }, + "4x3": { widthMm: 240, heightMm: 320 }, +}; + +function getPageSizeToPt(spec: TemplateSpec): { + widthPt: number; + heightPt: number; +} { + let widthMm: number; + let heightMm: number; + + if (spec.pageSizePreset === "Custom") { + if (spec.customWidthMm && spec.customHeightMm) { + widthMm = spec.customWidthMm; + heightMm = spec.customHeightMm; + } else { + const fallback = PAGE_PRESETS.A4; + widthMm = fallback.widthMm; + heightMm = fallback.heightMm; + } + } else { + const preset = PAGE_PRESETS[spec.pageSizePreset]; + widthMm = preset.widthMm; + heightMm = preset.heightMm; + } + + if (spec.orientation === "landscape") { + [widthMm, heightMm] = [heightMm, widthMm]; + } + + return { + widthPt: widthMm * MM_TO_PT, + heightPt: heightMm * MM_TO_PT, + }; +} + +function normalizeColor(raw: string | undefined | null): string { + if (!raw) return "#000000ff"; + + let c = raw.trim(); + if (!c.startsWith("#")) c = "#" + c; + + if (c.length === 4) { + const r = c[1], + g = c[2], + b = c[3]; + c = `#${r}${r}${g}${g}${b}${b}`; + } + + if (c.length === 7) { + c = c + "ff"; + } + + return c; +} + +function buildBackgroundTag(spec: TemplateSpec): string { + const color = normalizeColor(spec.backgroundColor || "#FFFFFF"); + + const tag = ``; + return tag; +} + +function buildTemplateXML(spec: TemplateSpec): string { + const { widthPt, heightPt } = getPageSizeToPt(spec); + const backgroundTag = buildBackgroundTag(spec); + + return ` + +Xournal++ document - see https://xournalpp.github.io/ + + ${backgroundTag} + + +`; +} + +export async function writeTemplateFile( + app: App, + folder: string, + spec: TemplateSpec +): Promise { + const safeFolder = folder.replace(/^\/+/, ""); + const fileName = spec.name.replace(/\.xopp$/i, "") + ".xopp"; + const vaultPath = normalizePath( + safeFolder ? `${safeFolder}/${fileName}` : fileName + ); + + if (safeFolder && !app.vault.getAbstractFileByPath(safeFolder)) { + await app.vault.createFolder(safeFolder); + } + + const xml = buildTemplateXML(spec); + + if(spec.gzip) { + const gzipped = pako.gzip(xml); + const arrayBuffer = gzipped.buffer.slice( + gzipped.byteOffset, + gzipped.byteOffset + gzipped.byteLength + ); + await app.vault.adapter.writeBinary(vaultPath, arrayBuffer); + } else { + await app.vault.adapter.write(vaultPath, xml); + } + + return vaultPath; +} + +export async function createTemplate( + plugin: XoppPlugin, + spec: TemplateSpec +): Promise { + if (!spec.name) { + new Notice("Please enter a template name"); + throw new Error("Missing template name"); + } + + const folder = + plugin.settings.templatesFolder?.trim() || "XournalTemplates"; + + if (!plugin.settings.templatesFolder) { + plugin.settings.templatesFolder = folder; + await plugin.saveSettings(); + } + + const vaultPath = await writeTemplateFile(plugin.app, folder, spec); + + new Notice(`Xournal++ template created: ${vaultPath}`); + + return vaultPath; +} diff --git a/src/TemplateEditingModalManager.ts b/src/TemplateEditingModalManager.ts new file mode 100644 index 0000000..f695752 --- /dev/null +++ b/src/TemplateEditingModalManager.ts @@ -0,0 +1,154 @@ +import { Notice, TFile, TFolder } from "obsidian"; +import { ParsedTemplateEditing } from "./modals/TemplateEditingModal"; +import { PAGE_PRESETS } from "./TemplateCreationModalManager"; +import { TemplateBackgroundStyle, TemplateSpec } from "./modals/TemplateCreationModal"; +import XoppPlugin from "main"; +import * as pako from "pako"; + +type PagePresetName = keyof typeof PAGE_PRESETS; + +const PT_TO_MM = 25.4 / 72; + +async function readXoppXml (plugin: XoppPlugin, file: TFile): Promise<{ xml: string; isGzipped: boolean }> { + const buffer = await plugin.app.vault.adapter.readBinary(file.path); + const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer); + + const isGzipped = bytes.length >= 2 && bytes[0] === 0x1f && bytes[1] === 0x8b; + + if (isGzipped) { + const xmlBytes = pako.ungzip(bytes); + const xml = new TextDecoder("utf-8").decode(xmlBytes); + return { xml, isGzipped: true }; + } else { + const xml = new TextDecoder("utf-8").decode(bytes); + return { xml, isGzipped: false }; + } +} + +function detectPageSizePreset(widthPt: number, heightPt: number) : { + preset: PagePresetName | "Custom"; + widthMm: number; + heightMm: number; + orientation: "portrait" | "landscape"; +} { + + const widthMm = widthPt * PT_TO_MM; + const heightMm = heightPt * PT_TO_MM; + + const presets = PAGE_PRESETS; + + const orientation: "portrait" | "landscape" = + widthMm >= heightMm ? "landscape" : "portrait"; + + const w = Math.min(widthMm, heightMm); + const h = Math.max(widthMm, heightMm); + + const toleranceMm = 1; + + for (const [name, preset] of Object.entries(presets) as [PagePresetName, {widthMm:number;heightMm:number}][]) { + const pw = preset.widthMm; + const ph = preset.heightMm; + + if (Math.abs(w - Math.min(pw, ph)) <= toleranceMm && Math.abs(h - Math.max(pw, ph)) <= toleranceMm) { + return { preset: name, widthMm, heightMm, orientation }; + } + } + + return { preset: "Custom", widthMm, heightMm, orientation }; +} + +function parseBackground(el: Element): { + backgroundStyle: TemplateBackgroundStyle; + backgroundColor: string; + spacingMm?: number; + marginMm?: number; +} { + const style = el.getAttribute("style") ?? ""; + + const backgroundStyle: TemplateBackgroundStyle = style as TemplateBackgroundStyle; + + const rawColor = el.getAttribute("color") || "#ffffffff"; + + return { + backgroundStyle, + backgroundColor: rawColor + }; +} + +function parseXoppTemplateSpec(xml: string, fileName: string): TemplateSpec { + const parser = new DOMParser(); + const doc = parser.parseFromString(xml, "application/xml"); + + const pageEl = doc.querySelector("page"); + if (!pageEl) throw new Error("Invalid template: missing element."); + + const widthPt = parseFloat(pageEl.getAttribute("width") ?? "0"); + const heightPt = parseFloat(pageEl.getAttribute("height") ?? "0"); + + const { preset: pageSizePreset, widthMm, heightMm, orientation } = detectPageSizePreset(widthPt, heightPt); + + const bgEl = pageEl.querySelector("background"); + const bg = bgEl + ? parseBackground(bgEl) + : { + backgroundStyle: "plain" as TemplateBackgroundStyle, + backgroundColor: "#ffffffff", + spacingMm: undefined, + marginMm: undefined, + }; + + const baseName = fileName.replace(/^.*\//, "").replace(/\.xopp$/i, ""); + + const spec: TemplateSpec = { + name: baseName, + pageSizePreset, + orientation, + backgroundStyle: bg.backgroundStyle, + backgroundColor: bg.backgroundColor, + }; + + if (pageSizePreset === "Custom") { + spec.customWidthMm = widthMm; + spec.customHeightMm = heightMm; + } + + return spec; +} + +export async function parseTemplateFile( + plugin: XoppPlugin, + templatePath: string, + onFinished: () => void +): Promise { + const file = plugin.app.vault.getAbstractFileByPath(templatePath); + if (!file || !(file instanceof TFile)) { + new Notice("Template file not found."); + console.error("Template file not found:", templatePath); + return; + } + + const { xml, isGzipped } = await readXoppXml(plugin, file); + + const spec: TemplateSpec = parseXoppTemplateSpec(xml, file.name); + spec.gzip = isGzipped; + + new ParsedTemplateEditing(plugin.app, plugin, spec, onFinished).open(); +} + +export async function fetchTemplates(plugin: XoppPlugin): Promise { + const folderPath = plugin.settings.templatesFolder?.trim(); + if (!folderPath) { + return []; + } + + const folder = plugin.app.vault.getAbstractFileByPath(folderPath); + if (!folder || !(folder instanceof TFolder)) { + return []; + } + + const templates = folder.children.filter( + (child) => child instanceof TFile && child.name.endsWith(".xopp") + ) as TFile[]; + + return templates; +} \ No newline at end of file diff --git a/src/XoppSettingsTab.ts b/src/XoppSettingsTab.ts index aa213c1..478dbef 100644 --- a/src/XoppSettingsTab.ts +++ b/src/XoppSettingsTab.ts @@ -2,6 +2,8 @@ import XoppPlugin from "main"; import { App, Setting, PluginSettingTab, TFile } from "obsidian"; import ConfirmationModal from "./modals/ConfirmationModal"; import { exportAllXoppToPDF } from "src/xopp2pdf"; +import TemplateCreationModal from "src/modals/TemplateCreationModal"; +import TemplateEditingModal from "./modals/TemplateEditingModal"; export class XoppSettingsTab extends PluginSettingTab { plugin: XoppPlugin; @@ -16,18 +18,6 @@ export class XoppSettingsTab extends PluginSettingTab { containerEl.empty(); - const templatesFolder = this.plugin.settings.templatesFolder?.trim(); - let templateFiles: TFile[] = []; - if (templatesFolder) { - templateFiles = this.app.vault - .getFiles() - .filter( - (f: TFile) => - f.path.startsWith(templatesFolder + "/") && - f.extension === "xopp" - ); - } - new Setting(containerEl) .setName("Auto export Xournal++ files") .setDesc( @@ -85,21 +75,6 @@ export class XoppSettingsTab extends PluginSettingTab { }); }); - // new Setting(containerEl) - // .setName("Xournal++ template path") - // .setDesc( - // "The relative path of the template for any new Xournal++ file (leave empty to use the default template)." - // ) - // .addText((toggle) => { - // toggle - // .setValue(this.plugin.settings.templatePath) - // .setPlaceholder("e.g. templates/template.xopp") - // .onChange(async (value) => { - // this.plugin.settings.templatePath = value; - // await this.plugin.saveSettings(); - // }); - // }); - new Setting(containerEl) .setName("Xournal++ templates folder") .setDesc( @@ -131,6 +106,19 @@ export class XoppSettingsTab extends PluginSettingTab { "The default template to use when creating new Xournal++ files from the templates folder." ) .addDropdown((dropdown) => { + const templatesFolder = + this.plugin.settings.templatesFolder?.trim(); + let templateFiles: TFile[] = []; + if (templatesFolder) { + templateFiles = this.app.vault + .getFiles() + .filter( + (f: TFile) => + f.path.startsWith(templatesFolder + "/") && + f.extension === "xopp" + ); + } + if (templateFiles.length === 0) { dropdown.addOption("", "No templates found"); } else { @@ -146,6 +134,44 @@ export class XoppSettingsTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName("Create a new Xournal++ template") + .setDesc( + "Create a new Xournal++ template file in the templates folder." + ) + .addButton((button) => { + button + .setButtonText("Create Template") + .setCta() + .onClick(async () => { + new TemplateCreationModal( + this.app, + this.plugin, + async (createdPath) => { + if (!this.plugin.settings.defaultTemplatePath) { + this.plugin.settings.defaultTemplatePath = + createdPath; + } + + await this.plugin.saveSettings(); + this.display(); + } + ).open(); + }); + }); + + new Setting(containerEl) + .setName("Edit existing Xournal++ templates") + .setDesc("A GUI to edit or delete existing Xournal++ templates.") + .addButton((button) => { + button + .setButtonText("Manage Templates") + .setCta() + .onClick(async () => { + new TemplateEditingModal(this.app, this.plugin).open(); + }); + }); + new Setting(containerEl) .setName("Default path for new Xournal++ files") .setDesc( diff --git a/src/modals/TemplateCreationModal.ts b/src/modals/TemplateCreationModal.ts new file mode 100644 index 0000000..aca50f7 --- /dev/null +++ b/src/modals/TemplateCreationModal.ts @@ -0,0 +1,177 @@ +import XoppPlugin from "main"; +import { App, ButtonComponent, Modal, Setting } from "obsidian"; +import { createTemplate } from "src/TemplateCreationModalManager"; + +export type TemplateBackgroundStyle = + | "plain" + | "lined" + | "ruled" + | "staves" + | "graph" + | "dotted" + | "isodotted" + | "isograph"; + +export interface TemplateSpec { + name: string; + pageSizePreset: + | "A3" + | "A4" + | "A5" + | "US Letter" + | "US Legal" + | "16x9" + | "4x3" + | "Custom"; + customWidthMm?: number; + customHeightMm?: number; + orientation: "portrait" | "landscape"; + backgroundStyle: TemplateBackgroundStyle; + backgroundColor: string; + gzip? : boolean; +} + +export default class TemplateCreationModal extends Modal { + plugin: XoppPlugin; + onCreated: (vaultPath: string) => void; + + constructor( + app: App, + plugin: XoppPlugin, + onCreated: (vaultPath: string) => void + ) { + super(app); + this.plugin = plugin; + this.onCreated = onCreated; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl("h3", { text: "Create Xournal++ template" }); + + const spec: TemplateSpec = { + name: "", + pageSizePreset: "A4", + orientation: "portrait", + backgroundStyle: "plain", + backgroundColor: "#ffffff", + gzip: false, + }; + + const doCreate = async () => { + try { + const vaultPath = await createTemplate(this.plugin, spec); + this.onCreated(vaultPath); + this.close(); + } catch (e) { + console.error(e); + } + }; + + new Setting(contentEl) + .setName("Template Name") + .setDesc("Name of the new template file") + .addText((text) => + text.onChange((value) => { + spec.name = value.trim(); + }) + ); + + new Setting(contentEl).setName("Page Size").addDropdown((dropdown) => { + dropdown + .addOption("A3", "A3") + .addOption("A4", "A4") + .addOption("A5", "A5") + .addOption("US Letter", "US Letter") + .addOption("US Legal", "US Legal") + .addOption("16x9", "16x9") + .addOption("4x3", "4x3") + .addOption("Custom", "Custom") + .onChange((value) => { + spec.pageSizePreset = + value as TemplateSpec["pageSizePreset"]; + }); + }); + + new Setting(contentEl) + .setName("Orientation") + .addDropdown((dropdown) => { + dropdown + .addOption("portrait", "Portrait") + .addOption("landscape", "Landscape") + .onChange((value) => { + spec.orientation = value as TemplateSpec["orientation"]; + }); + }); + + new Setting(contentEl) + .setName("Background Style") + .addDropdown((dropdown) => { + dropdown + .addOption("plain", "Plain") + .addOption("lined", "Lined") + .addOption("ruled", "Ruled") + .addOption("staves", "Staves") + .addOption("graph", "Graph") + .addOption("dotted", "Dotted") + .addOption("isodotted", "Isometric dotted") + .addOption("isograph", "Isometric graph") + .onChange((value) => { + spec.backgroundStyle = + value as TemplateSpec["backgroundStyle"]; + }); + }); + + new Setting(contentEl) + .setName("Background Color") + .setDesc("Hex color, e.g. #ffffff") + .addText((text) => + text.setValue("#ffffff").onChange((value) => { + spec.backgroundColor = value.trim(); + }) + ); + + new Setting(contentEl) + .setName("Compress as gzipped .xopp") + .setDesc("Whether to gzip compress the template file") + .addToggle((toggle) => + toggle.setValue(!!spec.gzip).onChange((value) => { + spec.gzip = value; + }) + ); + + const buttonRow = contentEl.createDiv({ + cls: "xopp-creation-editing-template-button-row", + }); + + new ButtonComponent(buttonRow) + .setButtonText("Create") + .setCta() + .onClick(async () => { + doCreate(); + this.close(); + }); + + new ButtonComponent(buttonRow) + .setButtonText("Cancel") + .onClick(() => this.close()); + + this.modalEl.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + + if (!e.shiftKey) { + doCreate(); + this.close(); + } + } + }); + } + + onClose() { + const { contentEl } = this; + contentEl.empty(); + } +} diff --git a/src/modals/TemplateEditingModal.ts b/src/modals/TemplateEditingModal.ts new file mode 100644 index 0000000..13dddda --- /dev/null +++ b/src/modals/TemplateEditingModal.ts @@ -0,0 +1,161 @@ +import XoppPlugin from "main"; +import { App, ButtonComponent, Modal, Setting } from "obsidian"; +import { + parseTemplateFile, + fetchTemplates, +} from "src/TemplateEditingModalManager"; +import { TemplateSpec } from "./TemplateCreationModal"; +import { createTemplate } from "src/TemplateCreationModalManager"; + +export default class TemplateEditingModal extends Modal { + plugin: XoppPlugin; + + constructor(app: App, plugin: XoppPlugin) { + super(app); + this.plugin = plugin; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl("h3", { text: "Edit Xournal++ templates" }); + + new Setting(contentEl) + .setName("Choose a template") + .addDropdown(async (dropdown) => { + const templates = await fetchTemplates(this.plugin); + + if (templates.length === 0) { + dropdown.addOption("", "No templates found"); + } else { + templates.forEach((template) => { + dropdown.addOption(template.path, template.name); + }); + } + + dropdown.setValue(""); + + dropdown.onChange(async (value) => { + await parseTemplateFile(this.plugin, value, () => { this.close()}); + }); + }); + } +} + +export class ParsedTemplateEditing extends Modal { + plugin: XoppPlugin; + initialSpec: TemplateSpec; + onFinished: () => void; + + constructor(app: App, plugin: XoppPlugin, templateSpec: TemplateSpec, onFinished: () => void) { + super(app); + this.plugin = plugin; + this.initialSpec = templateSpec; + this.onFinished = onFinished; + } + + onOpen() { + const { contentEl } = this; + contentEl.empty(); + contentEl.createEl("h3", { + text: `Editing template: ${this.initialSpec.name}`, + }); + + const spec: TemplateSpec = { ...this.initialSpec }; + + new Setting(contentEl).setName("Page Size").addDropdown((dropdown) => { + dropdown + .addOption("A3", "A3") + .addOption("A4", "A4") + .addOption("A5", "A5") + .addOption("US Letter", "US Letter") + .addOption("US Legal", "US Legal") + .addOption("16x9", "16x9") + .addOption("4x3", "4x3") + .addOption("Custom", "Custom") + .setValue(this.initialSpec.pageSizePreset) + .onChange((value) => { + spec.pageSizePreset = + value as TemplateSpec["pageSizePreset"]; + }); + }); + + new Setting(contentEl) + .setName("Orientation") + .addDropdown((dropdown) => { + dropdown + .addOption("portrait", "Portrait") + .addOption("landscape", "Landscape") + .setValue(this.initialSpec.orientation) + .onChange((value) => { + spec.orientation = value as TemplateSpec["orientation"]; + }); + }); + + new Setting(contentEl) + .setName("Background Style") + .addDropdown((dropdown) => { + dropdown + .addOption("plain", "Plain") + .addOption("lined", "Lined") + .addOption("ruled", "Ruled") + .addOption("staves", "Staves") + .addOption("graph", "Graph") + .addOption("dotted", "Dotted") + .addOption("isodotted", "Isometric dotted") + .addOption("isograph", "Isometric graph") + .setValue(this.initialSpec.backgroundStyle) + .onChange((value) => { + spec.backgroundStyle = + value as TemplateSpec["backgroundStyle"]; + }); + }); + + new Setting(contentEl).setName("Background color").addText((text) => { + text.setPlaceholder("#ffffffff") + .setValue(spec.backgroundColor || "") + .onChange((value) => { + spec.backgroundColor = value.trim(); + }); + }); + + new Setting(contentEl) + .setName("Compress as gzipped .xopp") + .setDesc("Whether to gzip compress the template file") + .addToggle((toggle) => + toggle.setValue(!!spec.gzip).onChange((value) => { + spec.gzip = value; + }) + ); + + const buttonRow = contentEl.createDiv({ + cls: "xopp-creation-editing-template-button-row", + }); + + new ButtonComponent(buttonRow) + .setButtonText("Confirm Edits") + .setCta() + .onClick(async () => { + await createTemplate(this.plugin, spec); + this.close(); + this.onFinished(); + }); + + new ButtonComponent(buttonRow) + .setButtonText("Cancel") + .onClick(() => this.close()); + + this.modalEl.addEventListener("keydown", (e) => { + if (e.key === "Enter") { + e.preventDefault(); + e.stopPropagation(); + + if (!e.shiftKey) { + createTemplate(this.plugin, spec); + this.close(); + this.onFinished(); + } + } + }); + } +} diff --git a/src/modals/XoppFileNameModal.ts b/src/modals/XoppFileNameModal.ts index 8c6dcb6..7729ffc 100644 --- a/src/modals/XoppFileNameModal.ts +++ b/src/modals/XoppFileNameModal.ts @@ -71,6 +71,7 @@ export default class XoppFileNameModal extends Modal { fileName = i; }); + textComponent.inputEl.focus(); // Using a timeout to ensure the focus is set after the modal is fully rendered. setTimeout(() => textComponent.inputEl.focus(), 0); diff --git a/styles.css b/styles.css index 54d4215..dd66074 100644 --- a/styles.css +++ b/styles.css @@ -17,3 +17,9 @@ .clickable-tag { cursor: pointer; } + +.xopp-creation-editing-template-button-row { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} \ No newline at end of file