diff --git a/module.json b/module.json
index 75f5ee9..3c5352b 100644
--- a/module.json
+++ b/module.json
@@ -1,22 +1,22 @@
{
"id": "user-notes",
"title": "User Notes",
- "description": "User Notes: ein einfaches lokales Notizfenster für Foundry VTT v14. Notizen werden im Browser-localStorage pro Welt und Benutzer gespeichert.",
- "version": "1.0.1",
+ "version": "1.0.0",
"authors": [
{
- "name": "ChatGPT"
+ "name": "Florian Zumpe"
}
],
"compatibility": {
"minimum": "14",
"verified": "14"
},
- "scripts": [
+ "esmodules": [
"scripts/user-notes.js"
],
"styles": [
- "styles/user-notes.css"
+ "styles/user-notes-window.css",
+ "styles/user-notes-settings.css"
],
"languages": [],
"url": "https://github.com/fzumpe/foundry-usernotes",
diff --git a/scripts/user-notes-constants.js b/scripts/user-notes-constants.js
new file mode 100644
index 0000000..5232591
--- /dev/null
+++ b/scripts/user-notes-constants.js
@@ -0,0 +1,19 @@
+export const USER_NOTES_MODULE_ID = "user-notes";
+export const USER_NOTES_WINDOW_ID = "user-notes-window";
+export const USER_NOTES_TOOL_ID = "user-notes-open";
+
+export const USER_NOTES_DEFAULT_POSITION = {
+ left: 200,
+ top: 150,
+ width: 500,
+ height: 400
+};
+
+export const USER_NOTES_MIN_WIDTH = 280;
+export const USER_NOTES_MIN_HEIGHT = 180;
+export const USER_NOTES_VIEWPORT_MARGIN = 40;
+
+export const USER_NOTES_DEFAULT_BACKGROUND = "rgba(25, 24, 19, 0.96)";
+export const USER_NOTES_DEFAULT_TEXT_COLOR = "#f0f0e0";
+export const USER_NOTES_DEFAULT_TEXTAREA_BACKGROUND = "rgba(255, 255, 255, 0.92)";
+export const USER_NOTES_DEFAULT_TEXTAREA_COLOR = "#111111";
diff --git a/scripts/user-notes-controls.js b/scripts/user-notes-controls.js
new file mode 100644
index 0000000..7f8d377
--- /dev/null
+++ b/scripts/user-notes-controls.js
@@ -0,0 +1,48 @@
+import {
+ USER_NOTES_MODULE_ID,
+ USER_NOTES_TOOL_ID
+} from "./user-notes-constants.js";
+
+import {
+ userNotesOpenNotes
+} from "./user-notes-window.js";
+
+export function userNotesRegisterTokenControl(controls) {
+ console.log("User Notes | registering token control", controls);
+
+ const tokenControl = controls?.tokens;
+
+ if (!tokenControl) {
+ console.warn("User Notes | controls.tokens fehlt", controls);
+ return;
+ }
+
+ if (!tokenControl.tools) {
+ console.warn("User Notes | controls.tokens.tools fehlt", tokenControl);
+ return;
+ }
+
+ tokenControl.tools[USER_NOTES_TOOL_ID] = {
+ name: USER_NOTES_TOOL_ID,
+ title: "User Notes öffnen",
+ icon: "fa-solid fa-note-sticky",
+ order: Object.keys(tokenControl.tools).length + 1,
+ button: true,
+ visible: true,
+
+ onChange: (event, active) => {
+ console.log("User Notes | onChange", { event, active });
+ userNotesOpenNotes();
+ },
+
+ onClick: event => {
+ console.log("User Notes | onClick", { event });
+ userNotesOpenNotes();
+ }
+ };
+
+ console.log(
+ "User Notes | token control registered",
+ tokenControl.tools[USER_NOTES_TOOL_ID]
+ );
+}
diff --git a/scripts/user-notes-settings.js b/scripts/user-notes-settings.js
new file mode 100644
index 0000000..9fec390
--- /dev/null
+++ b/scripts/user-notes-settings.js
@@ -0,0 +1,427 @@
+import {
+ USER_NOTES_MODULE_ID,
+ USER_NOTES_WINDOW_ID
+} from "./user-notes-constants.js";
+
+import {
+ userNotesLoadAppearance,
+ userNotesSaveAppearance,
+ userNotesRemoveSavedAppearance
+} from "./user-notes-storage.js";
+
+let resetPositionCallback = null;
+
+const USER_NOTES_APPEARANCE_DEFAULTS = {
+ windowBackgroundColor: "#191813",
+ windowBackgroundAlpha: 0.96,
+ windowTextColor: "#f0f0e0",
+ textareaBackgroundColor: "#ffffff",
+ textareaBackgroundAlpha: 0.92,
+ textareaTextColor: "#111111"
+};
+
+function userNotesClampAlpha(value, fallback = 1) {
+ const number = Number(value);
+
+ if (!Number.isFinite(number)) {
+ return fallback;
+ }
+
+ return Math.max(0, Math.min(1, number));
+}
+
+function userNotesNormalizeHexColor(value, fallback) {
+ const normalized = String(value ?? "").trim();
+
+ if (/^#[0-9a-f]{6}$/i.test(normalized)) {
+ return normalized;
+ }
+
+ return fallback;
+}
+
+function userNotesHexToRgba(hex, alpha) {
+ const normalized = userNotesNormalizeHexColor(hex, "#000000");
+ const match = normalized.match(/^#([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
+
+ if (!match) {
+ return `rgba(0, 0, 0, ${userNotesClampAlpha(alpha)})`;
+ }
+
+ const red = parseInt(match[1], 16);
+ const green = parseInt(match[2], 16);
+ const blue = parseInt(match[3], 16);
+
+ return `rgba(${red}, ${green}, ${blue}, ${userNotesClampAlpha(alpha)})`;
+}
+
+function userNotesLoadValidatedAppearance() {
+ const values = userNotesLoadAppearance(USER_NOTES_APPEARANCE_DEFAULTS);
+
+ return {
+ windowBackgroundColor: userNotesNormalizeHexColor(
+ values.windowBackgroundColor,
+ USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor
+ ),
+ windowBackgroundAlpha: userNotesClampAlpha(
+ values.windowBackgroundAlpha,
+ USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha
+ ),
+ windowTextColor: userNotesNormalizeHexColor(
+ values.windowTextColor,
+ USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor
+ ),
+ textareaBackgroundColor: userNotesNormalizeHexColor(
+ values.textareaBackgroundColor,
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor
+ ),
+ textareaBackgroundAlpha: userNotesClampAlpha(
+ values.textareaBackgroundAlpha,
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha
+ ),
+ textareaTextColor: userNotesNormalizeHexColor(
+ values.textareaTextColor,
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor
+ )
+ };
+}
+
+export function userNotesRegisterSettings(onResetPosition) {
+ resetPositionCallback = onResetPosition;
+
+ game.settings.registerMenu(USER_NOTES_MODULE_ID, "appearanceSettings", {
+ name: "Darstellung",
+ label: "Farben und Transparenz einstellen",
+ hint: "Öffnet lokale Colorpicker für Fenster, Notizfeld und Schriftfarben. Die Werte werden nur im localStorage dieses Browsers gespeichert.",
+ icon: "fas fa-palette",
+ type: UserNotesAppearanceSettings,
+ restricted: false
+ });
+
+ game.settings.registerMenu(USER_NOTES_MODULE_ID, "resetWindowPosition", {
+ name: "Position und Größe zurücksetzen",
+ label: "Jetzt zurücksetzen",
+ hint: "Setzt nur Position und Größe des User-Notes-Fensters für diesen Browser zurück. Notizen, Farben und Transparenzwerte bleiben unverändert.",
+ icon: "fas fa-undo",
+ type: UserNotesDirectResetWindowPosition,
+ restricted: false
+ });
+}
+
+export function userNotesApplySettingsToOpenWindow() {
+ const win = document.getElementById(USER_NOTES_WINDOW_ID);
+
+ if (!win) {
+ return;
+ }
+
+ userNotesApplyWindowSettings(win);
+}
+
+export function userNotesApplyWindowSettings(win) {
+ const appearance = userNotesLoadValidatedAppearance();
+
+ win.style.setProperty(
+ "--user-notes-window-background",
+ userNotesHexToRgba(
+ appearance.windowBackgroundColor,
+ appearance.windowBackgroundAlpha
+ )
+ );
+
+ win.style.setProperty(
+ "--user-notes-window-text-color",
+ appearance.windowTextColor
+ );
+
+ win.style.setProperty(
+ "--user-notes-textarea-background",
+ userNotesHexToRgba(
+ appearance.textareaBackgroundColor,
+ appearance.textareaBackgroundAlpha
+ )
+ );
+
+ win.style.setProperty(
+ "--user-notes-textarea-color",
+ appearance.textareaTextColor
+ );
+}
+
+class UserNotesDirectResetWindowPosition extends FormApplication {
+ render(_force, _options) {
+ if (typeof resetPositionCallback === "function") {
+ resetPositionCallback();
+ ui.notifications?.info("User Notes: Position und Größe wurden zurückgesetzt.");
+ } else {
+ console.warn("User Notes | resetPositionCallback is not available");
+ ui.notifications?.warn("User Notes: Reset-Funktion ist nicht verfügbar.");
+ }
+
+ return this;
+ }
+}
+
+class UserNotesAppearanceSettings extends FormApplication {
+ static get defaultOptions() {
+ return foundry.utils.mergeObject(super.defaultOptions, {
+ id: "user-notes-appearance-settings",
+ title: "User Notes Darstellung",
+ template: null,
+ width: 520,
+ height: "auto",
+ closeOnSubmit: false,
+ submitOnChange: false,
+ submitOnClose: false
+ });
+ }
+
+ getData() {
+ const appearance = userNotesLoadValidatedAppearance();
+
+ return {
+ ...appearance,
+ windowBackgroundAlphaPercent: Math.round(appearance.windowBackgroundAlpha * 100),
+ textareaBackgroundAlphaPercent: Math.round(appearance.textareaBackgroundAlpha * 100)
+ };
+ }
+
+ async _renderInner(data) {
+ const html = `
+
+
+ Diese Werte werden lokal im Browser gespeichert.
+ Foundry-Settings werden dadurch nicht überschrieben und die Seite wird nicht neu geladen.
+
+
+
+
+
+
+
+
+ `;
+
+ return $(html);
+ }
+
+ activateListeners(html) {
+ super.activateListeners(html);
+
+ const root = html[0];
+
+ if (!root) {
+ console.warn("User Notes | appearance settings root element not found");
+ return;
+ }
+
+ html.on("submit", event => {
+ event.preventDefault();
+ event.stopPropagation();
+ return false;
+ });
+
+ const syncColorPair = (colorName, textName) => {
+ const colorInput = root.querySelector(`input[name="${colorName}"]`);
+ const textInput = root.querySelector(`input[name="${textName}"]`);
+
+ if (!colorInput || !textInput) {
+ return;
+ }
+
+ colorInput.addEventListener("input", () => {
+ textInput.value = colorInput.value;
+ });
+
+ textInput.addEventListener("input", () => {
+ if (/^#[0-9a-f]{6}$/i.test(textInput.value)) {
+ colorInput.value = textInput.value;
+ }
+ });
+ };
+
+ syncColorPair("windowBackgroundColor", "windowBackgroundColorText");
+ syncColorPair("windowTextColor", "windowTextColorText");
+ syncColorPair("textareaBackgroundColor", "textareaBackgroundColorText");
+ syncColorPair("textareaTextColor", "textareaTextColorText");
+
+ for (const range of root.querySelectorAll('input[type="range"]')) {
+ const output = range
+ .closest(".user-notes-range-row")
+ ?.querySelector("output");
+
+ const updateOutput = () => {
+ if (output) {
+ output.textContent = `${Math.round(Number(range.value) * 100)}%`;
+ }
+ };
+
+ range.addEventListener("input", updateOutput);
+ range.addEventListener("change", updateOutput);
+ updateOutput();
+ }
+
+ root.querySelector(".user-notes-apply-appearance")?.addEventListener("click", event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.userNotesSaveAppearanceFromDialog(root, {
+ closeDialog: false,
+ notify: true
+ });
+ });
+
+ root.querySelector(".user-notes-save-appearance")?.addEventListener("click", event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ this.userNotesSaveAppearanceFromDialog(root, {
+ closeDialog: true,
+ notify: true
+ });
+ });
+
+ root.querySelector(".user-notes-reset-appearance")?.addEventListener("click", event => {
+ event.preventDefault();
+ event.stopPropagation();
+
+ userNotesRemoveSavedAppearance();
+ userNotesApplySettingsToOpenWindow();
+
+ ui.notifications?.info("User Notes: Standardfarben wurden wiederhergestellt.");
+ this.render(true);
+ });
+ }
+
+ userNotesSaveAppearanceFromDialog(root, options = {}) {
+ const closeDialog = options.closeDialog ?? true;
+ const notify = options.notify ?? true;
+
+ const getInputValue = name => {
+ const input = root.querySelector(`[name="${name}"]`);
+ return input?.value;
+ };
+
+ const appearance = {
+ windowBackgroundColor: userNotesNormalizeHexColor(
+ getInputValue("windowBackgroundColorText") || getInputValue("windowBackgroundColor"),
+ USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundColor
+ ),
+ windowBackgroundAlpha: userNotesClampAlpha(
+ getInputValue("windowBackgroundAlpha"),
+ USER_NOTES_APPEARANCE_DEFAULTS.windowBackgroundAlpha
+ ),
+ windowTextColor: userNotesNormalizeHexColor(
+ getInputValue("windowTextColorText") || getInputValue("windowTextColor"),
+ USER_NOTES_APPEARANCE_DEFAULTS.windowTextColor
+ ),
+ textareaBackgroundColor: userNotesNormalizeHexColor(
+ getInputValue("textareaBackgroundColorText") || getInputValue("textareaBackgroundColor"),
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundColor
+ ),
+ textareaBackgroundAlpha: userNotesClampAlpha(
+ getInputValue("textareaBackgroundAlpha"),
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaBackgroundAlpha
+ ),
+ textareaTextColor: userNotesNormalizeHexColor(
+ getInputValue("textareaTextColorText") || getInputValue("textareaTextColor"),
+ USER_NOTES_APPEARANCE_DEFAULTS.textareaTextColor
+ )
+ };
+
+ userNotesSaveAppearance(appearance);
+ userNotesApplySettingsToOpenWindow();
+
+ if (notify) {
+ ui.notifications?.info(
+ closeDialog
+ ? "User Notes: Darstellung wurde gespeichert."
+ : "User Notes: Darstellung wurde angewendet."
+ );
+ }
+
+ if (closeDialog) {
+ this.close();
+ }
+ }
+
+ async _updateObject(_event, _formData) {
+ // Wird absichtlich nicht verwendet.
+ // Die Buttons speichern direkt in localStorage, damit kein nativer GET-Submit stattfindet.
+ }
+}
diff --git a/scripts/user-notes-storage.js b/scripts/user-notes-storage.js
new file mode 100644
index 0000000..8ec28e8
--- /dev/null
+++ b/scripts/user-notes-storage.js
@@ -0,0 +1,67 @@
+import {
+ USER_NOTES_MODULE_ID
+} from "./user-notes-constants.js";
+
+export function userNotesStorageKey() {
+ const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
+ const userId = game.user?.id ?? "unknown-user";
+
+ return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.notes`;
+}
+
+export function userNotesPositionKey() {
+ const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
+ const userId = game.user?.id ?? "unknown-user";
+
+ return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.position`;
+}
+
+export function userNotesAppearanceKey() {
+ const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
+ const userId = game.user?.id ?? "unknown-user";
+
+ return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.appearance`;
+}
+
+export function userNotesLoadNotes() {
+ return window.localStorage.getItem(userNotesStorageKey()) ?? "";
+}
+
+export function userNotesSaveNotes(value) {
+ window.localStorage.setItem(userNotesStorageKey(), value);
+}
+
+export function userNotesRemoveSavedPosition() {
+ window.localStorage.removeItem(userNotesPositionKey());
+}
+
+export function userNotesLoadAppearance(defaults) {
+ try {
+ const raw = window.localStorage.getItem(userNotesAppearanceKey());
+
+ if (!raw) {
+ return { ...defaults };
+ }
+
+ const parsed = JSON.parse(raw);
+
+ return {
+ ...defaults,
+ ...parsed
+ };
+ } catch (err) {
+ console.warn("User Notes | Could not load appearance settings", err);
+ return { ...defaults };
+ }
+}
+
+export function userNotesSaveAppearance(values) {
+ window.localStorage.setItem(
+ userNotesAppearanceKey(),
+ JSON.stringify(values)
+ );
+}
+
+export function userNotesRemoveSavedAppearance() {
+ window.localStorage.removeItem(userNotesAppearanceKey());
+}
diff --git a/scripts/user-notes-window.js b/scripts/user-notes-window.js
new file mode 100644
index 0000000..2090b60
--- /dev/null
+++ b/scripts/user-notes-window.js
@@ -0,0 +1,358 @@
+import {
+ USER_NOTES_MODULE_ID,
+ USER_NOTES_WINDOW_ID,
+ USER_NOTES_DEFAULT_POSITION,
+ USER_NOTES_MIN_WIDTH,
+ USER_NOTES_MIN_HEIGHT,
+ USER_NOTES_VIEWPORT_MARGIN
+} from "./user-notes-constants.js";
+
+import {
+ userNotesLoadNotes,
+ userNotesSaveNotes,
+ userNotesPositionKey,
+ userNotesRemoveSavedPosition
+} from "./user-notes-storage.js";
+
+import {
+ userNotesApplyWindowSettings
+} from "./user-notes-settings.js";
+
+let userNotesSaveTimer = null;
+
+export function userNotesSetStatus(text) {
+ const status = document.querySelector(
+ `#${USER_NOTES_WINDOW_ID} .user-notes-status`
+ );
+
+ if (status) {
+ status.textContent = text;
+ }
+}
+
+export function userNotesDebouncedSave(value) {
+ window.clearTimeout(userNotesSaveTimer);
+ userNotesSetStatus("Ungespeichert …");
+
+ userNotesSaveTimer = window.setTimeout(() => {
+ userNotesSaveNotes(value);
+ userNotesSetStatus("Gespeichert");
+ }, 250);
+}
+
+export function userNotesApplyPosition(win, position) {
+ win.style.left = `${position.left}px`;
+ win.style.top = `${position.top}px`;
+ win.style.width = `${position.width}px`;
+ win.style.height = `${position.height}px`;
+}
+
+export function userNotesClampPosition(position) {
+ const viewportWidth = Math.max(
+ window.innerWidth,
+ USER_NOTES_MIN_WIDTH + USER_NOTES_VIEWPORT_MARGIN
+ );
+
+ const viewportHeight = Math.max(
+ window.innerHeight,
+ USER_NOTES_MIN_HEIGHT + USER_NOTES_VIEWPORT_MARGIN
+ );
+
+ const maxWidth = Math.max(
+ USER_NOTES_MIN_WIDTH,
+ viewportWidth - USER_NOTES_VIEWPORT_MARGIN
+ );
+
+ const maxHeight = Math.max(
+ USER_NOTES_MIN_HEIGHT,
+ viewportHeight - USER_NOTES_VIEWPORT_MARGIN
+ );
+
+ const width = Math.max(
+ USER_NOTES_MIN_WIDTH,
+ Math.min(position.width, maxWidth)
+ );
+
+ const height = Math.max(
+ USER_NOTES_MIN_HEIGHT,
+ Math.min(position.height, maxHeight)
+ );
+
+ const maxLeft = Math.max(0, viewportWidth - width - 20);
+ const maxTop = Math.max(0, viewportHeight - height - 20);
+
+ const left = Math.max(0, Math.min(position.left, maxLeft));
+ const top = Math.max(0, Math.min(position.top, maxTop));
+
+ return {
+ left,
+ top,
+ width,
+ height
+ };
+}
+
+export function userNotesRestorePosition(win) {
+ try {
+ const raw = window.localStorage.getItem(userNotesPositionKey());
+
+ if (!raw) {
+ userNotesApplyPosition(
+ win,
+ userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
+ );
+ return;
+ }
+
+ const pos = JSON.parse(raw);
+
+ const restoredPosition = {
+ left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left,
+ top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top,
+ width: Number.isFinite(pos.width) ? pos.width : USER_NOTES_DEFAULT_POSITION.width,
+ height: Number.isFinite(pos.height) ? pos.height : USER_NOTES_DEFAULT_POSITION.height
+ };
+
+ userNotesApplyPosition(
+ win,
+ userNotesClampPosition(restoredPosition)
+ );
+ } catch (err) {
+ console.warn(
+ `${USER_NOTES_MODULE_ID} | Could not restore note window position`,
+ err
+ );
+
+ userNotesApplyPosition(
+ win,
+ userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
+ );
+ }
+}
+
+export function userNotesSavePosition(win) {
+ if (!win || win.hidden) {
+ return;
+ }
+
+ const rect = win.getBoundingClientRect();
+
+ if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) {
+ return;
+ }
+
+ const position = userNotesClampPosition({
+ left: Math.round(rect.left),
+ top: Math.round(rect.top),
+ width: Math.round(rect.width),
+ height: Math.round(rect.height)
+ });
+
+ window.localStorage.setItem(
+ userNotesPositionKey(),
+ JSON.stringify(position)
+ );
+}
+
+export function userNotesResetPositionAndSize() {
+ userNotesRemoveSavedPosition();
+
+ const win = document.getElementById(USER_NOTES_WINDOW_ID);
+
+ if (win) {
+ userNotesApplyPosition(
+ win,
+ userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
+ );
+
+ userNotesSavePosition(win);
+ }
+}
+
+export function userNotesBringToFront(win) {
+ const currentTop = Number.parseInt(win.style.zIndex || "100000", 10);
+ win.style.zIndex = String(Math.max(currentTop + 1, 100000));
+}
+
+export function userNotesMakeDraggable(win) {
+ const handle = win.querySelector(".user-notes-titlebar");
+
+ if (!handle) {
+ return;
+ }
+
+ let drag = null;
+
+ handle.addEventListener("pointerdown", event => {
+ const target = event.target;
+
+ if (target instanceof HTMLElement && target.closest("button")) {
+ return;
+ }
+
+ const rect = win.getBoundingClientRect();
+
+ drag = {
+ pointerId: event.pointerId,
+ startX: event.clientX,
+ startY: event.clientY,
+ left: rect.left,
+ top: rect.top
+ };
+
+ userNotesBringToFront(win);
+ handle.setPointerCapture(event.pointerId);
+ });
+
+ handle.addEventListener("pointermove", event => {
+ if (!drag || drag.pointerId !== event.pointerId) {
+ return;
+ }
+
+ const rect = win.getBoundingClientRect();
+
+ const clamped = userNotesClampPosition({
+ left: drag.left + event.clientX - drag.startX,
+ top: drag.top + event.clientY - drag.startY,
+ width: rect.width,
+ height: rect.height
+ });
+
+ win.style.left = `${clamped.left}px`;
+ win.style.top = `${clamped.top}px`;
+ });
+
+ handle.addEventListener("pointerup", event => {
+ if (!drag || drag.pointerId !== event.pointerId) {
+ return;
+ }
+
+ drag = null;
+ userNotesSavePosition(win);
+ });
+
+ handle.addEventListener("pointercancel", event => {
+ if (!drag || drag.pointerId !== event.pointerId) {
+ return;
+ }
+
+ drag = null;
+ userNotesSavePosition(win);
+ });
+}
+
+export function userNotesOpenNotes() {
+ let win = document.getElementById(USER_NOTES_WINDOW_ID);
+
+ if (win) {
+ win.hidden = false;
+ userNotesRestorePosition(win);
+ userNotesApplyWindowSettings(win);
+ userNotesBringToFront(win);
+ win.querySelector("textarea")?.focus();
+ return;
+ }
+
+ win = document.createElement("section");
+ win.id = USER_NOTES_WINDOW_ID;
+ win.className = "user-notes-window";
+
+ win.innerHTML = `
+
+
+
+
+
+ `;
+
+ document.body.appendChild(win);
+
+ userNotesApplyWindowSettings(win);
+ userNotesRestorePosition(win);
+
+ const textarea = win.querySelector(".user-notes-textarea");
+
+ if (!(textarea instanceof HTMLTextAreaElement)) {
+ console.error(`${USER_NOTES_MODULE_ID} | Notes textarea could not be created.`);
+ return;
+ }
+
+ textarea.value = userNotesLoadNotes();
+
+ textarea.addEventListener("input", event => {
+ userNotesDebouncedSave(event.currentTarget.value);
+ });
+
+ win.querySelector(".user-notes-save")?.addEventListener("click", () => {
+ userNotesSaveNotes(textarea.value);
+ userNotesSetStatus("Gespeichert");
+ userNotesSavePosition(win);
+ });
+
+ win.querySelector(".user-notes-close")?.addEventListener("click", () => {
+ userNotesSaveNotes(textarea.value);
+ userNotesSavePosition(win);
+ win.hidden = true;
+ });
+
+ win.addEventListener("pointerdown", () => {
+ userNotesBringToFront(win);
+ });
+
+ const resizeObserver = new ResizeObserver(() => {
+ if (win.hidden) {
+ return;
+ }
+
+ const rect = win.getBoundingClientRect();
+
+ if (rect.width < USER_NOTES_MIN_WIDTH || rect.height < USER_NOTES_MIN_HEIGHT) {
+ return;
+ }
+
+ userNotesSavePosition(win);
+ });
+
+ resizeObserver.observe(win);
+
+ userNotesMakeDraggable(win);
+ userNotesBringToFront(win);
+ textarea.focus();
+}
+
+export function userNotesRefreshOpenWindow() {
+ const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
+
+ if (!oldWin) {
+ return;
+ }
+
+ const textarea = oldWin.querySelector(".user-notes-textarea");
+
+ if (textarea instanceof HTMLTextAreaElement) {
+ userNotesSaveNotes(textarea.value);
+ }
+
+ userNotesSavePosition(oldWin);
+
+ oldWin.remove();
+
+ userNotesOpenNotes();
+}
diff --git a/scripts/user-notes.js b/scripts/user-notes.js
index c55e7bc..572acb9 100644
--- a/scripts/user-notes.js
+++ b/scripts/user-notes.js
@@ -1,236 +1,44 @@
-/*
- * User Notes for Foundry VTT v14
- * Stores notes in window.localStorage per world and per user.
- */
+import {
+ userNotesRegisterSettings
+} from "./user-notes-settings.js";
-const LBN_MODULE_ID = "user-notes";
-const LBN_WINDOW_ID = "lbn-notes-window";
-const LBN_BUTTON_ID = "lbn-open-notes";
+import {
+ userNotesRegisterTokenControl
+} from "./user-notes-controls.js";
-let lbnSaveTimer = null;
-let lbnObserverTimer = null;
+import {
+ userNotesOpenNotes,
+ userNotesResetPositionAndSize,
+ userNotesRefreshOpenWindow
+} from "./user-notes-window.js";
-function lbnStorageKey() {
- const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
- const userId = game.user?.id ?? "unknown-user";
- return `${LBN_MODULE_ID}.${worldId}.${userId}.notes`;
-}
+console.log("User Notes | ES module loaded");
-function lbnPositionKey() {
- const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
- const userId = game.user?.id ?? "unknown-user";
- return `${LBN_MODULE_ID}.${worldId}.${userId}.position`;
-}
+globalThis.UserNotes = {
+ open: userNotesOpenNotes,
+ resetPosition: userNotesResetPositionAndSize,
+ refresh: userNotesRefreshOpenWindow
+};
-function lbnLoadNotes() {
- return window.localStorage.getItem(lbnStorageKey()) ?? "";
-}
+Hooks.once("init", () => {
+ console.log("User Notes | init hook fired");
-function lbnSaveNotes(value) {
- window.localStorage.setItem(lbnStorageKey(), value);
-}
-
-function lbnSetStatus(text) {
- const status = document.querySelector(`#${LBN_WINDOW_ID} .lbn-status`);
- if (status) status.textContent = text;
-}
-
-function lbnDebouncedSave(value) {
- window.clearTimeout(lbnSaveTimer);
- lbnSetStatus("Ungespeichert …");
- lbnSaveTimer = window.setTimeout(() => {
- lbnSaveNotes(value);
- lbnSetStatus("Gespeichert");
- }, 250);
-}
-
-function lbnRestorePosition(win) {
try {
- const raw = window.localStorage.getItem(lbnPositionKey());
- if (!raw) return;
- const pos = JSON.parse(raw);
- if (Number.isFinite(pos.left)) win.style.left = `${pos.left}px`;
- if (Number.isFinite(pos.top)) win.style.top = `${pos.top}px`;
- if (Number.isFinite(pos.width)) win.style.width = `${pos.width}px`;
- if (Number.isFinite(pos.height)) win.style.height = `${pos.height}px`;
+ userNotesRegisterSettings(userNotesResetPositionAndSize);
+ console.log("User Notes | settings registered");
} catch (err) {
- console.warn(`${LBN_MODULE_ID} | Could not restore note window position`, err);
- }
-}
-
-function lbnSavePosition(win) {
- const rect = win.getBoundingClientRect();
- window.localStorage.setItem(lbnPositionKey(), JSON.stringify({
- left: Math.round(rect.left),
- top: Math.round(rect.top),
- width: Math.round(rect.width),
- height: Math.round(rect.height)
- }));
-}
-
-function lbnMakeDraggable(win) {
- const handle = win.querySelector(".lbn-titlebar");
- if (!handle) return;
-
- let drag = null;
-
- handle.addEventListener("pointerdown", event => {
- if (event.target.closest("button")) return;
-
- const rect = win.getBoundingClientRect();
- drag = {
- pointerId: event.pointerId,
- startX: event.clientX,
- startY: event.clientY,
- left: rect.left,
- top: rect.top
- };
-
- handle.setPointerCapture(event.pointerId);
- event.preventDefault();
- });
-
- handle.addEventListener("pointermove", event => {
- if (!drag || drag.pointerId !== event.pointerId) return;
-
- const nextLeft = Math.max(0, Math.min(window.innerWidth - 80, drag.left + event.clientX - drag.startX));
- const nextTop = Math.max(0, Math.min(window.innerHeight - 40, drag.top + event.clientY - drag.startY));
-
- win.style.left = `${nextLeft}px`;
- win.style.top = `${nextTop}px`;
- });
-
- handle.addEventListener("pointerup", event => {
- if (!drag || drag.pointerId !== event.pointerId) return;
- drag = null;
- lbnSavePosition(win);
- });
-}
-
-function lbnOpenNotes() {
- let win = document.getElementById(LBN_WINDOW_ID);
-
- if (win) {
- win.hidden = false;
- win.classList.add("active");
- win.querySelector("textarea")?.focus();
- return;
- }
-
- win = document.createElement("section");
- win.id = LBN_WINDOW_ID;
- win.className = "lbn-notes-window";
- win.innerHTML = `
-
-
- `;
-
- document.body.appendChild(win);
-
- const textarea = win.querySelector(".lbn-textarea");
- textarea.value = lbnLoadNotes();
-
- textarea.addEventListener("input", event => {
- lbnDebouncedSave(event.currentTarget.value);
- });
-
- win.querySelector(".lbn-save").addEventListener("click", () => {
- lbnSaveNotes(textarea.value);
- lbnSetStatus("Gespeichert");
- });
-
- win.querySelector(".lbn-close").addEventListener("click", () => {
- lbnSaveNotes(textarea.value);
- lbnSavePosition(win);
- win.hidden = true;
- });
-
- new ResizeObserver(() => lbnSavePosition(win)).observe(win);
-
- lbnRestorePosition(win);
- lbnMakeDraggable(win);
- textarea.focus();
-}
-
-function lbnAsHTMLElement(html) {
- if (html instanceof HTMLElement) return html;
- if (html?.[0] instanceof HTMLElement) return html[0]; // legacy jQuery-style hook argument
- if (html?.element instanceof HTMLElement) return html.element;
- return null;
-}
-
-function lbnFindPlayersElement(renderedElement = null) {
- return renderedElement?.id === "players"
- ? renderedElement
- : renderedElement?.querySelector?.("#players")
- ?? document.querySelector("#players")
- ?? document.querySelector("[data-application-id='players']")
- ?? document.querySelector(".players");
-}
-
-function lbnInjectButton(renderedElement = null) {
- const players = lbnFindPlayersElement(renderedElement);
- if (!players || players.querySelector(`#${LBN_BUTTON_ID}`)) return;
-
- const header =
- players.querySelector(".window-header")
- ?? players.querySelector("header")
- ?? players.querySelector("h3")
- ?? players.querySelector("h2")
- ?? players;
-
- const button = document.createElement("button");
- button.id = LBN_BUTTON_ID;
- button.type = "button";
- button.className = "lbn-player-note-button";
- button.title = "User Notes öffnen";
- button.setAttribute("aria-label", "User Notes öffnen");
- button.innerHTML = ``;
-
- button.addEventListener("click", event => {
- event.preventDefault();
- event.stopPropagation();
- lbnOpenNotes();
- });
-
- header.appendChild(button);
-}
-
-Hooks.once("ready", () => {
- lbnInjectButton();
-
- // Fallback: Die Spieler-/Benutzerliste kann bei Theme-, Layout- oder Popout-Änderungen neu gerendert werden.
- const observer = new MutationObserver(() => {
- window.clearTimeout(lbnObserverTimer);
- lbnObserverTimer = window.setTimeout(() => lbnInjectButton(), 100);
- });
-
- observer.observe(document.body, { childList: true, subtree: true });
-});
-
-Hooks.on("renderApplicationV2", (app, html) => {
- const element = lbnAsHTMLElement(html) ?? app?.element ?? null;
- if (app?.constructor?.name === "Players" || lbnFindPlayersElement(element)) {
- window.queueMicrotask(() => lbnInjectButton(element));
+ console.error("User Notes | error during init", err);
+ ui.notifications?.error("User Notes: Fehler beim Initialisieren. Details stehen in der Browser-Konsole.");
}
});
-// Kompatibilitäts-Fallback für Installationen/Module, die noch einen spezifischen PlayerList-Hook auslösen.
-Hooks.on("renderPlayerList", (_app, html) => {
- const element = lbnAsHTMLElement(html);
- window.queueMicrotask(() => lbnInjectButton(element));
+Hooks.on("getSceneControlButtons", controls => {
+ console.log("User Notes | getSceneControlButtons fired");
+
+ try {
+ userNotesRegisterTokenControl(controls);
+ } catch (err) {
+ console.error("User Notes | error while registering token control", err);
+ ui.notifications?.error("User Notes: Fehler beim Registrieren des Token-Controls.");
+ }
});
diff --git a/styles/user-notes-settings.css b/styles/user-notes-settings.css
new file mode 100644
index 0000000..db1a3a9
--- /dev/null
+++ b/styles/user-notes-settings.css
@@ -0,0 +1,90 @@
+.user-notes-appearance-settings {
+ padding: 0.75rem;
+}
+
+.user-notes-appearance-settings .notes {
+ margin-top: 0;
+ margin-bottom: 0.75rem;
+}
+
+.user-notes-appearance-settings fieldset {
+ margin: 0 0 0.75rem 0;
+ padding: 0.75rem;
+
+ border: 1px solid var(--color-border-light-primary, #7a695a);
+ border-radius: 4px;
+}
+
+.user-notes-appearance-settings legend {
+ padding: 0 0.35rem;
+ font-weight: bold;
+}
+
+.user-notes-appearance-settings .form-group {
+ align-items: center;
+}
+
+.user-notes-appearance-settings .form-fields {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.user-notes-appearance-settings input[type="color"] {
+ width: 3.5rem;
+ min-width: 3.5rem;
+ height: 2rem;
+
+ padding: 0;
+ border: 1px solid var(--color-border-light-primary, #7a695a);
+ border-radius: 4px;
+
+ background: transparent;
+ cursor: pointer;
+}
+
+.user-notes-appearance-settings input[type="text"] {
+ flex: 1 1 auto;
+ min-width: 0;
+}
+
+.user-notes-range-row {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.user-notes-range-row input[type="range"] {
+ flex: 1 1 auto;
+}
+
+.user-notes-range-row output {
+ width: 3rem;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.user-notes-appearance-settings .sheet-footer,
+.user-notes-appearance-footer {
+ margin-top: 0.75rem;
+
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.user-notes-primary-actions {
+ flex: 1 1 auto;
+
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+}
+
+.user-notes-primary-actions button {
+ flex: 1 1 0;
+}
+
+.user-notes-appearance-footer > .user-notes-reset-appearance {
+ flex: 0 0 auto;
+}
diff --git a/styles/user-notes-window.css b/styles/user-notes-window.css
new file mode 100644
index 0000000..b59dd85
--- /dev/null
+++ b/styles/user-notes-window.css
@@ -0,0 +1,133 @@
+.user-notes-window {
+ position: fixed;
+ z-index: 100000;
+ left: 200px;
+ top: 150px;
+
+ width: 500px;
+ height: 400px;
+
+ min-width: 280px;
+ min-height: 180px;
+
+ max-width: calc(100vw - 40px);
+ max-height: calc(100vh - 40px);
+
+ display: flex;
+ flex-direction: column;
+
+ resize: both;
+ overflow: hidden;
+
+ box-sizing: border-box;
+
+ border: 1px solid var(--color-border-dark, #000);
+ border-radius: 6px;
+
+ background: var(--user-notes-window-background, rgba(25, 24, 19, 0.96));
+ color: var(--user-notes-window-text-color, #f0f0e0);
+
+ box-shadow: 0 0 16px rgba(0, 0, 0, 0.55);
+}
+
+.user-notes-window[hidden] {
+ display: none;
+}
+
+.user-notes-titlebar {
+ flex: 0 0 auto;
+
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.5rem;
+
+ padding: 0.45rem 0.55rem;
+
+ background: rgba(0, 0, 0, 0.35);
+ border-bottom: 1px solid var(--color-border-dark, #000);
+
+ cursor: move;
+ user-select: none;
+}
+
+.user-notes-title,
+.user-notes-controls {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+}
+
+.user-notes-title {
+ min-width: 0;
+ font-weight: bold;
+}
+
+.user-notes-status {
+ opacity: 0.75;
+ font-size: 0.78rem;
+ white-space: nowrap;
+}
+
+.user-notes-controls button {
+ width: 1.55rem;
+ height: 1.55rem;
+
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+
+ padding: 0;
+
+ border: 1px solid var(--color-border-light-primary, #7a695a);
+ border-radius: 4px;
+
+ background: rgba(0, 0, 0, 0.18);
+ color: var(--user-notes-window-text-color, #f0f0e0);
+
+ cursor: pointer;
+}
+
+.user-notes-controls button:hover {
+ background: rgba(255, 255, 255, 0.16);
+}
+
+.user-notes-content {
+ flex: 1 1 auto;
+ min-height: 0;
+
+ display: flex;
+ padding: 0.5rem;
+
+ box-sizing: border-box;
+}
+
+.user-notes-textarea {
+ flex: 1 1 auto;
+
+ width: 100%;
+ height: 100%;
+
+ min-width: 0;
+ min-height: 0;
+
+ resize: none;
+
+ box-sizing: border-box;
+ padding: 0.55rem;
+
+ border: 1px solid var(--color-border-light-primary, #7a695a);
+ border-radius: 4px;
+
+ background: var(--user-notes-textarea-background, rgba(255, 255, 255, 0.92));
+ color: var(--user-notes-textarea-color, #111111);
+
+ font-family: inherit;
+ font-size: 0.95rem;
+ line-height: 1.35;
+}
+
+.user-notes-textarea:focus {
+ outline: 1px solid var(--color-border-highlight, #ff6400);
+ outline-offset: 0;
+}
diff --git a/styles/user-notes.css b/styles/user-notes.css
deleted file mode 100644
index f9bd872..0000000
--- a/styles/user-notes.css
+++ /dev/null
@@ -1,100 +0,0 @@
-.lbn-player-note-button {
- display: inline-flex;
- align-items: center;
- justify-content: center;
- width: 1.65rem;
- height: 1.65rem;
- margin-left: 0.35rem;
- padding: 0;
- border: 1px solid var(--color-border-light-primary, #7a695a);
- border-radius: 4px;
- background: rgba(0, 0, 0, 0.18);
- color: var(--color-text-light-highlight, #f0f0e0);
- cursor: pointer;
-}
-
-.lbn-player-note-button:hover {
- background: rgba(255, 255, 255, 0.12);
-}
-
-.lbn-notes-window {
- position: fixed;
- z-index: 100000;
- left: 140px;
- top: 120px;
- width: 420px;
- height: 330px;
- min-width: 260px;
- min-height: 180px;
- display: flex;
- flex-direction: column;
- resize: both;
- overflow: hidden;
- border: 1px solid var(--color-border-dark, #000);
- border-radius: 6px;
- background: var(--color-bg, #1f1f1f);
- box-shadow: 0 0 16px rgba(0, 0, 0, 0.55);
-}
-
-.lbn-notes-window[hidden] {
- display: none;
-}
-
-.lbn-titlebar {
- flex: 0 0 auto;
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 0.5rem;
- min-height: 2rem;
- padding: 0.25rem 0.4rem;
- background: rgba(0, 0, 0, 0.34);
- color: var(--color-text-light-highlight, #f0f0e0);
- cursor: move;
- user-select: none;
-}
-
-.lbn-title,
-.lbn-controls {
- display: inline-flex;
- align-items: center;
- gap: 0.4rem;
-}
-
-.lbn-status {
- opacity: 0.75;
- font-size: 0.78rem;
- white-space: nowrap;
-}
-
-.lbn-controls button {
- width: 1.55rem;
- height: 1.55rem;
- padding: 0;
- border: 1px solid var(--color-border-light-primary, #7a695a);
- border-radius: 4px;
- background: rgba(255, 255, 255, 0.08);
- color: inherit;
- cursor: pointer;
-}
-
-.lbn-controls button:hover {
- background: rgba(255, 255, 255, 0.16);
-}
-
-.lbn-textarea {
- flex: 1 1 auto;
- width: 100%;
- min-height: 0;
- box-sizing: border-box;
- resize: none;
- border: 0;
- border-top: 1px solid rgba(255, 255, 255, 0.1);
- padding: 0.65rem;
- font-family: var(--font-primary, sans-serif);
- font-size: 0.95rem;
- line-height: 1.4;
- color: var(--color-text-light-highlight, #f0f0e0);
- background: rgba(255, 255, 255, 0.045);
- outline: none;
-}