mirror of
https://github.com/fzumpe/foundry-usernotes.git
synced 2026-06-06 21:00:03 +02:00
add import and export and fix local save structure
This commit is contained in:
parent
d4671086a4
commit
81410869f4
474
scripts/user-notes-backup.js
Normal file
474
scripts/user-notes-backup.js
Normal file
@ -0,0 +1,474 @@
|
|||||||
|
import {
|
||||||
|
USER_NOTES_MODULE_ID
|
||||||
|
} from "./user-notes-constants.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesDecodeBase64,
|
||||||
|
userNotesEncodeBase64,
|
||||||
|
userNotesLoadAppearance,
|
||||||
|
userNotesLoadNotes,
|
||||||
|
userNotesLoadPosition,
|
||||||
|
userNotesSaveAppearance,
|
||||||
|
userNotesSaveNotes,
|
||||||
|
userNotesSavePositionData
|
||||||
|
} from "./user-notes-storage.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesEscapePlainTextAsHtml,
|
||||||
|
userNotesSanitizeHtml
|
||||||
|
} from "./user-notes-sanitize.js";
|
||||||
|
|
||||||
|
const USER_NOTES_EXPORT_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
const USER_NOTES_APPEARANCE_DEFAULTS_FOR_BACKUP = {
|
||||||
|
windowBackgroundColor: "#191813",
|
||||||
|
windowBackgroundAlpha: 0.96,
|
||||||
|
windowTextColor: "#f0f0e0",
|
||||||
|
textareaBackgroundColor: "#ffffff",
|
||||||
|
textareaBackgroundAlpha: 0.92,
|
||||||
|
textareaTextColor: "#111111"
|
||||||
|
};
|
||||||
|
|
||||||
|
function userNotesDownloadJson(filename, data) {
|
||||||
|
const json = JSON.stringify(data, null, 2);
|
||||||
|
const blob = new Blob([json], { type: "application/json" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
const link = document.createElement("a");
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
|
||||||
|
document.body.appendChild(link);
|
||||||
|
link.click();
|
||||||
|
link.remove();
|
||||||
|
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesReadJsonFile(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = () => {
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(String(reader.result ?? "")));
|
||||||
|
} catch (err) {
|
||||||
|
reject(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesBuildExportData(options) {
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
if (options.includeNotes) {
|
||||||
|
data.notes = {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: userNotesEncodeBase64(
|
||||||
|
userNotesSanitizeHtml(userNotesLoadNotes())
|
||||||
|
)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includeAppearance) {
|
||||||
|
data.appearance = userNotesLoadAppearance(
|
||||||
|
USER_NOTES_APPEARANCE_DEFAULTS_FOR_BACKUP
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.includePosition) {
|
||||||
|
const position = userNotesLoadPosition(null);
|
||||||
|
|
||||||
|
if (position) {
|
||||||
|
data.position = position;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
module: USER_NOTES_MODULE_ID,
|
||||||
|
schemaVersion: USER_NOTES_EXPORT_SCHEMA_VERSION,
|
||||||
|
exportedAt: new Date().toISOString(),
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesNormalizeImportedNotes(notes) {
|
||||||
|
if (typeof notes === "string") {
|
||||||
|
return userNotesEscapePlainTextAsHtml(notes);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!notes || typeof notes !== "object") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.encoding === "base64") {
|
||||||
|
return userNotesSanitizeHtml(
|
||||||
|
userNotesDecodeBase64(notes.data ?? notes.content ?? "")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.format === "html") {
|
||||||
|
return userNotesSanitizeHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (notes.format === "text") {
|
||||||
|
return userNotesEscapePlainTextAsHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNotesSanitizeHtml(notes.content ?? notes.data ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesValidateImportData(importData) {
|
||||||
|
if (!importData || typeof importData !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (importData.module !== USER_NOTES_MODULE_ID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!importData.data || typeof importData.data !== "object") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesApplyImportData(importData, options) {
|
||||||
|
const data = importData?.data ?? {};
|
||||||
|
|
||||||
|
let changed = false;
|
||||||
|
|
||||||
|
if (options.importNotes && data.notes !== undefined) {
|
||||||
|
const importedNotes = userNotesNormalizeImportedNotes(data.notes);
|
||||||
|
|
||||||
|
if (options.noteMode === "append") {
|
||||||
|
const existing = userNotesSanitizeHtml(userNotesLoadNotes());
|
||||||
|
const separator = existing.trim() ? "<hr />" : "";
|
||||||
|
|
||||||
|
userNotesSaveNotes(`${existing}${separator}${importedNotes}`);
|
||||||
|
} else {
|
||||||
|
userNotesSaveNotes(importedNotes);
|
||||||
|
}
|
||||||
|
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.importAppearance && data.appearance) {
|
||||||
|
userNotesSaveAppearance(data.appearance);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.importPosition && data.position) {
|
||||||
|
userNotesSavePositionData(data.position);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
globalThis.UserNotes?.refresh?.({
|
||||||
|
saveCurrentContent: false
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
"User Notes | Import was saved, but refreshing the open window failed",
|
||||||
|
err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserNotesExportSettings extends FormApplication {
|
||||||
|
static get defaultOptions() {
|
||||||
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
id: "user-notes-export-settings",
|
||||||
|
title: "User Notes Export",
|
||||||
|
template: null,
|
||||||
|
width: 480,
|
||||||
|
height: "auto",
|
||||||
|
closeOnSubmit: false,
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnClose: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _renderInner() {
|
||||||
|
return $(`
|
||||||
|
<div class="user-notes-backup-settings">
|
||||||
|
<p class="notes">
|
||||||
|
Exportiert ausgewählte lokale Browserdaten als JSON-Datei.
|
||||||
|
Die Datei enthält keine Welt- oder Benutzerdaten und wird beim Import
|
||||||
|
immer in den aktuell geöffneten Scope übernommen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-notes">Notizen exportieren</label>
|
||||||
|
<input id="user-notes-export-notes" type="checkbox" name="includeNotes" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-appearance">Farben und Transparenz exportieren</label>
|
||||||
|
<input id="user-notes-export-appearance" type="checkbox" name="includeAppearance" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-export-position">Fensterposition und Größe exportieren</label>
|
||||||
|
<input id="user-notes-export-position" type="checkbox" name="includePosition" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="sheet-footer user-notes-backup-footer">
|
||||||
|
<button type="button" class="user-notes-export-now">
|
||||||
|
<i class="fas fa-file-export"></i>
|
||||||
|
Exportieren
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
|
||||||
|
html.on("click", ".user-notes-export-now", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const root = html[0];
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
console.warn("User Notes | export settings root element not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
includeNotes: root.querySelector('[name="includeNotes"]')?.checked ?? false,
|
||||||
|
includeAppearance: root.querySelector('[name="includeAppearance"]')?.checked ?? false,
|
||||||
|
includePosition: root.querySelector('[name="includePosition"]')?.checked ?? false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!options.includeNotes && !options.includeAppearance && !options.includePosition) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte mindestens einen Export-Inhalt auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = userNotesBuildExportData(options);
|
||||||
|
const date = new Date().toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
userNotesDownloadJson(`user-notes-${date}.json`, exportData);
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Export wurde erstellt.");
|
||||||
|
this.close();
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateObject(_event, _formData) {
|
||||||
|
// Nicht verwendet. Buttons verarbeiten den Export direkt.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserNotesImportSettings extends FormApplication {
|
||||||
|
constructor(...args) {
|
||||||
|
super(...args);
|
||||||
|
this.importData = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get defaultOptions() {
|
||||||
|
return foundry.utils.mergeObject(super.defaultOptions, {
|
||||||
|
id: "user-notes-import-settings",
|
||||||
|
title: "User Notes Import",
|
||||||
|
template: null,
|
||||||
|
width: 560,
|
||||||
|
height: "auto",
|
||||||
|
closeOnSubmit: false,
|
||||||
|
submitOnChange: false,
|
||||||
|
submitOnClose: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _renderInner() {
|
||||||
|
const hasData = Boolean(this.importData?.data);
|
||||||
|
const hasNotes = hasData && this.importData.data.notes !== undefined;
|
||||||
|
const hasAppearance = hasData && Boolean(this.importData.data.appearance);
|
||||||
|
const hasPosition = hasData && Boolean(this.importData.data.position);
|
||||||
|
|
||||||
|
return $(`
|
||||||
|
<div class="user-notes-backup-settings">
|
||||||
|
<p class="notes">
|
||||||
|
Importiert eine User-Notes-JSON-Datei in die aktuell geöffnete Welt
|
||||||
|
und für den aktuell eingeloggten Benutzer.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="notes warning">
|
||||||
|
Sicherheits-Hinweis: Importiertes HTML wird streng bereinigt.
|
||||||
|
Skripte, Eventhandler, eingebettete aktive Inhalte, unsichere URLs und
|
||||||
|
nicht erlaubte HTML-Elemente werden entfernt. Dadurch können
|
||||||
|
Formatierungen verloren gehen.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>JSON-Datei</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<input type="file" name="importFile" accept="application/json,.json">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-notes">Notizen importieren</label>
|
||||||
|
<input id="user-notes-import-notes" type="checkbox" name="importNotes" ${hasNotes ? "checked" : ""} ${hasNotes ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Notizmodus</label>
|
||||||
|
<div class="form-fields">
|
||||||
|
<select name="noteMode" ${hasNotes ? "" : "disabled"}>
|
||||||
|
<option value="overwrite">Vorhandene Notizen überschreiben</option>
|
||||||
|
<option value="append">Importierte Notizen anhängen</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-appearance">Farben und Transparenz importieren</label>
|
||||||
|
<input id="user-notes-import-appearance" type="checkbox" name="importAppearance" ${hasAppearance ? "checked" : ""} ${hasAppearance ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-notes-checkbox-row">
|
||||||
|
<label for="user-notes-import-position">Fensterposition und Größe importieren</label>
|
||||||
|
<input id="user-notes-import-position" type="checkbox" name="importPosition" ${hasPosition ? "checked" : ""} ${hasPosition ? "" : "disabled"}>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="sheet-footer user-notes-backup-footer">
|
||||||
|
<button type="button" class="user-notes-import-now" ${hasData ? "" : "disabled"}>
|
||||||
|
<i class="fas fa-file-import"></i>
|
||||||
|
Importieren
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
activateListeners(html) {
|
||||||
|
super.activateListeners(html);
|
||||||
|
|
||||||
|
html.on("change", 'input[name="importFile"]', async event => {
|
||||||
|
const fileInput = event.currentTarget;
|
||||||
|
const file = fileInput.files?.[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const json = await userNotesReadJsonFile(file);
|
||||||
|
|
||||||
|
if (!userNotesValidateImportData(json)) {
|
||||||
|
ui.notifications?.error("User Notes: Diese JSON-Datei ist kein gültiger User-Notes-Export.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.importData = json;
|
||||||
|
|
||||||
|
console.log("User Notes | import file loaded", {
|
||||||
|
hasNotes: json.data?.notes !== undefined,
|
||||||
|
hasAppearance: Boolean(json.data?.appearance),
|
||||||
|
hasPosition: Boolean(json.data?.position)
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Importdatei wurde gelesen.");
|
||||||
|
this.render(true);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("User Notes | Import failed while reading JSON", err);
|
||||||
|
ui.notifications?.error("User Notes: JSON-Datei konnte nicht gelesen werden.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html.on("click", ".user-notes-import-now", event => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
const root = html[0];
|
||||||
|
|
||||||
|
console.log("User Notes | import button clicked", {
|
||||||
|
hasImportData: Boolean(this.importData)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!root) {
|
||||||
|
console.warn("User Notes | import settings root element not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.importData) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte zuerst eine JSON-Datei auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
importNotes: root.querySelector('[name="importNotes"]')?.checked ?? false,
|
||||||
|
noteMode: root.querySelector('[name="noteMode"]')?.value ?? "overwrite",
|
||||||
|
importAppearance: root.querySelector('[name="importAppearance"]')?.checked ?? false,
|
||||||
|
importPosition: root.querySelector('[name="importPosition"]')?.checked ?? false
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log("User Notes | import options", options);
|
||||||
|
|
||||||
|
if (!options.importNotes && !options.importAppearance && !options.importPosition) {
|
||||||
|
ui.notifications?.warn("User Notes: Bitte mindestens einen Import-Inhalt auswählen.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const changed = userNotesApplyImportData(this.importData, options);
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
ui.notifications?.warn("User Notes: Es wurden keine importierbaren Daten angewendet.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.notifications?.info("User Notes: Import wurde angewendet.");
|
||||||
|
this.close();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("User Notes | Import failed while applying data", err);
|
||||||
|
ui.notifications?.error("User Notes: Import konnte nicht angewendet werden. Details stehen in der Browser-Konsole.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async _updateObject(_event, _formData) {
|
||||||
|
// Nicht verwendet. Buttons verarbeiten den Import direkt.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRegisterBackupSettings() {
|
||||||
|
game.settings.registerMenu(USER_NOTES_MODULE_ID, "exportData", {
|
||||||
|
name: "Export",
|
||||||
|
label: "Notizen exportieren",
|
||||||
|
hint: "Exportiert ausgewählte lokale User-Notes-Daten als JSON-Datei.",
|
||||||
|
icon: "fas fa-file-export",
|
||||||
|
type: UserNotesExportSettings,
|
||||||
|
restricted: false
|
||||||
|
});
|
||||||
|
|
||||||
|
game.settings.registerMenu(USER_NOTES_MODULE_ID, "importData", {
|
||||||
|
name: "Import",
|
||||||
|
label: "Notizen importieren",
|
||||||
|
hint: "Importiert Notizen, Darstellung und/oder Fensterposition aus einer JSON-Datei. Importiertes HTML wird aus Sicherheitsgründen streng bereinigt.",
|
||||||
|
icon: "fas fa-file-import",
|
||||||
|
type: UserNotesImportSettings,
|
||||||
|
restricted: false
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -2,66 +2,270 @@ import {
|
|||||||
USER_NOTES_MODULE_ID
|
USER_NOTES_MODULE_ID
|
||||||
} from "./user-notes-constants.js";
|
} from "./user-notes-constants.js";
|
||||||
|
|
||||||
export function userNotesStorageKey() {
|
const USER_NOTES_STORAGE_SCHEMA_VERSION = 2;
|
||||||
|
|
||||||
|
function userNotesBaseKey() {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
const userId = game.user?.id ?? "unknown-user";
|
||||||
|
|
||||||
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.notes`;
|
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesContentKey() {
|
||||||
|
return `${userNotesBaseKey()}.content`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesStateKey() {
|
||||||
|
return `${userNotesBaseKey()}.state`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy keys from older versions.
|
||||||
|
* Kept only for migration.
|
||||||
|
*/
|
||||||
|
export function userNotesStorageKey() {
|
||||||
|
return `${userNotesBaseKey()}.notes`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesPositionKey() {
|
export function userNotesPositionKey() {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
return `${userNotesBaseKey()}.position`;
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
|
||||||
|
|
||||||
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.position`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesAppearanceKey() {
|
export function userNotesAppearanceKey() {
|
||||||
const worldId = game.world?.id ?? game.world?.data?.id ?? "unknown-world";
|
return `${userNotesBaseKey()}.appearance`;
|
||||||
const userId = game.user?.id ?? "unknown-user";
|
|
||||||
|
|
||||||
return `${USER_NOTES_MODULE_ID}.${worldId}.${userId}.appearance`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesLoadNotes() {
|
export function userNotesEncodeBase64(value) {
|
||||||
return window.localStorage.getItem(userNotesStorageKey()) ?? "";
|
const bytes = new TextEncoder().encode(String(value ?? ""));
|
||||||
|
const chunkSize = 0x8000;
|
||||||
|
let binary = "";
|
||||||
|
|
||||||
|
for (let index = 0; index < bytes.length; index += chunkSize) {
|
||||||
|
const chunk = bytes.subarray(index, index + chunkSize);
|
||||||
|
binary += String.fromCharCode(...chunk);
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesSaveNotes(value) {
|
export function userNotesDecodeBase64(value) {
|
||||||
window.localStorage.setItem(userNotesStorageKey(), value);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userNotesRemoveSavedPosition() {
|
|
||||||
window.localStorage.removeItem(userNotesPositionKey());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function userNotesLoadAppearance(defaults) {
|
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(userNotesAppearanceKey());
|
const binary = window.atob(String(value ?? ""));
|
||||||
|
const bytes = new Uint8Array(binary.length);
|
||||||
|
|
||||||
if (!raw) {
|
for (let index = 0; index < binary.length; index++) {
|
||||||
return { ...defaults };
|
bytes[index] = binary.charCodeAt(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = JSON.parse(raw);
|
return new TextDecoder().decode(bytes);
|
||||||
|
|
||||||
return {
|
|
||||||
...defaults,
|
|
||||||
...parsed
|
|
||||||
};
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("User Notes | Could not load appearance settings", err);
|
console.warn("User Notes | Could not decode base64 content", err);
|
||||||
return { ...defaults };
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesSaveAppearance(values) {
|
function userNotesRemoveLegacyStorageKeys() {
|
||||||
|
window.localStorage.removeItem(userNotesStorageKey());
|
||||||
|
window.localStorage.removeItem(userNotesPositionKey());
|
||||||
|
window.localStorage.removeItem(userNotesAppearanceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesReadJson(key, fallback = null) {
|
||||||
|
try {
|
||||||
|
const raw = window.localStorage.getItem(key);
|
||||||
|
|
||||||
|
if (!raw) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`User Notes | Could not parse localStorage key: ${key}`, err);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesWriteJson(key, value) {
|
||||||
window.localStorage.setItem(
|
window.localStorage.setItem(
|
||||||
userNotesAppearanceKey(),
|
key,
|
||||||
JSON.stringify(values)
|
JSON.stringify(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesRemoveSavedAppearance() {
|
function userNotesCreateEmptyContent() {
|
||||||
window.localStorage.removeItem(userNotesAppearanceKey());
|
return {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
notes: {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: ""
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesCreateEmptyState() {
|
||||||
|
return {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
appearance: null,
|
||||||
|
position: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesLoadContentObject() {
|
||||||
|
const content = userNotesReadJson(
|
||||||
|
userNotesContentKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (content?.notes?.encoding === "base64") {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
const legacyNotes = window.localStorage.getItem(userNotesStorageKey());
|
||||||
|
|
||||||
|
if (legacyNotes !== null) {
|
||||||
|
const migrated = userNotesCreateEmptyContent();
|
||||||
|
migrated.notes.data = userNotesEncodeBase64(legacyNotes);
|
||||||
|
|
||||||
|
userNotesWriteJson(userNotesContentKey(), migrated);
|
||||||
|
window.localStorage.removeItem(userNotesStorageKey());
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return userNotesCreateEmptyContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesLoadStateObject() {
|
||||||
|
const state = userNotesReadJson(
|
||||||
|
userNotesStateKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (state && typeof state === "object") {
|
||||||
|
return {
|
||||||
|
...userNotesCreateEmptyState(),
|
||||||
|
...state
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const migrated = userNotesCreateEmptyState();
|
||||||
|
|
||||||
|
const legacyPosition = userNotesReadJson(
|
||||||
|
userNotesPositionKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
const legacyAppearance = userNotesReadJson(
|
||||||
|
userNotesAppearanceKey(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
|
||||||
|
if (legacyPosition) {
|
||||||
|
migrated.position = legacyPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyAppearance) {
|
||||||
|
migrated.appearance = legacyAppearance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyPosition || legacyAppearance) {
|
||||||
|
userNotesWriteJson(userNotesStateKey(), migrated);
|
||||||
|
window.localStorage.removeItem(userNotesPositionKey());
|
||||||
|
window.localStorage.removeItem(userNotesAppearanceKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return migrated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function userNotesSaveStateObject(state) {
|
||||||
|
userNotesWriteJson(
|
||||||
|
userNotesStateKey(),
|
||||||
|
{
|
||||||
|
...userNotesCreateEmptyState(),
|
||||||
|
...state,
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString()
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
userNotesRemoveLegacyStorageKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadNotes() {
|
||||||
|
const content = userNotesLoadContentObject();
|
||||||
|
|
||||||
|
if (content?.notes?.encoding === "base64") {
|
||||||
|
return userNotesDecodeBase64(content.notes.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSaveNotes(value) {
|
||||||
|
const content = {
|
||||||
|
schemaVersion: USER_NOTES_STORAGE_SCHEMA_VERSION,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
notes: {
|
||||||
|
format: "html",
|
||||||
|
encoding: "base64",
|
||||||
|
data: userNotesEncodeBase64(value)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
userNotesWriteJson(userNotesContentKey(), content);
|
||||||
|
userNotesRemoveLegacyStorageKeys();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadPosition(defaultValue = null) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
return state.position ?? defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSavePositionData(position) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.position = position;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRemoveSavedPosition() {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.position = null;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesLoadAppearance(defaults) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
if (!state.appearance || typeof state.appearance !== "object") {
|
||||||
|
return { ...defaults };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...defaults,
|
||||||
|
...state.appearance
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesSaveAppearance(values) {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.appearance = values;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userNotesRemoveSavedAppearance() {
|
||||||
|
const state = userNotesLoadStateObject();
|
||||||
|
|
||||||
|
state.appearance = null;
|
||||||
|
|
||||||
|
userNotesSaveStateObject(state);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,8 +9,9 @@ import {
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
userNotesLoadNotes,
|
userNotesLoadNotes,
|
||||||
|
userNotesLoadPosition,
|
||||||
userNotesSaveNotes,
|
userNotesSaveNotes,
|
||||||
userNotesPositionKey,
|
userNotesSavePositionData,
|
||||||
userNotesRemoveSavedPosition
|
userNotesRemoveSavedPosition
|
||||||
} from "./user-notes-storage.js";
|
} from "./user-notes-storage.js";
|
||||||
|
|
||||||
@ -99,9 +100,9 @@ export function userNotesClampPosition(position) {
|
|||||||
|
|
||||||
export function userNotesRestorePosition(win) {
|
export function userNotesRestorePosition(win) {
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(userNotesPositionKey());
|
const pos = userNotesLoadPosition(null);
|
||||||
|
|
||||||
if (!raw) {
|
if (!pos) {
|
||||||
userNotesApplyPosition(
|
userNotesApplyPosition(
|
||||||
win,
|
win,
|
||||||
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
|
userNotesClampPosition(USER_NOTES_DEFAULT_POSITION)
|
||||||
@ -109,8 +110,6 @@ export function userNotesRestorePosition(win) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const pos = JSON.parse(raw);
|
|
||||||
|
|
||||||
const restoredPosition = {
|
const restoredPosition = {
|
||||||
left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left,
|
left: Number.isFinite(pos.left) ? pos.left : USER_NOTES_DEFAULT_POSITION.left,
|
||||||
top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top,
|
top: Number.isFinite(pos.top) ? pos.top : USER_NOTES_DEFAULT_POSITION.top,
|
||||||
@ -153,10 +152,7 @@ export function userNotesSavePosition(win) {
|
|||||||
height: Math.round(rect.height)
|
height: Math.round(rect.height)
|
||||||
});
|
});
|
||||||
|
|
||||||
window.localStorage.setItem(
|
userNotesSavePositionData(position);
|
||||||
userNotesPositionKey(),
|
|
||||||
JSON.stringify(position)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesResetPositionAndSize() {
|
export function userNotesResetPositionAndSize() {
|
||||||
@ -315,10 +311,15 @@ function userNotesCreateEditor(win, editorElement) {
|
|||||||
const editor = globalThis.Jodit.make(editorElement, {
|
const editor = globalThis.Jodit.make(editorElement, {
|
||||||
height: "100%",
|
height: "100%",
|
||||||
minHeight: 150,
|
minHeight: 150,
|
||||||
|
|
||||||
|
allowResizeX: false,
|
||||||
|
allowResizeY: false,
|
||||||
|
|
||||||
toolbarAdaptive: false,
|
toolbarAdaptive: false,
|
||||||
askBeforePasteHTML: false,
|
askBeforePasteHTML: false,
|
||||||
askBeforePasteFromWord: false,
|
askBeforePasteFromWord: false,
|
||||||
defaultActionOnPaste: "insert_clear_html",
|
defaultActionOnPaste: "insert_clear_html",
|
||||||
|
|
||||||
disablePlugins: [
|
disablePlugins: [
|
||||||
"about",
|
"about",
|
||||||
"add-new-line",
|
"add-new-line",
|
||||||
@ -334,6 +335,7 @@ function userNotesCreateEditor(win, editorElement) {
|
|||||||
"speech-recognize",
|
"speech-recognize",
|
||||||
"video"
|
"video"
|
||||||
],
|
],
|
||||||
|
|
||||||
buttons: [
|
buttons: [
|
||||||
"bold",
|
"bold",
|
||||||
"italic",
|
"italic",
|
||||||
@ -527,14 +529,19 @@ export function userNotesOpenNotes() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function userNotesRefreshOpenWindow() {
|
export function userNotesRefreshOpenWindow(options = {}) {
|
||||||
|
const saveCurrentContent = options.saveCurrentContent ?? true;
|
||||||
|
|
||||||
const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
|
const oldWin = document.getElementById(USER_NOTES_WINDOW_ID);
|
||||||
|
|
||||||
if (!oldWin) {
|
if (!oldWin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
userNotesSaveNotes(userNotesGetEditorValue(oldWin));
|
if (saveCurrentContent) {
|
||||||
|
userNotesSaveNotes(userNotesGetEditorValue(oldWin));
|
||||||
|
}
|
||||||
|
|
||||||
userNotesSavePosition(oldWin);
|
userNotesSavePosition(oldWin);
|
||||||
userNotesDestroyEditor(oldWin);
|
userNotesDestroyEditor(oldWin);
|
||||||
oldWin.remove();
|
oldWin.remove();
|
||||||
|
|||||||
@ -2,6 +2,10 @@ import {
|
|||||||
userNotesRegisterSettings
|
userNotesRegisterSettings
|
||||||
} from "./user-notes-settings.js";
|
} from "./user-notes-settings.js";
|
||||||
|
|
||||||
|
import {
|
||||||
|
userNotesRegisterBackupSettings
|
||||||
|
} from "./user-notes-backup.js";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
userNotesRegisterTokenControl
|
userNotesRegisterTokenControl
|
||||||
} from "./user-notes-controls.js";
|
} from "./user-notes-controls.js";
|
||||||
@ -23,8 +27,10 @@ Hooks.once("init", () => {
|
|||||||
console.log("User Notes | application registered");
|
console.log("User Notes | application registered");
|
||||||
|
|
||||||
userNotesRegisterSettings(userNotesResetPositionAndSize);
|
userNotesRegisterSettings(userNotesResetPositionAndSize);
|
||||||
|
|
||||||
console.log("User Notes | settings registered");
|
console.log("User Notes | settings registered");
|
||||||
|
|
||||||
|
userNotesRegisterBackupSettings();
|
||||||
|
console.log("User Notes | export/import settings registered");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("User Notes | error during init", err);
|
console.error("User Notes | error during init", err);
|
||||||
|
|
||||||
@ -44,4 +50,4 @@ Hooks.on("getSceneControlButtons", controls => {
|
|||||||
"User Notes: Fehler beim Registrieren des Token-Controls."
|
"User Notes: Fehler beim Registrieren des Token-Controls."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -121,7 +121,7 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-notes-backup-settings .sheet-footer {
|
.user-notes-backup-footer {
|
||||||
margin-top: 0.75rem;
|
margin-top: 0.75rem;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -129,6 +129,34 @@
|
|||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-notes-backup-settings .sheet-footer button {
|
.user-notes-backup-footer button {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
|
height: 2rem;
|
||||||
|
min-height: 2rem;
|
||||||
|
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2rem;
|
||||||
|
align-items: center;
|
||||||
|
column-gap: 0.75rem;
|
||||||
|
|
||||||
|
margin: 0 0 0.45rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row label {
|
||||||
|
margin: 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-notes-checkbox-row input[type="checkbox"] {
|
||||||
|
justify-self: center;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user