diff --git a/main.js b/main.js
deleted file mode 100644
index 9978719..0000000
--- a/main.js
+++ /dev/null
@@ -1,6406 +0,0 @@
-var __defProp = Object.defineProperty;
-var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
-var __getOwnPropNames = Object.getOwnPropertyNames;
-var __hasOwnProp = Object.prototype.hasOwnProperty;
-var __export = (target, all) => {
- for (var name in all)
- __defProp(target, name, { get: all[name], enumerable: true });
-};
-var __copyProps = (to, from, except, desc) => {
- if (from && typeof from === "object" || typeof from === "function") {
- for (let key of __getOwnPropNames(from))
- if (!__hasOwnProp.call(to, key) && key !== except)
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
- }
- return to;
-};
-var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
-
-// main.ts
-var main_exports = {};
-__export(main_exports, {
- default: () => MediaSize
-});
-module.exports = __toCommonJS(main_exports);
-var import_obsidian = require("obsidian");
-var I18N = {
- en: {
- cloneSettingsTitle: "Clone customization",
- animation: "Animation",
- animationPopup: "Popup",
- animationFade: "Fade",
- backgroundOpacity: "Background opacity",
- controlsOpacity: "Controls opacity",
- showAudioHandles: "Show audio handles",
- presets: "Sizes",
- fit: "Fit to container",
- natural: "Natural size",
- add: "Add\u2026",
- addPresetTitle: "Add width preset \u2039px\u203A",
- placeholder: "e.g. 820",
- ok: "OK",
- cancel: "Cancel",
- invalid: "Invalid value",
- maxReached: "Maximum number of presets reached",
- added: "Preset {n} added",
- removed: "Preset {n} removed",
- openLocation: "Open file location",
- fileNotFound: "File not found in the vault",
- shellDenied: "No access to shell",
- localOnly: "Available for local vaults only",
- restorePos: "Restore original position",
- setSize: "Set size\u2026",
- width: "Width",
- height: "Height",
- keepAR: "Keep aspect ratio",
- sbReady: "ML \u2022 ready",
- sbTitle: "Media Layout: click to rescan",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 plugin active",
- posPresets: "Save clone positions",
- resetAll: "Reset all",
- saveLayout: "Save preset",
- layoutMax: "Maximum number of presets reached",
- saved: "Saved",
- layoutN: "Preset {n}",
- renameTitle: "Enter a name",
- addNote: "Add note box",
- copyLayout: "Copy page data",
- pasteLayout: "Paste page data",
- deleteNote: "Delete note box",
- detach: "Detach",
- bgColor: "Change background color",
- duplicate: "Duplicate",
- reset: "Reset",
- removeBg: "Remove background",
- deleteClone: "Delete",
- lock: "Lock",
- unlock: "Unlock",
- localOnlyCheckboxLabel: "Only for this file",
- makeInvisible: "Make invisible",
- invalidLayoutData: "Invalid page data in clipboard.",
- layoutPasted: "Page data and presets pasted.",
- pasteLayoutFailed: "Failed to paste page data from clipboard.",
- layoutCopied: "Page data copied to clipboard.",
- makeVisible: "Make visible",
- bringLayerUp: "Bring layer up",
- sendLayerDown: "Send layer down",
- showHandlesOnHoverLabel: "Show handles on hover",
- hideDetachButtonLabel: "Hide \xABDetach\xBB button",
- modalAnimationLabel: "Window animation",
- cloneRestoreDelayLabel: "Clone appearance delay",
- noteRestoreDelayLabel: "Note box appearance delay",
- hideDelayLabel: "Panel hide delay",
- changeTextColor: "Change text color",
- cloneNearFile: "Clone next to the file",
- addStroke: "Add stroke",
- addLink: "Add link",
- applyToHeader: "Apply to header",
- deleteSelected: "Delete selected ({n})",
- urlMediaClickToSave: "Click to save to vault",
- urlMediaDownloadAndReplace: "Download and replace",
- urlMediaDownloading: "Downloading image...",
- urlMediaDownloadError: "Download error: {status}",
- urlMediaFileSavedLinkFailed: "File saved, but failed to update link",
- urlMediaSuccess: "Image saved and link updated",
- urlMediaFileSavedPatternNotFound: "File saved, but link not found in text",
- urlMediaError: "Error: {error}",
- urlMediaUnknownError: "unknown error"
- },
- ru: {
- cloneSettingsTitle: "\u041A\u0430\u0441\u0442\u043E\u043C\u0438\u0437\u0430\u0446\u0438\u044F \u043A\u043B\u043E\u043D\u043E\u0432",
- animation: "\u0410\u043D\u0438\u043C\u0430\u0446\u0438\u044F",
- animationPopup: "\u0412\u0441\u043F\u043B\u044B\u0432\u0430\u044E\u0449\u0430\u044F",
- animationFade: "\u041F\u0440\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u0435",
- backgroundOpacity: "\u041F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u043E\u0441\u0442\u044C \u0444\u043E\u043D\u0430",
- controlsOpacity: "\u041F\u0440\u043E\u0437\u0440\u0430\u0447\u043D\u043E\u0441\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u0438",
- showAudioHandles: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0440\u0443\u0447\u043A\u0438 \u0443 \u0430\u0443\u0434\u0438\u043E",
- presets: "\u0420\u0430\u0437\u043C\u0435\u0440\u044B",
- fit: "\u041F\u043E\u0434\u043E\u0433\u043D\u0430\u0442\u044C \u043F\u043E\u0434 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440",
- natural: "\u041D\u0430\u0442\u0443\u0440\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440",
- add: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C\u2026",
- addPresetTitle: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043F\u0440\u0435\u0441\u0435\u0442 \u0448\u0438\u0440\u0438\u043D\u044B \u2039px\u203A",
- placeholder: "\u043D\u0430\u043F\u0440\u0438\u043C\u0435\u0440: 820",
- ok: "\u041E\u041A",
- cancel: "\u041E\u0442\u043C\u0435\u043D\u0430",
- invalid: "\u041D\u0435\u043A\u043E\u0440\u0440\u0435\u043A\u0442\u043D\u043E\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u0438\u0435",
- maxReached: "\u0414\u043E\u0441\u0442\u0438\u0433\u043D\u0443\u0442\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043F\u0440\u0435\u0441\u0435\u0442\u043E\u0432",
- added: "\u0414\u043E\u0431\u0430\u0432\u043B\u0435\u043D \u043F\u0440\u0435\u0441\u0435\u0442 {n}",
- removed: "\u0423\u0434\u0430\u043B\u0451\u043D \u043F\u0440\u0435\u0441\u0435\u0442 {n}",
- openLocation: "\u041E\u0442\u043A\u0440\u044B\u0442\u044C \u0440\u0430\u0441\u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0444\u0430\u0439\u043B\u0430",
- fileNotFound: "\u0424\u0430\u0439\u043B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432 \u0432\u0430\u0443\u043B\u0442\u0435",
- shellDenied: "\u041D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A shell",
- localOnly: "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u0442\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u0430\u0443\u043B\u0442\u043E\u0432",
- restorePos: "\u0412\u0435\u0440\u043D\u0443\u0442\u044C \u0438\u0441\u0445\u043E\u0434\u043D\u043E\u0435 \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435",
- setSize: "\u0417\u0430\u0434\u0430\u0442\u044C \u0440\u0430\u0437\u043C\u0435\u0440\u2026",
- width: "\u0428\u0438\u0440\u0438\u043D\u0430",
- height: "\u0412\u044B\u0441\u043E\u0442\u0430",
- keepAR: "\u0421\u043E\u0445\u0440\u0430\u043D\u044F\u0442\u044C \u043F\u0440\u043E\u043F\u043E\u0440\u0446\u0438\u0438",
- sbReady: "ML \u2022 \u0433\u043E\u0442\u043E\u0432\u043E",
- sbTitle: "Media Layout: \u043A\u043B\u0438\u043A \u2014 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 \u043F\u043B\u0430\u0433\u0438\u043D \u0430\u043A\u0442\u0438\u0432\u0435\u043D",
- posPresets: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u043A\u043B\u043E\u043D\u043E\u0432",
- resetAll: "\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C \u0432\u0441\u0451",
- saveLayout: "\u0421\u043E\u0445\u0440\u0430\u043D\u0438\u0442\u044C \u043F\u0440\u0435\u0441\u0435\u0442",
- layoutMax: "\u0414\u043E\u0441\u0442\u0438\u0433\u043D\u0443\u0442\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u043A\u043E\u043B\u0438\u0447\u0435\u0441\u0442\u0432\u043E \u043F\u0440\u0435\u0441\u0435\u0442\u043E\u0432",
- saved: "\u0421\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E",
- layoutN: "\u041F\u0440\u0435\u0441\u0435\u0442 {n}",
- renameTitle: "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u043D\u0430\u0437\u0432\u0430\u043D\u0438\u0435",
- addNote: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0447\u043A\u0443",
- copyLayout: "\u0421\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B",
- pasteLayout: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B",
- deleteNote: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0442\u0430\u0431\u043B\u0438\u0447\u043A\u0443",
- detach: "\u041E\u0442\u043A\u0440\u0435\u043F\u0438\u0442\u044C",
- bgColor: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0444\u043E\u043D\u0430",
- duplicate: "\u041A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
- reset: "\u0421\u0431\u0440\u043E\u0441\u0438\u0442\u044C",
- removeBg: "\u0423\u0431\u0440\u0430\u0442\u044C \u0444\u043E\u043D",
- deleteClone: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C",
- lock: "\u0417\u0430\u043A\u0440\u0435\u043F\u0438\u0442\u044C",
- unlock: "\u0420\u0430\u0437\u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u0430\u0442\u044C",
- localOnlyCheckboxLabel: "\u0422\u043E\u043B\u044C\u043A\u043E \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430",
- makeInvisible: "\u0421\u0434\u0435\u043B\u0430\u0442\u044C \u043D\u0435\u0432\u0438\u0434\u0438\u043C\u044B\u043C",
- invalidLayoutData: "\u041D\u0435\u0432\u0435\u0440\u043D\u044B\u0435 \u0434\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0432 \u0431\u0443\u0444\u0435\u0440\u0435 \u043E\u0431\u043C\u0435\u043D\u0430.",
- layoutPasted: "\u0414\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0438 \u043F\u0440\u0435\u0441\u0435\u0442\u044B \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u044B.",
- pasteLayoutFailed: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0434\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0438\u0437 \u0431\u0443\u0444\u0435\u0440\u0430 \u043E\u0431\u043C\u0435\u043D\u0430.",
- layoutCopied: "\u0414\u0430\u043D\u043D\u044B\u0435 \u0441\u0442\u0440\u0430\u043D\u0438\u0446\u044B \u0441\u043A\u043E\u043F\u0438\u0440\u043E\u0432\u0430\u043D\u044B \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0435\u043D\u0430.",
- makeVisible: "\u0421\u0434\u0435\u043B\u0430\u0442\u044C \u0432\u0438\u0434\u0438\u043C\u044B\u043C",
- bringLayerUp: "\u041F\u043E\u0434\u043D\u044F\u0442\u044C \u0441\u043B\u043E\u0439",
- sendLayerDown: "\u041E\u043F\u0443\u0441\u0442\u0438\u0442\u044C \u0441\u043B\u043E\u0439",
- showHandlesOnHoverLabel: "\u041F\u043E\u043A\u0430\u0437\u044B\u0432\u0430\u0442\u044C \u0440\u0443\u0447\u043A\u0438 \u043F\u0440\u0438 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u0438\u0438",
- hideDetachButtonLabel: "\u0421\u043A\u0440\u044B\u0442\u044C \u043A\u043D\u043E\u043F\u043A\u0443 \xAB\u043E\u0442\u043A\u0440\u0435\u043F\u0438\u0442\u044C\xBB",
- modalAnimationLabel: "\u0410\u043D\u0438\u043C\u0430\u0446\u0438\u044F \u043E\u043A\u043E\u043D",
- cloneRestoreDelayLabel: "\u0417\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u043F\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u043A\u043B\u043E\u043D\u043E\u0432",
- noteRestoreDelayLabel: "\u0417\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u043F\u043E\u044F\u0432\u043B\u0435\u043D\u0438\u044F \u0442\u0430\u0431\u043B\u0438\u0447\u0435\u043A",
- hideDelayLabel: "\u0417\u0430\u0434\u0435\u0440\u0436\u043A\u0430 \u0441\u043A\u0440\u044B\u0442\u0438\u044F \u043F\u0430\u043D\u0435\u043B\u0438",
- changeTextColor: "\u0418\u0437\u043C\u0435\u043D\u0438\u0442\u044C \u0446\u0432\u0435\u0442 \u0442\u0435\u043A\u0441\u0442\u0430",
- cloneNearFile: "\u041A\u043B\u043E\u043D\u0438\u0440\u043E\u0432\u0430\u0442\u044C \u0440\u044F\u0434\u043E\u043C \u0441 \u0444\u0430\u0439\u043B\u043E\u043C",
- addStroke: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u043E\u0431\u0432\u043E\u0434\u043A\u0443",
- applyToHeader: "\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0434\u043B\u044F \u0448\u0430\u043F\u043A\u0438",
- addLink: "\u0414\u043E\u0431\u0430\u0432\u0438\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443",
- deleteSelected: "\u0423\u0434\u0430\u043B\u0438\u0442\u044C \u0432\u044B\u0434\u0435\u043B\u0435\u043D\u043D\u043E\u0435 ({n})",
- urlMediaClickToSave: "\u041A\u043B\u0438\u043A \u0434\u043B\u044F \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u0438\u044F \u0432 vault",
- urlMediaDownloadAndReplace: "\u0421\u043A\u0430\u0447\u0430\u0442\u044C \u0438 \u0437\u0430\u043C\u0435\u043D\u0438\u0442\u044C",
- urlMediaDownloading: "\u0421\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u0435 \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F...",
- urlMediaDownloadError: "\u041E\u0448\u0438\u0431\u043A\u0430 \u0441\u043A\u0430\u0447\u0438\u0432\u0430\u043D\u0438\u044F: {status}",
- urlMediaFileSavedLinkFailed: "\u0424\u0430\u0439\u043B \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D, \u043D\u043E \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0441\u044B\u043B\u043A\u0443",
- urlMediaSuccess: "\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u043E \u0438 \u0441\u0441\u044B\u043B\u043A\u0430 \u043E\u0431\u043D\u043E\u0432\u043B\u0435\u043D\u0430",
- urlMediaFileSavedPatternNotFound: "\u0424\u0430\u0439\u043B \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D, \u043D\u043E \u0441\u0441\u044B\u043B\u043A\u0430 \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D\u0430 \u0432 \u0442\u0435\u043A\u0441\u0442\u0435",
- urlMediaError: "\u041E\u0448\u0438\u0431\u043A\u0430: {error}",
- urlMediaUnknownError: "\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u0430\u044F \u043E\u0448\u0438\u0431\u043A\u0430"
- },
- de: {
- cloneSettingsTitle: "Klon-Anpassung",
- animation: "Animation",
- animationPopup: "Popup",
- animationFade: "Einblenden",
- backgroundOpacity: "Hintergrund-Deckkraft",
- controlsOpacity: "Bedienfeld-Deckkraft",
- showAudioHandles: "Audio-Griffe anzeigen",
- presets: "Gr\xF6\xDFen",
- fit: "An Container anpassen",
- natural: "Nat\xFCrliche Gr\xF6\xDFe",
- add: "Hinzuf\xFCgen\u2026",
- addPresetTitle: "Breiten-Preset hinzuf\xFCgen \u2039px\u203A",
- placeholder: "z. B. 820",
- ok: "OK",
- cancel: "Abbrechen",
- invalid: "Ung\xFCltiger Wert",
- maxReached: "Maximale Anzahl an Presets erreicht",
- added: "Preset {n} hinzugef\xFCgt",
- removed: "Preset {n} entfernt",
- removeBg: "Hintergrund entfernen",
- openLocation: "Dateispeicherort \xF6ffnen",
- fileNotFound: "Datei im Vault nicht gefunden",
- shellDenied: "Kein Shell-Zugriff",
- localOnly: "Nur f\xFCr lokale Vaults verf\xFCgbar",
- restorePos: "Ausgangsposition wiederherstellen",
- setSize: "Gr\xF6\xDFe festlegen\u2026",
- width: "Breite",
- height: "H\xF6he",
- keepAR: "Seitenverh\xE4ltnis beibehalten",
- sbReady: "ML \u2022 bereit",
- sbTitle: "Media Layout: Klicken zum Rescan",
- cmdPing: "Media Layout: Ping",
- pingOk: "ML: OK \u2014 Plugin aktiv",
- posPresets: "Klonposition beibehalten",
- resetAll: "Alles zur\xFCcksetzen",
- saveLayout: "Preset speichern",
- layoutMax: "Maximale Anzahl an Presets erreicht",
- saved: "Gespeichert",
- layoutN: "Preset {n}",
- renameTitle: "Name eingeben",
- addNote: "Notiz-Box hinzuf\xFCgen",
- copyLayout: "Seitendaten kopieren",
- pasteLayout: "Seitendaten einf\xFCgen",
- deleteNote: "Notiz-Box l\xF6schen",
- detach: "Abl\xF6sen",
- bgColor: "Hintergrundfarbe \xE4ndern",
- duplicate: "Duplizieren",
- reset: "Zur\xFCcksetzen",
- deleteClone: "l\xF6schen",
- lock: "Sperren",
- unlock: "Entsperren",
- localOnlyCheckboxLabel: "Nur f\xFCr diese Datei",
- makeInvisible: "Unsichtbar machen",
- invalidLayoutData: "Ung\xFCltige Seitendaten in der Zwischenablage.",
- layoutPasted: "Seitendaten und Presets eingef\xFCgt.",
- pasteLayoutFailed: "Seitendaten konnten nicht aus der Zwischenablage eingef\xFCgt werden.",
- layoutCopied: "Seitendaten in die Zwischenablage kopiert.",
- makeVisible: "Sichtbar machen",
- bringLayerUp: "Ebene nach oben",
- sendLayerDown: "Ebene nach unten",
- showHandlesOnHoverLabel: "Ziehpunkte bei Hover anzeigen",
- hideDetachButtonLabel: "Schaltfl\xE4che \u201EAbl\xF6sen\u201C ausblenden",
- modalAnimationLabel: "Modal-Animation",
- cloneRestoreDelayLabel: "Verz\xF6gerung f\xFCr Klon-Einblendung",
- noteRestoreDelayLabel: "Verz\xF6gerung f\xFCr Notizbox-Einblendung",
- hideDelayLabel: "Verz\xF6gerung beim Ausblenden des Panels",
- changeTextColor: "Textfarbe \xE4ndern",
- cloneNearFile: "Neben der Datei klonen",
- addStroke: "Kontur hinzuf\xFCgen",
- applyToHeader: "Auf Kopfzeile anwenden",
- addLink: "Link hinzuf\xFCgen",
- deleteSelected: "Ausgew\xE4hlte l\xF6schen ({n})",
- urlMediaClickToSave: "Klicken um im Vault zu speichern",
- urlMediaDownloadAndReplace: "Herunterladen und ersetzen",
- urlMediaDownloading: "Bild wird heruntergeladen...",
- urlMediaDownloadError: "Download-Fehler: {status}",
- urlMediaFileSavedLinkFailed: "Datei gespeichert, aber Link-Aktualisierung fehlgeschlagen",
- urlMediaSuccess: "Bild gespeichert und Link aktualisiert",
- urlMediaFileSavedPatternNotFound: "Datei gespeichert, aber Link nicht im Text gefunden",
- urlMediaError: "Fehler: {error}",
- urlMediaUnknownError: "unbekannter Fehler"
- },
- pl: {
- cloneSettingsTitle: "Dostosowanie klon\xF3w",
- animation: "Animacja",
- animationPopup: "Wyskakuj\u0105ca",
- animationFade: "Wygaszanie",
- backgroundOpacity: "Przezroczysto\u015B\u0107 t\u0142a",
- controlsOpacity: "Przezroczysto\u015B\u0107 panelu",
- showAudioHandles: "Poka\u017C uchwyty audio",
- presets: "Rozmiary",
- fit: "Dopasuj do kontenera",
- natural: "Naturalny rozmiar",
- add: "Dodaj\u2026",
- addPresetTitle: "Dodaj preset szeroko\u015Bci \u2039px\u203A",
- placeholder: "np. 820",
- ok: "OK",
- removeBg: "Usu\u0144 t\u0142o",
- cancel: "Anuluj",
- invalid: "Nieprawid\u0142owa warto\u015B\u0107",
- maxReached: "Osi\u0105gni\u0119to maksymaln\u0105 liczb\u0119 preset\xF3w",
- added: "Dodano preset {n}",
- removed: "Usuni\u0119to preset {n}",
- openLocation: "Otw\xF3rz lokalizacj\u0119 pliku",
- fileNotFound: "Pliku nie znaleziono w vault",
- shellDenied: "Brak dost\u0119pu do shell",
- localOnly: "Dost\u0119pne tylko dla lokalnych vault\xF3w",
- restorePos: "Przywr\xF3\u0107 pozycj\u0119",
- setSize: "Ustaw rozmiar\u2026",
- width: "Szeroko\u015B\u0107",
- height: "Wysoko\u015B\u0107",
- keepAR: "Zachowaj proporcje",
- sbReady: "ML \u2022 gotowe",
- sbTitle: "Media Layout: kliknij, aby przeskanowa\u0107",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 wtyczka aktywna",
- posPresets: "Zapisz pozycje klon\xF3w",
- resetAll: "Zresetuj wszystko",
- saveLayout: "Zapisz preset",
- layoutMax: "Osi\u0105gni\u0119to maksymaln\u0105 liczb\u0119 preset\xF3w",
- saved: "Zapisano",
- layoutN: "Preset {n}",
- renameTitle: "Wpisz nazw\u0119",
- addNote: "Dodaj ramk\u0119 notatki",
- copyLayout: "Kopiuj dane strony",
- pasteLayout: "Wklej dane strony",
- deleteNote: "Usu\u0144 ramk\u0119 notatki",
- detach: "Od\u0142\u0105cz",
- bgColor: "Zmie\u0144 kolor t\u0142a",
- duplicate: "Duplikuj",
- reset: "Resetuj",
- deleteClone: "Usu\u0144",
- lock: "Zablokuj",
- unlock: "Odblokuj",
- localOnlyCheckboxLabel: "Tylko dla tego pliku",
- makeInvisible: "Uczy\u0144 niewidocznym",
- invalidLayoutData: "Nieprawid\u0142owe dane strony w schowku.",
- layoutPasted: "Wklejono dane strony i presety.",
- pasteLayoutFailed: "Nie uda\u0142o si\u0119 wklei\u0107 danych strony ze schowka.",
- layoutCopied: "Skopiowano dane strony do schowka.",
- makeVisible: "Uczy\u0144 widocznym",
- bringLayerUp: "Podnie\u015B warstw\u0119",
- sendLayerDown: "Opu\u015B\u0107 warstw\u0119",
- showHandlesOnHoverLabel: "Poka\u017C uchwyty po najechaniu",
- hideDetachButtonLabel: "Ukryj przycisk \u201EOdepnij\u201D",
- modalAnimationLabel: "Animacja okien modalnych",
- cloneRestoreDelayLabel: "Op\xF3\u017Anienie pojawiania si\u0119 klon\xF3w",
- noteRestoreDelayLabel: "Op\xF3\u017Anienie pojawiania si\u0119 okienek notatek",
- hideDelayLabel: "Op\xF3\u017Anienie ukrywania panelu",
- changeTextColor: "Zmie\u0144 kolor tekstu",
- cloneNearFile: "Klonuj obok pliku",
- addStroke: "Dodaj obrys",
- applyToHeader: "Zastosuj do nag\u0142\xF3wka",
- addLink: "Dodaj link",
- deleteSelected: "Usu\u0144 zaznaczone ({n})",
- urlMediaClickToSave: "Kliknij aby zapisa\u0107 do vault",
- urlMediaDownloadAndReplace: "Pobierz i zamie\u0144",
- urlMediaDownloading: "Pobieranie obrazu...",
- urlMediaDownloadError: "B\u0142\u0105d pobierania: {status}",
- urlMediaFileSavedLinkFailed: "Plik zapisany, ale nie uda\u0142o si\u0119 zaktualizowa\u0107 linku",
- urlMediaSuccess: "Obraz zapisany i link zaktualizowany",
- urlMediaFileSavedPatternNotFound: "Plik zapisany, ale link nie znaleziony w tek\u015Bcie",
- urlMediaError: "B\u0142\u0105d: {error}",
- urlMediaUnknownError: "nieznany b\u0142\u0105d"
- },
- es: {
- cloneSettingsTitle: "Personalizar clones",
- animation: "Animaci\xF3n",
- removeBg: "Eliminar fondo",
- animationPopup: "Emergente",
- animationFade: "Fundido",
- backgroundOpacity: "Opacidad del fondo",
- controlsOpacity: "Opacidad del panel",
- showAudioHandles: "Mostrar asas de audio",
- presets: "Tama\xF1os",
- fit: "Ajustar al contenedor",
- natural: "Tama\xF1o natural",
- add: "A\xF1adir\u2026",
- addPresetTitle: "A\xF1adir preset de ancho \u2039px\u203A",
- placeholder: "p. ej., 820",
- ok: "OK",
- cancel: "Cancelar",
- invalid: "Valor no v\xE1lido",
- maxReached: "Se alcanz\xF3 el n\xFAmero m\xE1ximo de presets",
- added: "Preset {n} a\xF1adido",
- removed: "Preset {n} eliminado",
- openLocation: "Abrir ubicaci\xF3n del archivo",
- fileNotFound: "Archivo no encontrado en el vault",
- shellDenied: "Sin acceso a shell",
- localOnly: "Disponible solo para vaults locales",
- restorePos: "Restaurar posici\xF3n original",
- setSize: "Establecer tama\xF1o\u2026",
- width: "Ancho",
- height: "Alto",
- keepAR: "Mantener proporci\xF3n",
- sbReady: "ML \u2022 listo",
- sbTitle: "Media Layout: clic para reescanear",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 complemento activo",
- posPresets: "Guardar posiciones de clones",
- resetAll: "Restablecer todo",
- saveLayout: "Guardar preset",
- layoutMax: "Se alcanz\xF3 el n\xFAmero m\xE1ximo de presets",
- saved: "Guardado",
- layoutN: "Preset {n}",
- renameTitle: "Introduce un nombre",
- addNote: "A\xF1adir tabla/nota",
- copyLayout: "Copiar datos de la p\xE1gina",
- pasteLayout: "Pegar datos de la p\xE1gina",
- deleteNote: "Eliminar tabla/nota",
- detach: "Desanclar",
- bgColor: "Cambiar color de fondo",
- duplicate: "Duplicar",
- reset: "Restablecer",
- deleteClone: "Eliminar",
- lock: "Bloquear",
- unlock: "Desbloquear",
- localOnlyCheckboxLabel: "Solo para este archivo",
- makeInvisible: "Hacer invisible",
- invalidLayoutData: "Datos de p\xE1gina no v\xE1lidos en el portapapeles.",
- layoutPasted: "Se pegaron los datos y presets de la p\xE1gina.",
- pasteLayoutFailed: "No se pudieron pegar los datos de la p\xE1gina desde el portapapeles.",
- layoutCopied: "Datos de la p\xE1gina copiados al portapapeles.",
- makeVisible: "Hacer visible",
- bringLayerUp: "Subir capa",
- sendLayerDown: "Bajar capa",
- showHandlesOnHoverLabel: "Mostrar tiradores al pasar el cursor",
- hideDetachButtonLabel: "Ocultar el bot\xF3n \xABDesanclar\xBB",
- modalAnimationLabel: "Animaci\xF3n de la ventana modal",
- cloneRestoreDelayLabel: "Retraso en la aparici\xF3n de los clones",
- noteRestoreDelayLabel: "Retraso en la aparici\xF3n de los cuadros de notas",
- hideDelayLabel: "Retraso al ocultar el panel",
- changeTextColor: "Cambiar color del texto",
- cloneNearFile: "Clonar junto al archivo",
- addStroke: "A\xF1adir contorno",
- applyToHeader: "Aplicar al encabezado",
- addLink: "A\xF1adir enlace",
- deleteSelected: "Eliminar seleccionados ({n})",
- urlMediaClickToSave: "Haga clic para guardar en vault",
- urlMediaDownloadAndReplace: "Descargar y reemplazar",
- urlMediaDownloading: "Descargando imagen...",
- urlMediaDownloadError: "Error de descarga: {status}",
- urlMediaFileSavedLinkFailed: "Archivo guardado, pero no se pudo actualizar el enlace",
- urlMediaSuccess: "Imagen guardada y enlace actualizado",
- urlMediaFileSavedPatternNotFound: "Archivo guardado, pero enlace no encontrado en el texto",
- urlMediaError: "Error: {error}",
- urlMediaUnknownError: "error desconocido"
- },
- fr: {
- cloneSettingsTitle: "Personnalisation des clones",
- removeBg: "Supprimer l\u2019arri\xE8re-plan",
- animation: "Animation",
- animationPopup: "Popup",
- animationFade: "Fondu",
- backgroundOpacity: "Opacit\xE9 du fond",
- controlsOpacity: "Opacit\xE9 du panneau",
- showAudioHandles: "Afficher les poign\xE9es audio",
- presets: "Tailles",
- fit: "Ajuster au conteneur",
- natural: "Taille naturelle",
- add: "Ajouter\u2026",
- addPresetTitle: "Ajouter un preset de largeur \u2039px\u203A",
- placeholder: "ex. : 820",
- ok: "OK",
- cancel: "Annuler",
- invalid: "Valeur invalide",
- maxReached: "Nombre maximal de presets atteint",
- added: "Preset {n} ajout\xE9",
- removed: "Preset {n} supprim\xE9",
- openLocation: "Ouvrir l\u2019emplacement du fichier",
- fileNotFound: "Fichier introuvable dans le vault",
- shellDenied: "Acc\xE8s au shell refus\xE9",
- localOnly: "Disponible uniquement pour les vaults locaux",
- restorePos: "Restaurer la position d\u2019origine",
- setSize: "D\xE9finir la taille\u2026",
- width: "Largeur",
- height: "Hauteur",
- keepAR: "Conserver le ratio",
- sbReady: "ML \u2022 pr\xEAt",
- sbTitle: "Media Layout : cliquer pour rescanner",
- cmdPing: "Media Layout : ping",
- pingOk: "ML : OK \u2014 plugin actif",
- posPresets: "Enregistrer les positions des clones",
- resetAll: "Tout r\xE9initialiser",
- saveLayout: "Enregistrer le preset",
- layoutMax: "Nombre maximal de presets atteint",
- saved: "Enregistr\xE9",
- layoutN: "Preset {n}",
- renameTitle: "Entrez un nom",
- addNote: "Ajouter un encart",
- copyLayout: "Copier les donn\xE9es de la page",
- pasteLayout: "Coller les donn\xE9es de la page",
- deleteNote: "Supprimer l\u2019encart",
- detach: "D\xE9tacher",
- bgColor: "Changer la couleur du fond",
- duplicate: "Dupliquer",
- reset: "R\xE9initialiser",
- deleteClone: "Supprimer le clone",
- lock: "Verrouiller",
- unlock: "D\xE9verrouiller",
- localOnlyCheckboxLabel: "Uniquement pour ce fichier",
- makeInvisible: "Rendre invisible",
- invalidLayoutData: "Donn\xE9es de page invalides dans le presse-papiers.",
- layoutPasted: "Donn\xE9es et presets de page coll\xE9s.",
- pasteLayoutFailed: "Impossible de coller les donn\xE9es de la page depuis le presse-papiers.",
- layoutCopied: "Donn\xE9es de page copi\xE9es dans le presse-papiers.",
- makeVisible: "Rendre visible",
- bringLayerUp: "Monter le calque",
- sendLayerDown: "Descendre le calque",
- showHandlesOnHoverLabel: "Afficher les poign\xE9es au survol",
- hideDetachButtonLabel: "Masquer le bouton \xAB D\xE9tacher \xBB",
- modalAnimationLabel: "Animation des fen\xEAtres modales",
- cloneRestoreDelayLabel: "D\xE9lai d\u2019apparition des clones",
- noteRestoreDelayLabel: "D\xE9lai d\u2019apparition des bo\xEEtes de notes",
- hideDelayLabel: "D\xE9lai avant masquage du panneau",
- changeTextColor: "Changer la couleur du texte",
- cloneNearFile: "Cloner \xE0 c\xF4t\xE9 du fichier",
- addStroke: "Ajouter un contour",
- applyToHeader: "Appliquer \xE0 l'en-t\xEAte",
- addLink: "Ajouter un lien",
- deleteSelected: "Supprimer les \xE9l\xE9ments s\xE9lectionn\xE9s ({n})",
- urlMediaClickToSave: "Cliquez pour enregistrer dans le vault",
- urlMediaDownloadAndReplace: "T\xE9l\xE9charger et remplacer",
- urlMediaDownloading: "T\xE9l\xE9chargement de l'image...",
- urlMediaDownloadError: "Erreur de t\xE9l\xE9chargement: {status}",
- urlMediaFileSavedLinkFailed: "Fichier enregistr\xE9, mais \xE9chec de mise \xE0 jour du lien",
- urlMediaSuccess: "Image enregistr\xE9e et lien mis \xE0 jour",
- urlMediaFileSavedPatternNotFound: "Fichier enregistr\xE9, mais lien introuvable dans le texte",
- urlMediaError: "Erreur: {error}",
- urlMediaUnknownError: "erreur inconnue"
- },
- it: {
- cloneSettingsTitle: "Personalizzazione cloni",
- removeBg: "Rimuovi sfondo",
- animation: "Animazione",
- animationPopup: "Popup",
- animationFade: "Dissolvenza",
- backgroundOpacity: "Opacit\xE0 sfondo",
- controlsOpacity: "Opacit\xE0 pannello",
- showAudioHandles: "Mostra maniglie audio",
- presets: "Dimensioni",
- fit: "Adatta al contenitore",
- natural: "Dimensione naturale",
- add: "Aggiungi\u2026",
- addPresetTitle: "Aggiungi preset larghezza \u2039px\u203A",
- placeholder: "es. 820",
- ok: "OK",
- cancel: "Annulla",
- invalid: "Valore non valido",
- maxReached: "Numero massimo di preset raggiunto",
- added: "Preset {n} aggiunto",
- removed: "Preset {n} rimosso",
- openLocation: "Apri posizione del file",
- fileNotFound: "File non trovato nel vault",
- shellDenied: "Nessun accesso alla shell",
- localOnly: "Disponibile solo per vault locali",
- restorePos: "Ripristina posizione iniziale",
- setSize: "Imposta dimensione\u2026",
- width: "Larghezza",
- height: "Altezza",
- keepAR: "Mantieni proporzioni",
- sbReady: "ML \u2022 pronto",
- sbTitle: "Media Layout: clic per ripetere la scansione",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 plugin attivo",
- posPresets: "Salva posizioni dei cloni",
- resetAll: "Reimposta tutto",
- saveLayout: "Salva preset",
- layoutMax: "Numero massimo di preset raggiunto",
- saved: "Salvato",
- layoutN: "Preset {n}",
- renameTitle: "Inserisci un nome",
- addNote: "Aggiungi riquadro",
- copyLayout: "Copia dati della pagina",
- pasteLayout: "Incolla dati della pagina",
- deleteNote: "Elimina riquadro",
- detach: "Stacca",
- bgColor: "Cambia colore sfondo",
- duplicate: "Duplica",
- reset: "Reset",
- deleteClone: "Elimina",
- lock: "Blocca",
- unlock: "Sblocca",
- localOnlyCheckboxLabel: "Solo per questo file",
- makeInvisible: "Rendi invisibile",
- invalidLayoutData: "Dati pagina non validi negli appunti.",
- layoutPasted: "Dati e preset della pagina incollati.",
- pasteLayoutFailed: "Impossibile incollare i dati della pagina dagli appunti.",
- layoutCopied: "Dati della pagina copiati negli appunti.",
- makeVisible: "Rendi visibile",
- bringLayerUp: "Porta livello in alto",
- sendLayerDown: "Manda livello in basso",
- showHandlesOnHoverLabel: "Mostra le maniglie al passaggio del mouse",
- hideDetachButtonLabel: "Nascondi il pulsante \xABSgancia\xBB",
- modalAnimationLabel: "Animazione delle finestre modali",
- cloneRestoreDelayLabel: "Ritardo nella comparsa dei cloni",
- noteRestoreDelayLabel: "Ritardo nella comparsa dei riquadri nota",
- hideDelayLabel: "Ritardo nel nascondere il pannello",
- changeTextColor: "Cambia colore del testo",
- cloneNearFile: "Clona accanto al file",
- addStroke: "Aggiungi contorno",
- applyToHeader: "Applica all'intestazione",
- addLink: "Aggiungi link",
- deleteSelected: "Elimina selezionati ({n})",
- urlMediaClickToSave: "Clicca per salvare nel vault",
- urlMediaDownloadAndReplace: "Scarica e sostituisci",
- urlMediaDownloading: "Download immagine...",
- urlMediaDownloadError: "Errore download: {status}",
- urlMediaFileSavedLinkFailed: "File salvato, ma aggiornamento link fallito",
- urlMediaSuccess: "Immagine salvata e link aggiornato",
- urlMediaFileSavedPatternNotFound: "File salvato, ma link non trovato nel testo",
- urlMediaError: "Errore: {error}",
- urlMediaUnknownError: "errore sconosciuto"
- },
- be: {
- cloneSettingsTitle: "\u041D\u0430\u043B\u0430\u0434\u043A\u0430 \u043A\u043B\u043E\u043D\u0430\u045E",
- removeBg: "\u0412\u044B\u0434\u0430\u043B\u0456\u0446\u044C \u0444\u043E\u043D",
- animation: "\u0410\u043D\u0456\u043C\u0430\u0446\u044B\u044F",
- animationPopup: "\u041F\u0430\u043F\u0430\u043F",
- animationFade: "\u0417\u2019\u044F\u045E\u043B\u0435\u043D\u043D\u0435",
- backgroundOpacity: "\u041F\u0440\u0430\u0437\u0440\u044B\u0441\u0442\u0430\u0441\u0446\u044C \u0444\u043E\u043D\u0443",
- controlsOpacity: "\u041F\u0440\u0430\u0437\u0440\u044B\u0441\u0442\u0430\u0441\u0446\u044C \u043F\u0430\u043D\u044D\u043B\u0456",
- showAudioHandles: "\u041F\u0430\u043A\u0430\u0437\u0432\u0430\u0446\u044C \u0440\u0443\u0447\u043A\u0456 \u0430\u045E\u0434\u044B\u0451",
- presets: "\u041F\u0430\u043C\u0435\u0440\u044B",
- fit: "\u041F\u0430\u0434\u0430\u0433\u043D\u0430\u0446\u044C \u0434\u0430 \u043A\u0430\u043D\u0442\u044D\u0439\u043D\u0435\u0440\u0430",
- natural: "\u041D\u0430\u0442\u0443\u0440\u0430\u043B\u044C\u043D\u044B \u043F\u0430\u043C\u0435\u0440",
- add: "\u0414\u0430\u0434\u0430\u0446\u044C\u2026",
- addPresetTitle: "\u0414\u0430\u0434\u0430\u0446\u044C \u043F\u0440\u044D\u0441\u0435\u0442 \u0448\u044B\u0440\u044B\u043D\u0456 \u2039px\u203A",
- placeholder: "\u043D\u0430\u043F\u0440\u044B\u043A\u043B\u0430\u0434: 820",
- ok: "OK",
- cancel: "\u0421\u043A\u0430\u0441\u0430\u0432\u0430\u0446\u044C",
- invalid: "\u041D\u044F\u043F\u0440\u0430\u0432\u0456\u043B\u044C\u043D\u0430\u0435 \u0437\u043D\u0430\u0447\u044D\u043D\u043D\u0435",
- maxReached: "\u0414\u0430\u0441\u0442\u0456\u0433\u043D\u0443\u0442\u044B \u043C\u0430\u043A\u0441\u0456\u043C\u0430\u043B\u044C\u043D\u044B \u043B\u0456\u043A \u043F\u0440\u044D\u0441\u0435\u0442\u0430\u045E",
- added: "\u0414\u0430\u0434\u0430\u0434\u0437\u0435\u043D\u044B \u043F\u0440\u044D\u0441\u0435\u0442 {n}",
- removed: "\u0412\u044B\u0434\u0430\u043B\u0435\u043D\u044B \u043F\u0440\u044D\u0441\u0435\u0442 {n}",
- openLocation: "\u0410\u0434\u043A\u0440\u044B\u0446\u044C \u0440\u0430\u0437\u043C\u044F\u0448\u0447\u044D\u043D\u043D\u0435 \u0444\u0430\u0439\u043B\u0430",
- fileNotFound: "\u0424\u0430\u0439\u043B \u0443 \u0441\u0445\u043E\u0432\u0456\u0448\u0447\u044B (vault) \u043D\u0435 \u0437\u043D\u043E\u0439\u0434\u0437\u0435\u043D\u044B",
- shellDenied: "\u041D\u044F\u043C\u0430 \u0434\u043E\u0441\u0442\u0443\u043F\u0443 \u0434\u0430 shell",
- localOnly: "\u0414\u0430\u0441\u0442\u0443\u043F\u043D\u0430 \u0442\u043E\u043B\u044C\u043A\u0456 \u0434\u043B\u044F \u043B\u0430\u043A\u0430\u043B\u044C\u043D\u044B\u0445 \u0441\u0445\u043E\u0432\u0456\u0448\u0447\u0430\u045E",
- restorePos: "\u0412\u044F\u0440\u043D\u0443\u0446\u044C \u043F\u0430\u0447\u0430\u0442\u043A\u043E\u0432\u0430\u0435 \u0441\u0442\u0430\u043D\u043E\u0432\u0456\u0448\u0447\u0430",
- setSize: "\u0417\u0430\u0434\u0430\u0446\u044C \u043F\u0430\u043C\u0435\u0440\u2026",
- width: "\u0428\u044B\u0440\u044B\u043D\u044F",
- height: "\u0412\u044B\u0448\u044B\u043D\u044F",
- keepAR: "\u0417\u0430\u0445\u043E\u045E\u0432\u0430\u0446\u044C \u043F\u0440\u0430\u043F\u043E\u0440\u0446\u044B\u0456",
- sbReady: "ML \u2022 \u0433\u0430\u0442\u043E\u0432\u0430",
- sbTitle: "Media Layout: \u043A\u043B\u0456\u043A \u2014 \u043F\u0435\u0440\u0430\u0441\u043A\u0430\u043D\u0430\u0432\u0430\u0446\u044C",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 \u0443\u0431\u0443\u0434\u043E\u0432\u0430 \u0430\u043A\u0442\u044B\u045E\u043D\u0430\u044F",
- posPresets: "\u0417\u0430\u0445\u0430\u0432\u0430\u0446\u044C \u0441\u0442\u0430\u043D\u043E\u0432\u0456\u0448\u0447\u044B \u043A\u043B\u043E\u043D\u0430\u045E",
- resetAll: "\u0421\u043A\u0456\u0434 \u0443\u0441\u0456\u0445 \u043D\u0430\u043B\u0430\u0434",
- saveLayout: "\u0417\u0430\u0445\u0430\u0432\u0430\u0446\u044C \u043F\u0440\u044D\u0441\u0435\u0442",
- layoutMax: "\u0414\u0430\u0441\u0442\u0456\u0433\u043D\u0443\u0442\u044B \u043C\u0430\u043A\u0441\u0456\u043C\u0430\u043B\u044C\u043D\u044B \u043B\u0456\u043A \u043F\u0440\u044D\u0441\u0435\u0442\u0430\u045E",
- saved: "\u0417\u0430\u0445\u0430\u0432\u0430\u043D\u0430",
- layoutN: "\u041F\u0440\u044D\u0441\u0435\u0442 {n}",
- renameTitle: "\u0423\u0432\u044F\u0434\u0437\u0456\u0446\u0435 \u043D\u0430\u0437\u0432\u0443",
- addNote: "\u0414\u0430\u0434\u0430\u0446\u044C \u0442\u0430\u0431\u043B\u0456\u0447\u043A\u0443",
- copyLayout: "\u0421\u043A\u0430\u043F\u0456\u0440\u0430\u0432\u0430\u0446\u044C \u0434\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456",
- pasteLayout: "\u0423\u0441\u0442\u0430\u0432\u0456\u0446\u044C \u0434\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456",
- deleteNote: "\u0412\u044B\u0434\u0430\u043B\u0456\u0446\u044C \u0442\u0430\u0431\u043B\u0456\u0447\u043A\u0443",
- detach: "\u0410\u0434\u0447\u0430\u043F\u0456\u0446\u044C",
- bgColor: "\u0417\u043C\u044F\u043D\u0456\u0446\u044C \u043A\u043E\u043B\u0435\u0440 \u0444\u043E\u043D\u0443",
- duplicate: "\u0414\u0443\u0431\u043B\u044F\u0432\u0430\u0446\u044C",
- reset: "\u0421\u043A\u0456\u0434",
- deleteClone: "\u0412\u044B\u0434\u0430\u043B\u0456\u0446\u044C",
- lock: "\u0417\u0430\u0444\u0456\u043A\u0441\u0430\u0432\u0430\u0446\u044C",
- unlock: "\u0420\u0430\u0437\u0431\u043B\u0430\u043A\u0456\u0440\u0430\u0432\u0430\u0446\u044C",
- localOnlyCheckboxLabel: "\u0422\u043E\u043B\u044C\u043A\u0456 \u0434\u043B\u044F \u0433\u044D\u0442\u0430\u0433\u0430 \u0444\u0430\u0439\u043B\u0430",
- makeInvisible: "\u0417\u0440\u0430\u0431\u0456\u0446\u044C \u043D\u044F\u0431\u0430\u0447\u043D\u044B\u043C",
- invalidLayoutData: "\u041D\u044F\u0441\u043B\u0443\u0448\u043D\u044B\u044F \u0434\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456 \u045E \u0431\u0443\u0444\u0435\u0440\u044B \u0430\u0431\u043C\u0435\u043D\u0443.",
- layoutPasted: "\u0414\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456 \u0456 \u043F\u0440\u044D\u0441\u0435\u0442\u044B \u045E\u0441\u0442\u0430\u045E\u043B\u0435\u043D\u044B.",
- pasteLayoutFailed: "\u041D\u0435 \u045E\u0434\u0430\u043B\u043E\u0441\u044F \u045E\u0441\u0442\u0430\u0432\u0456\u0446\u044C \u0434\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456 \u0437 \u0431\u0443\u0444\u0435\u0440\u0430 \u0430\u0431\u043C\u0435\u043D\u0443.",
- layoutCopied: "\u0414\u0430\u0434\u0437\u0435\u043D\u044B\u044F \u0441\u0442\u0430\u0440\u043E\u043D\u043A\u0456 \u0441\u043A\u0430\u043F\u0456\u0440\u0430\u0432\u0430\u043D\u044B \u045E \u0431\u0443\u0444\u0435\u0440 \u0430\u0431\u043C\u0435\u043D\u0443.",
- makeVisible: "\u0417\u0440\u0430\u0431\u0456\u0446\u044C \u0431\u0430\u0447\u043D\u044B\u043C",
- bringLayerUp: "\u041F\u0430\u0434\u043D\u044F\u0446\u044C \u0441\u043B\u043E\u0439",
- sendLayerDown: "\u0410\u043F\u0443\u0441\u0446\u0456\u0446\u044C \u0441\u043B\u043E\u0439",
- showHandlesOnHoverLabel: "\u041F\u0430\u043A\u0430\u0437\u0432\u0430\u0446\u044C \u0440\u0443\u0447\u043A\u0456 \u043F\u0440\u044B \u043D\u0430\u0432\u044F\u0434\u0437\u0435\u043D\u043D\u0456 \u043A\u0443\u0440\u0441\u043E\u0440\u0430",
- hideDetachButtonLabel: "\u0421\u0445\u0430\u0432\u0430\u0446\u044C \u043A\u043D\u043E\u043F\u043A\u0443 \xAB\u0430\u0434\u043C\u0430\u0446\u0430\u0432\u0430\u0446\u044C\xBB",
- modalAnimationLabel: "\u0410\u043D\u0456\u043C\u0430\u0446\u044B\u044F \u043C\u0430\u0434\u0430\u043B\u044C\u043D\u044B\u0445 \u0432\u043E\u043A\u043D\u0430\u045E",
- cloneRestoreDelayLabel: "\u0417\u0430\u0442\u0440\u044B\u043C\u043A\u0430 \u0437\u2019\u044F\u045E\u043B\u0435\u043D\u043D\u044F \u043A\u043B\u043E\u043D\u0430\u045E",
- noteRestoreDelayLabel: "\u0417\u0430\u0442\u0440\u044B\u043C\u043A\u0430 \u0437\u2019\u044F\u045E\u043B\u0435\u043D\u043D\u044F \u0431\u043B\u043E\u043A\u0430\u045E \u043D\u0430\u0442\u0430\u0442\u0430\u043A",
- hideDelayLabel: "\u0417\u0430\u0442\u0440\u044B\u043C\u043A\u0430 \u0441\u043A\u0440\u044B\u0446\u0446\u044F \u043F\u0430\u043D\u044D\u043B\u0456",
- changeTextColor: "\u0417\u043C\u044F\u043D\u0456\u0446\u044C \u043A\u043E\u043B\u0435\u0440 \u0442\u044D\u043A\u0441\u0442\u0443",
- cloneNearFile: "\u041A\u043B\u0430\u043D\u0456\u0440\u0430\u0432\u0430\u0446\u044C \u043F\u043E\u0431\u0430\u0447 \u0437 \u0444\u0430\u0439\u043B\u0430\u043C",
- addStroke: "\u0414\u0430\u0434\u0430\u0446\u044C \u0430\u0431\u0432\u043E\u0434\u043A\u0443",
- applyToHeader: "\u041F\u0440\u044B\u043C\u044F\u043D\u0456\u0446\u044C \u0434\u043B\u044F \u0448\u0430\u043F\u043A\u0456",
- addLink: "\u0414\u0430\u0434\u0430\u0446\u044C \u0441\u043F\u0430\u0441\u044B\u043B\u043A\u0443",
- deleteSelected: "\u0412\u044B\u0434\u0430\u043B\u0456\u0446\u044C \u0432\u044B\u0431\u0440\u0430\u043D\u044B\u044F ({n})",
- urlMediaClickToSave: "\u041A\u043B\u0456\u043A \u0434\u043B\u044F \u0437\u0430\u0445\u0430\u0432\u0430\u043D\u043D\u044F \u045E vault",
- urlMediaDownloadAndReplace: "\u0421\u043F\u0430\u043C\u043F\u0430\u0432\u0430\u0446\u044C \u0456 \u0437\u0430\u043C\u044F\u043D\u0456\u0446\u044C",
- urlMediaDownloading: "\u0421\u043F\u0430\u043C\u043F\u043E\u045E\u0432\u0430\u043D\u043D\u0435 \u0432\u044B\u044F\u0432\u044B...",
- urlMediaDownloadError: "\u041F\u0430\u043C\u044B\u043B\u043A\u0430 \u0441\u043F\u0430\u043C\u043F\u043E\u045E\u0432\u0430\u043D\u043D\u044F: {status}",
- urlMediaFileSavedLinkFailed: "\u0424\u0430\u0439\u043B \u0437\u0430\u0445\u0430\u0432\u0430\u043D\u044B, \u0430\u043B\u0435 \u043D\u0435 \u0430\u0442\u0440\u044B\u043C\u0430\u043B\u0430\u0441\u044F \u0430\u0431\u043D\u0430\u0432\u0456\u0446\u044C \u0441\u043F\u0430\u0441\u044B\u043B\u043A\u0443",
- urlMediaSuccess: "\u0412\u044B\u044F\u0432\u0430 \u0437\u0430\u0445\u0430\u0432\u0430\u043D\u0430 \u0456 \u0441\u043F\u0430\u0441\u044B\u043B\u043A\u0430 \u0430\u0431\u043D\u043E\u045E\u043B\u0435\u043D\u0430",
- urlMediaFileSavedPatternNotFound: "\u0424\u0430\u0439\u043B \u0437\u0430\u0445\u0430\u0432\u0430\u043D\u044B, \u0430\u043B\u0435 \u0441\u043F\u0430\u0441\u044B\u043B\u043A\u0430 \u043D\u0435 \u0437\u043D\u043E\u0439\u0434\u0437\u0435\u043D\u0430 \u045E \u0442\u044D\u043A\u0441\u0446\u0435",
- urlMediaError: "\u041F\u0430\u043C\u044B\u043B\u043A\u0430: {error}",
- urlMediaUnknownError: "\u043D\u0435\u0432\u044F\u0434\u043E\u043C\u0430\u044F \u043F\u0430\u043C\u044B\u043B\u043A\u0430"
- },
- uk: {
- cloneSettingsTitle: "\u041D\u0430\u043B\u0430\u0448\u0442\u0443\u0432\u0430\u043D\u043D\u044F \u043A\u043B\u043E\u043D\u0456\u0432",
- removeBg: "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0442\u043B\u043E",
- animation: "\u0410\u043D\u0456\u043C\u0430\u0446\u0456\u044F",
- animationPopup: "\u0421\u043F\u043B\u0438\u0432\u0430\u044E\u0447\u0430",
- animationFade: "\u041F\u043E\u0441\u0442\u0443\u043F\u043E\u0432\u0430",
- backgroundOpacity: "\u041F\u0440\u043E\u0437\u043E\u0440\u0456\u0441\u0442\u044C \u0444\u043E\u043D\u0443",
- controlsOpacity: "\u041F\u0440\u043E\u0437\u043E\u0440\u0456\u0441\u0442\u044C \u043F\u0430\u043D\u0435\u043B\u0456",
- showAudioHandles: "\u041F\u043E\u043A\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0440\u0443\u0447\u043A\u0438 \u0443 \u0430\u0443\u0434\u0456\u043E",
- presets: "\u0420\u043E\u0437\u043C\u0456\u0440\u0438",
- fit: "\u041F\u0456\u0434\u0456\u0433\u043D\u0430\u0442\u0438 \u043F\u0456\u0434 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440",
- natural: "\u041F\u0440\u0438\u0440\u043E\u0434\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440",
- add: "\u0414\u043E\u0434\u0430\u0442\u0438\u2026",
- addPresetTitle: "\u0414\u043E\u0434\u0430\u0442\u0438 \u043F\u0440\u0435\u0441\u0435\u0442 \u0448\u0438\u0440\u0438\u043D\u0438 \u2039px\u203A",
- placeholder: "\u043D\u0430\u043F\u0440\u0438\u043A\u043B\u0430\u0434: 820",
- ok: "OK",
- cancel: "\u0421\u043A\u0430\u0441\u0443\u0432\u0430\u0442\u0438",
- invalid: "\u041D\u0435\u0432\u0456\u0440\u043D\u0435 \u0437\u043D\u0430\u0447\u0435\u043D\u043D\u044F",
- maxReached: "\u0414\u043E\u0441\u044F\u0433\u043D\u0443\u0442\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C \u043F\u0440\u0435\u0441\u0435\u0442\u0456\u0432",
- added: "\u0414\u043E\u0434\u0430\u043D\u043E \u043F\u0440\u0435\u0441\u0435\u0442 {n}",
- removed: "\u0412\u0438\u043B\u0443\u0447\u0435\u043D\u043E \u043F\u0440\u0435\u0441\u0435\u0442 {n}",
- openLocation: "\u0412\u0456\u0434\u043A\u0440\u0438\u0442\u0438 \u0440\u043E\u0437\u0442\u0430\u0448\u0443\u0432\u0430\u043D\u043D\u044F \u0444\u0430\u0439\u043B\u0443",
- fileNotFound: "\u0424\u0430\u0439\u043B \u043D\u0435 \u0437\u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0443 \u0441\u0445\u043E\u0432\u0438\u0449\u0456 (vault)",
- shellDenied: "\u041D\u0435\u043C\u0430\u0454 \u0434\u043E\u0441\u0442\u0443\u043F\u0443 \u0434\u043E shell",
- localOnly: "\u0414\u043E\u0441\u0442\u0443\u043F\u043D\u043E \u043B\u0438\u0448\u0435 \u0434\u043B\u044F \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u0438\u0445 \u0441\u0445\u043E\u0432\u0438\u0449",
- restorePos: "\u041F\u043E\u0432\u0435\u0440\u043D\u0443\u0442\u0438 \u043F\u043E\u0447\u0430\u0442\u043A\u043E\u0432\u0435 \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044F",
- setSize: "\u0417\u0430\u0434\u0430\u0442\u0438 \u0440\u043E\u0437\u043C\u0456\u0440\u2026",
- width: "\u0428\u0438\u0440\u0438\u043D\u0430",
- height: "\u0412\u0438\u0441\u043E\u0442\u0430",
- keepAR: "\u0417\u0431\u0435\u0440\u0456\u0433\u0430\u0442\u0438 \u043F\u0440\u043E\u043F\u043E\u0440\u0446\u0456\u0457",
- sbReady: "ML \u2022 \u0433\u043E\u0442\u043E\u0432\u043E",
- sbTitle: "Media Layout: \u043A\u043B\u0456\u043A \u2014 \u043F\u0435\u0440\u0435\u0441\u043A\u0430\u043D\u0443\u0432\u0430\u0442\u0438",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 \u043F\u043B\u0430\u0433\u0456\u043D \u0430\u043A\u0442\u0438\u0432\u043D\u0438\u0439",
- posPresets: "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u043F\u043E\u043B\u043E\u0436\u0435\u043D\u043D\u044F \u043A\u043B\u043E\u043D\u0456\u0432",
- resetAll: "\u0421\u043A\u0438\u043D\u0443\u0442\u0438 \u0432\u0441\u0435",
- saveLayout: "\u0417\u0431\u0435\u0440\u0435\u0433\u0442\u0438 \u043F\u0440\u0435\u0441\u0435\u0442",
- layoutMax: "\u0414\u043E\u0441\u044F\u0433\u043D\u0443\u0442\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0443\u043C \u043F\u0440\u0435\u0441\u0435\u0442\u0456\u0432",
- saved: "\u0417\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E",
- layoutN: "\u041F\u0440\u0435\u0441\u0435\u0442 {n}",
- renameTitle: "\u0412\u0432\u0435\u0434\u0456\u0442\u044C \u043D\u0430\u0437\u0432\u0443",
- addNote: "\u0414\u043E\u0434\u0430\u0442\u0438 \u0442\u0430\u0431\u043B\u0438\u0447\u043A\u0443",
- copyLayout: "\u041A\u043E\u043F\u0456\u044E\u0432\u0430\u0442\u0438 \u0434\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438",
- pasteLayout: "\u0412\u0441\u0442\u0430\u0432\u0438\u0442\u0438 \u0434\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438",
- deleteNote: "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0442\u0430\u0431\u043B\u0438\u0447\u043A\u0443",
- detach: "\u0412\u0456\u0434\u0447\u0435\u043F\u0438\u0442\u0438",
- bgColor: "\u0417\u043C\u0456\u043D\u0438\u0442\u0438 \u043A\u043E\u043B\u0456\u0440 \u0444\u043E\u043D\u0443",
- duplicate: "\u0414\u0443\u0431\u043B\u044E\u0432\u0430\u0442\u0438",
- reset: "\u0421\u043A\u0438\u043D\u0443\u0442\u0438",
- deleteClone: "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",
- lock: "\u0417\u0430\u043A\u0440\u0456\u043F\u0438\u0442\u0438",
- unlock: "\u0420\u043E\u0437\u0431\u043B\u043E\u043A\u0443\u0432\u0430\u0442\u0438",
- localOnlyCheckboxLabel: "\u041B\u0438\u0448\u0435 \u0434\u043B\u044F \u0446\u044C\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0443",
- makeInvisible: "\u0417\u0440\u043E\u0431\u0438\u0442\u0438 \u043D\u0435\u0432\u0438\u0434\u0438\u043C\u0438\u043C",
- invalidLayoutData: "\u041D\u0435\u043A\u043E\u0440\u0435\u043A\u0442\u043D\u0456 \u0434\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438 \u0432 \u0431\u0443\u0444\u0435\u0440\u0456 \u043E\u0431\u043C\u0456\u043D\u0443.",
- layoutPasted: "\u0414\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438 \u0442\u0430 \u043F\u0440\u0435\u0441\u0435\u0442\u0438 \u0432\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u043E.",
- pasteLayoutFailed: "\u041D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u0432\u0441\u0442\u0430\u0432\u0438\u0442\u0438 \u0434\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438 \u0437 \u0431\u0443\u0444\u0435\u0440\u0430 \u043E\u0431\u043C\u0456\u043D\u0443.",
- layoutCopied: "\u0414\u0430\u043D\u0456 \u0441\u0442\u043E\u0440\u0456\u043D\u043A\u0438 \u0441\u043A\u043E\u043F\u0456\u0439\u043E\u0432\u0430\u043D\u043E \u0432 \u0431\u0443\u0444\u0435\u0440 \u043E\u0431\u043C\u0456\u043D\u0443.",
- makeVisible: "\u0417\u0440\u043E\u0431\u0438\u0442\u0438 \u0432\u0438\u0434\u0438\u043C\u0438\u043C",
- bringLayerUp: "\u041F\u0456\u0434\u043D\u044F\u0442\u0438 \u0448\u0430\u0440",
- sendLayerDown: "\u041E\u043F\u0443\u0441\u0442\u0438\u0442\u0438 \u0448\u0430\u0440",
- showHandlesOnHoverLabel: "\u041F\u043E\u043A\u0430\u0437\u0443\u0432\u0430\u0442\u0438 \u0440\u0443\u0447\u043A\u0438 \u043F\u0456\u0434 \u0447\u0430\u0441 \u043D\u0430\u0432\u0435\u0434\u0435\u043D\u043D\u044F",
- hideDetachButtonLabel: "\u041F\u0440\u0438\u0445\u043E\u0432\u0430\u0442\u0438 \u043A\u043D\u043E\u043F\u043A\u0443 \xAB\u0432\u0456\u0434\u043A\u0440\u0456\u043F\u0438\u0442\u0438\xBB",
- modalAnimationLabel: "\u0410\u043D\u0456\u043C\u0430\u0446\u0456\u044F \u043C\u043E\u0434\u0430\u043B\u044C\u043D\u0438\u0445 \u0432\u0456\u043A\u043E\u043D",
- cloneRestoreDelayLabel: "\u0417\u0430\u0442\u0440\u0438\u043C\u043A\u0430 \u043F\u043E\u044F\u0432\u0438 \u043A\u043B\u043E\u043D\u0456\u0432",
- noteRestoreDelayLabel: "\u0417\u0430\u0442\u0440\u0438\u043C\u043A\u0430 \u043F\u043E\u044F\u0432\u0438 \u0431\u043B\u043E\u043A\u0456\u0432 \u043D\u043E\u0442\u0430\u0442\u043E\u043A",
- hideDelayLabel: "\u0417\u0430\u0442\u0440\u0438\u043C\u043A\u0430 \u043F\u0440\u0438\u0445\u043E\u0432\u0443\u0432\u0430\u043D\u043D\u044F \u043F\u0430\u043D\u0435\u043B\u0456",
- changeTextColor: "\u0417\u043C\u0456\u043D\u0438\u0442\u0438 \u043A\u043E\u043B\u0456\u0440 \u0442\u0435\u043A\u0441\u0442\u0443",
- cloneNearFile: "\u041A\u043B\u043E\u043D\u0443\u0432\u0430\u0442\u0438 \u043F\u043E\u0440\u0443\u0447 \u0456\u0437 \u0444\u0430\u0439\u043B\u043E\u043C",
- addStroke: "\u0414\u043E\u0434\u0430\u0442\u0438 \u043E\u0431\u0432\u0435\u0434\u0435\u043D\u043D\u044F",
- applyToHeader: "\u0417\u0430\u0441\u0442\u043E\u0441\u0443\u0432\u0430\u0442\u0438 \u0434\u043E \u0448\u0430\u043F\u043A\u0438",
- addLink: "\u0414\u043E\u0434\u0430\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
- deleteSelected: "\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438 \u0432\u0438\u0431\u0440\u0430\u043D\u0456 ({n})",
- urlMediaClickToSave: "\u041A\u043B\u0456\u043A \u0434\u043B\u044F \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043D\u044F \u0432 vault",
- urlMediaDownloadAndReplace: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0438\u0442\u0438 \u0456 \u0437\u0430\u043C\u0456\u043D\u0438\u0442\u0438",
- urlMediaDownloading: "\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F...",
- urlMediaDownloadError: "\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F: {status}",
- urlMediaFileSavedLinkFailed: "\u0424\u0430\u0439\u043B \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E, \u0430\u043B\u0435 \u043D\u0435 \u0432\u0434\u0430\u043B\u043E\u0441\u044F \u043E\u043D\u043E\u0432\u0438\u0442\u0438 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F",
- urlMediaSuccess: "\u0417\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E \u0456 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F \u043E\u043D\u043E\u0432\u043B\u0435\u043D\u043E",
- urlMediaFileSavedPatternNotFound: "\u0424\u0430\u0439\u043B \u0437\u0431\u0435\u0440\u0435\u0436\u0435\u043D\u043E, \u0430\u043B\u0435 \u043F\u043E\u0441\u0438\u043B\u0430\u043D\u043D\u044F \u043D\u0435 \u0437\u043D\u0430\u0439\u0434\u0435\u043D\u043E \u0432 \u0442\u0435\u043A\u0441\u0442\u0456",
- urlMediaError: "\u041F\u043E\u043C\u0438\u043B\u043A\u0430: {error}",
- urlMediaUnknownError: "\u043D\u0435\u0432\u0456\u0434\u043E\u043C\u0430 \u043F\u043E\u043C\u0438\u043B\u043A\u0430"
- },
- uz: {
- cloneSettingsTitle: "Klon sozlamalari",
- removeBg: "Fonni olib tashlash",
- animation: "Animatsiya",
- animationPopup: "Popup",
- animationFade: "So\u2018nish",
- backgroundOpacity: "Fon shaffofligi",
- controlsOpacity: "Panel shaffofligi",
- showAudioHandles: "Audio qo\u2018llarini ko\u2018rsatish",
- presets: "O\u2018lchamlar",
- fit: "Konteynerga moslashtir",
- natural: "Tabiiy o\u2018lcham",
- add: "Qo\u2018shish\u2026",
- addPresetTitle: "Kenglik preseti qo\u2018shish \u2039px\u203A",
- placeholder: "masalan: 820",
- ok: "OK",
- cancel: "Bekor qilish",
- invalid: "Noto\u2018g\u2018ri qiymat",
- maxReached: "Presetlar sonining chegarasi",
- added: "Preset {n} qo\u2018shildi",
- removed: "Preset {n} o\u2018chirildi",
- openLocation: "Fayl joyini ochish",
- fileNotFound: "Fayl vault ichida topilmadi",
- shellDenied: "Shell\u2019ga ruxsat yo\u2018q",
- localOnly: "Faqat lokal vaultlar uchun",
- restorePos: "Boshlang\u2018ich joyni tiklash",
- setSize: "O\u2018lchamni o\u2018rnatish\u2026",
- width: "Kenglik",
- height: "Balandlik",
- keepAR: "Nisbatni saqlash",
- sbReady: "ML \u2022 tayyor",
- sbTitle: "Media Layout: qayta skan qilish uchun bosing",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 plagin faol",
- posPresets: "Klon joylashuvlarini saqlash",
- resetAll: "Hammasini tiklash",
- saveLayout: "Presetni saqlash",
- layoutMax: "Presetlar sonining chegarasi",
- saved: "Saqlandi",
- layoutN: "Preset {n}",
- renameTitle: "Nom kiriting",
- addNote: "Yorliq qo\u2018shish",
- copyLayout: "Sahifa ma\u2019lumotlarini nusxalash",
- pasteLayout: "Sahifa ma\u2019lumotlarini joylash",
- deleteNote: "Yorliqni o\u2018chirish",
- detach: "Ajratish",
- bgColor: "Fon rangini o\u2018zgartirish",
- duplicate: "Nusxa olish",
- reset: "Qayta o\u2018rnatish",
- deleteClone: "Klonni o\u2018chirish",
- lock: "Qulflash",
- unlock: "Qulfni ochish",
- localOnlyCheckboxLabel: "Faqat ushbu fayl uchun",
- makeInvisible: "Ko\u2018rinmas qilish",
- invalidLayoutData: "Buferdagi sahifa ma\u2019lumotlari noto\u2018g\u2018ri.",
- layoutPasted: "Sahifa ma\u2019lumotlari va presetlar joylandi.",
- pasteLayoutFailed: "Sahifa ma\u2019lumotlarini buferdan joylab bo\u2018lmadi.",
- layoutCopied: "Sahifa ma\u2019lumotlari buferga nusxalandi.",
- makeVisible: "Ko\u2018rinadigan qilish",
- bringLayerUp: "Qatlamni yuqoriga",
- sendLayerDown: "Qatlamni pastga",
- showHandlesOnHoverLabel: "Kursorni olib o\u2018tganda tutqichlarni ko\u2018rsatish",
- hideDetachButtonLabel: "\u201CBiriktirishni yechish\u201D tugmasini yashirish",
- modalAnimationLabel: "Modal oynalar animatsiyasi",
- cloneRestoreDelayLabel: "Klonlarning paydo bo\u2018lish kechikishi",
- noteRestoreDelayLabel: "Eslatma bloklarining paydo bo\u2018lish kechikishi",
- hideDelayLabel: "Panelni yashirish kechikishi",
- changeTextColor: "Matn rangini o'zgartirish",
- cloneNearFile: "Fayl yoniga klon yaratish",
- addStroke: "Chiziq qo'shish",
- applyToHeader: "Sarlavhaga qo'llash",
- addLink: "Havola qo\u02BBshish",
- deleteSelected: "Tanlanganlarni o\u02BBchirish ({n})",
- urlMediaClickToSave: "Vaultga saqlash uchun bosing",
- urlMediaDownloadAndReplace: "Yuklab olish va almashtirish",
- urlMediaDownloading: "Rasm yuklanmoqda...",
- urlMediaDownloadError: "Yuklash xatosi: {status}",
- urlMediaFileSavedLinkFailed: "Fayl saqlandi, lekin havolani yangilab bo'lmadi",
- urlMediaSuccess: "Rasm saqlandi va havola yangilandi",
- urlMediaFileSavedPatternNotFound: "Fayl saqlandi, lekin matnda havola topilmadi",
- urlMediaError: "Xato: {error}",
- urlMediaUnknownError: "noma'lum xato"
- },
- vi: {
- cloneSettingsTitle: "T\xF9y ch\u1EC9nh b\u1EA3n sao",
- removeBg: "X\xF3a n\u1EC1n",
- animation: "Hi\u1EC7u \u1EE9ng",
- animationPopup: "Popup",
- animationFade: "M\u1EDD d\u1EA7n",
- backgroundOpacity: "\u0110\u1ED9 m\u1EDD n\u1EC1n",
- controlsOpacity: "\u0110\u1ED9 m\u1EDD b\u1EA3ng \u0111i\u1EC1u khi\u1EC3n",
- showAudioHandles: "Hi\u1EC7n tay n\u1EAFm audio",
- presets: "K\xEDch th\u01B0\u1EDBc",
- fit: "V\u1EEBa v\u1EDBi khung",
- natural: "K\xEDch th\u01B0\u1EDBc g\u1ED1c",
- add: "Th\xEAm\u2026",
- addPresetTitle: "Th\xEAm preset chi\u1EC1u r\u1ED9ng \u2039px\u203A",
- placeholder: "vd: 820",
- ok: "OK",
- cancel: "Hu\u1EF7",
- invalid: "Gi\xE1 tr\u1ECB kh\xF4ng h\u1EE3p l\u1EC7",
- maxReached: "\u0110\xE3 \u0111\u1EA1t s\u1ED1 l\u01B0\u1EE3ng preset t\u1ED1i \u0111a",
- added: "\u0110\xE3 th\xEAm preset {n}",
- removed: "\u0110\xE3 xo\xE1 preset {n}",
- openLocation: "M\u1EDF v\u1ECB tr\xED t\u1EC7p",
- fileNotFound: "Kh\xF4ng t\xECm th\u1EA5y t\u1EC7p trong vault",
- shellDenied: "Kh\xF4ng c\xF3 quy\u1EC1n shell",
- localOnly: "Ch\u1EC9 d\xF9ng cho vault c\u1EE5c b\u1ED9",
- restorePos: "Kh\xF4i ph\u1EE5c v\u1ECB tr\xED g\u1ED1c",
- setSize: "\u0110\u1EB7t k\xEDch th\u01B0\u1EDBc\u2026",
- width: "R\u1ED9ng",
- height: "Cao",
- keepAR: "Gi\u1EEF t\u1EC9 l\u1EC7",
- sbReady: "ML \u2022 s\u1EB5n s\xE0ng",
- sbTitle: "Media Layout: nh\u1EA5p \u0111\u1EC3 qu\xE9t l\u1EA1i",
- cmdPing: "Media Layout: ping",
- pingOk: "ML: OK \u2014 plugin ho\u1EA1t \u0111\u1ED9ng",
- posPresets: "L\u01B0u v\u1ECB tr\xED c\u1EE7a c\xE1c b\u1EA3n sao",
- resetAll: "\u0110\u1EB7t l\u1EA1i t\u1EA5t c\u1EA3",
- saveLayout: "L\u01B0u preset",
- layoutMax: "\u0110\xE3 \u0111\u1EA1t s\u1ED1 l\u01B0\u1EE3ng preset t\u1ED1i \u0111a",
- saved: "\u0110\xE3 l\u01B0u",
- layoutN: "Preset {n}",
- renameTitle: "Nh\u1EADp t\xEAn",
- addNote: "Th\xEAm h\u1ED9p ghi ch\xFA",
- copyLayout: "Sao ch\xE9p d\u1EEF li\u1EC7u trang",
- pasteLayout: "D\xE1n d\u1EEF li\u1EC7u trang",
- deleteNote: "Xo\xE1 h\u1ED9p ghi ch\xFA",
- detach: "T\xE1ch ra",
- bgColor: "\u0110\u1ED5i m\xE0u n\u1EC1n",
- duplicate: "Nh\xE2n b\u1EA3n",
- reset: "\u0110\u1EB7t l\u1EA1i",
- deleteClone: "Xo\xE1",
- lock: "Kho\xE1",
- unlock: "M\u1EDF kho\xE1",
- localOnlyCheckboxLabel: "Ch\u1EC9 cho t\u1EC7p n\xE0y",
- makeInvisible: "\u1EA8n \u0111i",
- invalidLayoutData: "D\u1EEF li\u1EC7u trang trong clipboard kh\xF4ng h\u1EE3p l\u1EC7.",
- layoutPasted: "\u0110\xE3 d\xE1n d\u1EEF li\u1EC7u v\xE0 preset c\u1EE7a trang.",
- pasteLayoutFailed: "Kh\xF4ng th\u1EC3 d\xE1n d\u1EEF li\u1EC7u trang t\u1EEB clipboard.",
- layoutCopied: "\u0110\xE3 sao ch\xE9p d\u1EEF li\u1EC7u trang v\xE0o clipboard.",
- makeVisible: "Hi\u1EC7n l\xEAn",
- bringLayerUp: "\u0110\u01B0a l\u1EDBp l\xEAn",
- sendLayerDown: "\u0110\u01B0a l\u1EDBp xu\u1ED1ng",
- showHandlesOnHoverLabel: "Hi\u1EC7n tay c\u1EA7m khi r\xEA chu\u1ED9t",
- hideDetachButtonLabel: "\u1EA8n n\xFAt \u201CG\u1EE1 ghim\u201D",
- modalAnimationLabel: "Hi\u1EC7u \u1EE9ng cho c\u1EEDa s\u1ED5 modal",
- cloneRestoreDelayLabel: "\u0110\u1ED9 tr\u1EC5 xu\u1EA5t hi\u1EC7n c\u1EE7a b\u1EA3n sao",
- noteRestoreDelayLabel: "\u0110\u1ED9 tr\u1EC5 xu\u1EA5t hi\u1EC7n c\u1EE7a h\u1ED9p ghi ch\xFA",
- hideDelayLabel: "\u0110\u1ED9 tr\u1EC5 \u1EA9n b\u1EA3ng \u0111i\u1EC1u khi\u1EC3n",
- changeTextColor: "\u0110\u1ED5i m\xE0u ch\u1EEF",
- cloneNearFile: "Nh\xE2n b\u1EA3n b\xEAn c\u1EA1nh t\u1EC7p",
- addStroke: "Th\xEAm vi\u1EC1n",
- applyToHeader: "\xC1p d\u1EE5ng cho ti\xEAu \u0111\u1EC1",
- addLink: "Th\xEAm li\xEAn k\u1EBFt",
- deleteSelected: "X\xF3a m\u1EE5c \u0111\xE3 ch\u1ECDn ({n})",
- urlMediaClickToSave: "Nh\u1EA5p \u0111\u1EC3 l\u01B0u v\xE0o vault",
- urlMediaDownloadAndReplace: "T\u1EA3i xu\u1ED1ng v\xE0 thay th\u1EBF",
- urlMediaDownloading: "\u0110ang t\u1EA3i h\xECnh \u1EA3nh...",
- urlMediaDownloadError: "L\u1ED7i t\u1EA3i xu\u1ED1ng: {status}",
- urlMediaFileSavedLinkFailed: "T\u1EC7p \u0111\xE3 l\u01B0u, nh\u01B0ng kh\xF4ng th\u1EC3 c\u1EADp nh\u1EADt li\xEAn k\u1EBFt",
- urlMediaSuccess: "H\xECnh \u1EA3nh \u0111\xE3 l\u01B0u v\xE0 li\xEAn k\u1EBFt \u0111\xE3 c\u1EADp nh\u1EADt",
- urlMediaFileSavedPatternNotFound: "T\u1EC7p \u0111\xE3 l\u01B0u, nh\u01B0ng kh\xF4ng t\xECm th\u1EA5y li\xEAn k\u1EBFt trong v\u0103n b\u1EA3n",
- urlMediaError: "L\u1ED7i: {error}",
- urlMediaUnknownError: "l\u1ED7i kh\xF4ng x\xE1c \u0111\u1ECBnh"
- },
- "zh-CN": {
- cloneSettingsTitle: "\u514B\u9686\u81EA\u5B9A\u4E49",
- removeBg: "\u79FB\u9664\u80CC\u666F",
- animation: "\u52A8\u753B",
- animationPopup: "\u5F39\u51FA",
- animationFade: "\u6DE1\u5165",
- backgroundOpacity: "\u80CC\u666F\u4E0D\u900F\u660E\u5EA6",
- controlsOpacity: "\u9762\u677F\u4E0D\u900F\u660E\u5EA6",
- showAudioHandles: "\u663E\u793A\u97F3\u9891\u624B\u67C4",
- presets: "\u5C3A\u5BF8",
- fit: "\u9002\u914D\u5BB9\u5668",
- natural: "\u539F\u59CB\u5C3A\u5BF8",
- add: "\u6DFB\u52A0\u2026",
- addPresetTitle: "\u6DFB\u52A0\u5BBD\u5EA6\u9884\u8BBE \u2039px\u203A",
- placeholder: "\u4F8B\u5982\uFF1A820",
- ok: "\u786E\u5B9A",
- cancel: "\u53D6\u6D88",
- invalid: "\u65E0\u6548\u7684\u503C",
- maxReached: "\u5DF2\u8FBE\u5230\u9884\u8BBE\u7684\u6700\u5927\u6570\u91CF",
- added: "\u5DF2\u6DFB\u52A0\u9884\u8BBE {n}",
- removed: "\u5DF2\u5220\u9664\u9884\u8BBE {n}",
- openLocation: "\u6253\u5F00\u6587\u4EF6\u4F4D\u7F6E",
- fileNotFound: "\u5728 Vault \u4E2D\u672A\u627E\u5230\u6587\u4EF6",
- shellDenied: "\u65E0\u6743\u8BBF\u95EE shell",
- localOnly: "\u4EC5\u9002\u7528\u4E8E\u672C\u5730 Vault",
- restorePos: "\u6062\u590D\u521D\u59CB\u4F4D\u7F6E",
- setSize: "\u8BBE\u7F6E\u5C3A\u5BF8\u2026",
- width: "\u5BBD\u5EA6",
- height: "\u9AD8\u5EA6",
- keepAR: "\u4FDD\u6301\u7EB5\u6A2A\u6BD4",
- sbReady: "ML \u2022 \u5C31\u7EEA",
- sbTitle: "Media Layout\uFF1A\u70B9\u51FB\u91CD\u65B0\u626B\u63CF",
- cmdPing: "Media Layout\uFF1Aping",
- pingOk: "ML\uFF1AOK \u2014 \u63D2\u4EF6\u5DF2\u6FC0\u6D3B",
- posPresets: "\u4FDD\u5B58\u514B\u9686\u7684\u4F4D\u7F6E",
- resetAll: "\u5168\u90E8\u91CD\u7F6E",
- saveLayout: "\u4FDD\u5B58\u9884\u8BBE",
- layoutMax: "\u5DF2\u8FBE\u5230\u9884\u8BBE\u7684\u6700\u5927\u6570\u91CF",
- saved: "\u5DF2\u4FDD\u5B58",
- layoutN: "\u9884\u8BBE {n}",
- renameTitle: "\u8F93\u5165\u540D\u79F0",
- addNote: "\u6DFB\u52A0\u6807\u6CE8\u6846",
- copyLayout: "\u590D\u5236\u9875\u9762\u6570\u636E",
- pasteLayout: "\u7C98\u8D34\u9875\u9762\u6570\u636E",
- deleteNote: "\u5220\u9664\u6807\u6CE8\u6846",
- detach: "\u5206\u79BB",
- bgColor: "\u66F4\u6539\u80CC\u666F\u989C\u8272",
- duplicate: "\u590D\u5236",
- reset: "\u91CD\u7F6E",
- deleteClone: "\u79FB\u8D70",
- lock: "\u9501\u5B9A",
- unlock: "\u89E3\u9501",
- localOnlyCheckboxLabel: "\u4EC5\u5BF9\u6B64\u6587\u4EF6",
- makeInvisible: "\u8BBE\u4E3A\u4E0D\u53EF\u89C1",
- invalidLayoutData: "\u526A\u8D34\u677F\u4E2D\u7684\u9875\u9762\u6570\u636E\u65E0\u6548\u3002",
- layoutPasted: "\u5DF2\u7C98\u8D34\u9875\u9762\u6570\u636E\u4E0E\u9884\u8BBE\u3002",
- pasteLayoutFailed: "\u65E0\u6CD5\u4ECE\u526A\u8D34\u677F\u7C98\u8D34\u9875\u9762\u6570\u636E\u3002",
- layoutCopied: "\u9875\u9762\u6570\u636E\u5DF2\u590D\u5236\u5230\u526A\u8D34\u677F\u3002",
- makeVisible: "\u8BBE\u4E3A\u53EF\u89C1",
- bringLayerUp: "\u4E0A\u79FB\u56FE\u5C42",
- sendLayerDown: "\u4E0B\u79FB\u56FE\u5C42",
- showHandlesOnHoverLabel: "\u60AC\u505C\u65F6\u663E\u793A\u63A7\u5236\u624B\u67C4",
- hideDetachButtonLabel: "\u9690\u85CF\u201C\u53D6\u6D88\u56FA\u5B9A\u201D\u6309\u94AE",
- modalAnimationLabel: "\u6A21\u6001\u7A97\u53E3\u52A8\u753B",
- cloneRestoreDelayLabel: "\u514B\u9686\u663E\u793A\u5EF6\u8FDF",
- noteRestoreDelayLabel: "\u4FBF\u7B3A\u6846\u663E\u793A\u5EF6\u8FDF",
- hideDelayLabel: "\u9762\u677F\u9690\u85CF\u5EF6\u8FDF",
- changeTextColor: "\u66F4\u6539\u6587\u672C\u989C\u8272",
- cloneNearFile: "\u5728\u6587\u4EF6\u65C1\u8FB9\u514B\u9686",
- addStroke: "\u6DFB\u52A0\u63CF\u8FB9",
- applyToHeader: "\u5E94\u7528\u4E8E\u6807\u9898",
- addLink: "\u6DFB\u52A0\u94FE\u63A5",
- deleteSelected: "\u5220\u9664\u6240\u9009 ({n})",
- urlMediaClickToSave: "\u70B9\u51FB\u4FDD\u5B58\u5230 vault",
- urlMediaDownloadAndReplace: "\u4E0B\u8F7D\u5E76\u66FF\u6362",
- urlMediaDownloading: "\u6B63\u5728\u4E0B\u8F7D\u56FE\u7247...",
- urlMediaDownloadError: "\u4E0B\u8F7D\u9519\u8BEF: {status}",
- urlMediaFileSavedLinkFailed: "\u6587\u4EF6\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u65E0\u6CD5\u66F4\u65B0\u94FE\u63A5",
- urlMediaSuccess: "\u56FE\u7247\u5DF2\u4FDD\u5B58\u5E76\u66F4\u65B0\u94FE\u63A5",
- urlMediaFileSavedPatternNotFound: "\u6587\u4EF6\u5DF2\u4FDD\u5B58\uFF0C\u4F46\u5728\u6587\u672C\u4E2D\u627E\u4E0D\u5230\u94FE\u63A5",
- urlMediaError: "\u9519\u8BEF: {error}",
- urlMediaUnknownError: "\u672A\u77E5\u9519\u8BEF"
- },
- "zh-TW": {
- cloneSettingsTitle: "\u8907\u88FD\u9805\u81EA\u8A02",
- removeBg: "\u79FB\u9664\u80CC\u666F",
- animation: "\u52D5\u756B",
- animationPopup: "\u5F48\u51FA",
- animationFade: "\u6DE1\u5165",
- backgroundOpacity: "\u80CC\u666F\u4E0D\u900F\u660E\u5EA6",
- controlsOpacity: "\u9762\u677F\u4E0D\u900F\u660E\u5EA6",
- showAudioHandles: "\u986F\u793A\u97F3\u8A0A\u63A7\u5236\u9EDE",
- presets: "\u5C3A\u5BF8",
- fit: "\u9069\u914D\u5BB9\u5668",
- natural: "\u539F\u59CB\u5C3A\u5BF8",
- add: "\u65B0\u589E\u2026",
- addPresetTitle: "\u65B0\u589E\u5BEC\u5EA6\u9810\u8A2D \u2039px\u203A",
- placeholder: "\u4F8B\u5982\uFF1A820",
- ok: "\u78BA\u5B9A",
- cancel: "\u53D6\u6D88",
- invalid: "\u7121\u6548\u7684\u503C",
- maxReached: "\u5DF2\u9054\u5230\u9810\u8A2D\u7684\u6700\u5927\u6578\u91CF",
- added: "\u5DF2\u65B0\u589E\u9810\u8A2D {n}",
- removed: "\u5DF2\u522A\u9664\u9810\u8A2D {n}",
- openLocation: "\u958B\u555F\u6A94\u6848\u4F4D\u7F6E",
- fileNotFound: "\u5728 Vault \u4E2D\u627E\u4E0D\u5230\u6A94\u6848",
- shellDenied: "\u7121\u6CD5\u5B58\u53D6 shell",
- localOnly: "\u50C5\u9069\u7528\u65BC\u672C\u6A5F Vault",
- restorePos: "\u9084\u539F\u521D\u59CB\u4F4D\u7F6E",
- setSize: "\u8A2D\u5B9A\u5C3A\u5BF8\u2026",
- width: "\u5BEC\u5EA6",
- height: "\u9AD8\u5EA6",
- keepAR: "\u4FDD\u6301\u9577\u5BEC\u6BD4",
- sbReady: "ML \u2022 \u5C31\u7DD2",
- sbTitle: "Media Layout\uFF1A\u9EDE\u64CA\u91CD\u65B0\u6383\u63CF",
- cmdPing: "Media Layout\uFF1Aping",
- pingOk: "ML\uFF1AOK \u2014 \u5916\u639B\u5DF2\u555F\u7528",
- posPresets: "\u4FDD\u5B58\u514B\u9686\u7684\u4F4D\u7F6E",
- resetAll: "\u5168\u90E8\u91CD\u8A2D",
- saveLayout: "\u5132\u5B58\u9810\u8A2D",
- layoutMax: "\u5DF2\u9054\u5230\u9810\u8A2D\u7684\u6700\u5927\u6578\u91CF",
- saved: "\u5DF2\u5132\u5B58",
- layoutN: "\u9810\u8A2D {n}",
- renameTitle: "\u8F38\u5165\u540D\u7A31",
- addNote: "\u65B0\u589E\u6A19\u8A3B\u6846",
- copyLayout: "\u8907\u88FD\u9801\u9762\u8CC7\u6599",
- pasteLayout: "\u8CBC\u4E0A\u9801\u9762\u8CC7\u6599",
- deleteNote: "\u522A\u9664\u6A19\u8A3B\u6846",
- detach: "\u5206\u96E2",
- bgColor: "\u8B8A\u66F4\u80CC\u666F\u984F\u8272",
- duplicate: "\u91CD\u88FD",
- reset: "\u91CD\u8A2D",
- deleteClone: "\u79FB\u8D70",
- lock: "\u9396\u5B9A",
- unlock: "\u89E3\u9664\u9396\u5B9A",
- localOnlyCheckboxLabel: "\u50C5\u6B64\u6A94\u6848",
- makeInvisible: "\u8A2D\u70BA\u4E0D\u53EF\u898B",
- invalidLayoutData: "\u526A\u8CBC\u7C3F\u4E2D\u7684\u9801\u9762\u8CC7\u6599\u7121\u6548\u3002",
- layoutPasted: "\u5DF2\u8CBC\u4E0A\u9801\u9762\u8CC7\u6599\u8207\u9810\u8A2D\u3002",
- pasteLayoutFailed: "\u7121\u6CD5\u5F9E\u526A\u8CBC\u7C3F\u8CBC\u4E0A\u9801\u9762\u8CC7\u6599\u3002",
- layoutCopied: "\u9801\u9762\u8CC7\u6599\u5DF2\u8907\u88FD\u5230\u526A\u8CBC\u7C3F\u3002",
- makeVisible: "\u8A2D\u70BA\u53EF\u898B",
- bringLayerUp: "\u4E0A\u79FB\u5716\u5C64",
- sendLayerDown: "\u4E0B\u79FB\u5716\u5C64",
- showHandlesOnHoverLabel: "\u6ED1\u9F20\u61F8\u505C\u6642\u986F\u793A\u63A7\u5236\u628A\u624B",
- hideDetachButtonLabel: "\u96B1\u85CF\u300C\u53D6\u6D88\u91D8\u9078\u300D\u6309\u9215",
- modalAnimationLabel: "\u6A21\u614B\u8996\u7A97\u52D5\u756B",
- cloneRestoreDelayLabel: "\u8907\u672C\u986F\u793A\u5EF6\u9072",
- noteRestoreDelayLabel: "\u5099\u8A3B\u65B9\u584A\u986F\u793A\u5EF6\u9072",
- hideDelayLabel: "\u9762\u677F\u96B1\u85CF\u5EF6\u9072",
- changeTextColor: "\u8B8A\u66F4\u6587\u5B57\u984F\u8272",
- cloneNearFile: "\u5728\u6A94\u6848\u65C1\u908A\u514B\u9686",
- addStroke: "\u65B0\u589E\u5916\u6846",
- applyToHeader: "\u5957\u7528\u5230\u6A19\u984C",
- addLink: "\u65B0\u589E\u9023\u7D50",
- deleteSelected: "\u522A\u9664\u6240\u9078 ({n})",
- urlMediaClickToSave: "\u9EDE\u64CA\u5132\u5B58\u5230 vault",
- urlMediaDownloadAndReplace: "\u4E0B\u8F09\u4E26\u66FF\u63DB",
- urlMediaDownloading: "\u6B63\u5728\u4E0B\u8F09\u5716\u7247...",
- urlMediaDownloadError: "\u4E0B\u8F09\u932F\u8AA4: {status}",
- urlMediaFileSavedLinkFailed: "\u6A94\u6848\u5DF2\u5132\u5B58\uFF0C\u4F46\u7121\u6CD5\u66F4\u65B0\u9023\u7D50",
- urlMediaSuccess: "\u5716\u7247\u5DF2\u5132\u5B58\u4E26\u66F4\u65B0\u9023\u7D50",
- urlMediaFileSavedPatternNotFound: "\u6A94\u6848\u5DF2\u5132\u5B58\uFF0C\u4F46\u5728\u6587\u5B57\u4E2D\u627E\u4E0D\u5230\u9023\u7D50",
- urlMediaError: "\u932F\u8AA4: {error}",
- urlMediaUnknownError: "\u672A\u77E5\u932F\u8AA4"
- }
-};
-function t(app, key, vars) {
- const locale = app.getLanguage?.() ?? "en";
- const loc = String(locale).toLowerCase();
- const dict = I18N[loc] ?? I18N[loc.split("-")[0]] ?? I18N.en;
- let s = dict[key] ?? I18N.en[key];
- if (vars) for (const k in vars) s = s.replace(new RegExp(`\\{${k}\\}`, "g"), String(vars[k]));
- return s;
-}
-function findContentSizer(root) {
- if (!root) return null;
- let readingSizer = root.querySelector(":scope > .markdown-preview-sizer");
- if (readingSizer) return readingSizer;
- readingSizer = root.querySelector(".markdown-preview-view > .markdown-preview-sizer");
- if (readingSizer) return readingSizer;
- readingSizer = root.querySelector(".markdown-preview-sizer");
- if (readingSizer) return readingSizer;
- const editorSizer = root.querySelector(".cm-sizer");
- if (editorSizer) return editorSizer;
- if (root.classList.contains("markdown-preview-sizer") || root.classList.contains("cm-sizer")) {
- return root;
- }
- console.warn("[Media Layout] Could not find content sizer in root:", root);
- return null;
-}
-var MAX_WIDTH_PRESETS = 15;
-var MAX_LAYOUT_PRESETS = 150;
-var clamp = (n, min, max) => Math.max(min, Math.min(max, n));
-var snap = (n, grid = 20) => Math.round(n / grid) * grid;
-var WidthPresetModal = class extends import_obsidian.Modal {
- onSubmit;
- constructor(app, onSubmit) {
- super(app);
- this.onSubmit = onSubmit;
- }
- onOpen() {
- const { contentEl } = this;
- this.modalEl.addClass("ms-modal-width-355");
- contentEl.addClass("ms-bgcolor-modal-content");
- contentEl.addClass("ms-width-preset-modal-content");
- contentEl.empty();
- contentEl.createEl("h3", { text: t(this.app, "addPresetTitle") });
- let val = "";
- const input = contentEl.createEl("input", {
- type: "number",
- placeholder: t(this.app, "placeholder"),
- attr: { min: "64", max: "8192", step: "1" },
- cls: "ms-input-width-300"
- });
- input.addEventListener("keydown", (e) => {
- if (e.key === "Enter") this.closeWithValue();
- });
- input.addEventListener("input", () => val = input.value);
- input.focus();
- const row = contentEl.createDiv({ cls: "modal-button-container" });
- const ok = row.createEl("button", { text: t(this.app, "ok") });
- const cancel = row.createEl("button", { text: t(this.app, "cancel") });
- ok.addEventListener("click", () => this.closeWithValue());
- cancel.addEventListener("click", () => {
- this.onSubmit(null);
- this.close();
- });
- this.closeWithValue = () => {
- const n = Number.parseInt(val || input.value, 10);
- if (!Number.isFinite(n) || n < 64 || n > 8192) {
- new import_obsidian.Notice(t(this.app, "invalid"));
- return;
- }
- this.onSubmit(n);
- this.close();
- };
- }
- onClose() {
- this.contentEl.empty();
- }
- closeWithValue;
-};
-var RenamePresetModal = class extends import_obsidian.Modal {
- onSubmit;
- init;
- constructor(app, init, onSubmit) {
- super(app);
- this.init = init || "";
- this.onSubmit = onSubmit;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("ms-rename-modal");
- contentEl.createEl("h3", { text: t(this.app, "renameTitle") });
- const wrap = contentEl.createDiv({ cls: "ms-rename-form" });
- const inp = wrap.createEl("input", { type: "text", value: this.init, attr: { spellcheck: "false" } });
- const row = contentEl.createDiv({ cls: "ms-rename-actions" });
- const ok = row.createEl("button", { text: t(this.app, "ok") });
- const cancel = row.createEl("button", { text: t(this.app, "cancel") });
- const submit = () => {
- this.onSubmit(inp.value.trim() || null);
- this.close();
- };
- ok.onclick = submit;
- cancel.onclick = () => {
- this.onSubmit(null);
- this.close();
- };
- inp.onkeydown = (e) => {
- if (e.key === "Enter") submit();
- if (e.key === "Escape") {
- this.onSubmit(null);
- this.close();
- }
- };
- setTimeout(() => {
- inp.focus();
- inp.select();
- }, 0);
- }
- onClose() {
- this.contentEl.empty();
- }
-};
-var LinkModal = class extends import_obsidian.Modal {
- onSubmit;
- init;
- constructor(app, init, onSubmit) {
- super(app);
- this.init = init || "";
- this.onSubmit = onSubmit;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("ms-rename-modal");
- contentEl.createEl("h3", { text: t(this.app, "addLink") });
- const wrap = contentEl.createDiv({ cls: "ms-rename-form" });
- const inp = wrap.createEl("input", { type: "text", value: this.init, attr: { spellcheck: "false" } });
- inp.placeholder = "https://...";
- const row = contentEl.createDiv({ cls: "ms-rename-actions" });
- const ok = row.createEl("button", { text: t(this.app, "ok") });
- const cancel = row.createEl("button", { text: t(this.app, "cancel") });
- const submit = () => {
- this.onSubmit(inp.value.trim());
- this.close();
- };
- ok.onclick = submit;
- cancel.onclick = () => {
- this.onSubmit(null);
- this.close();
- };
- inp.onkeydown = (e) => {
- if (e.key === "Enter") submit();
- if (e.key === "Escape") {
- this.onSubmit(null);
- this.close();
- }
- };
- setTimeout(() => {
- inp.focus();
- inp.select();
- }, 0);
- }
- onClose() {
- this.contentEl.empty();
- }
-};
-var ImageSizeModal = class extends import_obsidian.Modal {
- onSubmit;
- initW;
- initH;
- constructor(app, w, h, onSubmit) {
- super(app);
- this.onSubmit = onSubmit;
- this.initW = w;
- this.initH = h;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- const form = contentEl.createDiv({ cls: "ms-imgsz-form" });
- form.createEl("h3", { text: t(this.app, "setSize") });
- const rowW = form.createDiv({ cls: "ms-row" });
- rowW.createEl("label", { text: t(this.app, "width") });
- const inW = rowW.createEl("input", { type: "number", value: String(Math.round(this.initW)), attr: { min: "1", step: "1" } });
- const rowH = form.createDiv({ cls: "ms-row" });
- rowH.createEl("label", { text: t(this.app, "height") });
- const inH = rowH.createEl("input", { type: "number", value: String(Math.round(this.initH)), attr: { min: "1", step: "1" } });
- const keep = form.createDiv({ cls: "ms-row" });
- const keepCb = keep.createEl("input", { type: "checkbox" });
- keepCb.checked = true;
- keep.createEl("span", { text: t(this.app, "keepAR") });
- const ar = this.initW / Math.max(1, this.initH);
- const syncFrom = (src) => {
- if (!keepCb.checked) return;
- if (src === "w") {
- const w = Number(inW.value);
- if (Number.isFinite(w) && w > 0) inH.value = String(Math.round(w / ar));
- } else {
- const h = Number(inH.value);
- if (Number.isFinite(h) && h > 0) inW.value = String(Math.round(h * ar));
- }
- };
- inW.addEventListener("input", () => syncFrom("w"));
- inH.addEventListener("input", () => syncFrom("h"));
- keepCb.addEventListener("change", () => syncFrom("w"));
- const buttons = contentEl.createDiv({ cls: "modal-button-container" });
- const ok = buttons.createEl("button", { text: t(this.app, "ok") });
- const cancel = buttons.createEl("button", { text: t(this.app, "cancel") });
- ok.addEventListener("click", () => {
- const w = Number(inW.value);
- const h = Number(inH.value);
- if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
- new import_obsidian.Notice(t(this.app, "invalid"));
- return;
- }
- this.onSubmit(w, h, keepCb.checked);
- this.close();
- });
- cancel.addEventListener("click", () => {
- this.onSubmit(null, null, true);
- this.close();
- });
- }
- onClose() {
- this.contentEl.empty();
- }
-};
-var BgColorModal = class extends import_obsidian.Modal {
- value;
- onOk;
- t;
- isClone;
- isNoteBox;
- isTextColor;
- initialStrokeState;
- constructor(app, initial, onOk, t2, options) {
- super(app);
- this.value = initial || "#242424";
- this.onOk = onOk;
- this.t = t2;
- this.isClone = !!options.isClone;
- this.isNoteBox = !!options.isNoteBox;
- this.isTextColor = !!options.isTextColor;
- this.initialStrokeState = !!options.initialStrokeState;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.addClass("ms-bgcolor-modal-content");
- contentEl.empty();
- contentEl.createEl("h3", { text: this.isTextColor ? this.t("changeTextColor") : this.t("bgColor") });
- const wrap = contentEl.createDiv({ cls: "ms-bgcolor-wrap" });
- const colorInp = wrap.createEl("input", { type: "color" });
- colorInp.value = this.coerceHex(this.value);
- const textInp = wrap.createEl("input", { type: "text" });
- textInp.placeholder = "#242424";
- textInp.value = this.coerceHex(this.value);
- colorInp.addEventListener("input", () => {
- textInp.value = colorInp.value;
- });
- textInp.addEventListener("input", () => {
- const v = this.coerceHex(textInp.value);
- if (v) colorInp.value = v;
- });
- let localOnlyCb = null;
- let applyToHeaderCb = null;
- let removeBgCb = null;
- let addStrokeCb = null;
- const optionsContainer = contentEl.createDiv({ attr: { style: "display: flex; flex-direction: column; align-items: flex-start;" } });
- const createCheckbox = (text) => {
- const row = optionsContainer.createEl("label", { cls: "ms-row ms-flex-align-center ms-margin-bottom-8" });
- row.style.cursor = "pointer";
- const cb = row.createEl("input", { type: "checkbox", cls: "ms-margin-right-6" });
- const span = row.createEl("span", { text, cls: "ms-position-relative-bottom-0" });
- return [row, cb];
- };
- if (!this.isTextColor) {
- if (this.isClone) {
- [, localOnlyCb] = createCheckbox(this.t("localOnlyCheckboxLabel"));
- }
- if (this.isNoteBox) {
- [, applyToHeaderCb] = createCheckbox(this.t("applyToHeader"));
- }
- if (this.isClone || this.isNoteBox) {
- [, removeBgCb] = createCheckbox(this.t("removeBg"));
- }
- } else {
- [, addStrokeCb] = createCheckbox(this.t("addStroke"));
- if (addStrokeCb) addStrokeCb.checked = this.initialStrokeState;
- }
- if (optionsContainer.hasChildNodes()) {
- optionsContainer.addClass("ms-margin-bottom-12");
- }
- const actions = contentEl.createDiv({ cls: "modal-button-container" });
- const okBtn = actions.createEl("button", { text: this.t("ok") });
- const cancelBtn = actions.createEl("button", { text: this.t("cancel") });
- const resetBtn = actions.createEl("button", { text: this.t("reset") });
- actions.insertBefore(resetBtn, cancelBtn);
- resetBtn.addEventListener("click", () => {
- const resetColor = this.isTextColor ? "#FFFFFF" : this.isNoteBox ? "#262626" : "#242424";
- colorInp.value = resetColor;
- textInp.value = resetColor;
- });
- okBtn.addEventListener("click", () => {
- const v = this.coerceHex(textInp.value) || "#242424";
- this.onOk({
- hex: v,
- isLocal: localOnlyCb?.checked ?? false,
- removeBg: removeBgCb?.checked ?? false,
- applyToHeader: applyToHeaderCb?.checked ?? false,
- addStroke: addStrokeCb?.checked ?? false
- });
- this.close();
- });
- cancelBtn.addEventListener("click", () => this.close());
- }
- coerceHex(v) {
- v = (v || "").trim();
- if (!v) return "";
- if (v[0] !== "#") v = "#" + v;
- if (/^#([0-9a-f]{6})$/i.test(v)) return v.toUpperCase();
- return "";
- }
-};
-var CloneSettingsModal = class _CloneSettingsModal extends import_obsidian.Modal {
- onSubmit;
- initial;
- constructor(app, initial, onSubmit) {
- super(app);
- this.initial = { ...initial };
- this.onSubmit = onSubmit;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("ms-clone-settings-modal");
- contentEl.createEl("h3", { text: t(this.app, "cloneSettingsTitle") });
- new import_obsidian.Setting(contentEl).setName(t(this.app, "animation")).addButton((btn) => {
- btn.setButtonText(this.initial.animation === "popup" ? t(this.app, "animationPopup") : t(this.app, "animationFade")).onClick(() => {
- this.initial.animation = this.initial.animation === "popup" ? "fade" : "popup";
- btn.setButtonText(this.initial.animation === "popup" ? t(this.app, "animationPopup") : t(this.app, "animationFade"));
- });
- });
- new import_obsidian.Setting(contentEl).setName(t(this.app, "backgroundOpacity")).addSlider((slider) => slider.setLimits(0, 100, 1).setValue(this.initial.bgOpacity * 100).setDynamicTooltip().onChange((value) => {
- this.initial.bgOpacity = value / 100;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "controlsOpacity")).addSlider((slider) => slider.setLimits(0, 100, 1).setValue(this.initial.controlsOpacity * 100).setDynamicTooltip().onChange((value) => {
- this.initial.controlsOpacity = value / 100;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "hideDelayLabel")).addSlider((slider) => slider.setLimits(0, 100, 1).setValue(this.initial.hideDelay * 10).setDynamicTooltip().onChange((value) => {
- this.initial.hideDelay = value / 10;
- }));
- const buttons = contentEl.createDiv({ cls: "modal-button-container" });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "ok")).setCta().onClick(() => {
- this.onSubmit(this.initial);
- this.close();
- });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "reset")).onClick(() => {
- this.initial = { animation: "popup", bgOpacity: 1, controlsOpacity: 0.35, hideDelay: 2, showAudioHandles: true };
- this.close();
- const newModal = new _CloneSettingsModal(
- this.app,
- this.initial,
- this.onSubmit
- );
- newModal.open();
- });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "cancel")).onClick(() => {
- this.onSubmit(null);
- this.close();
- });
- }
- onClose() {
- this.contentEl.empty();
- }
-};
-var MediaLayoutSettingsModal = class _MediaLayoutSettingsModal extends import_obsidian.Modal {
- plugin;
- constructor(app, plugin) {
- super(app);
- this.plugin = plugin;
- }
- onOpen() {
- const { contentEl } = this;
- contentEl.empty();
- contentEl.addClass("ms-layout-settings-modal");
- const form = contentEl.createDiv({ cls: "modal-form" });
- let handlesOnHoverValue = this.plugin.showHandlesOnHover;
- let hideDetachValue = this.plugin.hideDetachButton;
- let modalAnimationValue = this.plugin.enableModalAnimation;
- let cloneNearFileValue = this.plugin.cloneNearFile;
- let cloneRestoreDelayValue = this.plugin.cloneRestoreDelay;
- let noteRestoreDelayValue = this.plugin.noteRestoreDelay;
- new import_obsidian.Setting(contentEl).setName(t(this.app, "showHandlesOnHoverLabel")).addToggle((toggle) => toggle.setValue(handlesOnHoverValue).onChange((value) => {
- handlesOnHoverValue = value;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "hideDetachButtonLabel")).addToggle((toggle) => toggle.setValue(hideDetachValue).onChange((value) => {
- hideDetachValue = value;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "modalAnimationLabel")).addToggle((toggle) => toggle.setValue(modalAnimationValue).onChange((value) => {
- modalAnimationValue = value;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "cloneNearFile")).addToggle((toggle) => toggle.setValue(cloneNearFileValue).onChange((value) => {
- cloneNearFileValue = value;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "cloneRestoreDelayLabel")).addSlider((slider) => slider.setLimits(0, 1e3, 10).setValue(cloneRestoreDelayValue).setDynamicTooltip().onChange((value) => {
- cloneRestoreDelayValue = value;
- }));
- new import_obsidian.Setting(contentEl).setName(t(this.app, "noteRestoreDelayLabel")).addSlider((slider) => slider.setLimits(0, 1e3, 10).setValue(noteRestoreDelayValue).setDynamicTooltip().onChange((value) => {
- noteRestoreDelayValue = value;
- }));
- const buttons = contentEl.createDiv({ cls: "modal-button-container" });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "ok")).setCta().onClick(async () => {
- this.plugin.showHandlesOnHover = handlesOnHoverValue;
- if (!handlesOnHoverValue) {
- this.plugin.cloneShowAudioHandles = false;
- }
- this.plugin.hideDetachButton = hideDetachValue;
- this.plugin.enableModalAnimation = modalAnimationValue;
- this.plugin.cloneRestoreDelay = cloneRestoreDelayValue;
- this.plugin.cloneNearFile = cloneNearFileValue;
- this.plugin.noteRestoreDelay = noteRestoreDelayValue;
- await this.plugin.persistAll();
- this.plugin.applyCssVariables();
- this.close();
- });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "reset")).onClick(async () => {
- this.plugin.showHandlesOnHover = true;
- this.plugin.cloneNearFile = true;
- this.plugin.cloneShowAudioHandles = true;
- this.plugin.hideDetachButton = true;
- this.plugin.enableModalAnimation = true;
- this.plugin.cloneRestoreDelay = 50;
- this.plugin.noteRestoreDelay = 50;
- await this.plugin.persistAll();
- this.plugin.applyCssVariables();
- new import_obsidian.Notice("\u2714\u27F2");
- this.close();
- new _MediaLayoutSettingsModal(this.app, this.plugin).open();
- });
- new import_obsidian.ButtonComponent(buttons).setButtonText(t(this.app, "cancel")).onClick(() => {
- this.close();
- });
- }
- onClose() {
- this.contentEl.empty();
- }
-};
-var MediaSize = class extends import_obsidian.Plugin {
- notes = {};
- presets = {};
- store = {};
- userPresets = [];
- observers = [];
- meBgColor = "#242424";
- cloneAnimation = "popup";
- cloneBgOpacity = 1;
- cloneControlsOpacity = 0.35;
- cloneHideDelay = 2;
- noteRestoreDelay = 50;
- cloneRestoreDelay = 50;
- enableModalAnimation = true;
- hideDetachButton = true;
- showHandlesOnHover = true;
- cloneNearFile = true;
- cloneShowAudioHandles = true;
- currentNotePath = null;
- pendingTimers = /* @__PURE__ */ new Set();
- presetTimerId = null;
- persistInProgress = false;
- persistQueue = null;
- persistDebounceTimer = null;
- persistDebounceDelay = 150;
- maxPersistDelay = 3e3;
- lastPersistTime = 0;
- hasPendingChanges = false;
- selectedKeys = /* @__PURE__ */ new Set();
- isMarqueeSelecting = false;
- marqueeStart = null;
- marqueeWithCtrl = false;
- scrollRatioX = 0;
- scrollRatioY = 0;
- hScrollEl = null;
- vScrollEl = null;
- wasLockedBeforeFs = false;
- debugLog = [];
- maxDebugLines = 2e3;
- enableDebugMode = false;
- logFilePath = "";
- autoSaveLogTimer = null;
- backupTimer = null;
- lastBackupHash = "";
- computeOriginalAnchor(host) {
- const media = host.querySelector("video, img, audio");
- const embed = host.closest(".cm-embed-block, .media-embed, .internal-embed");
- const src = media?.getAttribute("src") || media?.dataset?.src || "" || (embed?.getAttribute?.("src") || embed?.dataset?.src || "") || "";
- const root = host.closest(".markdown-reading-view, .markdown-preview-view, .cm-editor");
- let nth = 0;
- if (src && root) {
- const all = Array.from(root.querySelectorAll("video, img, audio")).filter((el) => {
- const s = el.getAttribute("src") || el?.dataset?.src || "";
- return s === src;
- });
- nth = Math.max(0, all.indexOf(media));
- }
- const sig = media?.getAttribute("alt") || media?.getAttribute("title") || (media?.textContent || "").slice(0, 64) || "";
- return `${src || "nosrc"}#${nth}:${sig}`;
- }
- ensureOriginalAnchor(host) {
- let a = host.dataset.msAnchor;
- if (!a) {
- a = this.computeOriginalAnchor(host);
- host.dataset.msAnchor = a;
- }
- return a;
- }
- buildAnchorIndex(root) {
- const map = /* @__PURE__ */ new Map();
- root.querySelectorAll(".ms-host[data-ms-key]").forEach((h) => {
- const a = this.ensureOriginalAnchor(h);
- const k = h.dataset.msKey;
- if (a && k) map.set(a, k);
- });
- return map;
- }
- assetUrl(relPath) {
- const full = (0, import_obsidian.normalizePath)(`${this.app.vault.configDir}/plugins/${this.manifest.id}/${relPath}`);
- return this.app.vault.adapter.getResourcePath(full);
- }
- ensureFloatLayer(root) {
- const isReading = !!root.closest(".markdown-reading-view, .markdown-preview-view");
- this.debug("ensureFloatLayer called", {
- isReading,
- rootClass: root.className,
- rootTag: root.tagName
- });
- let sizer = findContentSizer(root.closest(".markdown-source-view, .markdown-reading-view"));
- this.debug("Found sizer", {
- found: !!sizer,
- sizerClass: sizer?.className,
- sizerTag: sizer?.tagName
- });
- if (!sizer) {
- this.warn("No sizer found, using root as fallback", {});
- sizer = root;
- }
- let layer = sizer.querySelector(":scope > .ms-float-layer");
- if (!layer) {
- this.debug("Creating new float-layer in sizer", {});
- layer = document.createElement("div");
- layer.className = "ms-float-layer";
- layer.style.display = "block";
- layer.style.visibility = "visible";
- sizer.appendChild(layer);
- this.debug("Float-layer created and appended", {
- parent: sizer.className,
- layerInDOM: document.contains(layer)
- });
- } else {
- this.debug("Float-layer already exists", {});
- }
- const path = this.getActiveNote()?.path ?? "";
- if (path) {
- layer.dataset.msNote = path;
- layer.querySelectorAll(".ms-host[data-ms-note]").forEach((el) => {
- const n = el.dataset.msNote || "";
- if (n && n !== path) el.remove();
- });
- }
- this.debug("Float-layer ready", {
- childCount: layer.children.length,
- path
- });
- return layer;
- }
- lastCtxEvt = null;
- controlsProxy = /* @__PURE__ */ new WeakMap();
- layoutPresets = {};
- layoutNames = {};
- async onload() {
- const persisted = await this.loadData();
- let dataCorrupted = false;
- if (persisted && typeof persisted === "object") {
- if (persisted.notes) {
- this.notes = persisted.notes ?? {};
- this.presets = persisted.presets ?? {};
- const hasNotes = Object.keys(this.notes).length > 0;
- if (!hasNotes && persisted.userPresets && persisted.userPresets.length > 0) {
- console.error("[Media Layout] WARNING: Loaded data has empty notes but has other data - possible corruption!");
- dataCorrupted = true;
- }
- } else if (persisted.items) {
- console.log("Media-Size: Migrating old data structure.");
- this.notes = {};
- for (const notePath in persisted.items) {
- this.notes[notePath] = {
- originals: persisted.items[notePath],
- clones: {}
- };
- }
- this.presets = {};
- this.store = persisted.items;
- } else {
- this.notes = {};
- this.presets = {};
- }
- if (dataCorrupted) {
- console.warn("[Media Layout] Attempting auto-recovery from latest backup...");
- const backups = await this.listBackups();
- if (backups.length > 0) {
- try {
- const latestBackup = backups[0];
- const content = await this.app.vault.adapter.read(latestBackup.path);
- const backupData = JSON.parse(content);
- if (backupData.notes && Object.keys(backupData.notes).length > 0) {
- this.notes = backupData.notes;
- this.presets = backupData.presets || {};
- console.log(`[Media Layout] \u2705 Auto-recovered from backup: ${latestBackup.name}`);
- new import_obsidian.Notice(`\u{1F504} Data recovered from backup: ${latestBackup.name}`);
- }
- } catch (e) {
- console.error("[Media Layout] Auto-recovery failed:", e);
- }
- }
- }
- this.meBgColor = persisted.meBgColor ?? "#242424";
- this.userPresets = Array.isArray(persisted.userPresets) ? persisted.userPresets.slice(0, MAX_WIDTH_PRESETS) : [];
- this.layoutPresets = persisted.layoutPresets ?? {};
- this.layoutNames = persisted.layoutNames ?? {};
- this.cloneAnimation = persisted.cloneAnimation ?? "popup";
- this.cloneBgOpacity = persisted.cloneBgOpacity ?? 1;
- this.cloneControlsOpacity = persisted.cloneControlsOpacity ?? 0.35;
- this.cloneHideDelay = persisted.cloneHideDelay ?? 2;
- this.noteRestoreDelay = persisted.noteRestoreDelay ?? 200;
- this.cloneRestoreDelay = persisted.cloneRestoreDelay ?? 300;
- this.showHandlesOnHover = persisted.showHandlesOnHover ?? true;
- this.enableModalAnimation = persisted.enableModalAnimation ?? true;
- this.hideDetachButton = persisted.hideDetachButton ?? true;
- this.cloneShowAudioHandles = persisted.cloneShowAudioHandles ?? true;
- this.cloneNearFile = persisted.cloneNearFile ?? true;
- } else {
- this.notes = {};
- this.presets = {};
- }
- this.applyCssVariables();
- const ctxHandler = (e) => this.lastCtxEvt = e;
- document.addEventListener("contextmenu", ctxHandler, true);
- this.register(() => document.removeEventListener("contextmenu", ctxHandler, true));
- this.registerDomEvent(document, "mousedown", (e) => {
- if (e.ctrlKey || e.metaKey) this.handleMarqueeStart(e);
- }, { capture: true });
- document.addEventListener("mousedown", this.handleDeselectClick, true);
- this.register(() => document.removeEventListener("mousedown", this.handleDeselectClick, true));
- this.registerMarkdownPostProcessor((el, ctx) => {
- const notePath = ctx?.sourcePath || this.getActiveNote()?.path || "";
- this.processRoot(el, notePath);
- });
- this.app.workspace.onLayoutReady(() => {
- this.schedule(() => {
- this.scanActive();
- const notePath = this.getActiveNote()?.path;
- if (notePath) this.restoreClonesFromStore(notePath);
- this.applyCloneSettingsToAll();
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (view) this.app.workspace.trigger("markdown-render", view.contentEl);
- }, 500);
- });
- let switchDebounceTimer = null;
- let switchInProgress = false;
- let lastSwitchPath = null;
- const scheduleSwitch = (file) => {
- if (switchDebounceTimer) clearTimeout(switchDebounceTimer);
- switchDebounceTimer = window.setTimeout(() => handleFileSwitch(file), 0);
- };
- const handleFileSwitch = async (file) => {
- const newPath = file?.path ?? this.getActiveNote()?.path ?? null;
- if (switchInProgress) {
- console.warn("[Media Layout] Switch already in progress, ignoring duplicate call");
- return;
- }
- if (newPath === lastSwitchPath && Date.now() - this.lastSwitchTime < 100) {
- console.warn("[Media Layout] Duplicate switch to same file, ignoring");
- return;
- }
- switchInProgress = true;
- this.lastSwitchTime = Date.now();
- lastSwitchPath = newPath;
- try {
- this.cancelScheduled?.();
- await this.flushPersist();
- await this.hardCleanupForSwitch(this.currentNotePath ?? this.getActiveNote()?.path ?? void 0);
- this.currentNotePath = newPath;
- this.scanActive();
- } finally {
- switchInProgress = false;
- }
- };
- this.registerEvent(this.app.workspace.on("file-open", (file) => scheduleSwitch(file)));
- this.registerEvent(this.app.workspace.on("active-leaf-change", () => {
- scheduleSwitch(this.app.workspace.getActiveFile());
- this.schedule(() => {
- const notePath = this.getActiveNote()?.path;
- if (notePath) this.realignClonesIfNeeded(notePath);
- }, 100);
- }));
- this.app.workspace.onLayoutReady(() => scheduleSwitch(this.app.workspace.getActiveFile()));
- this.registerEvent(this.app.workspace.on("layout-change", () => {
- this.scanActive();
- this.schedule(() => {
- const leaves = this.app.workspace.getLeavesOfType("markdown");
- leaves.forEach((leaf) => {
- const file = leaf.view?.file;
- if (file?.path) {
- this.restoreClonesFromStore(file.path);
- this.realignClonesIfNeeded(file.path);
- }
- });
- }, 150);
- }));
- this.registerDomEvent(window, "resize", () => {
- this.schedule(() => {
- const notePath = this.getActiveNote()?.path;
- if (!notePath) return;
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (!view) return;
- const containerEl = view.containerEl;
- const read = containerEl.querySelector(".markdown-reading-view");
- const isReadingMode = read && getComputedStyle(read).display !== "none";
- if (isReadingMode) {
- this.restoreClonesFromStore(notePath);
- this.ensureNotesFromStore(notePath);
- }
- }, 50);
- });
- let scrollTimeout = null;
- this.registerDomEvent(document, "scroll", (e) => {
- const target = e.target;
- if (!target || !target.closest || !target.closest(".markdown-reading-view")) return;
- if (scrollTimeout !== null) {
- clearTimeout(scrollTimeout);
- }
- scrollTimeout = window.setTimeout(() => {
- const notePath = this.getActiveNote()?.path;
- if (!notePath) return;
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (!view) return;
- const containerEl = view.containerEl;
- const read = containerEl.querySelector(".markdown-reading-view");
- const isReadingMode = read && getComputedStyle(read).display !== "none";
- if (isReadingMode) {
- const root = this.getAttachRoot();
- if (!root) return;
- const layer = root.querySelector(".ms-float-layer");
- const noteData = this.notes[notePath];
- const hasStoredClones = noteData?.clones && Object.keys(noteData.clones).length > 0;
- const hasStoredNotes = noteData?.originals && Object.values(noteData.originals).some((o) => o.txt !== void 0);
- const hasDomClones = layer?.querySelector("[data-ms-parent-key]");
- const hasDomNotes = layer?.querySelector(".ms-note-host");
- if (hasStoredClones && !hasDomClones) {
- this.restoreClonesFromStore(notePath);
- }
- if (hasStoredNotes && !hasDomNotes) {
- this.ensureNotesFromStore(notePath);
- }
- }
- scrollTimeout = null;
- }, 100);
- }, true);
- this.registerEvent(
- this.app.workspace.on("editor-menu", (menu, _editor, view) => {
- const ev = this.lastCtxEvt;
- if (!ev) return;
- const target = ev.target;
- if (!target) return;
- if (this.isMarqueeSelecting || ev.ctrlKey && target.closest(".ms-host")) {
- return;
- }
- menu.addItem((i) => {
- i.setTitle(t(this.app, "addNote")).setIcon("layout-template");
- i.onClick(async () => {
- const viewRoot = this.getAttachRoot();
- if (!viewRoot) return;
- const notePath2 = view?.file?.path ?? this.getActiveNote()?.path ?? "";
- const key2 = this.makeNoteKey();
- const defaultWidth = 260;
- const defaultHeight = 160;
- const vr = viewRoot.getBoundingClientRect();
- const x = ev.clientX - vr.left + viewRoot.scrollLeft - defaultWidth * 2 | 0;
- const y = ev.clientY - vr.top + viewRoot.scrollTop - defaultHeight * 1.5 | 0;
- this.createNoteHost(viewRoot, key2, x, y, { w: defaultWidth, h: defaultHeight, txt: "", fontSize: 16, focus: true });
- await this.saveState(notePath2, key2, { w: defaultWidth, h: defaultHeight, x, y, txt: "", fontSize: 16 });
- });
- });
- const host = target.closest(".ms-host") || target.closest(".internal-embed, .media-embed, .mx-media-embed, img, video, audio");
- if (!host) return;
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.classList.contains("ms-note-host");
- const media = isNote ? host.querySelector(".ms-note") : host.querySelector("video, img, audio") || (["IMG", "VIDEO", "AUDIO"].includes(host.tagName) ? host : null);
- if (!media && !isNote) return;
- const kind = isNote ? "note" : media.tagName === "IMG" ? "image" : media.tagName === "AUDIO" ? "audio" : "video";
- const notePath = view?.file?.path ?? this.getActiveNote()?.path ?? "";
- const key = isNote ? host.dataset.msKey ?? this.makeNoteKey() : host.dataset.msKey ?? this.makeEmbedKey(this.getSrcAndBase(host, media).src);
- host.dataset.msKey = key;
- const isUrlMedia = !isNote && !isClone && media && media.src && media.src.startsWith("http");
- const isTargetSelected = this.selectedKeys.has(key);
- if (isTargetSelected && this.selectedKeys.size > 1) {
- this.buildGroupContextMenu(menu, host, key, kind, notePath);
- return;
- }
- if (isUrlMedia) {
- menu.addItem(
- (i) => i.setTitle(t(this.app, "urlMediaDownloadAndReplace")).setIcon("download").onClick(async () => {
- const url = media.src;
- if (url && url.startsWith("http")) {
- await this.convertUrlMediaToLocal(url, notePath);
- }
- })
- );
- return;
- }
- if (isClone) {
- if (kind !== "note") {
- this.addSizePresetsMenu(menu, notePath, key, host, media, kind);
- }
- this.addPositionPresetsMenu(menu, notePath);
- if (kind === "image") {
- menu.addItem(
- (i) => i.setTitle(t(this.app, "setSize")).setIcon("scaling").onClick(async () => {
- this.ensureParentAnchor(host);
- const cur = this.readAbsRect(host);
- new ImageSizeModal(
- this.app,
- cur.w || media.clientWidth || 0,
- cur.h || media.clientHeight || 0,
- async (w, h, keep) => {
- if (w && h) await this.applyAndSaveSize(notePath, key, host, media, kind, { w, h }, keep ? false : true);
- }
- ).open();
- })
- );
- }
- if (kind === "image") {
- menu.addItem(
- (i) => i.setTitle(t(this.app, "addLink")).setIcon("link").onClick(async () => {
- const currentState = this.notes[notePath]?.clones?.[key];
- const initialLink = currentState?.link || "";
- new LinkModal(this.app, initialLink, async (link) => {
- if (link !== null) {
- await this.saveState(notePath, key, { link });
- this.applyLinkOverlay(host, link);
- }
- }).open();
- })
- );
- }
- menu.addItem(
- (i) => i.setTitle(t(this.app, "openLocation")).setIcon("folder-open").onClick(async () => {
- const notePath2 = view?.file?.path ?? this.getActiveNote()?.path ?? "";
- const { src } = this.getSrcAndBase(host, media);
- const tf = this.resolveTFile(notePath2, src);
- if (!tf) return new import_obsidian.Notice(t(this.app, "fileNotFound"));
- const adapter = this.app.vault.adapter;
- if (!(adapter instanceof import_obsidian.FileSystemAdapter)) {
- new import_obsidian.Notice(t(this.app, "localOnly"));
- return;
- }
- const full = adapter.getFullPath(tf.path);
- try {
- const shell = window.require?.("electron")?.shell;
- if (shell?.showItemInFolder) shell.showItemInFolder(full);
- else this.revealInOS(full);
- } catch {
- this.revealInOS(full);
- }
- })
- );
- if (kind === "video" || kind === "audio") {
- menu.addItem((i) => {
- i.setTitle(t(this.app, "bgColor")).setIcon("palette");
- i.onClick(() => {
- const tr = (k) => t(this.app, k);
- const initialColor = this.notes[notePath]?.clones?.[key]?.bgColor || this.meBgColor;
- new BgColorModal(this.app, initialColor, async ({ hex, isLocal, removeBg }) => {
- if (removeBg) {
- host.querySelector(".ms-media-bg")?.remove();
- host.querySelector(".ms-local-bg")?.remove();
- await this.saveState(notePath, key, { bgColor: "transparent" });
- } else if (isLocal) {
- const localBg = this.ensureLocalBg(host);
- localBg.style.backgroundColor = hex;
- await this.saveState(notePath, key, { bgColor: hex });
- } else {
- await this.setMediaBgColor(hex, false, host, notePath, key);
- host.querySelector(".ms-local-bg")?.remove();
- await this.saveState(notePath, key, { bgColor: void 0 });
- }
- }, tr, { isClone: true }).open();
- });
- });
- }
- menu.addItem((i) => {
- i.setTitle(t(this.app, "cloneSettingsTitle")).setIcon("sliders-horizontal").onClick(() => {
- new CloneSettingsModal(this.app, {
- animation: this.cloneAnimation,
- bgOpacity: this.cloneBgOpacity,
- controlsOpacity: this.cloneControlsOpacity,
- hideDelay: this.cloneHideDelay,
- showAudioHandles: this.cloneShowAudioHandles
- }, async (settings) => {
- if (settings) {
- this.cloneAnimation = settings.animation;
- this.cloneBgOpacity = settings.bgOpacity;
- this.cloneControlsOpacity = settings.controlsOpacity;
- this.cloneHideDelay = settings.hideDelay;
- this.cloneShowAudioHandles = settings.showAudioHandles;
- await this.persistAll();
- this.applyCloneSettingsToAll();
- }
- }).open();
- });
- });
- menu.addItem((i) => {
- i.setTitle(t(this.app, "bringLayerUp")).setIcon("arrow-up-from-line");
- i.onClick(async () => {
- this.adjustZIndex(host, 1);
- });
- });
- menu.addItem((i) => {
- i.setTitle(t(this.app, "sendLayerDown")).setIcon("arrow-down-from-line");
- i.onClick(async () => {
- this.adjustZIndex(host, -1);
- });
- });
- menu.addSeparator();
- const isLocked = host.dataset.msLocked === "1";
- menu.addItem((item) => {
- item.setTitle(isLocked ? t(this.app, "unlock") : t(this.app, "lock")).setIcon(isLocked ? "unlock" : "lock");
- item.onClick(async () => {
- const newState = !isLocked;
- host.dataset.msLocked = newState ? "1" : "0";
- await this.saveState(notePath, key, { locked: newState });
- });
- });
- menu.addItem((item) => {
- item.setTitle(t(this.app, "deleteClone")).setIcon("trash");
- item.setTitle(t(this.app, "deleteClone")).setIcon("trash");
- item.onClick(async () => {
- host.remove();
- if (notePath && key) {
- delete this.notes[notePath]?.clones?.[key];
- await this.persistAll();
- }
- });
- });
- return;
- }
- if (kind !== "note") {
- this.addSizePresetsMenu(menu, notePath, key, host, media, kind);
- }
- if (kind !== "note") {
- this.addPositionPresetsMenu(menu, notePath);
- }
- if (kind === "image") {
- menu.addItem(
- (i) => i.setTitle(t(this.app, "setSize")).setIcon("scaling").onClick(async () => {
- this.ensureParentAnchor(host);
- const cur = this.readAbsRect(host);
- new ImageSizeModal(
- this.app,
- cur.w || media.clientWidth || 0,
- cur.h || media.clientHeight || 0,
- async (w, h, keep) => {
- if (w && h) await this.applyAndSaveSize(notePath, key, host, media, kind, { w, h }, keep ? false : true);
- }
- ).open();
- })
- );
- }
- menu.addItem(
- (i) => i.setTitle(t(this.app, "openLocation")).setIcon("folder-open").onClick(async () => {
- const notePath2 = view?.file?.path ?? this.getActiveNote()?.path ?? "";
- const { src } = this.getSrcAndBase(host, media);
- const tf = this.resolveTFile(notePath2, src);
- if (!tf) return new import_obsidian.Notice(t(this.app, "fileNotFound"));
- const adapter = this.app.vault.adapter;
- if (!(adapter instanceof import_obsidian.FileSystemAdapter)) {
- new import_obsidian.Notice(t(this.app, "localOnly"));
- return;
- }
- const full = adapter.getFullPath(tf.path);
- try {
- const shell = window.require?.("electron")?.shell;
- if (shell?.showItemInFolder) {
- shell.showItemInFolder(full);
- } else {
- this.revealInOS(full);
- }
- } catch {
- this.revealInOS(full);
- }
- })
- );
- const isDetached = host.dataset.msDetached === "1";
- if (!this.hideDetachButton && !isDetached && kind !== "note") {
- menu.addItem((item) => {
- item.setTitle(t(this.app, "detach")).setIcon("pin-off");
- item.onClick(async () => {
- if (host.dataset.msDetached === "1") return;
- await this.maybeDetach(notePath, key, host);
- const el = item.dom;
- el?.remove();
- });
- });
- }
- if (isDetached) {
- menu.addItem(
- (i) => i.setTitle(t(this.app, "restorePos")).setIcon("home").onClick(async () => {
- await this.applyAndSaveSize(notePath, key, host, media, kind, { x: 0, y: 0, w: void 0, h: void 0, reset: true });
- })
- );
- }
- if (kind !== "note") {
- menu.addItem((item) => {
- item.setTitle(t(this.app, "duplicate")).setIcon("copy");
- item.onClick(async () => {
- const viewRoot = this.getHostRoot(host);
- if (!viewRoot) return;
- const cloneHost = this.createCleanCloneElement(host);
- const cloneKey = `${key}::clone::${this.makeNoteKey()}`;
- const defaultWidth = 480;
- const defaultHeight = 270;
- const vr = viewRoot.getBoundingClientRect();
- let x, y;
- if (this.cloneNearFile) {
- x = ev.clientX - vr.left - defaultWidth + viewRoot.scrollLeft | 0;
- y = ev.clientY - vr.top - defaultHeight + viewRoot.scrollTop | 0;
- } else {
- x = ev.clientX - vr.left - defaultWidth | 0;
- y = ev.clientY - vr.top - defaultHeight | 0;
- }
- this.createCloneHost(viewRoot, cloneHost, cloneKey, key, x, y, true);
- this.schedule(() => {
- this.applyCloneSettingsToAll();
- }, 150);
- });
- });
- }
- if (kind === "video" || kind === "audio") {
- menu.addItem((i) => {
- i.setTitle(t(this.app, "bgColor")).setIcon("palette");
- i.onClick(() => {
- const tr = (k) => t(this.app, k);
- new BgColorModal(this.app, this.meBgColor, ({ hex, isLocal }) => this.setMediaBgColor(hex, isLocal, host, notePath, key), tr, { isClone }).open();
- });
- });
- }
- if (!isClone && kind !== "note") {
- const firstChild = host.children[0];
- const isVisible = !firstChild || !firstChild.style.opacity || parseFloat(firstChild.style.opacity) > 0;
- menu.addItem((i) => {
- i.setTitle(isVisible ? t(this.app, "makeInvisible") : t(this.app, "makeVisible")).setIcon(isVisible ? "eye-off" : "eye");
- i.onClick(async () => {
- const newOpacity = isVisible ? "0" : "1";
- Array.from(host.children).forEach((child) => {
- if (child instanceof HTMLElement && !child.classList.contains("ms-overlay")) {
- child.style.opacity = newOpacity;
- }
- });
- await this.saveState(notePath, key, { opacity: parseFloat(newOpacity) });
- });
- });
- }
- if (kind !== "note" && host.dataset.msDetached === "1") {
- const isLocked = host.dataset.msLocked === "1";
- if (!isLocked) {
- menu.addItem((item) => {
- item.setTitle(t(this.app, "lock")).setIcon("lock");
- item.onClick(async () => {
- host.dataset.msLocked = "1";
- await this.saveState(notePath, key, { locked: true });
- item.dom?.remove();
- });
- });
- } else {
- menu.addItem((item) => {
- item.setTitle(t(this.app, "unlock")).setIcon("unlock");
- item.onClick(async () => {
- host.dataset.msLocked = "0";
- await this.saveState(notePath, key, { locked: false });
- item.dom?.remove();
- });
- });
- }
- }
- {
- const noteHost = target.closest(".ms-note-host");
- if (noteHost) {
- const key2 = noteHost.dataset.msKey;
- const notePath2 = view?.file?.path ?? this.getActiveNote()?.path ?? "";
- menu.addSeparator();
- menu.addItem(
- (mi) => mi.setTitle(t(this.app, "deleteNote")).onClick(async () => {
- noteHost.remove();
- if (notePath2 && key2) {
- delete this.notes[notePath2]?.originals?.[key2];
- await this.persistAll();
- }
- })
- );
- }
- }
- })
- );
- this.addCommand({
- id: "ping",
- name: t(this.app, "cmdPing"),
- callback: () => new import_obsidian.Notice(t(this.app, "pingOk"))
- });
- this.addCommand({
- id: "open-settings",
- name: "Settings",
- callback: () => {
- new MediaLayoutSettingsModal(this.app, this).open();
- }
- });
- this.addCommand({
- id: "export-logs",
- name: "\u{1F4CB} Export debug logs to note",
- callback: async () => {
- await this.exportLogsToNote();
- }
- });
- this.addCommand({
- id: "save-logs",
- name: "\u{1F4BE} Save debug logs to file",
- callback: async () => {
- const path = await this.saveDebugLog();
- new import_obsidian.Notice(`\u{1F4BE} Logs saved to: ${path}`);
- }
- });
- this.addCommand({
- id: "clear-logs",
- name: "\u{1F5D1}\uFE0F Clear debug logs",
- callback: () => {
- this.clearDebugLog();
- }
- });
- this.addCommand({
- id: "toggle-debug",
- name: "\u{1F50D} Toggle debug mode",
- callback: () => {
- this.enableDebugMode = !this.enableDebugMode;
- new import_obsidian.Notice(`\u{1F50D} Debug mode: ${this.enableDebugMode ? "ON" : "OFF"}`);
- this.info("Debug mode toggled", { enabled: this.enableDebugMode });
- if (this.enableDebugMode) {
- this.autoSaveLogTimer = this.registerInterval(window.setInterval(() => {
- this.saveDebugLog();
- }, 3e4));
- } else if (this.autoSaveLogTimer !== null) {
- window.clearInterval(this.autoSaveLogTimer);
- this.autoSaveLogTimer = null;
- }
- }
- });
- this.addCommand({
- id: "create-backup",
- name: "\u{1F4BE} Create backup now",
- callback: async () => {
- await this.createBackup();
- new import_obsidian.Notice("\u2705 Backup created successfully");
- }
- });
- this.addCommand({
- id: "restore-backup",
- name: "\u267B\uFE0F Restore from backup",
- callback: async () => {
- const backups = await this.listBackups();
- if (backups.length === 0) {
- new import_obsidian.Notice("\u26A0\uFE0F No backups found");
- return;
- }
- const menu = new import_obsidian.Menu();
- backups.forEach((backup) => {
- const dateStr = backup.date.toLocaleString();
- menu.addItem((item) => {
- item.setTitle(`${dateStr}`).setIcon("clock").onClick(async () => {
- const confirmed = await new Promise((resolve) => {
- const modal = new import_obsidian.Modal(this.app);
- modal.titleEl.setText("Restore from backup?");
- modal.contentEl.createEl("p", {
- text: `This will restore data from: ${dateStr}
-Current data will be overwritten.`
- });
- const btnContainer = modal.contentEl.createDiv({ cls: "modal-button-container" });
- const btnRestore = btnContainer.createEl("button", { text: "Restore" });
- const btnCancel = btnContainer.createEl("button", { text: "Cancel" });
- btnRestore.addEventListener("click", () => {
- modal.close();
- resolve(true);
- });
- btnCancel.addEventListener("click", () => {
- modal.close();
- resolve(false);
- });
- modal.open();
- });
- if (confirmed) {
- await this.restoreFromBackup(backup.path);
- }
- });
- });
- });
- menu.showAtMouseEvent(window.event);
- }
- });
- this.addCommand({
- id: "list-backups",
- name: "\u{1F4CB} List all backups",
- callback: async () => {
- const backups = await this.listBackups();
- if (backups.length === 0) {
- new import_obsidian.Notice("\u26A0\uFE0F No backups found");
- return;
- }
- let message = `Found ${backups.length} backup(s):
-
-`;
- backups.forEach((backup, index) => {
- message += `${index + 1}. ${backup.date.toLocaleString()}
-`;
- });
- new import_obsidian.Notice(message, 1e4);
- console.log("[Media Layout] Available backups:", backups);
- }
- });
- const sb = this.addStatusBarItem();
- sb.setText(t(this.app, "sbReady"));
- sb.setAttr("title", t(this.app, "sbTitle"));
- sb.addEventListener("click", () => this.scanActive());
- this.backupTimer = this.registerInterval(window.setInterval(() => {
- this.createBackup();
- }, 10 * 60 * 1e3));
- this.schedule(() => this.createBackup(), 1e4);
- this.register(() => {
- window.removeEventListener("beforeunload", this.emergencyFlush);
- });
- window.addEventListener("beforeunload", this.emergencyFlush);
- }
- emergencyFlush = () => {
- if (this.persistDebounceTimer !== null) {
- clearTimeout(this.persistDebounceTimer);
- this.persistDebounceTimer = null;
- }
- this.persistAll().catch((err) => {
- console.error("[Media Layout] Emergency flush failed:", err);
- });
- };
- async onunload() {
- if (this.enableDebugMode && this.debugLog.length > 0) {
- await this.saveDebugLog();
- }
- if (this.autoSaveLogTimer) {
- clearInterval(this.autoSaveLogTimer);
- }
- if (this.backupTimer) {
- clearInterval(this.backupTimer);
- this.backupTimer = null;
- }
- await this.flushPersist();
- await this.persistAll();
- this.observers.forEach((o) => o.disconnect());
- this.observers.length = 0;
- }
- log(level, message, data) {
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
- const entry = {
- timestamp,
- level,
- message,
- data
- };
- this.debugLog.push(entry);
- if (this.debugLog.length > this.maxDebugLines) {
- this.debugLog.shift();
- }
- if (this.enableDebugMode || level === "WARN" || level === "ERROR") {
- const dataStr = data ? ` ${JSON.stringify(data)}` : "";
- const fullMessage = `[Media Layout] [${level}] ${message}${dataStr}`;
- switch (level) {
- case "ERROR":
- console.error(fullMessage);
- break;
- case "WARN":
- console.warn(fullMessage);
- break;
- default:
- console.log(fullMessage);
- }
- }
- }
- debug(message, data) {
- this.log("DEBUG", message, data);
- }
- info(message, data) {
- this.log("INFO", message, data);
- }
- warn(message, data) {
- this.log("WARN", message, data);
- }
- error(message, data) {
- this.log("ERROR", message, data);
- }
- async saveDebugLog() {
- try {
- const logPath = (0, import_obsidian.normalizePath)(`${this.manifest.dir}/debug.log`);
- const logContent = this.debugLog.map((entry) => {
- const dataStr = entry.data ? ` | Data: ${JSON.stringify(entry.data)}` : "";
- return `${entry.timestamp} [${entry.level}] ${entry.message}${dataStr}`;
- }).join("\n");
- await this.app.vault.adapter.write(logPath, logContent);
- this.logFilePath = logPath;
- console.log("[Media Layout] Debug log saved:", logPath, `(${this.debugLog.length} entries)`);
- return logPath;
- } catch (err) {
- console.error("[Media Layout] Failed to save debug log:", err);
- throw err;
- }
- }
- async exportLogsToNote() {
- try {
- const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, -5);
- const noteName = `Media Layout Debug ${timestamp}.md`;
- let content = `# Media Layout Debug Log
-
-`;
- content += `Generated: ${(/* @__PURE__ */ new Date()).toLocaleString()}
-`;
- content += `Total entries: ${this.debugLog.length}
-`;
- content += `Debug mode: ${this.enableDebugMode ? "ON" : "OFF"}
-
-`;
- content += `---
-
-`;
- const byLevel = {
- ERROR: this.debugLog.filter((e) => e.level === "ERROR"),
- WARN: this.debugLog.filter((e) => e.level === "WARN"),
- INFO: this.debugLog.filter((e) => e.level === "INFO"),
- DEBUG: this.debugLog.filter((e) => e.level === "DEBUG")
- };
- content += `## Summary
-
-`;
- content += `- Errors: ${byLevel.ERROR.length}
-`;
- content += `- Warnings: ${byLevel.WARN.length}
-`;
- content += `- Info: ${byLevel.INFO.length}
-`;
- content += `- Debug: ${byLevel.DEBUG.length}
-
-`;
- if (byLevel.ERROR.length > 0) {
- content += `## Recent Errors
-
-`;
- byLevel.ERROR.slice(-10).forEach((entry) => {
- content += `### ${entry.timestamp}
-`;
- content += `**${entry.message}**
-`;
- if (entry.data) {
- content += `\`\`\`json
-${JSON.stringify(entry.data, null, 2)}
-\`\`\`
-`;
- }
- content += `
-`;
- });
- }
- content += `## Full Log
-
-`;
- content += `\`\`\`
-`;
- this.debugLog.forEach((entry) => {
- const dataStr = entry.data ? ` | ${JSON.stringify(entry.data)}` : "";
- content += `${entry.timestamp} [${entry.level}] ${entry.message}${dataStr}
-`;
- });
- content += `\`\`\`
-`;
- await this.app.vault.create(noteName, content);
- new import_obsidian.Notice(`\u2705 Logs exported to: ${noteName}`);
- const file = this.app.vault.getAbstractFileByPath(noteName);
- if (file instanceof import_obsidian.TFile) {
- await this.app.workspace.getLeaf(false).openFile(file);
- }
- } catch (err) {
- this.error("Failed to export logs to note", { error: String(err) });
- new import_obsidian.Notice("\u274C Failed to export logs");
- }
- }
- clearDebugLog() {
- this.debugLog = [];
- this.info("Debug log cleared");
- new import_obsidian.Notice("\u{1F5D1}\uFE0F Debug log cleared");
- }
- simpleHash(str) {
- let hash = 0;
- for (let i = 0; i < str.length; i++) {
- hash = (hash << 5) - hash + str.charCodeAt(i);
- hash = hash & hash;
- }
- return hash.toString();
- }
- async createBackup() {
- try {
- const pluginDir = this.manifest.dir;
- if (!pluginDir) {
- console.warn("[Media Layout] Plugin dir is undefined, cannot create backup");
- return;
- }
- const data = {
- notes: this.notes ?? {},
- presets: this.presets ?? {},
- meBgColor: this.meBgColor ?? "#242424",
- userPresets: this.userPresets ?? [],
- layoutPresets: this.layoutPresets ?? {},
- layoutNames: this.layoutNames ?? {},
- cloneAnimation: this.cloneAnimation,
- cloneBgOpacity: this.cloneBgOpacity,
- cloneControlsOpacity: this.cloneControlsOpacity,
- cloneHideDelay: this.cloneHideDelay,
- noteRestoreDelay: this.noteRestoreDelay,
- cloneRestoreDelay: this.cloneRestoreDelay,
- cloneShowAudioHandles: this.cloneShowAudioHandles,
- enableModalAnimation: this.enableModalAnimation,
- hideDetachButton: this.hideDetachButton,
- cloneNearFile: this.cloneNearFile,
- showHandlesOnHover: this.showHandlesOnHover,
- backupTimestamp: (/* @__PURE__ */ new Date()).toISOString()
- };
- const dataStr = JSON.stringify(data);
- const currentHash = this.simpleHash(dataStr);
- if (currentHash === this.lastBackupHash) {
- console.log("[Media Layout] Data unchanged, skipping backup");
- return;
- }
- this.lastBackupHash = currentHash;
- const hasNotes = this.notes && Object.keys(this.notes).length > 0;
- if (!hasNotes) {
- console.warn("[Media Layout] Skipping backup: this.notes is empty");
- return;
- }
- const now = /* @__PURE__ */ new Date();
- const year = now.getFullYear();
- const month = String(now.getMonth() + 1).padStart(2, "0");
- const day = String(now.getDate()).padStart(2, "0");
- const hour = String(now.getHours()).padStart(2, "0");
- const minute = String(now.getMinutes()).padStart(2, "0");
- const backupFileName = `${day}_${month}_${year}_${hour}\uA789${minute}_data_backup.json`;
- const backupPath = (0, import_obsidian.normalizePath)(`${pluginDir}/${backupFileName}`);
- await this.app.vault.adapter.write(backupPath, JSON.stringify(data, null, 2));
- await this.cleanupOldBackups();
- this.log("INFO", "Backup created successfully", { fileName: backupFileName });
- console.log(`[Media Layout] Backup created: ${backupFileName}`);
- } catch (e) {
- console.error("[Media Layout] Failed to create backup:", e);
- this.error("Failed to create backup", { error: String(e) });
- }
- }
- async cleanupOldBackups() {
- try {
- const pluginDir = this.manifest.dir;
- if (!pluginDir) return;
- const files = await this.app.vault.adapter.list(pluginDir);
- const backupFiles = files.files.filter((f) => f.endsWith("_data_backup.json")).map((f) => ({
- path: f,
- name: f.split("/").pop() || f
- })).sort((a, b) => b.name.localeCompare(a.name));
- if (backupFiles.length > 4) {
- const toDelete = backupFiles.slice(4);
- for (const file of toDelete) {
- await this.app.vault.adapter.remove(file.path);
- this.log("INFO", "Old backup removed", { fileName: file.name });
- console.log(`[Media Layout] Old backup removed: ${file.name}`);
- }
- }
- } catch (e) {
- console.error("[Media Layout] Failed to cleanup old backups:", e);
- this.error("Failed to cleanup old backups", { error: String(e) });
- }
- }
- async listBackups() {
- try {
- const pluginDir = this.manifest.dir;
- if (!pluginDir) return [];
- const files = await this.app.vault.adapter.list(pluginDir);
- const backupFiles = files.files.filter((f) => f.endsWith("_data_backup.json")).map((f) => {
- const name = f.split("/").pop() || f;
- const match = name.match(/^(\d{2})_(\d{2})_(\d{4})_(\d{2})꞉(\d{2})_data_backup\.json$/);
- let date = /* @__PURE__ */ new Date();
- if (match) {
- const [, day, month, year, hour, minute] = match;
- date = new Date(parseInt(year), parseInt(month) - 1, parseInt(day), parseInt(hour), parseInt(minute));
- }
- return { path: f, name, date };
- }).sort((a, b) => b.date.getTime() - a.date.getTime());
- return backupFiles;
- } catch (e) {
- console.error("[Media Layout] Failed to list backups:", e);
- return [];
- }
- }
- async restoreFromBackup(backupPath) {
- try {
- const content = await this.app.vault.adapter.read(backupPath);
- const data = JSON.parse(content);
- this.notes = data.notes || {};
- this.presets = data.presets || {};
- this.userPresets = data.userPresets || [];
- this.layoutPresets = data.layoutPresets || {};
- this.layoutNames = data.layoutNames || {};
- this.meBgColor = data.meBgColor || "#242424";
- this.showHandlesOnHover = data.showHandlesOnHover ?? true;
- this.hideDetachButton = data.hideDetachButton ?? true;
- this.cloneNearFile = data.cloneNearFile ?? true;
- this.cloneAnimation = data.cloneAnimation || "popup";
- this.cloneBgOpacity = data.cloneBgOpacity ?? 1;
- this.cloneControlsOpacity = data.cloneControlsOpacity ?? 0.35;
- this.cloneHideDelay = data.cloneHideDelay ?? 2;
- this.cloneShowAudioHandles = data.cloneShowAudioHandles ?? true;
- this.cloneRestoreDelay = data.cloneRestoreDelay ?? 300;
- this.noteRestoreDelay = data.noteRestoreDelay ?? 200;
- this.enableModalAnimation = data.enableModalAnimation ?? true;
- await this.persistAll();
- const fileName = backupPath.split("/").pop() || backupPath;
- new import_obsidian.Notice(`\u2714\uFE0F Data restored from: ${fileName}`);
- this.log("INFO", "Backup restored successfully", { fileName });
- this.scanActive();
- } catch (e) {
- console.error("[Media Layout] Failed to restore from backup:", e);
- this.error("Failed to restore from backup", { error: String(e) });
- new import_obsidian.Notice("\u274C Failed to restore from backup");
- }
- }
- scanActive() {
- this.disconnectObservers();
- const root = this.getActiveContainerEl();
- if (!root) return;
- const notePath = this.getActiveNote()?.path ?? "";
- this.currentNotePath = notePath;
- this.processRoot(root, notePath);
- const mo = new MutationObserver((mutations) => {
- for (const mutation of mutations) {
- if (mutation.type === "childList") {
- mutation.addedNodes.forEach((node) => {
- if (node instanceof HTMLElement) {
- const currentPath = this.getActiveNote()?.path ?? "";
- if (currentPath) {
- this.processRoot(node, currentPath);
- }
- }
- });
- }
- }
- });
- mo.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "src"] });
- this.observers.push(mo);
- this.restoreClonesFromStore(notePath);
- this.ensureNotesFromStore(notePath);
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (view) {
- setTimeout(() => {
- this.app.workspace.trigger("markdown-render", view.contentEl);
- }, 100);
- }
- }
- disconnectObservers() {
- this.observers.forEach((o) => o.disconnect());
- this.observers.length = 0;
- }
- processRoot(rootEl, notePath) {
- const elementsToProcess = rootEl.matches(".internal-embed, .media-embed, .mx-media-embed, img, video, audio, .ms-note-host") ? [rootEl] : Array.from(rootEl.querySelectorAll(
- ".internal-embed, .media-embed, .mx-media-embed, img, video, audio, .markdown-reading-view video, .markdown-preview-view video, .ms-note-host"
- ));
- const nodes = elementsToProcess;
- nodes.forEach((hostLike, idx) => {
- const isNoteHost = hostLike.classList?.contains?.("ms-note-host");
- const host = isNoteHost ? hostLike : ["IMG", "VIDEO", "AUDIO"].includes(hostLike.tagName) ? hostLike.closest(".internal-embed, .media-embed, .mx-media-embed") ?? hostLike : hostLike;
- if (!(host instanceof HTMLElement)) return;
- if (host.dataset.msInit === "1") return;
- const media = isNoteHost ? host.querySelector(".ms-note") : host.querySelector("video, img, audio") ?? (["IMG", "VIDEO", "AUDIO"].includes(host.tagName) ? host : null);
- if (!media) return;
- const tag = isNoteHost ? "NOTE" : (media.tagName || "").toUpperCase();
- const kind = tag === "NOTE" ? "note" : tag === "IMG" ? "image" : tag === "AUDIO" ? "audio" : "video";
- host.dataset.msInit = "1";
- host.classList.add("ms-host");
- const { src, base } = isNoteHost ? { src: "", base: "" } : this.getSrcAndBase(host, media);
- const key = isNoteHost ? host.dataset.msKey ?? this.makeNoteKey() : this.makeEmbedKey(src);
- host.dataset.msKey = key;
- const overlay = this.ensureOverlay(host);
- const handles = this.ensureHandles(overlay, false, kind === "note");
- host.append(overlay);
- if (kind === "image") {
- overlay.classList.add("--img");
- const onDown = (e) => {
- const m = e;
- if (m.button !== 0) return;
- window.getSelection()?.removeAllRanges();
- };
- overlay.addEventListener("pointerdown", onDown, { capture: true });
- overlay.addEventListener("mousedown", onDown, { capture: true });
- const killAfterUp = (e) => {
- const m = e;
- if (m.button !== 0) return;
- e.preventDefault();
- e.stopImmediatePropagation?.();
- requestAnimationFrame(() => window.getSelection()?.removeAllRanges());
- };
- overlay.addEventListener("mouseup", killAfterUp, { capture: true });
- overlay.addEventListener("click", killAfterUp, { capture: true });
- overlay.addEventListener("dblclick", killAfterUp, { capture: true });
- }
- this.applyKindBaseStyles(host, media, kind);
- const saved = this.loadStateWithFallback(notePath, key, base);
- if (kind === "image" && saved) {
- console.log(`\u{1F7E2} [RESTORE] Applying saved state for image:`, {
- key,
- savedW: saved.w,
- savedH: saved.h,
- domW: host.getBoundingClientRect().width,
- domH: host.getBoundingClientRect().height
- });
- }
- if (saved) this.applyState(host, media, kind, saved);
- else {
- if (kind !== "image" && kind !== "note") {
- const rect = host.getBoundingClientRect();
- if (rect.width) this.applyState(host, media, kind, { w: Math.round(rect.width) });
- }
- }
- this.mountControlsForHost(host, media);
- this._attachDragMoveHandler(host, overlay, notePath, key, kind);
- this._attachResizeHandlers(host, overlay, handles, notePath, key, kind, media);
- });
- }
- async applyAndSaveSize(notePath, key, host, media, kind, patch, forceFreeForImage) {
- const cur = this.readAbsRect(host);
- const next = {
- w: patch.w ?? cur.w,
- h: patch.h ?? cur.h,
- x: typeof patch.x === "number" ? patch.x : cur.x || 0,
- y: typeof patch.y === "number" ? patch.y : cur.y || 0,
- free: forceFreeForImage ? true : host.dataset.msFree === "1"
- };
- const wasDetached = host.dataset.msDetached === "1";
- next.detached = patch?.detached ?? wasDetached;
- if (kind === "image" && !forceFreeForImage) {
- const ar = this.getAR("image", media);
- if (typeof patch.w === "number" && typeof patch.h !== "number") next.h = Math.round(patch.w / ar);
- if (typeof patch.h === "number" && typeof patch.w !== "number") next.w = Math.round(patch.h * ar);
- }
- if (patch.reset) {
- next.x = 0;
- next.y = 0;
- next.w = void 0;
- next.h = void 0;
- next.free = false;
- next.detached = false;
- next.locked = false;
- this.applyKindBaseStyles(host, media, kind);
- this.reattach(host);
- } else {
- this.applyState(host, media, kind, next);
- }
- await this.saveState(notePath, key, next);
- return next;
- }
- async applyLayout(notePath, layout) {
- const root = this.getAttachRoot();
- if (root) {
- for (const [key, st] of Object.entries(layout)) {
- if (!key.includes("::clone::")) continue;
- const existingClone = root.querySelector(`[data-ms-key="${CSS.escape(key)}"]`);
- if (existingClone) {
- const media = existingClone.querySelector("video, img, audio");
- const tag = media?.tagName?.toUpperCase();
- const kind = tag === "IMG" ? "image" : tag === "AUDIO" ? "audio" : "video";
- if (media) {
- this.applyState(existingClone, media, kind, st);
- await this.saveState(notePath, key, st, false);
- }
- continue;
- }
- let parentKey = st.parentKey;
- const parentAnchor = st.parentAnchor;
- let originalHost = parentKey ? this.findHostByKey(parentKey) : null;
- if (!originalHost && parentAnchor) {
- const anchorIndex = this.buildAnchorIndex(root);
- const remappedKey = anchorIndex.get(parentAnchor);
- if (remappedKey) {
- parentKey = remappedKey;
- originalHost = this.findHostByKey(parentKey);
- }
- }
- if (!originalHost && parentKey) {
- const virtualHost = await this.createHostFromTFile(notePath, parentKey);
- if (virtualHost) {
- originalHost = virtualHost;
- }
- }
- if (!originalHost || !parentKey) {
- console.warn(`[MS] applyLayout: Could not find or create original for clone ${key}. Skipping.`);
- continue;
- }
- const cloneHost = this.createCleanCloneElement(originalHost);
- const cloneState = this.notes[notePath]?.clones?.[key];
- const fileNameFromStore = cloneState?.fileName;
- const fileNameFromPreset = st?.fileName;
- let fileName = fileNameFromStore ?? fileNameFromPreset;
- if (!fileName && parentKey) {
- const originalHost2 = this.findHostByKey(parentKey);
- const mediaEl = originalHost2?.querySelector("video, audio, img");
- if (originalHost2 && mediaEl) {
- const { base } = this.getSrcAndBase(originalHost2, mediaEl);
- fileName = base || void 0;
- }
- }
- await this.saveState(notePath, key, st, false);
- this.createCloneHost(root, cloneHost, key, parentKey, st.x ?? 0, st.y ?? 0, true, fileName);
- }
- }
- const hosts = this.enumerateHostsInNote(notePath);
- if (!hosts.length) return;
- const toMove = [];
- const noteData = this.notes[notePath] ??= { originals: {}, clones: {} };
- const originals = noteData.originals ??= {};
- const clones = noteData.clones ??= {};
- for (const { host, media, kind, key } of hosts) {
- const st0 = layout[key];
- if (!st0) continue;
- const prev = originals[key] ?? {};
- const st = {
- ...st0,
- detached: st0.detached ?? host.dataset.msDetached === "1",
- locked: st0.locked ?? host.dataset.msLocked === "1",
- x: st0.x ?? 0,
- y: st0.y ?? 0,
- bgColor: st0.bgColor,
- headerColor: st0.headerColor,
- transparent: st0.transparent,
- txt: st0.txt,
- fontSize: st0.fontSize,
- textStrokeColor: st0.textStrokeColor,
- textStroke: st0.textStroke,
- textColor: st0.textColor,
- free: st0.free ?? prev.free ?? false,
- volume: st0.volume
- };
- if (!key.includes("::clone::") && kind !== "note") continue;
- if (kind === "note") {
- host.classList.remove("ms-note-transparent");
- if (st.bgColor || st.headerColor) st.transparent = false;
- }
- const hasRotate = st.rotate !== void 0 && st.rotate !== 0;
- let w, h;
- if (st.w !== void 0 && st.h !== void 0) {
- w = st.w;
- h = st.h;
- } else if (hasRotate) {
- w = parseFloat(host.style.width) || host.offsetWidth || media?.clientWidth || 0;
- h = parseFloat(host.style.height) || host.offsetHeight || media?.clientHeight || 0;
- } else {
- w = host.getBoundingClientRect().width || media?.clientWidth || 0;
- h = host.getBoundingClientRect().height || media?.clientHeight || 0;
- }
- if (st.detached) {
- this.ensureParentAnchor(host);
- host.style.position = "absolute";
- host.style.left = "0px";
- host.style.top = "0px";
- host.dataset.msDetached = "1";
- host.dataset.msLocked = st.locked ? "1" : "0";
- } else {
- host.dataset.msDetached = "0";
- host.dataset.msLocked = "0";
- this.reattach(host);
- }
- this.applyBox(host, media, kind, w, h, st.free === true);
- const rotateAngle = typeof st.rotate === "number" ? st.rotate : void 0;
- this.applyTransform(host, st.x || 0, st.y || 0, rotateAngle);
- if (host.dataset.msParentKey) {
- clones[key] = {
- ...clones[key] || {},
- parentKey: host.dataset.msParentKey,
- w,
- h,
- x: st.x,
- y: st.y,
- locked: !!st.locked,
- volume: st.volume,
- zIndex: st.zIndex,
- bgColor: st.bgColor,
- opacity: st.opacity,
- rotate: st.rotate
- };
- } else if (key.includes("::clone::")) {
- clones[key] = {
- ...clones[key] || {},
- parentKey: st.parentKey,
- w,
- h,
- x: st.x,
- y: st.y,
- locked: !!st.locked,
- rotate: st.rotate
- };
- } else {
- originals[key] = {
- ...prev,
- w,
- h,
- x: st.x,
- y: st.y,
- free: !!st.free,
- detached: !!st.detached,
- zIndex: st.zIndex,
- opacity: st.opacity,
- transparent: st.transparent,
- bgColor: st.bgColor,
- textStrokeColor: st.textStrokeColor,
- textStroke: st.textStroke,
- textColor: st.textColor,
- headerColor: st.headerColor,
- locked: !!st.locked,
- rotate: st.rotate
- };
- }
- toMove.push({ host, x: st.x || 0, y: st.y || 0 });
- this.applyState(host, media, kind, st);
- this.debug("applyLayout: z-index applied", {
- key,
- zIndexInState: st.zIndex,
- zIndexInDOM: host.style.zIndex,
- isClone: !!host.dataset.msParentKey
- });
- await this.saveState(notePath, key, st, false);
- }
- await new Promise((r) => requestAnimationFrame(() => r()));
- for (const { host, x, y } of toMove) this.applyTransform(host, x, y);
- const currentNotePath = this.getActiveNote()?.path;
- if (currentNotePath) {
- const attachRoot = this.getAttachRoot();
- if (attachRoot) {
- const layer = this.ensureFloatLayer(attachRoot);
- for (const [key, st] of Object.entries(layout)) {
- if (key.startsWith("note::")) {
- const exists = layer.querySelector(`.ms-note-host[data-ms-key="${CSS.escape(key)}"]`);
- if (!exists) {
- const isTransparent = st.bgColor || st.headerColor ? false : st.transparent;
- const host = this.createNoteHost(attachRoot, key, st.x ?? 0, st.y ?? 0, {
- w: st.w,
- h: st.h,
- txt: st.txt ?? "",
- fontSize: st.fontSize ?? 16,
- bgColor: st.bgColor,
- headerColor: st.headerColor
- });
- if (st.transparent) {
- host.classList.add("ms-note-transparent");
- }
- if (isTransparent) {
- host.querySelector(".ms-note-body").style.color = st.textColor;
- host.classList.add("ms-note-transparent");
- }
- if (currentNotePath) {
- await this.saveState(currentNotePath, key, st);
- }
- }
- }
- }
- }
- }
- for (const { host } of toMove) {
- if (host.dataset.msInit !== "1") {
- host.dataset.msInit = "1";
- const overlay = this.ensureOverlay(host);
- const isNote = host.dataset.msKind === "note";
- const handles = this.ensureHandles(overlay, false, isNote);
- host.append(overlay);
- }
- }
- for (const { host, x, y } of toMove) {
- const r = this.readAbsRect(host);
- const dx = x - (r.x || 0);
- const dy = y - (r.y || 0);
- if (dx !== 0 || dy !== 0) {
- const st = getComputedStyle(host);
- const m = /matrix\(([^)]+)\)/.exec(st.transform || "");
- const tx = m ? parseFloat(m[1].split(",")[4]) || 0 : 0;
- const ty = m ? parseFloat(m[1].split(",")[5]) || 0 : 0;
- this.applyTransform(host, tx + dx, ty + dy);
- }
- }
- const stopAt = Date.now() + 500;
- const realign = (host, x, y) => {
- const r = this.readAbsRect(host);
- const dx = x - (r.x || 0);
- const dy = y - (r.y || 0);
- if (dx || dy) {
- const st = getComputedStyle(host);
- const m = /matrix\(([^)]+)\)/.exec(st.transform || "");
- const tx = m ? parseFloat(m[1].split(",")[4]) || 0 : 0;
- const ty = m ? parseFloat(m[1].split(",")[5]) || 0 : 0;
- this.applyTransform(host, tx + dx, ty + dy);
- }
- };
- const observers = [];
- for (const { host, x, y } of toMove) {
- const ro = new ResizeObserver(() => {
- if (Date.now() > stopAt) return;
- realign(host, x, y);
- });
- ro.observe(host);
- observers.push(ro);
- setTimeout(() => {
- if (Date.now() <= stopAt) realign(host, x, y);
- }, 10);
- setTimeout(() => {
- if (Date.now() <= stopAt) realign(host, x, y);
- }, 100);
- setTimeout(() => {
- if (Date.now() <= stopAt) realign(host, x, y);
- }, 300);
- requestAnimationFrame(() => realign(host, x, y));
- }
- setTimeout(() => observers.forEach((o) => o.disconnect()), 600);
- }
- async saveLayoutToState(notePath, layout) {
- for (const [key, st] of Object.entries(layout)) {
- await this.saveState(notePath, key, st);
- }
- }
- applyState(host, media, kind, st) {
- const isClone = !!host.dataset.msParentKey;
- host.dataset.msKind = kind;
- const hasRotate = (isClone || kind === "note") && typeof st.rotate === "number" && st.rotate !== 0;
- let w, h;
- if (st.w !== void 0 && st.h !== void 0) {
- w = st.w;
- h = st.h;
- } else if (hasRotate) {
- w = parseFloat(host.style.width) || host.offsetWidth || media?.clientWidth || 0;
- h = parseFloat(host.style.height) || host.offsetHeight || media?.clientHeight || 0;
- } else {
- w = host.getBoundingClientRect().width || media?.clientWidth || 0;
- h = host.getBoundingClientRect().height || media?.clientHeight || 0;
- }
- if (isClone && (typeof st.w !== "number" || typeof st.h !== "number")) {
- this.debug("applyState: Clone has missing size in state, using fallback", {
- key: host.dataset.msKey,
- hasW: typeof st.w === "number",
- hasH: typeof st.h === "number",
- fallbackW: w,
- fallbackH: h
- });
- }
- this.applyBox(host, media, kind, w, h, st.free === true);
- if (st.detached || isClone || kind === "note") {
- this.ensureParentAnchor(host);
- host.style.position = "absolute";
- host.style.left = "0px";
- host.style.top = "0px";
- host.dataset.msDetached = "1";
- const rotateAngle = (isClone || kind === "note") && typeof st.rotate === "number" ? st.rotate : void 0;
- this.applyTransform(host, st.x || 0, st.y || 0, rotateAngle);
- host.dataset.msLocked = st.locked ? "1" : "0";
- } else {
- host.dataset.msDetached = "0";
- host.dataset.msLocked = "0";
- if (!isClone) this.reattach(host);
- }
- if (isClone && typeof st.volume === "number" && media instanceof HTMLMediaElement) {
- media.volume = clamp(st.volume, 0, 1);
- media.muted = st.volume === 0;
- }
- if (isClone && st.bgColor) {
- const localBg = this.ensureLocalBg(host);
- if (st.bgColor === "transparent") {
- host.querySelector(".ms-media-bg")?.remove();
- localBg.remove();
- return;
- }
- localBg.style.backgroundColor = st.bgColor;
- }
- if ((isClone || st.detached) && typeof st.zIndex === "number") {
- host.style.zIndex = String(st.zIndex);
- }
- if (isClone && kind === "image" && st.link) {
- this.applyLinkOverlay(host, st.link);
- }
- if (!isClone && typeof st.opacity === "number") {
- const opacityValue = String(st.opacity);
- Array.from(host.children).forEach((child) => {
- if (child instanceof HTMLElement && !child.classList.contains("ms-overlay")) {
- child.style.opacity = opacityValue;
- }
- });
- }
- if (kind === "note") {
- const noteBody = host.querySelector(".ms-note");
- const textBody = host.querySelector(".ms-note-body");
- if (textBody) {
- textBody.textContent = st.txt ?? "";
- textBody.style.fontSize = `${Math.max(10, st.fontSize ?? 16)}px`;
- }
- const noteHeader = host.querySelector(".ms-note-top");
- if (noteBody) {
- noteBody.style.backgroundColor = st.bgColor || "";
- }
- const isTransparent = st.bgColor || st.headerColor ? false : st.transparent;
- if (noteHeader) {
- const headerColor = st.headerColor;
- noteHeader.style.backgroundColor = headerColor || "";
- }
- if (isTransparent) {
- host.classList.add("ms-note-transparent");
- }
- if (kind === "note") {
- const noteBody2 = host.querySelector(".ms-note-body");
- if (noteBody2) {
- const hasStroke = !!st.textStroke;
- noteBody2.style.color = st.textColor || "";
- noteBody2.classList.toggle("ms-note-text-stroked", hasStroke);
- if (hasStroke) {
- noteBody2.style.setProperty("--note-stroke-color", st.textStrokeColor || st.textColor || "#FFFFFF");
- }
- }
- }
- }
- }
- applyBox(host, media, kind, w, h, freeImage = false) {
- if (kind === "image") {
- console.log(`\u{1F7E1} [APPLY BOX] Setting image size:`, {
- key: host.dataset.msKey,
- applyW: w,
- applyH: h,
- currentDomW: host.getBoundingClientRect().width,
- free: freeImage
- });
- }
- if (kind === "video") {
- host.style.width = `${Math.round(w)}px`;
- host.style.maxWidth = `${Math.round(w)}px`;
- const isClone = !!host.dataset.msParentKey;
- const hasME = this.isMediaExtendedActive(host);
- const videoAR = isClone || hasME ? 16 / 9 : media ? this.getAR(kind, media) : 16 / 9;
- host.style.aspectRatio = `${videoAR}`;
- if (isClone) {
- host.style.height = `${Math.round(w / videoAR)}px`;
- }
- if (media) media.style.width = "100%";
- if (media) media.style.height = "100%";
- if (media) media.style.objectFit = "contain";
- if (media) media.style.objectPosition = "center center";
- } else if (kind === "audio") {
- host.style.width = `${Math.round(w)}px`;
- host.style.maxWidth = `${Math.round(w)}px`;
- if (host.dataset.msParentKey) {
- host.style.height = "70px";
- } else {
- host.style.aspectRatio = "12 / 1";
- }
- if (media) media.style.width = "100%";
- if (media) media.style.height = "100%";
- } else if (kind === "note") {
- const iw = Math.round(w);
- const ih = Math.max(60, Math.round(typeof h === "number" ? h : 160));
- host.style.width = `${iw}px`;
- host.style.maxWidth = `${iw}px`;
- host.style.height = `${ih}px`;
- host.style.aspectRatio = "auto";
- host.style.display = "block";
- } else {
- if (!media) return;
- const ar = this.getAR("image", media);
- const iw = Math.round(w);
- const ih = Math.max(1, Math.round(typeof h === "number" ? h : iw / ar));
- const isWrapped = host !== media;
- if (isWrapped) {
- host.style.width = `${iw}px`;
- host.style.maxWidth = `${iw}px`;
- host.style.height = `${ih}px`;
- host.style.aspectRatio = "auto";
- if (media) media.style.width = "100%";
- if (media) media.style.height = "100%";
- if (media) media.style.objectFit = "fill";
- }
- host.dataset.msFree = freeImage ? "1" : "0";
- }
- host.style.display = "block";
- }
- readAbsRect(host) {
- const sizer = host.closest(".cm-sizer, .markdown-preview-sizer");
- if (!sizer) {
- console.warn("[Media Layout] readAbsRect: No sizer found for", host.dataset.msKey, "- returning zeros");
- return { x: 0, y: 0, w: 0, h: 0 };
- }
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.dataset.msKind === "note";
- const hasRotate = (isClone || isNote) && host.style.transform && host.style.transform.includes("rotate(");
- if (hasRotate) {
- const transformMatch = host.style.transform.match(/translate\(([^,)]+)(?:px)?(?:,\s*|\s+)([^)]+)(?:px)?\)/);
- let x2 = 0, y2 = 0;
- if (transformMatch) {
- x2 = parseFloat(transformMatch[1]) || 0;
- y2 = parseFloat(transformMatch[2]) || 0;
- }
- const isInFloatLayer = !!host.closest(".ms-float-layer");
- const isReadingMode = !!host.closest(".markdown-reading-view, .markdown-preview-view");
- if (isInFloatLayer && isReadingMode) {
- x2 = x2 - 29;
- y2 = y2 + 53;
- }
- const w2 = parseFloat(host.style.width) || host.offsetWidth;
- const h2 = parseFloat(host.style.height) || host.offsetHeight;
- return { x: x2, y: y2, w: w2, h: h2 };
- }
- const hostRect = host.getBoundingClientRect();
- const sizerRect = sizer.getBoundingClientRect();
- const x = hostRect.left - sizerRect.left;
- const y = hostRect.top - sizerRect.top;
- const w = hostRect.width;
- const h = hostRect.height;
- return { x, y, w, h };
- }
- readAbsOrig(host) {
- const st = getComputedStyle(host);
- const w = parseFloat(st.width) || void 0;
- const h = parseFloat(st.height) || void 0;
- const tr = st.transform;
- let x = 0, y = 0;
- if (tr && tr !== "none") {
- const m = tr.match(/matrix\(([^)]+)\)/);
- if (m) {
- const parts = m[1].split(",").map((s) => parseFloat(s.trim()));
- x = parts[4] || 0;
- y = parts[5] || 0;
- }
- }
- const free = host.dataset.msFree === "1";
- return { w, h, x, y, free };
- }
- readVisualRectForSave(host) {
- const root = this.getHostRoot(host);
- const r = host.getBoundingClientRect();
- if (!root) return { x: r.left, y: r.top, w: r.width, h: r.height };
- const rp = root.getBoundingClientRect();
- const cs = getComputedStyle(root);
- const originLeft = rp.left + (parseFloat(cs.borderLeftWidth) || 0) - root.scrollLeft;
- const originTop = rp.top + (parseFloat(cs.borderTopWidth) || 0) - root.scrollTop;
- return { x: r.left - originLeft, y: r.top - originTop, w: r.width, h: r.height };
- }
- applyTransform(host, x, y, rotate) {
- const isInFloatLayer = !!host.closest(".ms-float-layer");
- const isReadingMode = !!host.closest(".markdown-reading-view, .markdown-preview-view");
- let finalX = x;
- let finalY = y;
- if (isInFloatLayer && isReadingMode) {
- finalX = x + 29;
- finalY = y - 53;
- }
- let rotateValue = "";
- if (typeof rotate === "number") {
- rotateValue = ` rotate(${rotate}deg)`;
- } else {
- const currentTransform = host.style.transform || "";
- const rotateMatch = currentTransform.match(/rotate\(([^)]+)\)/);
- rotateValue = rotateMatch ? ` ${rotateMatch[0]}` : "";
- }
- host.style.transform = `translate(${finalX}px, ${finalY}px)${rotateValue}`;
- if (this.enableDebugMode && host.dataset.msParentKey) {
- const leafCount = this.app.workspace.getLeavesOfType("markdown").length;
- this.debug("applyTransform to clone", {
- cloneId: host.dataset.msKey,
- x: finalX,
- y: finalY,
- isReadingMode,
- isInFloatLayer,
- originalX: x,
- originalY: y,
- leafCount
- });
- }
- }
- readAppliedRect(host) {
- return this.readAbsRect(host);
- }
- async saveState(notePath, key, partial, autoPersist = true) {
- if (!notePath) return;
- this.notes ||= {};
- this.notes[notePath] ||= { originals: {}, clones: {} };
- const isCloneKey = key.includes("::clone::");
- const isCloneDom = !!this.getActiveContainerEl()?.querySelector(`[data-ms-key="${CSS.escape(key)}"][data-ms-parent-key]`);
- const isClone = isCloneKey || isCloneDom;
- const bucket = isClone ? this.notes[notePath].clones : this.notes[notePath].originals;
- if (!bucket[key]) {
- bucket[key] = {};
- }
- const sanitized = {};
- for (const [k, v] of Object.entries(partial)) {
- if (k === "x" || k === "y" || k === "w" || k === "h" || k === "rotate" || k === "zIndex" || k === "opacity" || k === "volume" || k === "fontSize") {
- const num = Number(v);
- if (!Number.isFinite(num)) {
- console.warn(`[Media Layout] Invalid ${k} value: ${v}, skipping`);
- continue;
- }
- if ((k === "w" || k === "h") && num <= 0) {
- console.warn(`[Media Layout] Invalid ${k} value (must be > 0): ${v}, skipping`);
- continue;
- }
- if ((k === "opacity" || k === "volume") && (num < 0 || num > 1)) {
- sanitized[k] = Math.max(0, Math.min(1, num));
- continue;
- }
- sanitized[k] = num;
- } else if (k === "free" || k === "detached" || k === "locked" || k === "transparent" || k === "textStroke" || k === "showAudioHandles") {
- sanitized[k] = Boolean(v);
- } else if (k === "txt" || k === "bgColor" || k === "headerColor" || k === "textColor" || k === "textStrokeColor" || k === "link" || k === "fileName" || k === "animation" || k === "dock" || k === "parentKey" || k === "parentAnchor") {
- if (typeof v === "string" || v === void 0) {
- sanitized[k] = v;
- } else {
- console.warn(`[Media Layout] Invalid ${k} value (expected string): ${v}, skipping`);
- }
- } else if (k === "bgOpacity" || k === "controlsOpacity" || k === "hideDelay" || k === "order") {
- const num = Number(v);
- if (Number.isFinite(num) && num >= 0) {
- sanitized[k] = num;
- } else {
- console.warn(`[Media Layout] Invalid ${k} value: ${v}, skipping`);
- }
- } else {
- sanitized[k] = v;
- }
- }
- bucket[key] = { ...bucket[key] || {}, ...sanitized };
- if (partial.w !== void 0 || partial.h !== void 0) {
- const hostEl = this.getActiveContainerEl()?.querySelector(`[data-ms-key="${CSS.escape(key)}"]`);
- const kind = hostEl?.dataset.msKind;
- if (kind === "image") {
- console.log(`\u{1F535} [SAVE] Image size change:`, {
- key,
- newW: partial.w,
- newH: partial.h,
- currentInMemory: { w: bucket[key].w, h: bucket[key].h },
- stackTrace: new Error().stack?.split("\n").slice(2, 4).join("\n")
- });
- }
- }
- this.hasPendingChanges = true;
- if (autoPersist === "immediate") {
- await this.persistAll();
- } else if (autoPersist === true) {
- this.persistAllDebounced();
- }
- }
- async persistAll() {
- if (this.persistInProgress) {
- if (this.persistQueue) {
- await this.persistQueue;
- }
- if (this.hasPendingChanges) {
- return this.persistAll();
- }
- return;
- }
- this.persistInProgress = true;
- const currentPersist = (async () => {
- try {
- if (!this.notes || typeof this.notes !== "object") {
- console.error("[Media Layout] CRITICAL: this.notes is invalid, aborting save to prevent data loss!", {
- notesType: typeof this.notes,
- notesValue: this.notes
- });
- return;
- }
- const hasNotes = Object.keys(this.notes).length > 0;
- if (!hasNotes && this.lastPersistTime > 0) {
- console.error("[Media Layout] CRITICAL: this.notes is empty but we had data before, aborting save to prevent data loss!");
- return;
- }
- const hadChangesAtStart = this.hasPendingChanges;
- this.hasPendingChanges = false;
- const data = {
- notes: this.notes,
- presets: this.presets ?? {},
- meBgColor: this.meBgColor ?? "#242424",
- userPresets: this.userPresets ?? [],
- layoutPresets: this.layoutPresets ?? {},
- layoutNames: this.layoutNames ?? {},
- cloneAnimation: this.cloneAnimation,
- cloneBgOpacity: this.cloneBgOpacity,
- cloneControlsOpacity: this.cloneControlsOpacity,
- cloneHideDelay: this.cloneHideDelay,
- noteRestoreDelay: this.noteRestoreDelay,
- cloneRestoreDelay: this.cloneRestoreDelay,
- cloneShowAudioHandles: this.cloneShowAudioHandles,
- enableModalAnimation: this.enableModalAnimation,
- hideDetachButton: this.hideDetachButton,
- cloneNearFile: this.cloneNearFile,
- showHandlesOnHover: this.showHandlesOnHover
- };
- await this.saveData(data);
- this.lastPersistTime = Date.now();
- } catch (error) {
- console.error("[Media Layout] Failed to persist data:", error);
- this.hasPendingChanges = true;
- } finally {
- this.persistInProgress = false;
- this.persistQueue = null;
- }
- })();
- this.persistQueue = currentPersist;
- await currentPersist;
- if (this.hasPendingChanges) {
- return this.persistAll();
- }
- }
- persistAllDebounced() {
- if (!this.hasPendingChanges) {
- return;
- }
- const now = Date.now();
- const timeSinceLastPersist = now - this.lastPersistTime;
- if (timeSinceLastPersist >= this.maxPersistDelay) {
- if (this.persistDebounceTimer !== null) {
- clearTimeout(this.persistDebounceTimer);
- this.persistDebounceTimer = null;
- }
- this.persistAll().catch((err) => {
- console.error("[Media Layout] Force persist failed:", err);
- });
- return;
- }
- if (this.persistDebounceTimer !== null) {
- clearTimeout(this.persistDebounceTimer);
- }
- this.persistDebounceTimer = window.setTimeout(() => {
- this.persistDebounceTimer = null;
- if (this.hasPendingChanges) {
- this.persistAll().catch((err) => {
- console.error("[Media Layout] Debounced persist failed:", err);
- });
- }
- }, this.persistDebounceDelay);
- }
- async flushPersist() {
- if (this.persistDebounceTimer !== null) {
- clearTimeout(this.persistDebounceTimer);
- this.persistDebounceTimer = null;
- }
- await this.persistAll();
- }
- addUserPreset(n) {
- this.userPresets = [n, ...this.userPresets.filter((v) => v !== n)].slice(0, MAX_WIDTH_PRESETS);
- return this.persistAll();
- }
- removeUserPreset(n) {
- this.userPresets = this.userPresets.filter((v) => v !== n);
- return this.persistAll();
- }
- loadStateWithFallback(notePath, key, basename) {
- const noteData = this.notes[notePath];
- if (!noteData) return void 0;
- const originals = noteData.originals ?? {};
- const byKey = originals[key];
- if (byKey) return byKey;
- if (basename) {
- for (const k of Object.keys(originals)) {
- if (k.startsWith("note::") || k.includes("::clone::")) continue;
- const b = this.extractBaseFromKey(k);
- if (b && b === basename) {
- const stateToMigrate = originals[k];
- originals[key] = stateToMigrate;
- this.persistAll();
- console.log(`Media-Size: Migrated state from old key "${k}" to new key "${key}"`);
- return stateToMigrate;
- }
- }
- }
- return void 0;
- }
- isMediaExtendedActive(host) {
- const hasPlugin = this.app.plugins?.enabledPlugins?.has?.("media-extended") || this.app.plugins?.plugins?.["media-extended"];
- if (host) {
- const hasMEContainer = !!host.closest(".mx-media-embed, .media-embed");
- return hasPlugin && hasMEContainer;
- }
- return !!hasPlugin;
- }
- getAR(kind, media) {
- if (kind === "image") {
- const img = media;
- const iw = img.naturalWidth || img.clientWidth || 1;
- const ih = img.naturalHeight || img.clientHeight || 1;
- return iw / Math.max(1, ih);
- }
- if (kind === "video") {
- const vid = media;
- const vw = vid.videoWidth || 1920;
- const vh = vid.videoHeight || 1080;
- return vw / Math.max(1, vh);
- }
- return 9 / 1;
- }
- getAttachRoot() {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (!view) return null;
- const src = view.containerEl.querySelector(".markdown-source-view .cm-scroller");
- const read = view.containerEl.querySelector(".markdown-reading-view");
- const readVisible = read ? getComputedStyle(read).display !== "none" : false;
- return readVisible ? read || src : src || read;
- }
- getHostRoot(host) {
- const containerEl = host.closest(".workspace-leaf-content");
- if (!containerEl) return null;
- const src = containerEl.querySelector(".markdown-source-view .cm-scroller");
- if (src) return src;
- const read = containerEl.querySelector(".markdown-reading-view");
- if (read) return read;
- return null;
- }
- getHostContainerEl(host) {
- return host.closest(".workspace-leaf-content");
- }
- isControlsTarget(el) {
- if (!el) return false;
- return !!el.closest(
- ".ob-video-controls, .media-controls, .mejs__controls, .plyr__controls, .vjs-control-bar, .me-controls, [class*='controls']"
- );
- }
- isControlsPoint(host, x, y) {
- const sels = [
- ".ob-video-controls",
- ".media-controls",
- ".mejs__controls",
- ".plyr__controls",
- ".vjs-control-bar",
- ".me-controls",
- "[class*='controls']"
- ];
- let panel = null;
- for (const s of sels) {
- panel = host.querySelector(s);
- if (panel) break;
- }
- if (!panel) return false;
- const btns = panel.querySelectorAll("button, [role='button'], .plyr__control, .vjs-control");
- for (const b of Array.from(btns)) {
- const r = b.getBoundingClientRect();
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return true;
- }
- return false;
- }
- extractBaseFromKey(key) {
- const parts = key.split("::");
- const fromSrc = parts.find((p) => p.includes("/")) ?? parts[0];
- const lastSlash = Math.max(fromSrc.lastIndexOf("/"), fromSrc.lastIndexOf("\\"));
- return fromSrc.substring(lastSlash + 1) || null;
- }
- getSrcAndBase(host, el) {
- const s = host.getAttribute("src") || host.getAttribute("alt") || el.getAttribute("src") || "";
- const lastSlash = Math.max(s.lastIndexOf("/"), s.lastIndexOf("\\"));
- const base = s.substring(lastSlash + 1);
- return { src: s, base };
- }
- realignClonesIfNeeded(notePath) {
- const leaves = this.app.workspace.getLeavesOfType("markdown").filter((leaf) => {
- const view = leaf.view;
- return view instanceof import_obsidian.MarkdownView && view.file?.path === notePath;
- });
- if (!leaves.length) return;
- const noteData = this.notes[notePath];
- if (!noteData?.clones) return;
- leaves.forEach((leaf) => {
- const view = leaf.view;
- if (!(view instanceof import_obsidian.MarkdownView)) return;
- const containerEl = view.containerEl;
- if (!containerEl) return;
- for (const [key, st] of Object.entries(noteData.clones)) {
- const host = containerEl.querySelector(`[data-ms-key="${CSS.escape(key)}"]`);
- if (!host || !st.x || !st.y) continue;
- const r = this.readAbsRect(host);
- const dx = (st.x || 0) - (r.x || 0);
- const dy = (st.y || 0) - (r.y || 0);
- if (Math.abs(dx) > 5 || Math.abs(dy) > 5) {
- const style = getComputedStyle(host);
- const m = /matrix\(([^)]+)\)/.exec(style.transform || "");
- const tx = m ? parseFloat(m[1].split(",")[4]) || 0 : 0;
- const ty = m ? parseFloat(m[1].split(",")[5]) || 0 : 0;
- this.applyTransform(host, tx + dx, ty + dy);
- }
- }
- });
- }
- findHostByKey(key) {
- const root = this.getActiveContainerEl();
- if (!root) return null;
- const hosts = root.querySelectorAll(`.ms-host[data-ms-key="${CSS.escape(key)}"]`);
- return hosts.length > 0 ? hosts[0] : null;
- }
- enumerateHostsInNote(notePath) {
- const root = this.getActiveContainerEl();
- if (!root) return [];
- const nodes = root.querySelectorAll(
- ".internal-embed, .media-embed, .mx-media-embed, img, video, audio, .ms-note-host"
- );
- const list = [];
- nodes.forEach((hostLike) => {
- const isNote = hostLike.classList.contains("ms-note-host");
- const host = isNote ? hostLike : ["IMG", "VIDEO", "AUDIO"].includes(hostLike.tagName) ? hostLike.closest(".internal-embed, .media-embed, .mx-media-embed") ?? hostLike : hostLike;
- const media = isNote ? host.querySelector(".ms-note") : host.querySelector("video, img, audio") ?? host;
- if (!media) return;
- const tag = isNote ? "NOTE" : (media.tagName || "").toUpperCase();
- const kind = tag === "NOTE" ? "note" : tag === "IMG" ? "image" : tag === "AUDIO" ? "audio" : "video";
- const { src } = isNote ? { src: "" } : this.getSrcAndBase(host, media);
- let key = host.dataset.msKey;
- if (!key) key = isNote ? this.makeNoteKey() : this.makeEmbedKey(src);
- host.dataset.msKey = key;
- list.push({ host, media, kind, key });
- });
- return list;
- }
- addSizePresetsMenu(menu, notePath, key, host, media, kind) {
- menu.addItem((i) => {
- i.setTitle(t(this.app, "presets")).setIcon("expand");
- const sub = i.setSubmenu();
- this.userPresets.forEach((val) => {
- sub.addItem((si) => {
- si.setTitle(String(val)).onClick(async () => {
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w: val });
- });
- const root = si.dom;
- root.classList.add("ms-preset-item");
- const btn = root.createDiv({ cls: "ms-preset-del" });
- btn.onclick = async (ev) => {
- ev.stopPropagation();
- this.removeUserPreset(val);
- root.remove();
- };
- });
- });
- if (this.userPresets.length) sub.addSeparator();
- sub.addItem(
- (si) => si.setTitle(t(this.app, "fit")).onClick(async () => {
- const parent = host.parentElement ?? host;
- const pw = Math.round(parent.clientWidth || parent.getBoundingClientRect().width);
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w: pw });
- })
- );
- if (kind !== "audio") {
- sub.addItem(
- (si) => si.setTitle(t(this.app, "natural")).onClick(async () => {
- if (kind === "image") {
- const iw = media.naturalWidth || media.clientWidth || 0;
- const ih = media.naturalHeight || media.clientHeight || 0;
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w: iw, h: ih });
- } else {
- const v = media;
- const w = v.videoWidth > 0 ? v.videoWidth : media.clientWidth || 0;
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w });
- }
- })
- );
- }
- sub.addSeparator();
- [144, 480, 640, 720, 1280, 1920].forEach(
- (sz) => sub.addItem((si) => si.setTitle(String(sz)).onClick(async () => {
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w: sz });
- }))
- );
- sub.addItem(
- (si) => si.setTitle(t(this.app, "add")).setIcon("plus").onClick(async () => {
- if (this.userPresets.length >= MAX_WIDTH_PRESETS) {
- new import_obsidian.Notice("\u274C " + t(this.app, "maxReached"));
- return;
- }
- new WidthPresetModal(this.app, async (n) => {
- if (n == null) return;
- await this.addUserPreset(n);
- await this.applyAndSaveSize(notePath, key, host, media, kind, { w: n });
- new import_obsidian.Notice(t(this.app, "added", { n }));
- }).open();
- })
- );
- });
- }
- addPositionPresetsMenu(menu, notePath) {
- menu.addItem((i) => {
- i.setTitle(t(this.app, "posPresets")).setIcon("save");
- const sub = i.setSubmenu();
- sub.addItem((si) => {
- si.setTitle(t(this.app, "resetAll")).setIcon("rotate-ccw").onClick(async () => {
- await this.resetAll(notePath);
- });
- });
- sub.addItem((si) => {
- si.setTitle(t(this.app, "saveLayout")).setIcon("save").onClick(async (si2) => {
- const layout = this.collectLayout(notePath);
- const arr = this.layoutPresets[notePath] ??= [];
- if (arr.length >= MAX_LAYOUT_PRESETS) {
- new import_obsidian.Notice("\u2716 " + t(this.app, "layoutMax"));
- return;
- }
- arr.push(JSON.parse(JSON.stringify(layout)));
- const names = this.layoutNames[notePath] ??= [];
- const baseName = t(this.app, "layoutN", { n: "" }).replace("{n}", "").trim();
- let maxNum = 0;
- names.forEach((name) => {
- if (name.startsWith(baseName)) {
- const num = parseInt(name.substring(baseName.length).trim(), 10);
- if (!isNaN(num) && num > maxNum) maxNum = num;
- }
- });
- names.push(t(this.app, "layoutN", { n: maxNum + 1 }));
- await this.persistAll();
- });
- });
- (this.layoutPresets[notePath] ?? []).forEach((layout, idx) => {
- sub.addItem((si) => {
- const names = this.layoutNames[notePath] ??= [];
- const title = names[idx] ?? t(this.app, "layoutN", { n: idx + 1 });
- si.setTitle(title).onClick(async () => {
- if (this.presetTimerId !== null) {
- clearTimeout(this.presetTimerId);
- this.pendingTimers.delete(this.presetTimerId);
- this.presetTimerId = null;
- }
- await this.applyLayout(notePath, layout);
- await this.persistAll();
- this.presetTimerId = this.schedule(async () => {
- const currentPath = this.getActiveNote()?.path;
- if (currentPath !== notePath) {
- console.log("[Media Layout] Preset application cancelled: file changed");
- this.presetTimerId = null;
- return;
- }
- await this.applyLayout(notePath, layout);
- this.applyCloneSettingsToAll();
- await this.persistAll();
- this.presetTimerId = null;
- }, 500);
- });
- const root = si.dom;
- root.classList.add("ms-preset-item");
- const edit = root.createDiv({ cls: "ms-preset-edit" });
- edit.onclick = async (ev) => {
- ev.stopPropagation();
- new RenamePresetModal(this.app, names[idx] ?? "", async (val) => {
- if (val == null) return;
- names[idx] = val;
- si.dom.querySelector(".menu-item-title")?.setText(val);
- await this.persistAll();
- }).open();
- };
- const del = root.createDiv({ cls: "ms-preset-del" });
- del.onclick = async (ev) => {
- ev.stopPropagation();
- const arr = this.layoutPresets[notePath] ?? [];
- const pos = arr.indexOf(layout);
- if (pos >= 0) {
- arr.splice(pos, 1);
- if (names && pos < names.length) names.splice(pos, 1);
- }
- root.remove();
- await this.persistAll();
- };
- });
- });
- sub.addSeparator();
- sub.addItem((si) => {
- si.setTitle(t(this.app, "copyLayout")).setIcon("copy").onClick(async () => {
- const noteData = this.notes[notePath];
- const presets = this.layoutPresets[notePath];
- const names = this.layoutNames[notePath];
- const hasNoteData = noteData && (Object.keys(noteData.originals).length || Object.keys(noteData.clones).length);
- const hasPresets = presets && presets.length > 0;
- if (!hasNoteData && !hasPresets) {
- new import_obsidian.Notice("No layout data to copy.");
- return;
- }
- const dataToCopy = {
- noteData: noteData ?? { originals: {}, clones: {} },
- presets: presets ?? [],
- names: names ?? []
- };
- await navigator.clipboard.writeText(JSON.stringify(dataToCopy, null, 2));
- new import_obsidian.Notice("\u2714\uFE0F " + t(this.app, "layoutCopied"));
- });
- });
- sub.addItem((si) => {
- si.setTitle(t(this.app, "pasteLayout")).setIcon("paste").onClick(async () => {
- try {
- const text = await navigator.clipboard.readText();
- const data = JSON.parse(text);
- if (data && data.noteData && Array.isArray(data.presets) && Array.isArray(data.names)) {
- this.notes[notePath] = data.noteData;
- this.layoutPresets[notePath] = data.presets;
- this.layoutNames[notePath] = data.names;
- await this.persistAll();
- new import_obsidian.Notice("\u2714\uFE0F " + t(this.app, "layoutPasted"));
- this.scanActive();
- } else {
- new import_obsidian.Notice("\u274C " + t(this.app, "invalidLayoutData"));
- }
- } catch (e) {
- new import_obsidian.Notice("\u274C " + t(this.app, "pasteLayoutFailed"));
- console.error("Media-Size paste error:", e);
- }
- });
- });
- });
- }
- restoreClonesFromStore(notePath) {
- try {
- if (!notePath) return;
- const clones = this.notes[notePath]?.clones;
- if (!clones || !Object.keys(clones).length) return;
- this.debug("restoreClonesFromStore started", {
- notePath,
- cloneCount: Object.keys(clones).length,
- cloneKeys: Object.keys(clones)
- });
- const leaves = this.app.workspace.getLeavesOfType("markdown").filter((leaf) => {
- const view = leaf.view;
- return view instanceof import_obsidian.MarkdownView && view.file?.path === notePath;
- });
- this.debug("Found leaves for note", {
- notePath,
- leafCount: leaves.length
- });
- if (!leaves.length) return;
- leaves.forEach((leaf) => {
- const view = leaf.view;
- if (!(view instanceof import_obsidian.MarkdownView)) return;
- const containerEl = view.containerEl;
- if (!containerEl) return;
- const ATTEMPTS = 20;
- let left = ATTEMPTS;
- const tick = () => {
- const src = containerEl.querySelector(".markdown-source-view .cm-scroller");
- const read = containerEl.querySelector(".markdown-reading-view");
- const srcVisible = src ? getComputedStyle(src.parentElement).display !== "none" : false;
- const readVisible = read ? getComputedStyle(read).display !== "none" : false;
- const root = readVisible ? read || src : src || read;
- const isReadingModeActive = readVisible;
- this.debug("tick - searching for root", {
- notePath,
- hasSrc: !!src,
- hasRead: !!read,
- hasRoot: !!root,
- srcVisible,
- readVisible,
- isReadingModeActive,
- selectedRootClass: root?.className,
- attemptsLeft: left
- });
- if (!root) {
- if (--left > 0) {
- this.schedule(tick, 50);
- }
- return;
- }
- this.debug("Root found, calling ensureFloatLayer", {
- notePath,
- rootClass: root.className,
- isReadingModeActive
- });
- const mount = this.ensureFloatLayer(root);
- if ((mount.dataset.msNote || "") !== notePath) return;
- this.debug("Layer ready, scheduling clone restore", { notePath });
- this.schedule(async () => {
- const hosts = root.querySelectorAll(".ms-host[data-ms-key]");
- if (!hosts.length) {
- this.debug("No hosts found yet, retrying", { notePath, attemptsLeft: left });
- if (--left > 0) {
- this.schedule(tick, 50);
- }
- return;
- }
- this.debug("Found hosts, building anchor index", { notePath, hostCount: hosts.length });
- const anchorIndex = this.buildAnchorIndex(root);
- this.debug("Starting PARALLEL clone restoration", { notePath, totalClones: Object.keys(clones).length });
- const clonePromises = [];
- for (const [cloneId, state] of Object.entries(clones)) {
- if (mount.querySelector(`[data-ms-key="${CSS.escape(cloneId)}"]`)) {
- this.debug("Clone already exists, skipping", { cloneId });
- continue;
- }
- const st = state;
- let parentKey = st.parentKey;
- const parentAnchor = st.parentAnchor;
- if (!parentKey || !root.querySelector(`.ms-host[data-ms-key="${CSS.escape(parentKey)}"]`)) {
- if (parentAnchor) {
- const remap = anchorIndex.get(parentAnchor);
- if (remap) {
- parentKey = st.parentKey = remap;
- }
- }
- }
- clonePromises.push(this.tryRestoreSingleClone(notePath, cloneId, st, root));
- }
- await Promise.all(clonePromises);
- this.debug("All clones restored in parallel", { notePath, count: clonePromises.length });
- this.schedule(() => this.applyCloneSettingsToAll(), 150);
- }, this.cloneRestoreDelay);
- };
- this.schedule(tick, 50);
- });
- } catch (error) {
- console.error("[Media Layout] Failed to restore clones from store:", error);
- this.error("Failed to restore clones", { notePath, error: String(error) });
- }
- }
- async tryRestoreSingleClone(notePath, cloneId, state, root) {
- if (!root) return Promise.resolve();
- this.debug("tryRestoreSingleClone started", { cloneId, parentKey: state.parentKey });
- const mount = this.ensureFloatLayer(root);
- if (mount.querySelector(`[data-ms-key="${CSS.escape(cloneId)}"]`)) return Promise.resolve();
- let parentKey = state.parentKey;
- if (!parentKey || !root.querySelector(`.ms-host[data-ms-key="${CSS.escape(parentKey)}"]`)) {
- const a = state.parentAnchor;
- if (a) {
- const idx = this.buildAnchorIndex(root);
- const remap = idx.get(a);
- if (remap) {
- parentKey = state.parentKey = remap;
- }
- }
- }
- if (!parentKey) {
- this.warn("Parent not found for clone", { cloneId, originalParentKey: state.parentKey, parentAnchor: state.parentAnchor });
- return Promise.resolve();
- }
- this.debug("Parent found, creating clone from TFile", { cloneId, parentKey });
- let cloneHost = null;
- let originalHost = null;
- const newHost = await this.createHostFromTFile(notePath, state.parentKey);
- if (newHost) {
- cloneHost = newHost;
- originalHost = newHost;
- }
- if (!cloneHost) {
- console.warn(`[MS] Failed to create clone ${cloneId}: TFile not found for ${state.parentKey}`);
- return Promise.resolve();
- }
- this.createCloneHost(root, cloneHost, cloneId, state.parentKey, state.x, state.y, true);
- const newClonedMedia = cloneHost.querySelector("video, img, audio");
- if (newClonedMedia) {
- const tag = (newClonedMedia.tagName || "").toUpperCase();
- const kind = tag === "IMG" ? "image" : tag === "AUDIO" ? "audio" : "video";
- let w = state.w;
- let h = state.h;
- if (typeof w !== "number" || typeof h !== "number") {
- const savedCloneState = this.notes[notePath]?.clones?.[cloneId];
- if (savedCloneState) {
- w = savedCloneState.w;
- h = savedCloneState.h;
- }
- }
- const fullState = { ...state, w, h, link: state.link, zIndex: state.zIndex };
- this.debug("tryRestoreSingleClone: applying state", {
- cloneId,
- hasW: typeof fullState.w === "number",
- hasH: typeof fullState.h === "number",
- hasZIndex: typeof fullState.zIndex === "number",
- w: fullState.w,
- h: fullState.h,
- zIndex: fullState.zIndex,
- zIndexFromCloneHost: cloneHost.style.zIndex
- });
- this.applyState(cloneHost, newClonedMedia, kind, fullState);
- const actualZIndex = parseInt(cloneHost.style.zIndex || "", 10);
- if (typeof state.zIndex === "number" && actualZIndex !== state.zIndex) {
- this.warn("tryRestoreSingleClone: z-index mismatch, forcing correct value", {
- cloneId,
- expectedZIndex: state.zIndex,
- actualZIndex,
- fixing: true
- });
- cloneHost.style.zIndex = String(state.zIndex);
- }
- }
- return Promise.resolve();
- }
- async createHostFromTFile(notePath, parentKey) {
- const basename = this.extractBaseFromKey(parentKey);
- if (!basename) return null;
- const files = this.app.vault.getFiles().filter((f) => f.basename === basename || f.name === basename);
- const file = files[0];
- if (!file) return null;
- const fileExt = file.extension.toLowerCase();
- const mediaTypes = {
- "jpg": "image",
- "jpeg": "image",
- "png": "image",
- "gif": "image",
- "webp": "image",
- "svg": "image",
- "mp4": "video",
- "webm": "video",
- "ogv": "video",
- "mov": "video",
- "mkv": "video",
- "mp3": "audio",
- "wav": "audio",
- "m4a": "audio",
- "ogg": "audio",
- "3gp": "audio",
- "flac": "audio"
- };
- const kind = mediaTypes[fileExt];
- if (!kind) return null;
- const host = document.createElement("div");
- host.className = "internal-embed media-embed is-loaded";
- let mediaEl;
- const resourcePath = this.app.vault.adapter.getResourcePath(file.path);
- if (kind === "image") {
- mediaEl = document.createElement("img");
- mediaEl.loading = "eager";
- } else if (kind === "video") {
- mediaEl = document.createElement("video");
- mediaEl.preload = "metadata";
- } else {
- mediaEl = document.createElement("audio");
- mediaEl.preload = "metadata";
- }
- mediaEl.src = resourcePath;
- host.appendChild(mediaEl);
- return host;
- }
- layoutClonesInDock(notePath, side = "right") {
- const root = this.getAttachRoot();
- if (!root) return;
- const layer = this.ensureFloatLayer(root);
- const dock = this.ensureDock(root, side);
- const clones = Array.from(layer.querySelectorAll("[data-ms-parent-key]"));
- const gridY = 24;
- const pad = 12;
- let y = pad, x = pad;
- clones.forEach((el, i) => {
- el.style.position = "absolute";
- el.style.left = "0";
- el.style.top = "0";
- this.applyTransform(el, x, y);
- y += (el.getBoundingClientRect().height || 120) + gridY;
- });
- }
- ensureDock(root, side = "right") {
- const layer = this.ensureFloatLayer(root);
- let dock = layer.querySelector(`.ms-dock.ms-dock--${side}`);
- if (!dock) {
- dock = document.createElement("div");
- dock.className = `ms-dock ms-dock--${side}`;
- layer.appendChild(dock);
- }
- return dock;
- }
- async resetLayoutForActive(side = "right") {
- const notePath = this.getActiveNote()?.path ?? "";
- const root = this.getAttachRoot();
- if (!notePath || !root) return;
- for (const it of this.enumerateHostsInNote(notePath)) {
- const isClone = !!it.host.dataset.msParentKey;
- if (isClone) continue;
- it.host.dataset.msDetached = "0";
- it.host.dataset.msLocked = "0";
- this.reattach(it.host);
- this.applyTransform(it.host, 0, 0);
- }
- const layer = this.ensureFloatLayer(root);
- layer.querySelectorAll("[data-ms-parent-key]").forEach((el) => el.remove());
- this.restoreClonesFromStore(notePath);
- this.layoutClonesInDock(notePath, side);
- await this.persistAll();
- }
- collectLayout(notePath) {
- const layout = {};
- const noteData = this.notes[notePath] ?? { originals: {}, clones: {} };
- const originals = noteData.originals ?? {};
- const clones = noteData.clones ?? {};
- for (const it of this.enumerateHostsInNote(notePath)) {
- const isClone = !!it.host.dataset.msParentKey;
- const isNote = it.kind === "note";
- if (!isClone && !isNote) continue;
- const r = this.readAbsRect(it.host);
- const prev = isClone ? clones[it.key] ?? {} : originals[it.key] ?? {};
- if (isClone) {
- let fileName = clones[it.key]?.fileName ?? it.host.dataset.msFileName;
- if (!fileName) {
- const mediaEl = it.host.querySelector("img,video,audio");
- if (mediaEl) {
- if (this.getSrcAndBase) {
- const { base } = this.getSrcAndBase(it.host, mediaEl);
- fileName = base || void 0;
- } else {
- const src = mediaEl.currentSrc || mediaEl.src || "";
- fileName = src ? src.split("/").pop() : void 0;
- }
- }
- }
- const hasRotate = typeof clones[it.key]?.rotate === "number" && clones[it.key].rotate !== 0;
- const stateW = clones[it.key]?.w;
- const stateH = clones[it.key]?.h;
- const finalW = hasRotate && typeof stateW === "number" ? stateW : r.w;
- const finalH = hasRotate && typeof stateH === "number" ? stateH : r.h;
- layout[it.key] = {
- ...prev,
- parentKey: it.host.dataset.msParentKey,
- w: finalW,
- h: finalH,
- x: Math.round(r.x),
- y: Math.round(r.y),
- locked: it.host.dataset.msLocked === "1",
- bgColor: clones[it.key]?.bgColor,
- fileName,
- link: clones[it.key]?.link,
- rotate: clones[it.key]?.rotate
- };
- } else {
- const originals2 = noteData.originals || {};
- const hasRotateNote = typeof originals2[it.key]?.rotate === "number" && originals2[it.key].rotate !== 0;
- const stateWNote = originals2[it.key]?.w;
- const stateHNote = originals2[it.key]?.h;
- const finalWNote = hasRotateNote && typeof stateWNote === "number" ? stateWNote : r.w;
- const finalHNote = hasRotateNote && typeof stateHNote === "number" ? stateHNote : r.h;
- layout[it.key] = {
- ...prev,
- w: finalWNote,
- h: finalHNote,
- x: Math.round(r.x),
- y: Math.round(r.y),
- detached: it.host.dataset.msDetached === "1",
- locked: it.host.dataset.msLocked === "1",
- rotate: originals2[it.key]?.rotate
- };
- }
- const zi = parseInt(it.host.style.zIndex || "", 10);
- if (Number.isFinite(zi)) layout[it.key].zIndex = zi;
- if (isNote && (layout[it.key].bgColor || layout[it.key].headerColor)) {
- delete layout[it.key].transparent;
- }
- }
- for (const [key, st] of Object.entries(originals)) {
- if (key.startsWith("note::") && !layout[key]) {
- layout[key] = { ...st };
- }
- }
- const currentClones = this.notes[notePath]?.clones ?? {};
- for (const [key, st] of Object.entries(currentClones)) {
- if (!layout[key]) layout[key] = st;
- }
- return layout;
- }
- ensureNotesFromStore(notePath) {
- this.schedule(() => {
- if (!notePath) return;
- const attachRoot = this.getAttachRoot();
- if (!attachRoot) {
- this.warn("ensureNotesFromStore: no attachRoot found", {});
- return;
- }
- this.debug("ensureNotesFromStore starting", {
- notePath,
- attachRootClass: attachRoot.className
- });
- const layer = this.ensureFloatLayer(attachRoot);
- const saved = this.notes[notePath]?.originals ?? {};
- const noteKeys = Object.keys(saved).filter((k) => k.startsWith("note::"));
- this.debug("Found notes to restore", {
- notePath,
- noteCount: noteKeys.length,
- noteKeys
- });
- for (const [key, st] of Object.entries(saved)) {
- if (!key.startsWith("note::")) continue;
- const exists = layer.querySelector(`.ms-note-host[data-ms-key="${CSS.escape(key)}"]`);
- if (exists) {
- this.debug("Note already exists, skipping", { key });
- continue;
- }
- this.debug("Creating note host", {
- key,
- x: st.x,
- y: st.y
- });
- const x = st.x ?? 0, y = st.y ?? 0;
- const host = this.createNoteHost(attachRoot, key, x, y, {
- w: st.w,
- h: st.h,
- txt: st.txt ?? "",
- fontSize: st.fontSize ?? 16
- });
- const media = host.querySelector(".ms-note");
- if (media) this.applyState(host, null, "note", st);
- }
- }, this.noteRestoreDelay);
- }
- async resetAll(notePath) {
- if (!notePath) return;
- const noteData = this.notes[notePath];
- if (noteData) {
- noteData.clones = {};
- noteData.originals = {};
- const root2 = this.getActiveContainerEl();
- if (root2) {
- root2.querySelectorAll("[data-ms-parent-key]").forEach((el) => el.remove());
- }
- }
- const root = this.getActiveContainerEl();
- if (root) {
- root.querySelectorAll(".ms-note-host").forEach((noteHost) => {
- noteHost.remove();
- });
- }
- if (noteData?.originals) {
- for (const key in noteData.originals) if (key.startsWith("note::")) delete noteData.originals[key];
- }
- for (const it of this.enumerateHostsInNote(notePath)) {
- if (it.kind === "note") continue;
- this.reattach(it.host);
- this.applyTransform(it.host, 0, 0);
- it.host.classList.remove("ms-host");
- it.host.removeAttribute("data-ms-detached");
- it.host.removeAttribute("data-ms-locked");
- it.host.removeAttribute("data-ms-free");
- it.host.removeAttribute("data-ms-kind");
- it.host.removeAttribute("data-ms-init");
- it.host.removeAttribute("data-ms-key");
- const overlay = it.host.querySelector(".ms-overlay");
- if (overlay) overlay.remove();
- it.host.style.position = "";
- it.host.style.left = "";
- it.host.style.top = "";
- it.host.style.transform = "";
- it.host.style.width = "";
- it.host.style.height = "";
- it.host.style.maxWidth = "";
- it.host.style.aspectRatio = "";
- it.host.style.display = "";
- if (it.media) {
- it.media.style.width = "";
- it.media.style.height = "";
- it.media.style.objectFit = "";
- it.media.style.objectPosition = "";
- it.media.style.display = "";
- }
- }
- await this.persistAll();
- await new Promise((r) => setTimeout(r, 50));
- const activeRoot = this.getActiveContainerEl();
- if (activeRoot) {
- this.processRoot(activeRoot, notePath);
- this.applyCloneSettingsToAll();
- }
- }
- makeEmbedKey(src) {
- const last = Math.max(src.lastIndexOf("/"), src.lastIndexOf("\\"));
- const base = last >= 0 ? src.substring(last + 1) : src;
- return base || "media";
- }
- resolveTFile(notePath, linkPath) {
- if (!linkPath) return null;
- return this.app.metadataCache.getFirstLinkpathDest(linkPath, notePath) ?? null;
- }
- getActiveNote() {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- return view?.file ?? null;
- }
- getActiveContainerEl() {
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- return view?.containerEl ?? null;
- }
- isInReadingMode(el) {
- if (el) {
- return !!el.closest(".markdown-reading-view, .markdown-preview-view");
- }
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (!view) return false;
- return !!view.containerEl.querySelector(".markdown-reading-view");
- }
- ensureOverlay(host) {
- const overlay = host.querySelector(".ms-overlay") ?? (() => {
- const d = document.createElement("div");
- d.className = "ms-overlay";
- host.appendChild(d);
- return d;
- })();
- return overlay;
- }
- ensureHandles(overlay, isClone = false, isNote = false) {
- const need = (cls) => overlay.querySelector(`.${cls.split(" ").join(".")}`) ?? (() => {
- const h = document.createElement("div");
- h.className = cls;
- overlay.appendChild(h);
- return h;
- })();
- const hRB = need("ms-handle ms-handle--rb");
- const hLB = need("ms-handle ms-handle--lb");
- const hRT = need("ms-handle ms-handle--rt");
- const hLT = need("ms-handle ms-handle--lt");
- const needRotation = isClone || isNote;
- const hT = needRotation ? need("ms-handle ms-handle--t ms-handle--rotate") : null;
- const hB = needRotation ? need("ms-handle ms-handle--b ms-handle--rotate") : null;
- const hL = needRotation ? need("ms-handle ms-handle--l ms-handle--rotate") : null;
- const hR = needRotation ? need("ms-handle ms-handle--r ms-handle--rotate") : null;
- return { rb: hRB, lb: hLB, rt: hRT, lt: hLT, t: hT, b: hB, l: hL, r: hR };
- }
- applyKindBaseStyles(host, media, kind) {
- host.classList.add("ms-host");
- host.dataset.msKind = kind;
- const isFloating = host.dataset.msDetached === "1" || kind === "note";
- if (!isFloating) {
- host.style.position = "relative";
- host.style.display = "block";
- host.style.transform = "translate(0px, 0px)";
- host.dataset.msFree = "0";
- } else {
- host.style.display = "block";
- }
- if (kind === "video") {
- const isClone = !!host.dataset.msParentKey;
- const hasME = this.isMediaExtendedActive(host);
- const videoAR = isClone || hasME ? 16 / 9 : this.getAR(kind, media);
- host.style.aspectRatio = `${videoAR}`;
- media.style.width = "100%";
- media.style.height = "100%";
- media.style.objectFit = "contain";
- media.style.objectPosition = "center center";
- media.style.display = "block";
- } else if (kind === "audio") {
- host.style.aspectRatio = "9 / 1";
- media.style.width = "100%";
- media.style.height = "100%";
- media.style.display = "block";
- } else if (kind === "image") {
- host.style.aspectRatio = "auto";
- media.style.display = "block";
- const isWrapped = host !== media;
- if (isWrapped) {
- media.style.width = "100%";
- media.style.height = "100%";
- media.style.objectFit = "fill";
- }
- host.classList.add("cm-ignore");
- host.setAttribute("contenteditable", "false");
- host.tabIndex = -1;
- try {
- media.draggable = false;
- } catch {
- }
- } else if (kind === "note") {
- }
- }
- revealInOS(full) {
- try {
- const w = window;
- if (!w.require) return;
- const cp = w.require("child_process");
- if (!cp?.exec) return;
- const platform = (w.process?.platform || navigator.platform || "").toLowerCase();
- if (platform.includes("win")) {
- cp.exec(`explorer.exe /select,"${full.replace(/\//g, "\\")}"`);
- return;
- }
- if (platform.includes("mac") || platform.includes("darwin")) {
- cp?.exec?.(`open -R "${full}"`);
- return;
- }
- const dir = full.replace(/\/[^\/]+$/, "");
- cp?.exec?.(`xdg-open "${dir}"`);
- } catch {
- }
- }
- async convertUrlMediaToLocal(url, notePath) {
- try {
- this.info("Starting URL conversion", { url, notePath });
- new import_obsidian.Notice(t(this.app, "urlMediaDownloading"));
- this.debug("Fetching URL via requestUrl", { url });
- const response = await (0, import_obsidian.requestUrl)({ url });
- this.debug("Response received", { status: response.status });
- if (response.status !== 200) {
- new import_obsidian.Notice(t(this.app, "urlMediaDownloadError", { status: response.status }));
- this.error("Download failed", { url, status: response.status });
- return;
- }
- const arrayBuffer = response.arrayBuffer;
- const contentType = response.headers["content-type"] || "image/png";
- this.debug("Data received", { contentType, size: arrayBuffer.byteLength });
- const urlObj = new URL(url);
- let fileName = urlObj.pathname.split("/").pop() || "image";
- this.debug("Original fileName", { fileName });
- try {
- fileName = decodeURIComponent(fileName);
- this.debug("Decoded fileName", { fileName });
- } catch (e) {
- this.warn("Failed to decode fileName, using as is", { fileName, error: e });
- }
- if (!fileName.includes(".")) {
- const ext = contentType.split("/")[1]?.split(";")[0] || "png";
- fileName = `${fileName}.${ext}`;
- }
- fileName = fileName.replace(/[\\/:*?"<>|]/g, "_");
- this.debug("Cleaned fileName", { fileName });
- const attachmentFolder = this.app.vault.getConfig("attachmentFolderPath") || "";
- this.debug("Attachment folder setting", { attachmentFolder });
- let savePath;
- if (attachmentFolder) {
- const cleanFolder = attachmentFolder.startsWith("/") ? attachmentFolder.slice(1) : attachmentFolder;
- savePath = (0, import_obsidian.normalizePath)(cleanFolder + "/" + fileName);
- } else {
- const noteDir = notePath.substring(0, notePath.lastIndexOf("/")) || "";
- savePath = (0, import_obsidian.normalizePath)((noteDir ? noteDir + "/" : "") + fileName);
- }
- this.debug("Save path calculated", { savePath });
- let finalPath = savePath;
- let counter = 1;
- while (await this.app.vault.adapter.exists(finalPath)) {
- const ext = fileName.substring(fileName.lastIndexOf("."));
- const name = fileName.substring(0, fileName.lastIndexOf("."));
- finalPath = savePath.replace(fileName, `${name}_${counter}${ext}`);
- counter++;
- }
- this.debug("Final path determined", { finalPath, counter });
- const folderPath = finalPath.substring(0, finalPath.lastIndexOf("/"));
- if (folderPath && !await this.app.vault.adapter.exists(folderPath)) {
- this.debug("Creating folder", { folderPath });
- await this.app.vault.createFolder(folderPath);
- }
- this.debug("Writing file", { finalPath, size: arrayBuffer.byteLength });
- await this.app.vault.adapter.writeBinary(finalPath, arrayBuffer);
- this.info("File saved successfully", { finalPath });
- const view = this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView);
- if (!view) {
- this.warn("No active MarkdownView found", { notePath });
- new import_obsidian.Notice(t(this.app, "urlMediaFileSavedLinkFailed"));
- return;
- }
- const editor = view.editor;
- const content = editor.getValue();
- this.debug("Editor content retrieved", { contentLength: content.length });
- const escapedUrl = url.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
- const pattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedUrl}\\)`, "g");
- this.debug("Regex pattern created", { pattern: pattern.source });
- const baseName = finalPath.split("/").pop() || fileName;
- this.debug("Base name for link", { baseName });
- const newContent = content.replace(pattern, `[$1](${url}) ![[${baseName}]]`);
- if (newContent !== content) {
- this.info("Content changed, updating editor", { baseName });
- editor.setValue(newContent);
- new import_obsidian.Notice(t(this.app, "urlMediaSuccess"));
- } else {
- this.warn("Content did not change - pattern not found", { url, pattern: pattern.source });
- new import_obsidian.Notice(t(this.app, "urlMediaFileSavedPatternNotFound"));
- }
- } catch (error) {
- const errorMsg = error instanceof Error ? error.message : String(error);
- const errorStack = error instanceof Error ? error.stack : void 0;
- this.error("Error converting URL media", {
- url,
- notePath,
- error: errorMsg,
- stack: errorStack
- });
- new import_obsidian.Notice(t(this.app, "urlMediaError", { error: errorMsg || t(this.app, "urlMediaUnknownError") }));
- }
- }
- isControlsProxyHost(host, x, y) {
- const wrap = this.controlsProxy.get(host)?.root;
- if (!wrap) return false;
- for (const el of Array.from(wrap.children)) {
- const r = el.getBoundingClientRect();
- if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return true;
- }
- return false;
- }
- forwardFrom(ev, target, type, cx, cy) {
- const host = ev.currentTarget?.closest(".ms-host");
- const isClone = !!host?.dataset.msParentKey;
- const media = host?.querySelector("video, audio");
- const btn = target;
- if (isClone && type !== "click") return;
- if (!isClone && btn?.isConnected && type === "click") {
- try {
- btn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
- return;
- } catch {
- }
- }
- if (type === "click" && media) {
- const hint = `${btn?.getAttribute("aria-label") || ""} ${btn?.getAttribute("title") || ""} ${btn?.className || ""}`.toLowerCase();
- if (/play|pause|toggle/.test(hint)) {
- media.paused ? media.play().catch(() => {
- }) : media.pause();
- media.focus?.();
- return;
- }
- if (/mute/.test(hint)) {
- media.muted = !media.muted;
- media.focus?.();
- return;
- }
- if (/full.?screen|fs/.test(hint)) {
- const box = host?.closest(".mejs__container") || host;
- if (box && !document.fullscreenElement) box?.requestFullscreen?.();
- else document.exitFullscreen?.();
- return;
- }
- const sb = btn.closest(".plyr__progress, .vjs-progress-holder, .mejs__time-slider");
- if (sb && !isNaN(media.duration)) {
- const r = sb.getBoundingClientRect();
- const frac = r.width ? Math.min(Math.max(ev.clientX - r.left, 0), r.width) / r.width : 0;
- media.currentTime = media.duration * frac;
- return;
- }
- const vb = btn.closest('.plyr__volume, .vjs-volume-bar, .mejs__volume, input[type=range][aria-label*="volume" i]');
- if (vb) {
- const r = vb.getBoundingClientRect();
- const frac = r.width ? Math.min(Math.max(ev.clientX - r.left, 0), r.width) / r.width : 0;
- media.volume = Math.min(1, Math.max(0, frac));
- return;
- }
- }
- }
- makeNoteKey() {
- return `note::${Date.now()}::${Math.random().toString(36).slice(2, 8)}`;
- }
- ensureParentAnchor(host) {
- const p = host.parentElement;
- if (!p) return null;
- const cs = getComputedStyle(p);
- if (cs.position === "static") {
- p.style.position = "relative";
- }
- return p;
- }
- applyAudioHandlesToAll() {
- const root = this.getActiveContainerEl();
- if (!root) return;
- root.querySelectorAll('.ms-host[data-ms-kind="audio"]').forEach((h) => {
- h.dataset.msShowHandles = this.cloneShowAudioHandles ? "true" : "false";
- });
- }
- ensureLocalBg(host) {
- let localBg = host.querySelector(".ms-local-bg");
- if (!localBg) {
- localBg = document.createElement("div");
- localBg.className = "ms-local-bg";
- Object.assign(localBg.style, {
- position: "absolute",
- inset: "0",
- zIndex: "1",
- borderRadius: "inherit"
- });
- host.insertBefore(localBg, host.firstChild);
- }
- return localBg;
- }
- ensureMediaBg(host) {
- let mediaBg = host.querySelector(".ms-media-bg");
- if (!mediaBg) {
- mediaBg = document.createElement("div");
- mediaBg.className = "ms-media-bg";
- Object.assign(mediaBg.style, {
- position: "absolute",
- left: "0",
- top: "0",
- width: "100%",
- height: "100%",
- zIndex: "-1",
- borderRadius: "inherit",
- backgroundColor: this.meBgColor
- });
- host.insertBefore(mediaBg, host.firstChild);
- }
- return mediaBg;
- }
- applyCssVariables() {
- const root = document.documentElement;
- const color = /^#([0-9a-f]{6})$/i.test(this.meBgColor) ? this.meBgColor : "#242424";
- const delay = Math.max(0, this.cloneHideDelay).toFixed(2);
- root.style.setProperty("--ms-media-bg-color", color);
- root.style.setProperty("--ms-clone-hide-delay", `${delay}s`);
- root.style.setProperty("--ms-handles-opacity", this.showHandlesOnHover ? "1" : "0");
- root.style.setProperty("--ms-modal-animation", this.enableModalAnimation ? "enabled" : "disabled");
- }
- async setMediaBgColor(hex, isLocal, host, notePath, key) {
- if (!isLocal) {
- this.meBgColor = /^#([0-9a-f]{6})$/i.test(hex) ? hex.toUpperCase() : "#242424";
- this.applyCssVariables();
- await this.persistAll();
- if (host) {
- this.ensureMediaBg(host);
- }
- } else if (host && notePath && key) {
- const localBg = this.ensureLocalBg(host);
- localBg.style.backgroundColor = hex;
- await this.saveState(notePath, key, { bgColor: hex });
- }
- }
- async maybeDetach(notePath, key, host) {
- if (!notePath || !key) return;
- if (host.dataset.msDetached === "1") return;
- const parent = host.parentElement;
- if (!parent) return;
- const rNow = this.readAbsRect(host);
- const x = Math.round(rNow.x || 0);
- const y = Math.round(rNow.y || 0);
- this.ensureParentAnchor(host);
- host.style.position = "absolute";
- host.style.left = "0px";
- host.style.top = "0px";
- host.dataset.msDetached = "1";
- this.applyTransform(host, x, y);
- const r = this.readAbsRect(host);
- const currentState = this.loadStateWithFallback(notePath, key, "") ?? {};
- await this.saveState(notePath, key, {
- ...currentState,
- detached: true,
- locked: host.dataset.msLocked === "1"
- });
- }
- reattach(host) {
- host.dataset.msDetached = "0";
- host.style.position = "relative";
- host.style.left = "";
- host.style.top = "";
- this.applyTransform(host, 0, 0);
- const p = host.parentElement;
- if (p && p.dataset.msAnchor === "1") {
- p.style.position = "";
- delete p.dataset.msAnchor;
- }
- }
- createCleanCloneElement(originalHost) {
- const mediaToClone = originalHost.querySelector("video, audio, img");
- if (mediaToClone && (mediaToClone.tagName === "VIDEO" || mediaToClone.tagName === "AUDIO")) {
- const cloneHost2 = document.createElement("div");
- cloneHost2.className = originalHost.className;
- cloneHost2.classList.remove("is-loaded");
- const mediaClone = mediaToClone.cloneNode(true);
- mediaClone.removeAttribute("id");
- mediaClone.removeAttribute("data-mx-id");
- cloneHost2.appendChild(mediaClone);
- return cloneHost2;
- }
- const cloneHost = originalHost.cloneNode(true);
- cloneHost.querySelectorAll(".ms-overlay, .ms-proxy-ctrl, .ms-handle, .ms-dock, .ms-clone-controls").forEach((n) => n.remove());
- ["msInit", "msKind", "msFree", "msLocked", "msDetached", "msKey", "data-mx-id"].forEach((a) => delete cloneHost.dataset[a]);
- cloneHost.removeAttribute("id");
- cloneHost.querySelectorAll("[id]").forEach((el) => el.removeAttribute("id"));
- cloneHost.style.zIndex = "";
- return cloneHost;
- }
- createCloneHost(container, cloneHost, key, parentKey, x, y, initialize = true, fileName) {
- cloneHost.dataset.msKey = key;
- cloneHost.dataset.msParentKey = parentKey;
- const notePath = this.getActiveNote()?.path ?? "";
- if (!notePath) return;
- cloneHost.dataset.msNote = notePath;
- if (fileName) {
- cloneHost.dataset.msFileName = fileName;
- }
- const parentHost = container.querySelector(`.ms-host[data-ms-key="${CSS.escape(parentKey)}"]`);
- if (parentHost) {
- const anchor = this.ensureOriginalAnchor(parentHost);
- cloneHost.dataset.msParentAnchor = anchor;
- try {
- const note = this.notes[notePath] ||= { originals: {}, clones: {} };
- const st = note.clones[key] ||= {
- parentKey,
- x: 0,
- y: 0,
- w: 0,
- h: 0
- };
- st.parentAnchor = anchor;
- this.persistAll?.();
- } catch (e) {
- console.warn("Failed to persist clone parentAnchor", e);
- }
- }
- cloneHost.style.position = "absolute";
- cloneHost.style.left = "0px";
- cloneHost.style.top = "0px";
- const existingZIndex = this.notes[notePath]?.clones?.[key]?.zIndex;
- cloneHost.style.zIndex = typeof existingZIndex === "number" ? String(existingZIndex) : "2900";
- this.debug("createCloneHost: z-index set", {
- key,
- hasExistingZIndex: typeof existingZIndex === "number",
- zIndex: cloneHost.style.zIndex
- });
- cloneHost.style.transform = "";
- cloneHost.style.willChange = "transform";
- cloneHost.style.pointerEvents = "auto";
- this.debug("Clone host element created", { key, x, y });
- const mount = this.ensureFloatLayer(container);
- mount.appendChild(cloneHost);
- if (this.enableDebugMode) {
- const sizer = mount.parentElement;
- const leafCount = this.app.workspace.getLeavesOfType("markdown").length;
- if (!sizer) {
- this.warn("createCloneHost: NO SIZER FOUND!", {
- key,
- x,
- y,
- leafCount,
- containerClass: container.className
- });
- } else {
- const sizerRect = sizer.getBoundingClientRect();
- const containerRect = container.getBoundingClientRect();
- const mountRect = mount.getBoundingClientRect();
- this.debug(`createCloneHost: sizer found`, {
- key,
- requestedCoords: { x, y },
- leafCount,
- sizerPos: { left: sizerRect.left, top: sizerRect.top, width: sizerRect.width, height: sizerRect.height },
- containerPos: { left: containerRect.left, top: containerRect.top },
- mountPos: { left: mountRect.left, top: mountRect.top }
- });
- }
- }
- this.applyTransform(cloneHost, Math.round(x), Math.round(y));
- setTimeout(async () => {
- const existingState = this.notes[notePath]?.clones?.[key];
- const hasValidSize = existingState && typeof existingState.w === "number" && typeof existingState.h === "number";
- const hasRotate = typeof existingState?.rotate === "number" && existingState.rotate !== 0;
- let fallbackW, fallbackH;
- if (hasRotate) {
- fallbackW = parseFloat(cloneHost.style.width) || cloneHost.offsetWidth || 480;
- fallbackH = parseFloat(cloneHost.style.height) || cloneHost.offsetHeight || 270;
- } else {
- const rect = cloneHost.getBoundingClientRect();
- fallbackW = rect.width || 480;
- fallbackH = rect.height || 270;
- }
- const currentZIndex = parseInt(cloneHost.style.zIndex || "", 10);
- const zIndexToSave = Number.isFinite(currentZIndex) ? currentZIndex : existingState?.zIndex;
- this.debug("createCloneHost setTimeout: saving z-index", {
- key,
- currentZIndexFromDOM: currentZIndex,
- existingZIndexFromState: existingState?.zIndex,
- zIndexToSave,
- hasRotate,
- sizeSaved: hasValidSize
- });
- await this.saveState(notePath, key, {
- ...existingState || {},
- parentKey,
- w: hasValidSize ? existingState.w : fallbackW,
- h: hasValidSize ? existingState.h : fallbackH,
- fileName: cloneHost.dataset.msFileName || existingState?.fileName,
- x,
- y,
- link: existingState?.link,
- zIndex: zIndexToSave,
- detached: true
- });
- }, 100);
- if (!initialize) return;
- const media = cloneHost.querySelector("video, img, audio") ?? (["IMG", "VIDEO", "AUDIO"].includes(cloneHost.tagName) ? cloneHost : null);
- if (!media) return;
- if (media instanceof HTMLMediaElement) {
- media.controls = false;
- }
- if (media.tagName === "VIDEO") {
- if (!media.parentElement?.classList.contains("ms-media-wrapper")) {
- const wrapper = document.createElement("div");
- wrapper.className = "ms-media-wrapper";
- wrapper.style.position = "relative";
- wrapper.style.zIndex = "2";
- wrapper.style.width = "100%";
- wrapper.style.height = "100%";
- cloneHost.insertBefore(wrapper, media).appendChild(media);
- }
- media.style.objectFit = "contain";
- media.style.aspectRatio = "16 / 9";
- media.style.willChange = "transform";
- cloneHost.addEventListener("fullscreenchange", () => {
- if (document.fullscreenElement === cloneHost) {
- this.wasLockedBeforeFs = cloneHost.dataset.msLocked === "1";
- cloneHost.dataset.msLocked = "1";
- } else {
- const restore = () => {
- if (this.hScrollEl) this.hScrollEl.scrollLeft = this.scrollRatioX * (this.hScrollEl.scrollWidth - this.hScrollEl.clientWidth);
- if (this.vScrollEl) this.vScrollEl.scrollTop = this.scrollRatioY * (this.vScrollEl.scrollHeight - this.vScrollEl.clientHeight);
- };
- let attempts = 0;
- const maxAttempts = 10;
- const interval = 20;
- const tryRestore = () => {
- restore();
- attempts++;
- if (attempts < maxAttempts) {
- setTimeout(tryRestore, interval);
- }
- };
- if (this.hScrollEl || this.vScrollEl) {
- requestAnimationFrame(tryRestore);
- }
- cloneHost.dataset.msLocked = this.wasLockedBeforeFs ? "1" : "0";
- }
- });
- }
- const tag = (media.tagName || "").toUpperCase();
- const kind = tag === "IMG" ? "image" : tag === "AUDIO" ? "audio" : "video";
- const defaultWidth = 480;
- let defaultHeight = void 0;
- if (kind === "image") {
- const ar = this.getAR(kind, media);
- defaultHeight = defaultWidth / ar;
- }
- this.applyBox(cloneHost, media, kind, defaultWidth, defaultHeight, false);
- cloneHost.dataset.msDetached = "1";
- cloneHost.dataset.msInit = "1";
- cloneHost.classList.add("ms-host");
- this.applyKindBaseStyles(cloneHost, media, kind);
- const overlay = this.ensureOverlay(cloneHost);
- const handles = this.ensureHandles(overlay, true);
- const savedState = this.notes[notePath]?.clones?.[key];
- if (savedState && savedState.bgColor === "transparent") {
- } else if (kind === "audio" || kind === "video" && cloneHost.dataset.msParentKey) {
- const bg = document.createElement("div");
- bg.className = "ms-media-bg";
- Object.assign(bg.style, {
- position: "absolute",
- left: "0",
- top: "0",
- width: "100%",
- height: "100%",
- zIndex: "-1",
- borderRadius: "inherit",
- backgroundColor: this.meBgColor
- });
- if (!media.parentElement?.classList.contains("ms-media-wrapper")) {
- const wrapper = document.createElement("div");
- wrapper.className = "ms-media-wrapper";
- }
- if (kind === "audio") {
- bg.style.top = "auto";
- bg.style.bottom = "0";
- bg.style.height = "70px";
- }
- cloneHost.insertBefore(bg, cloneHost.firstChild);
- }
- cloneHost.append(overlay);
- this.mountControlsForHost(cloneHost, media);
- this._attachDragMoveHandler(cloneHost, overlay, notePath, key, kind);
- this._attachResizeHandlers(cloneHost, overlay, handles, notePath, key, kind, media);
- cloneHost.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- e.stopPropagation();
- const existingMenus = document.querySelectorAll(".menu.media-size-clone-menu");
- existingMenus.forEach((menu2) => {
- const menuInstance = menu2;
- if (menuInstance.app && typeof menuInstance.app.workspace.trigger === "function") {
- menuInstance.hide();
- }
- });
- this.lastCtxEvt = e;
- const menu = new import_obsidian.Menu();
- this.app.workspace.trigger("editor-menu", menu, null, this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView));
- menu.showAtMouseEvent(e);
- menu.dom.addClass("media-size-clone-menu");
- });
- }
- schedule(fn, ms) {
- const id = window.setTimeout(() => {
- this.pendingTimers.delete(id);
- try {
- fn();
- } catch (e) {
- console.error(e);
- }
- }, ms);
- this.pendingTimers.add(id);
- return id;
- }
- cancelScheduled() {
- this.pendingTimers.forEach((id) => clearTimeout(id));
- this.pendingTimers.clear();
- }
- async hardCleanupForSwitch(prevNotePath) {
- this.debug("hardCleanupForSwitch started", { prevNotePath });
- this.cancelScheduled?.();
- try {
- this.disconnectObservers?.();
- } catch {
- }
- if (prevNotePath) {
- const leaves = this.app.workspace.getLeavesOfType("markdown").filter((leaf) => {
- const view = leaf.view;
- return view instanceof import_obsidian.MarkdownView && view.file?.path === prevNotePath;
- });
- if (leaves.length) {
- const noteData = this.notes[prevNotePath] ??= { originals: {}, clones: {} };
- leaves.forEach((leaf) => {
- const view = leaf.view;
- if (!(view instanceof import_obsidian.MarkdownView)) return;
- const containerEl = view.containerEl;
- if (!containerEl) return;
- const src = containerEl.querySelector(".markdown-source-view .cm-scroller");
- const read = containerEl.querySelector(".markdown-reading-view");
- const root = src || read;
- if (!root) return;
- const floatLayer = root.querySelector(".ms-float-layer");
- if (floatLayer) {
- floatLayer.querySelectorAll("[data-ms-key]").forEach((host) => {
- const key = host.dataset.msKey;
- if (!key) return;
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.classList.contains("ms-note-host");
- if (!isClone && !isNote) return;
- try {
- const r = this.readAbsRect(host);
- if (!r.x && !r.y && !r.w && !r.h) {
- console.warn(`[MS] Skipping save for ${key}: invalid rect (no wrapper?)`, r);
- return;
- }
- const bucket = isClone ? noteData.clones : noteData.originals;
- if (bucket[key]) {
- bucket[key].x = r.x ?? 0;
- bucket[key].y = r.y ?? 0;
- bucket[key].w = r.w ?? 0;
- bucket[key].h = r.h ?? 0;
- const leafCount = this.app.workspace.getLeavesOfType("markdown").length;
- if (leafCount > 1 && isClone) {
- console.log(`[Media Layout] SAVING coords (${leafCount} tabs):`, {
- file: prevNotePath,
- key,
- savedCoords: { x: r.x, y: r.y, w: r.w, h: r.h }
- });
- }
- }
- } catch (e) {
- console.warn(`[MS] Failed to save coords for ${key}:`, e);
- }
- });
- }
- root.querySelectorAll('.ms-host[data-ms-detached="1"]').forEach((host) => {
- const key = host.dataset.msKey;
- if (!key) return;
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.classList.contains("ms-note-host");
- if (isClone || isNote) return;
- try {
- const r = this.readAbsRect(host);
- const originals = noteData.originals;
- if (originals[key]) {
- originals[key].x = r.x ?? 0;
- originals[key].y = r.y ?? 0;
- originals[key].w = r.w ?? 0;
- originals[key].h = r.h ?? 0;
- }
- } catch (e) {
- console.warn(`[MS] Failed to save coords for detached original ${key}:`, e);
- }
- });
- root.querySelectorAll('.ms-host:not([data-ms-detached="1"])').forEach((host) => {
- const key = host.dataset.msKey;
- if (!key) return;
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.classList.contains("ms-note-host");
- if (isClone || isNote) return;
- try {
- const originals = noteData.originals;
- if (originals[key]) {
- originals[key].free = host.dataset.msFree === "1";
- if (!originals[key].w || !originals[key].h || originals[key].w < 1 || originals[key].h < 1) {
- const domRect = host.getBoundingClientRect();
- if (domRect.width > 0 && domRect.height > 0) {
- originals[key].w = domRect.width;
- originals[key].h = domRect.height;
- console.warn(`[Media Layout] Restored invalid size for ${key} from DOM`);
- }
- }
- }
- } catch (e) {
- console.warn(`[MS] Failed to process in-flow original ${key}:`, e);
- }
- });
- });
- await this.persistAll();
- }
- }
- const allHosts = document.querySelectorAll(".ms-host[data-ms-key]");
- allHosts.forEach((host) => {
- const proxy = this.controlsProxy.get(host);
- if (proxy) {
- try {
- proxy.ro.disconnect();
- proxy.root.remove();
- this.controlsProxy.delete(host);
- } catch (e) {
- console.warn("[Media Layout] Failed to cleanup controlsProxy:", e);
- }
- }
- });
- const rootToClean = this.getAttachRoot?.();
- rootToClean?.querySelector(".ms-float-layer")?.replaceChildren();
- }
- createNoteHost(container, key, x, y, st) {
- const host = document.createElement("div");
- host.className = "ms-note-host ms-host";
- host.dataset.msDetached = "1";
- host.dataset.msKey = key;
- host.style.position = "absolute";
- host.style.left = "0px";
- host.style.top = "0px";
- host.style.zIndex = "3000";
- this.applyTransform(host, Math.round(x), Math.round(y));
- host.style.width = `${Math.round(st?.w ?? 260)}px`;
- host.style.height = `${Math.round(st?.h ?? 160)}px`;
- host.dataset.msNote = this.getActiveNote()?.path ?? "";
- const note = document.createElement("div");
- note.className = "ms-note";
- const top = document.createElement("div");
- top.className = "ms-note-top";
- const close = document.createElement("div");
- close.className = "ms-note-close";
- top.appendChild(close);
- const body = document.createElement("div");
- body.className = "ms-note-body";
- const isReadingMode = this.isInReadingMode(container);
- body.setAttribute("contenteditable", isReadingMode ? "false" : "true");
- if (isReadingMode) {
- body.style.cursor = "default";
- }
- const stateForColor = st ?? this.notes[this.getActiveNote()?.path ?? ""]?.originals?.[key];
- if (stateForColor?.bgColor) {
- note.style.backgroundColor = stateForColor.bgColor;
- }
- if (stateForColor?.headerColor) {
- top.style.backgroundColor = stateForColor.headerColor;
- }
- body.textContent = st?.txt ?? "";
- body.style.fontSize = `${Math.max(10, st?.fontSize ?? 16)}px`;
- let saveTimer = null;
- const flushText = async () => {
- const notePath = host.dataset.msNote || "";
- if (!notePath) return;
- await this.saveState(notePath, key, {
- txt: body.textContent ?? "",
- fontSize: parseFloat(body.style.fontSize) || 16
- });
- };
- const scheduleTextSave = () => {
- clearTimeout(saveTimer);
- saveTimer = setTimeout(flushText, 300);
- };
- if (!isReadingMode) {
- body.addEventListener("input", scheduleTextSave);
- body.addEventListener("blur", () => {
- clearTimeout(saveTimer);
- flushText();
- });
- }
- note.append(top, body);
- host.appendChild(note);
- const mount = this.ensureFloatLayer(container);
- mount.appendChild(host);
- const overlay = this.ensureOverlay(host);
- this.ensureHandles(overlay, false, true);
- host.append(overlay);
- let sx = 0, sy = 0, ox = 0, oy = 0, dragging = false;
- const dragController = new AbortController();
- if (!isReadingMode) {
- top.addEventListener("pointerdown", (e) => {
- if (e.button !== 0) return;
- e.preventDefault();
- e.stopPropagation();
- dragging = true;
- const currentRect = this.readAppliedRect(host);
- ox = currentRect.x || 0;
- oy = currentRect.y || 0;
- sx = e.clientX;
- sy = e.clientY;
- });
- const onMove = (e) => {
- if (!dragging) return;
- const nx = ox + (e.clientX - sx);
- const ny = oy + (e.clientY - sy);
- this.applyTransform(host, nx, ny);
- };
- const onUp = async () => {
- if (!dragging) return;
- dragging = false;
- const notePath = host.dataset.msNote || "";
- if (!notePath) return;
- const r = this.readAbsRect(host);
- await this.saveState(notePath, key, { w: r.w, h: r.h, x: r.x, y: r.y, txt: body.textContent ?? "", fontSize: parseFloat(body.style.fontSize) || 16 });
- };
- window.addEventListener("pointermove", onMove, { capture: true, signal: dragController.signal });
- window.addEventListener("pointerup", onUp, { capture: true, signal: dragController.signal });
- }
- if (!isReadingMode) {
- host.addEventListener("wheel", async (e) => {
- if (!e.ctrlKey && !e.shiftKey) return;
- e.preventDefault();
- const cur = parseFloat(body.style.fontSize) || 16;
- const delta = e.shiftKey ? 1 : 10;
- const next = Math.max(8, Math.min(300, cur + (e.deltaY < 0 ? delta : -delta)));
- body.style.fontSize = `${next}px`;
- const notePath = host.dataset.msNote || "";
- if (notePath) {
- const r = this.readAbsRect(host);
- await this.saveState(notePath, key, { w: r.w, h: r.h, x: r.x, y: r.y, txt: body.textContent ?? "", fontSize: next });
- }
- }, { passive: false });
- }
- if (isReadingMode) {
- close.style.display = "none";
- } else {
- close.onclick = async (ev) => {
- ev.stopPropagation();
- const notePath = host.dataset.msNote || "";
- dragController.abort();
- host.remove();
- if (notePath) {
- delete this.notes[notePath]?.originals?.[key];
- await this.persistAll();
- }
- };
- }
- const cleanupObserver = new MutationObserver(() => {
- if (!document.contains(host)) {
- dragController.abort();
- cleanupObserver.disconnect();
- }
- });
- if (host.parentElement) {
- cleanupObserver.observe(host.parentElement, { childList: true });
- }
- if (!isReadingMode) {
- host.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- e.stopPropagation();
- const key2 = host.dataset.msKey;
- if (key2 && this.selectedKeys.has(key2) && this.selectedKeys.size > 1) {
- const menu2 = new import_obsidian.Menu();
- this.app.workspace.trigger("editor-menu", menu2, null, this.app.workspace.getActiveViewOfType(import_obsidian.MarkdownView));
- menu2.showAtMouseEvent(e);
- return;
- }
- const menu = new import_obsidian.Menu();
- menu.addItem((mi) => {
- mi.setTitle(t(this.app, "bgColor")).setIcon("palette");
- mi.onClick(() => {
- const tr = (k) => t(this.app, k);
- const notePath = host.dataset.msNote;
- if (!notePath || !key2) return;
- const currentState = this.notes[notePath]?.originals?.[key2] ?? {};
- const initialColor = currentState.bgColor || "#242424";
- new BgColorModal(this.app, initialColor, async (result) => {
- const { hex, removeBg, applyToHeader } = result;
- const noteBody = host.querySelector(".ms-note");
- const noteHeader = host.querySelector(".ms-note-top");
- if (hex === "#262626" && !applyToHeader) {
- if (noteHeader) noteHeader.style.backgroundColor = "";
- await this.saveState(notePath, key2, { headerColor: void 0 });
- }
- if (removeBg) {
- host.classList.add("ms-note-transparent");
- await this.saveState(notePath, key2, { transparent: true, bgColor: void 0, headerColor: void 0 });
- } else {
- host.classList.remove("ms-note-transparent");
- const stateUpdate = { transparent: false };
- if (applyToHeader) {
- if (noteHeader) noteHeader.style.backgroundColor = hex;
- stateUpdate.headerColor = hex;
- } else {
- if (noteBody) noteBody.style.backgroundColor = hex;
- stateUpdate.bgColor = hex;
- }
- await this.saveState(notePath, key2, stateUpdate);
- }
- }, tr, { isNoteBox: true }).open();
- });
- });
- menu.addItem((mi) => {
- mi.setTitle(t(this.app, "changeTextColor")).setIcon("highlighter");
- mi.onClick(() => {
- const tr = (k) => t(this.app, k);
- const notePath = host.dataset.msNote;
- if (!notePath || !key2) return;
- const currentState = this.notes[notePath]?.originals?.[key2] ?? {};
- const initialColor = currentState.textColor || "#FFFFFF";
- const hasStroke = currentState.textStroke || false;
- const colorToEdit = hasStroke ? currentState.textStrokeColor || initialColor : initialColor;
- new BgColorModal(this.app, initialColor, async (result) => {
- const { hex, addStroke } = result;
- const currentFillColor = body.style.color || currentState.textColor || "#FFFFFF";
- body.classList.toggle("ms-note-text-stroked", addStroke);
- if (addStroke) {
- body.style.setProperty("--note-stroke-color", hex);
- await this.saveState(notePath, key2, { textStroke: true, textStrokeColor: hex, textColor: currentFillColor });
- } else {
- body.style.color = hex;
- await this.saveState(notePath, key2, { textStroke: false, textColor: hex });
- }
- }, tr, { isTextColor: true, initialStrokeState: hasStroke }).open();
- });
- });
- menu.addSeparator();
- menu.addItem((item) => {
- item.setTitle(t(this.app, "deleteNote")).setIcon("trash").onClick(async () => {
- const notePath = host.dataset.msNote || "";
- host.remove();
- if (notePath && key2) {
- delete this.notes[notePath]?.originals?.[key2];
- await this.persistAll();
- }
- });
- });
- menu.showAtMouseEvent(e);
- });
- }
- if (st?.focus && !isReadingMode) {
- setTimeout(() => body.focus(), 0);
- }
- this.processRoot(this.getActiveContainerEl(), this.getActiveNote()?.path ?? "");
- return host;
- }
- findScrollableParents(startEl) {
- let hScroll = null;
- let vScroll = null;
- let el = startEl;
- while (el && el !== document.body && (!hScroll || !vScroll)) {
- const style = getComputedStyle(el);
- if (!hScroll && (style.overflowX === "auto" || style.overflowX === "scroll") && el.scrollWidth > el.clientWidth) {
- hScroll = el;
- }
- if (!vScroll && (style.overflowY === "auto" || style.overflowY === "scroll") && el.scrollHeight > el.clientHeight) {
- vScroll = el;
- }
- el = el.parentElement;
- }
- return { hScroll: hScroll ?? this.getAttachRoot(), vScroll: vScroll ?? this.getAttachRoot() };
- }
- _attachDragMoveHandler(host, overlay, notePath, key, kind) {
- const startMove = (e) => {
- if (this.isInReadingMode(host)) {
- return;
- }
- const isClone = !!host.dataset.msParentKey;
- const isNote = kind === "note";
- if ((e.ctrlKey || e.metaKey) && (isClone || isNote)) {
- this.handleSelectionClick(e, key);
- return;
- }
- const t2 = e.target;
- if (t2?.closest(".ms-clone-controls, .ms-cc-btn, .ms-cc-timeline, .ms-proxy-btn, .ms-proxy-ctrl, .ms-handle, .ms-note-body")) {
- return;
- }
- if (e.button !== 0) return;
- if (kind !== "note" && !isClone && host.dataset.msDetached !== "1") return;
- if (host.dataset.msLocked === "1") return;
- const sx = e.clientX ?? e.clientX ?? 0;
- const sy = e.clientY ?? e.clientY ?? 0;
- if (this.isControlsTarget(t2) || this.isControlsPoint(host, sx, sy) || this.isControlsProxyHost(host, sx, sy)) return;
- const isMovingGroup = this.selectedKeys.has(key) && this.selectedKeys.size > 1;
- const groupElements = [];
- if (isMovingGroup) {
- this.selectedKeys.forEach((selectedKey) => {
- const el = this.findHostByKey(selectedKey);
- if (el) {
- const elCur = this.readAbsRect(el);
- groupElements.push({ host: el, x0: elCur.x || 0, y0: elCur.y || 0 });
- }
- });
- }
- let moved = false;
- const isDetachedOriginal = !isClone && kind !== "note" && host.dataset.msDetached === "1";
- const cur = isDetachedOriginal ? this.readAbsOrig(host) : this.readAbsRect(host);
- let x0 = cur.x || 0;
- let y0 = cur.y || 0;
- const controller = new AbortController();
- const onMove = (ev) => {
- const dx = (ev.clientX ?? ev.clientX ?? 0) - sx;
- const dy = (ev.clientY ?? ev.clientY ?? 0) - sy;
- if (!moved && Math.hypot(dx, dy) < 4) return;
- if (!moved) {
- moved = true;
- overlay.classList.add("--dragmove");
- document.body.classList.add("ms-noselect");
- }
- ev.preventDefault();
- if (isMovingGroup) {
- groupElements.forEach((item) => {
- let nx = item.x0 + dx, ny = item.y0 + dy;
- if (ev.shiftKey) {
- nx = snap(nx, 20);
- ny = snap(ny, 20);
- }
- this.applyTransform(item.host, nx, ny);
- });
- } else {
- let nx = x0 + dx, ny = y0 + dy;
- if (ev.shiftKey) {
- nx = snap(nx, 20);
- ny = snap(ny, 20);
- }
- this.applyTransform(host, nx, ny);
- }
- };
- const onUp = async () => {
- controller.abort();
- overlay.classList.remove("--drag", "--dragmove");
- document.body.classList.remove("ms-noselect");
- if (!moved) return;
- if (isMovingGroup) {
- for (const item of groupElements) {
- const rect = this.readAbsRect(item.host);
- const itemKey = item.host.dataset.msKey;
- await this.saveState(notePath, itemKey, { x: rect.x || 0, y: rect.y || 0 });
- }
- } else {
- const rect = this.readAbsRect(host);
- const bucket = isClone ? this.notes[notePath]?.clones : this.notes[notePath]?.originals;
- const existingRotate = bucket?.[key]?.rotate;
- const hasRotate = isClone && typeof existingRotate === "number" || isNote && typeof existingRotate === "number";
- const stateToSave = {
- x: rect.x || 0,
- y: rect.y || 0,
- free: host.dataset.msFree === "1",
- detached: host.dataset.msDetached === "1" || isClone,
- locked: host.dataset.msLocked === "1"
- };
- if (!hasRotate) {
- stateToSave.w = rect.w;
- stateToSave.h = rect.h;
- } else {
- stateToSave.rotate = existingRotate;
- }
- if (kind === "note") {
- const body = host.querySelector(".ms-note-body");
- stateToSave.txt = body?.textContent ?? "";
- stateToSave.fontSize = body ? parseFloat(body.style.fontSize) || 16 : 16;
- }
- if (isClone && (kind === "video" || kind === "audio")) {
- stateToSave.fileName = host.dataset.msFileName;
- }
- await this.saveState(notePath, key, stateToSave);
- }
- };
- window.addEventListener("pointermove", onMove, { capture: true, signal: controller.signal });
- window.addEventListener("pointerup", onUp, { capture: true, signal: controller.signal });
- };
- host.addEventListener("pointerdown", startMove, { capture: true });
- }
- _attachResizeHandlers(host, overlay, handles, notePath, key, kind, media) {
- if (this.isInReadingMode(host)) {
- return;
- }
- const map = { lt: { h: "left", v: "top" }, rt: { h: "right", v: "top" }, lb: { h: "left", v: "bottom" }, rb: { h: "right", v: "bottom" } };
- if (this.selectedKeys.size > 1) {
- return;
- }
- ["lt", "rt", "lb", "rb"].forEach((keyH) => {
- const handle = handles[keyH];
- const startDrag = (e) => {
- e.preventDefault();
- e.stopPropagation();
- overlay.classList.add("--drag");
- document.body.classList.add("ms-noselect");
- const startX = e.clientX;
- const startY = e.clientY;
- const r0 = host.getBoundingClientRect();
- const isClone2 = !!host.dataset.msParentKey;
- const isDetachedOriginal = !isClone2 && kind !== "note" && host.dataset.msDetached === "1";
- const cur = isDetachedOriginal ? this.readAbsOrig(host) : this.readAbsRect(host);
- const startW = cur.w ?? r0.width;
- const startH = cur.h ?? r0.height;
- const startXoff = cur.x || 0;
- const startYoff = cur.y || 0;
- const from = map[keyH];
- const hasME = this.isMediaExtendedActive(host);
- const baseAR = kind === "image" ? startW / Math.max(1, startH) : kind === "video" ? isClone2 || hasME ? 16 / 9 : media ? this.getAR(kind, media) : 16 / 9 : kind === "audio" ? 9 / 1 : 0;
- const controller = new AbortController();
- const rotateAngle = isClone2 ? this.notes[notePath]?.clones?.[key]?.rotate || 0 : kind === "note" ? this.notes[notePath]?.originals?.[key]?.rotate || 0 : 0;
- const hasRotate = rotateAngle !== 0;
- const onMove = (ev) => {
- ev.preventDefault();
- let dx = ev.clientX - startX;
- let dy = ev.clientY - startY;
- if (hasRotate) {
- const angleRad = rotateAngle * Math.PI / 180;
- const cos = Math.cos(angleRad);
- const sin = Math.sin(angleRad);
- const rotatedDx = dx * cos + dy * sin;
- const rotatedDy = -dx * sin + dy * cos;
- dx = rotatedDx;
- dy = rotatedDy;
- }
- const alt = ev.altKey;
- const shift = ev.shiftKey;
- const ctrl = ev.ctrlKey || ev.metaKey;
- const isDetached = host.dataset.msDetached === "1" || isClone2;
- let newW = startW, newH = startH;
- if (isDetached) {
- if (from.h === "right") newW = startW + dx;
- else newW = startW - dx;
- if (from.v === "bottom") newH = startH + dy;
- else newH = startH - dy;
- } else {
- newW = startW + dx;
- newH = startH + dy;
- }
- if (alt) {
- if (from.h === "right" || from.h === "left") newW = startW + 2 * (from.h === "right" ? dx : -dx);
- if (from.v === "bottom" || from.v === "top") newH = startH + 2 * (from.v === "bottom" ? dy : -dy);
- }
- let aw = clamp(newW, 64, 8192);
- let ah = clamp(newH, 32, 8192);
- if (shift) {
- aw = snap(aw, 20);
- ah = snap(ah, 20);
- }
- const free = ctrl;
- if (kind === "image") {
- if (!free) {
- const useW = Math.abs(dx) >= Math.abs(dy);
- if (useW) ah = Math.round(aw / baseAR);
- else aw = Math.round(ah * baseAR);
- }
- } else if (kind === "note") {
- } else {
- ah = Math.round(aw / baseAR);
- }
- if (isDetached) {
- let newX = startXoff, newY = startYoff;
- if (hasRotate || alt) {
- newX = startXoff - Math.round((aw - startW) / 2);
- newY = startYoff - Math.round((ah - startH) / 2);
- } else {
- if (from.h === "left") newX = startXoff + startW - aw;
- if (from.v === "top" && kind !== "audio") newY = startYoff + startH - ah;
- }
- if (shift) {
- newX = snap(newX, 20);
- newY = snap(newY, 20);
- }
- this.applyTransform(host, newX, newY);
- }
- this.applyBox(host, media, kind, aw, ah, free);
- };
- const onUp = async () => {
- controller.abort();
- overlay.classList.remove("--drag");
- document.body.classList.remove("ms-noselect");
- const bucket = isClone2 ? this.notes[notePath]?.clones : this.notes[notePath]?.originals;
- const existingRotate = bucket?.[key]?.rotate;
- const hasRotate2 = isClone2 && typeof existingRotate === "number" || isNote && typeof existingRotate === "number";
- const rect = this.readAbsRect(host);
- let finalW = rect.w;
- let finalH = rect.h;
- if (hasRotate2) {
- finalW = parseFloat(host.style.width) || rect.w;
- finalH = parseFloat(host.style.height) || rect.h;
- }
- const stateToSave = {
- w: finalW,
- h: finalH,
- x: rect.x || 0,
- y: rect.y || 0,
- free: host.dataset.msFree === "1",
- detached: host.dataset.msDetached === "1" || isClone2,
- locked: host.dataset.msLocked === "1"
- };
- if (hasRotate2) {
- stateToSave.rotate = existingRotate;
- }
- if (kind === "note") {
- const body = host.querySelector(".ms-note-body");
- stateToSave.txt = body?.textContent ?? "";
- stateToSave.fontSize = body ? parseFloat(body.style.fontSize) || 16 : 16;
- }
- if (isClone2 && (kind === "video" || kind === "audio")) {
- stateToSave.fileName = host.dataset.msFileName;
- }
- await this.saveState(notePath, key, stateToSave);
- };
- if ("pointerId" in e && typeof handle.setPointerCapture === "function") {
- handle.setPointerCapture(e.pointerId);
- }
- window.addEventListener("pointermove", onMove, { capture: true, signal: controller.signal });
- window.addEventListener("pointerup", onUp, { capture: true, signal: controller.signal });
- };
- handle.addEventListener("pointerdown", startDrag);
- });
- const isClone = !!host.dataset.msParentKey;
- const isNote = kind === "note";
- if ((isClone || isNote) && handles.t && handles.b && handles.l && handles.r) {
- const cursorCache = {};
- const getRotationCursor = (effectiveAngle) => {
- const roundedAngle = Math.round(effectiveAngle / 5) * 5;
- if (cursorCache[roundedAngle]) {
- return cursorCache[roundedAngle];
- }
- const baseCursor = "iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAAAsTAAALEwEAmpwYAAABNmlDQ1BQaG90b3Nob3AgSUNDIHByb2ZpbGUAAHjarY6xSsNQFEDPi6LiUCsEcXB4kygotupgxqQtRRCs1SHJ1qShSmkSXl7VfoSjWwcXd7/AyVFwUPwC/0Bx6uAQIYODCJ7p3MPlcsGo2HWnYZRhEGvVbjrS9Xw5+8QMUwDQCbPUbrUOAOIkjvjB5ysC4HnTrjsN/sZ8mCoNTIDtbpSFICpA/0KnGsQYMIN+qkHcAaY6addAPAClXu4vQCnI/Q0oKdfzQXwAZs/1fDDmADPIfQUwdXSpAWpJOlJnvVMtq5ZlSbubBJE8HmU6GmRyPw4TlSaqo6MukP8HwGK+2G46cq1qWXvr/DOu58vc3o8QgFh6LFpBOFTn3yqMnd/n4sZ4GQ5vYXpStN0ruNmAheuirVahvAX34y/Axk/96FpPYgAAACBjSFJNAAB6JQAAgIMAAPn/AACA6AAAUggAARVYAAA6lwAAF2/XWh+QAAAJeUlEQVR42uybe0xU2R3HPxdYmMOAlJAaFIQuNBlZwfVN6gu7prUWqq21krpGZYtYjcE0uD6jTXzEWOkawVWsGmuqqLFb2sVXs4qOoCtxUxVdtW7Xqosa8IUKHpRhbv/gXvZCh2FAzM5c9pfczJ0753W/8zvf3+Oco9A18gbQACQAAvgXPiKqqr6Wdv21zyTgT4CqgaJq16fAW74CkLtL6WB7bwNzgNlAKTAKICAgAIfDgc1mw2KxcPHiRYBnQH8p5U1vBshisbj9XWln2sQCNmCkBsYIAD8/P5xOJ/369cNms5Genk5MTAyxsbGEhYWRmZnJ3r17AT6UUs4zE0ARQBwwS7taSFJSEkIIpk+fTlRUFMOHDyc4OJiAgIAW5SoqKkhOTgaokFK+7csA6W8WAmwH0pt/CAggJiaGsLAwJk2ahNVqJT09HavVSlBQkNtGDx48qN+W4uOiA3QQSAkPDyc0NJSJEycyZMgQUlNTAbBarR436HQ6KS8vx9CuT4uiWZvPExISKCoqokePHoSHhzcxuKJ0uMHHjx8TFxdHfX09wGFgjpTytq9zkAogpXzlDmtraxk6dCg3b7YwXieAXVLKXb4GkJ/2WQaQn5//yh2GhIRw5coVqqqqmDx5sj6AHwJ/FkLYhRA/97UpBjAJ+EgHKTMzs8s6aGhoYPfu3SxcuJDa2lojeRdLKdf7kpmfBvwFYP369cyb17Xui9PpJD8/n8WLFxsfnwIWSCnP+Yof1AxSTk4Oq1evfi2u/aFDh5g7dy7379/XH3+kkfl9X/Ckfwn8FWDmzJls2bLltQzM6XSyePFiI++VA6VSyvd9IdQYAZQAgVOnTmXHjh2vbYDV1dUMGjSIhw8f6o/swCop5XFvsmKt5TQwGKgtLCzsUtJuLT179qSyshK73U5wcDBACnBMCJHrDVbMz81vl4EfAHV79uxxC1JjYyO5ublcv3690/mVYcOGUV1dzbJly/RHOUKIo0KIN73BzLuTROAsYJ02bRrbtm1zWWjMmDGcP3+eiIgIxo8fT0ZGBgMHDsTf37/Dgzp37hzvvPMODodD99EWSCnLvS3d0RqkciDYFUgVFRWMGjWKly9fYgx2FUUhOTmZgoIC4uPjOzTwuro64uLiePr0qf5ou2bpHN7AQa6mWzLwfPfu3WRlZbX48auvvjLGbW8B2xwOBw0NDZSVlZGYmMiAAQM4ffq0xwO3Wq1UVVUxf/58/VEm8IkQoq83e96JwHNAnTFjhiqlVB88eKBmZWXpqdYdUko9pnsDsAA7gS8A1c/PTx09erR67949VUrp8XXhwgW9fVXzwhP1fl716uqUqw7SZ0DQuHHjGDx4MMXFxVy6dAlggJTyoqtKQogCmlK1AMydO5e1a9cSGBjoUacOh4PQ0FAMseMcKeVlb+EgVyAVA9+Dr1OwQICUsrGtSkKIKGAL8DOdpwoLC0lLS/M4tdK7d28eP34MUAlkSCmPeQMHueKk/kAqsFUDZ6s7cLR0yh0p5QRgEFDicDiYMmUKGRkZvHjxwqOO7969qyfyojVO+uk3beZp56URQvi3B04bGpUDLAfCYmNjKSkpoXfv3h7VXbNmjTFWTJVSHvYmDWoNUmMn6/0RGAocv3XrFvHx8ezfv9+jusuWLWPlypXNLoAQItFrNehVRQgRrnFTOkBKSgpHjx71qO6UKVMoLi7uNHG/LpLuUoAMQM0BsoG+QUFB1NTUeFQvJiZGT510GCSfAkgDKVHTppHQlOP2JFwRQhhdgPeklF94DQd1pWj//hztRQkJCfFIkwx/1EgN4NcezX/TIGVpuSF69eplzBe1KQYgxwohfmtagDSQrgLvag4p0dHRNDQ0uK0TFBTEzp079a/vdoVl8zoOcsEtkUAhTUtHHvVns9m4ffu2zkfpUsq7piHpNkCyaemOkZ72abVa9fDnYynlRNOQdBt/wr+NxB0ZGdluncrKSv12ghAiw3Qc5M66PXnyhNmzZ7stHx4eTkFBgf71PSFEjCk5qA0/6RxgKS8vp3///p76R4ellKmmnGIuNGkHQHJyMnV1dW7LV1VV6bejOmPVfA4gDaR5wD+haTXE3UpKjx49GDhwIEBoZxxInwRIkzlA6Y0bN1iyZInbgmfOnGn2soUQv+sWAEkp/wu8D7Bx40bjzhGXsm/fPv12kqlJ2gUJHwF+IoTg0aNHbsuGhYXpS1N5Usr5piTpNqbap1JKzp4967ag3W43Evb3u4UGaVq0DljoyXgiIiJ4/vw5wDEp5Y+6gwYhpVyke9mGtX2XcufOHaMWxXcLDdK0aDhNu1I6okWfqKr6Y9NrkAbKGV2Lli9f7mmcNlJRlO90Cw3StGgk2u7+9sZlsGj7VFX9tek1SAOlDKiAph0n7uTKlSvNXNSsLYoSpihKpGkB0qQUYPLkyW4LRUVFNd8qirJVUZSzQA1wT1GUE4qi9DXdFNOmmT/g8PPzo7a21u2a/9SpUykqKmoRt1ksFqqrqwGuAf1Np0HaKu9/nE4nN27ccFs2NzeXyMhI8vPzsdvtnD9/ni+//JKUlBSAvsCHZtSg7wJ/AGampaVx4MABt+WfPXtm3FYDwLVr1/QMwLUAs2iOECINmA78Cpq25Jw8ebLdeq3BAThy5EgznwWYBJztwG90J9Bms5GUlMTYsWP13Scet9XY2MiuXc2Hkg6aIZoPAuoDAwNZsWIFI0aMICEhAYvF0u7JyLamXHR0tO4jWcygQVaA4OBgcnJyXrmxq1ev6tuPARw+b8WklI+AspqaGnJzX31zvt1u19fTtqqq2miWdEcq2vnYdevWkZ2d3al2Xr58SZ8+ffS92amqqh42S7rjEFoqddGiReTl5XWqnRcvXhg3w58wWzRfZARpw4YNHY9RSkv1w8hHVFWVZgxWm0FaunQpH3zwQYfqG1K2ZaZMdxg4aQLwD4BVq1axYMGCduvU19cTERGhE/RoVVVLzRrNI6X8GJgATcmz9evbPztcV1dn3PX/mZnTHTpIxUAawIoVK9oFqaSkROefv+v8Y2qADNYtVQepLT9JVVVj/NXiaIMpOcidn5SXl8esWbP+7x0iIyN1E/+mqqo3u4UGtdKkaQDZ2dkcP97yvHB1dbUOzi0jON0GIA2kPcDvAdLS0jh16lQL/0e/bV2v2wCkgbQSWA0wbtw4Nm3axOXLl9m8eXNzKMa3AsAqvj7BqF/lrk4c+ndTgE4AnwMPaTo2+jdVVX/hquD/BgANh4vTAjsODAAAAABJRU5ErkJggg==";
- const svgRotation = roundedAngle - 45 + 90;
- const svgCursor = ``;
- const encoded = encodeURIComponent(svgCursor).replace(/'/g, "%27");
- const cursor = `url('data:image/svg+xml;charset=utf-8,${encoded}') 36 36, grabbing`;
- cursorCache[roundedAngle] = cursor;
- return cursor;
- };
- const rotationHandles = [
- { handle: handles.t, name: "t" },
- { handle: handles.b, name: "b" },
- { handle: handles.l, name: "l" },
- { handle: handles.r, name: "r" }
- ];
- rotationHandles.forEach(({ handle, name }) => {
- if (!handle) return;
- let isRotating = false;
- const updateCursorOnHover = (e) => {
- if (isRotating) return;
- const currentTransform = host.style.transform || "";
- const rotateMatch = currentTransform.match(/rotate\(([^)]+)deg\)/);
- const currentRotate = rotateMatch ? parseFloat(rotateMatch[1]) : 0;
- let baseAngle = 0;
- if (name === "t") baseAngle = 270;
- else if (name === "r") baseAngle = 0;
- else if (name === "b") baseAngle = 90;
- else if (name === "l") baseAngle = 180;
- const effectiveAngle = baseAngle + currentRotate;
- const cursor = getRotationCursor(effectiveAngle);
- handle.style.cursor = cursor;
- handle.style.setProperty("cursor", cursor, "important");
- };
- handle.addEventListener("mouseenter", updateCursorOnHover);
- handle.addEventListener("mousemove", updateCursorOnHover);
- updateCursorOnHover(new MouseEvent("mousemove"));
- const startRotate = (e) => {
- e.preventDefault();
- e.stopPropagation();
- isRotating = true;
- overlay.classList.add("--drag");
- document.body.classList.add("ms-noselect");
- const savedState = this.notes[notePath]?.clones?.[key];
- const realWidth = savedState?.w || parseFloat(host.style.width) || host.offsetWidth;
- const realHeight = savedState?.h || parseFloat(host.style.height) || host.offsetHeight;
- const rect = host.getBoundingClientRect();
- const centerX = rect.left + rect.width / 2;
- const centerY = rect.top + rect.height / 2;
- const currentTransform = host.style.transform || "";
- const rotateMatch = currentTransform.match(/rotate\(([^)]+)deg\)/);
- const startRotate2 = rotateMatch ? parseFloat(rotateMatch[1]) : 0;
- const startAngle = Math.atan2(
- e.clientY - centerY,
- e.clientX - centerX
- ) * (180 / Math.PI);
- let baseAngle = 0;
- if (name === "t") baseAngle = 270;
- else if (name === "r") baseAngle = 0;
- else if (name === "b") baseAngle = 90;
- else if (name === "l") baseAngle = 180;
- let cursorUpdateTimer = null;
- let currentRotation = startRotate2;
- let currentCursor = getRotationCursor(baseAngle + startRotate2);
- const controller = new AbortController();
- const onMove = (ev) => {
- ev.preventDefault();
- document.body.style.cursor = currentCursor;
- handle.style.cursor = currentCursor;
- handle.style.setProperty("cursor", currentCursor, "important");
- if (cursorUpdateTimer) {
- clearTimeout(cursorUpdateTimer);
- cursorUpdateTimer = null;
- }
- const currentAngle = Math.atan2(
- ev.clientY - centerY,
- ev.clientX - centerX
- ) * (180 / Math.PI);
- let deltaAngle = currentAngle - startAngle;
- let newRotate = startRotate2 + deltaAngle;
- if (ev.shiftKey) {
- newRotate = Math.round(newRotate / 15) * 15;
- }
- currentRotation = newRotate;
- const translateMatch = currentTransform.match(/translate\(([^)]+)\)/);
- const translateValue = translateMatch ? translateMatch[1] : "0px, 0px";
- host.style.transform = `translate(${translateValue}) rotate(${newRotate}deg)`;
- cursorUpdateTimer = window.setTimeout(() => {
- const effectiveAngle = baseAngle + currentRotation;
- currentCursor = getRotationCursor(effectiveAngle);
- document.body.style.cursor = currentCursor;
- handle.style.cursor = currentCursor;
- handle.style.setProperty("cursor", currentCursor, "important");
- }, 50);
- };
- const onUp = async () => {
- if (cursorUpdateTimer) {
- clearTimeout(cursorUpdateTimer);
- cursorUpdateTimer = null;
- }
- isRotating = false;
- controller.abort();
- overlay.classList.remove("--drag");
- document.body.classList.remove("ms-noselect");
- document.body.style.cursor = "";
- const finalTransform = host.style.transform || "";
- const finalRotateMatch = finalTransform.match(/rotate\(([^)]+)deg\)/);
- const finalRotate = finalRotateMatch ? parseFloat(finalRotateMatch[1]) : 0;
- await this.saveState(notePath, key, { rotate: finalRotate });
- };
- if ("pointerId" in e && typeof handle.setPointerCapture === "function") {
- handle.setPointerCapture(e.pointerId);
- }
- window.addEventListener("pointermove", onMove, { capture: true, signal: controller.signal });
- window.addEventListener("pointerup", onUp, { capture: true, signal: controller.signal });
- };
- handle.addEventListener("pointerdown", startRotate);
- });
- }
- }
- adjustZIndex(host, direction) {
- const currentZ = parseInt(host.style.zIndex || "2900", 10);
- const newZ = currentZ + direction;
- host.style.zIndex = String(newZ);
- const notePath = this.getActiveNote()?.path;
- const key = host.dataset.msKey;
- if (notePath && key && host.dataset.msParentKey) {
- this.saveState(notePath, key, { zIndex: newZ });
- }
- }
- handleMarqueeStart(e) {
- const attachRoot = this.getAttachRoot();
- if (!attachRoot?.contains(e.target) || e.target.closest(".ms-handle")) {
- return;
- }
- const host = e.target.closest(".ms-host");
- if (host && host.dataset.msKey) {
- return;
- }
- e.preventDefault();
- e.stopPropagation();
- this.marqueeWithCtrl = e.ctrlKey || e.metaKey;
- const startX = e.clientX;
- const startY = e.clientY;
- const onMove = (moveEvent) => {
- this.isMarqueeSelecting = true;
- if (!this.marqueeStart) {
- this.marqueeStart = { x: startX, y: startY };
- }
- this.ensureMarquee();
- if (this.isMarqueeSelecting) {
- this.updateMarquee(moveEvent.clientX, moveEvent.clientY);
- }
- };
- const onUp = (upEvent) => {
- window.removeEventListener("pointermove", onMove, true);
- window.removeEventListener("pointerup", onUp, true);
- if (this.isMarqueeSelecting) {
- this.applyMarqueeSelection();
- this.removeMarquee();
- this.isMarqueeSelecting = false;
- this.marqueeStart = null;
- this.marqueeWithCtrl = false;
- }
- this.updateSelectionVisuals();
- };
- window.addEventListener("pointermove", onMove, true);
- window.addEventListener("pointerup", onUp, true);
- }
- handleSelectionClick(e, key) {
- e.preventDefault();
- e.stopPropagation();
- this.toggleSelection(key);
- this.updateSelectionVisuals();
- }
- toggleSelection(key) {
- if (this.selectedKeys.has(key)) {
- this.selectedKeys.delete(key);
- } else {
- this.selectedKeys.add(key);
- }
- }
- updateSelectionVisuals() {
- this.getActiveContainerEl()?.querySelectorAll(".ms-host").forEach((el) => {
- const host = el;
- const key = host.dataset.msKey;
- if (key && this.selectedKeys.has(key)) {
- host.classList.add("--selected");
- } else {
- host.classList.remove("--selected");
- }
- });
- this.getActiveContainerEl()?.querySelectorAll(".ms-overlay").forEach((overlay) => {
- const handles = overlay.querySelectorAll(".ms-handle");
- const display = this.selectedKeys.size > 1 ? "none" : "";
- handles.forEach((h) => h.style.display = display);
- });
- }
- handleDeselectClick = (e) => {
- if (e.button !== 0) return;
- if (e.ctrlKey || e.metaKey) return;
- if (e.target.closest(".ms-host, .menu")) return;
- if (this.selectedKeys.size > 0) {
- this.selectedKeys.clear();
- this.updateSelectionVisuals();
- }
- };
- ensureMarquee() {
- let marquee = document.getElementById("ms-marquee");
- if (!marquee) {
- marquee = document.createElement("div");
- marquee.id = "ms-marquee";
- document.body.appendChild(marquee);
- }
- return marquee;
- }
- updateMarquee(endX, endY) {
- const marquee = this.ensureMarquee();
- if (!this.marqueeStart) return;
- const x = Math.min(this.marqueeStart.x, endX);
- const y = Math.min(this.marqueeStart.y, endY);
- const width = Math.abs(this.marqueeStart.x - endX);
- const height = Math.abs(this.marqueeStart.y - endY);
- Object.assign(marquee.style, {
- left: `${x}px`,
- top: `${y}px`,
- width: `${width}px`,
- height: `${height}px`,
- display: "block"
- });
- }
- applyMarqueeSelection() {
- const marquee = document.getElementById("ms-marquee");
- if (!marquee) return;
- const marqueeRect = marquee.getBoundingClientRect();
- if (!this.marqueeWithCtrl) {
- this.selectedKeys.clear();
- }
- const hosts = this.getActiveContainerEl()?.querySelectorAll(".ms-host");
- hosts?.forEach((host) => {
- const isClone = !!host.dataset.msParentKey;
- const isNote = host.classList.contains("ms-note-host");
- if (isClone || isNote) {
- const hostRect = host.getBoundingClientRect();
- if (hostRect.left < marqueeRect.right && hostRect.right > marqueeRect.left && hostRect.top < marqueeRect.bottom && hostRect.bottom > marqueeRect.top) {
- const key = host.dataset.msKey;
- if (key) {
- if (this.marqueeWithCtrl) {
- this.toggleSelection(key);
- } else {
- this.selectedKeys.add(key);
- }
- }
- }
- }
- });
- }
- async buildGroupContextMenu(menu, host, key, kind, notePath) {
- menu.addItem((item) => {
- item.setTitle(t(this.app, "deleteSelected", { n: this.selectedKeys.size })).setIcon("trash").onClick(async () => {
- const noteData = this.notes[notePath];
- if (!noteData) return;
- const keysToDelete = new Set(this.selectedKeys);
- for (const selectedKey of keysToDelete) {
- const el = this.findHostByKey(selectedKey);
- if (!el) continue;
- const isClone = !!el.dataset.msParentKey;
- const isNote = el.classList.contains("ms-note-host");
- el.remove();
- if (isClone) delete noteData.clones?.[selectedKey];
- if (isNote) delete noteData.originals?.[selectedKey];
- }
- this.selectedKeys.clear();
- this.updateSelectionVisuals();
- await this.persistAll();
- });
- });
- }
- removeMarquee() {
- document.getElementById("ms-marquee")?.remove();
- }
- mountControlsForHost(host, mediaEl) {
- const isClone = !!host.dataset.msParentKey;
- host.style.pointerEvents = "auto";
- if (isClone) {
- this.hydrateCloneMedia(host);
- host.querySelectorAll(".mejs__controls, .plyr__controls, .vjs-control-bar").forEach((el) => el.style.display = "none");
- this.mountCloneControls(host, mediaEl);
- this.installClickToggleDebug(host, mediaEl);
- } else {
- this.mountControlsProxy(host, mediaEl);
- }
- }
- hydrateCloneMedia(host) {
- const parentKey = host.dataset.msParentKey;
- if (!parentKey) return;
- const origHost = this.findHostByKey(parentKey);
- const cloned = host.querySelector("video, audio");
- const original = origHost?.querySelector("video, audio");
- if (!cloned || !original) return;
- const src = (original.currentSrc || original.src || "").trim();
- if (!src) return;
- if (cloned.src !== src) {
- cloned.src = src;
- try {
- cloned.load();
- } catch {
- }
- }
- }
- mountControlsProxy(host, media) {
- host.querySelectorAll(".ms-proxy-ctrl").forEach((n) => n.remove());
- const sels = [
- ".ob-video-controls",
- ".media-controls",
- ".mejs__controls",
- ".plyr__controls",
- ".vjs-control-bar",
- ".me-controls",
- "[class*='controls']"
- ];
- let panel = null;
- for (const s of sels) {
- panel = host.querySelector(s);
- if (panel) break;
- }
- if (!panel) return;
- const prev = this.controlsProxy.get(host);
- if (prev) {
- prev.ro.disconnect();
- prev.root.remove();
- this.controlsProxy.delete(host);
- }
- const wrap = document.createElement("div");
- wrap.className = "ms-proxy-ctrl";
- wrap.style.position = "absolute";
- wrap.style.inset = "0";
- wrap.style.pointerEvents = "none";
- wrap.style.zIndex = "2147483647";
- host.appendChild(wrap);
- const rebuild = () => {
- wrap.innerHTML = "";
- const hostR = host.getBoundingClientRect();
- const btns = panel.querySelectorAll(
- "button, [role='button'], .plyr__control, .vjs-control"
- );
- btns.forEach((btn) => {
- const rb = btn.getBoundingClientRect();
- const proxy = document.createElement("div");
- proxy.className = "ms-proxy-btn";
- proxy.style.position = "absolute";
- proxy.style.pointerEvents = "auto";
- proxy.style.left = `${rb.left - hostR.left}px`;
- proxy.style.top = `${rb.top - hostR.top}px`;
- proxy.style.width = `${rb.width}px`;
- proxy.style.height = `${rb.height}px`;
- const down = (ev) => {
- ev.stopPropagation();
- this.forwardFrom(ev, btn, "pointerdown");
- };
- const up = (ev) => {
- ev.stopPropagation();
- this.forwardFrom(ev, btn, "pointerup");
- };
- const isClone = !!host.dataset.msParentKey;
- const clk = (ev) => {
- ev.stopPropagation();
- ev.preventDefault();
- try {
- btn.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true, view: window }));
- btn.click?.();
- } catch {
- }
- if (isClone && media instanceof HTMLMediaElement) {
- const hint = `${btn.getAttribute("aria-label") || btn.getAttribute("title") || btn.className || ""}`.toLowerCase();
- if (/play|pause|toggle/.test(hint)) {
- media.paused ? media.play().catch(() => {
- }) : media.pause();
- media.focus?.();
- return;
- }
- if (/mute/.test(hint)) {
- media.muted = !media.muted;
- media.focus?.();
- return;
- }
- const tl = btn.closest(".plyr__progress, .vjs-progress-holder, .mejs__total-time, input[type=range]");
- if (tl && Number.isFinite(media.duration)) {
- const r = tl.getBoundingClientRect();
- const frac = r.width ? Math.min(Math.max(ev.clientX - r.left, 0), r.width) / r.width : 0;
- media.currentTime = media.duration * frac;
- return;
- }
- const vb = btn.closest('.plyr__volume, .vjs-volume-bar, .mejs__volume, input[type=range][aria-label*="volume" i]');
- if (vb) {
- const r = vb.getBoundingClientRect();
- const frac = r.width ? Math.min(Math.max(ev.clientX - r.left, 0), r.width) / r.width : 0;
- media.volume = Math.min(1, Math.max(0, frac));
- return;
- }
- }
- };
- proxy.addEventListener("pointerdown", down, { capture: true });
- proxy.addEventListener("pointerup", up, { capture: true });
- proxy.addEventListener("mousedown", down, { capture: true });
- proxy.addEventListener("mouseup", up, { capture: true });
- proxy.addEventListener("click", clk, { capture: true });
- wrap.appendChild(proxy);
- });
- };
- const ro = new ResizeObserver(() => rebuild());
- ro.observe(host);
- ro.observe(panel);
- this.controlsProxy.set(host, { root: wrap, ro });
- rebuild();
- }
- mountCloneControls(host, mediaEl) {
- if (!host.dataset.msParentKey) return;
- const media = mediaEl;
- if (!(media instanceof HTMLMediaElement)) return;
- if (host.querySelector(".ms-clone-controls")) return;
- const isVideo = media.tagName === "VIDEO";
- const root = document.createElement("div");
- root.className = "ms-clone-controls";
- Object.assign(root.style, {
- position: "absolute",
- left: "8px",
- right: "8px",
- bottom: "8px",
- display: "flex",
- flexDirection: "column",
- gap: "6px",
- pointerEvents: "none",
- background: "rgba(0,0,0,.35)",
- borderRadius: "8px",
- padding: "6px 8px",
- backdropFilter: "blur(2px)",
- zIndex: "2147483647",
- userSelect: "none"
- });
- const stop = (e) => e.stopPropagation();
- root.addEventListener("mousedown", stop, false);
- root.addEventListener("click", stop, false);
- const smartClickHandler = (e, action) => {
- const target = e.currentTarget;
- e.preventDefault();
- e.stopPropagation();
- const onUp2 = (upEvent) => {
- window.removeEventListener("mouseup", onUp2, true);
- if (upEvent.button === 2) {
- return;
- }
- const dx = upEvent.clientX - e.clientX;
- const dy = upEvent.clientY - e.clientY;
- if (Math.hypot(dx, dy) < 4) {
- action(e);
- }
- };
- window.addEventListener("mouseup", onUp2, true);
- };
- let fullscreenHideTimer = null;
- const onFsMouseMove = () => {
- if (document.fullscreenElement === host) {
- host.classList.remove("ms-fs-inactive");
- clearTimeout(fullscreenHideTimer);
- fullscreenHideTimer = setTimeout(() => {
- host.classList.add("ms-fs-inactive");
- }, 5e3);
- }
- };
- const onFsChange = () => {
- if (document.fullscreenElement === host) {
- window.addEventListener("mousemove", onFsMouseMove, true);
- onFsMouseMove();
- } else {
- window.removeEventListener("mousemove", onFsMouseMove, true);
- clearTimeout(fullscreenHideTimer);
- host.classList.remove("ms-fs-inactive");
- }
- };
- host.addEventListener("fullscreenchange", onFsChange);
- const ro = new ResizeObserver((entries) => {
- const { width } = entries[0].contentRect;
- root.classList.toggle("ms-cc-compact", width < 240);
- });
- ro.observe(host);
- const timeline = document.createElement("div");
- timeline.className = "ms-cc-timeline";
- Object.assign(timeline.style, {
- position: "relative",
- width: "100%",
- height: "6px",
- borderRadius: "3px",
- background: "rgba(255,255,255,.25)",
- cursor: "pointer",
- pointerEvents: "auto"
- });
- const progress = document.createElement("div");
- progress.className = "ms-cc-progress";
- Object.assign(progress.style, {
- position: "absolute",
- left: "0",
- top: "0",
- bottom: "0",
- width: "0%",
- background: "rgba(255,255,255,.9)",
- borderRadius: "3px",
- pointerEvents: "none"
- });
- timeline.appendChild(progress);
- const thumb = document.createElement("div");
- thumb.className = "ms-cc-thumb";
- Object.assign(thumb.style, {
- position: "absolute",
- top: "50%",
- width: "12px",
- height: "12px",
- background: "#fff",
- borderRadius: "50%",
- transform: "translate(-50%, -50%)",
- pointerEvents: "none",
- opacity: "0",
- transition: "opacity 0.1s ease-out"
- });
- timeline.appendChild(thumb);
- const btnBar = document.createElement("div");
- btnBar.className = "ms-cc-btn-bar";
- Object.assign(btnBar.style, { display: "flex", alignItems: "center", gap: "8px", width: "100%" });
- const makeBtn = (svgIcon, title) => {
- const b = document.createElement("div");
- b.className = "ms-cc-btn";
- Object.assign(b.style, {
- width: "26px",
- height: "26px",
- borderRadius: "6px",
- display: "inline-flex",
- alignItems: "center",
- justifyContent: "center",
- pointerEvents: "auto",
- cursor: "pointer",
- color: "#fff",
- background: "rgba(255,255,255,.15)"
- });
- b.onmouseenter = () => b.style.background = "rgba(255,255,255,.25)";
- b.onmouseleave = () => b.style.background = "rgba(255,255,255,.15)";
- b.innerHTML = svgIcon;
- b.title = title;
- return b;
- };
- const ICONS = {
- play: ``,
- pause: ``,
- mute: ``,
- volumeMute: ``,
- volumeLow: ``,
- volumeHigh: ``,
- volumeOff: ``,
- rewind: `
`,
- forward: `
`,
- fullscreen: ``,
- edit: ``,
- more: ``
- };
- const leftGroup = document.createElement("div");
- leftGroup.style.display = "flex";
- leftGroup.style.alignItems = "center";
- if (!isVideo) {
- const btnRewind = makeBtn(ICONS.rewind, "Rewind 30s");
- btnRewind.addEventListener("mousedown", (e) => smartClickHandler(e, () => media.currentTime = Math.max(0, media.currentTime - 10)));
- btnRewind.style.marginRight = "15px";
- leftGroup.appendChild(btnRewind);
- }
- const btnPlay = makeBtn(ICONS.play, "Play/Pause");
- if (!isVideo) {
- const btnForward = makeBtn(ICONS.forward, "Forward 30s");
- btnForward.classList.add("ms-cc-hide-compact");
- btnForward.addEventListener("mousedown", (e) => smartClickHandler(e, () => media.currentTime = Math.min(media.duration, media.currentTime + 30)));
- btnPlay.style.marginRight = "15px";
- leftGroup.appendChild(btnPlay);
- btnForward.style.marginRight = "15px";
- leftGroup.appendChild(btnForward);
- } else {
- leftGroup.appendChild(btnPlay);
- btnPlay.style.marginRight = "8px";
- }
- const btnMute = makeBtn(ICONS.mute, "Mute/Unmute");
- const volumeSlider = document.createElement("input");
- volumeSlider.type = "range";
- volumeSlider.min = "0";
- volumeSlider.max = "1";
- volumeSlider.step = "0.05";
- volumeSlider.classList.add("ms-cc-hide-compact");
- Object.assign(volumeSlider.style, { width: "70px", cursor: "pointer", accentColor: "#fff", pointerEvents: "auto" });
- if (!isVideo) {
- volumeSlider.style.marginLeft = "0px";
- }
- leftGroup.append(btnMute, volumeSlider);
- const nameLabel = document.createElement("div");
- nameLabel.className = "ms-cc-name-label";
- Object.assign(nameLabel.style, {
- flex: "1 1 auto",
- minWidth: "0",
- textAlign: "center",
- color: "rgba(255,255,255,.7)",
- fontSize: "12px",
- whiteSpace: "nowrap",
- overflow: "hidden",
- textOverflow: "ellipsis",
- pointerEvents: "none",
- padding: "0 8px"
- });
- const rightGroup = document.createElement("div");
- rightGroup.style.display = "flex";
- rightGroup.style.alignItems = "center";
- rightGroup.style.marginLeft = "auto";
- if (isVideo) {
- let wasLockedBeforeFs = false;
- let scrollRatioX = 0, scrollRatioY = 0, hScrollEl = null, vScrollEl = null;
- const btnFs = makeBtn(ICONS.fullscreen, "Fullscreen");
- btnFs.addEventListener("mousedown", (e) => smartClickHandler(e, () => {
- if (!document.fullscreenElement) {
- const { hScroll, vScroll } = this.findScrollableParents(host.parentElement);
- this.hScrollEl = hScroll;
- this.vScrollEl = vScroll;
- if (this.hScrollEl && this.hScrollEl.scrollWidth > this.hScrollEl.clientWidth) {
- this.scrollRatioX = this.hScrollEl.scrollLeft / (this.hScrollEl.scrollWidth - this.hScrollEl.clientWidth);
- }
- if (this.vScrollEl && this.vScrollEl.scrollHeight > this.vScrollEl.clientHeight) {
- this.scrollRatioY = this.vScrollEl.scrollTop / (this.vScrollEl.scrollHeight - this.vScrollEl.clientHeight);
- }
- }
- if (!document.fullscreenElement) host.requestFullscreen().catch(() => {
- });
- else document.exitFullscreen().catch(() => {
- });
- }));
- btnFs.style.marginRight = "10px";
- rightGroup.appendChild(btnFs);
- }
- const btnEdit = makeBtn(ICONS.edit, t(this.app, "edit"));
- const btnMore = makeBtn(ICONS.more, "More options");
- btnEdit.classList.add("ms-cc-hide-compact");
- btnMore.classList.add("ms-cc-hide-compact");
- btnEdit.style.marginRight = "10px";
- btnEdit.addEventListener("mousedown", (e) => smartClickHandler(e, (evt) => {
- new CloneSettingsModal(this.app, {
- animation: this.cloneAnimation,
- bgOpacity: this.cloneBgOpacity,
- controlsOpacity: this.cloneControlsOpacity,
- hideDelay: this.cloneHideDelay,
- showAudioHandles: this.cloneShowAudioHandles
- }, async (settings) => {
- if (settings) {
- this.cloneAnimation = settings.animation;
- this.cloneBgOpacity = settings.bgOpacity;
- this.cloneControlsOpacity = settings.controlsOpacity;
- this.cloneHideDelay = settings.hideDelay;
- this.cloneShowAudioHandles = settings.showAudioHandles;
- await this.persistAll();
- this.applyCloneSettingsToAll();
- }
- }).open();
- }));
- btnMore.addEventListener("mousedown", (e) => smartClickHandler(e, (evt) => {
- const origHost = this.findHostByKey(host.dataset.msParentKey);
- const meEmbed = origHost?.closest(".mx-media-embed");
- const target = meEmbed || origHost;
- if (target) {
- const rect = btnMore.getBoundingClientRect();
- target.dispatchEvent(new MouseEvent("contextmenu", { bubbles: true, cancelable: true, clientX: rect.left, clientY: rect.bottom }));
- }
- }));
- rightGroup.append(btnEdit, btnMore);
- const centerGroup = document.createElement("div");
- Object.assign(centerGroup.style, {
- flex: "1 1 auto",
- minWidth: "0",
- display: "flex",
- alignItems: "center",
- justifyContent: "center",
- pointerEvents: "none"
- });
- const timeLabel = document.createElement("div");
- timeLabel.className = "ms-cc-time-label";
- Object.assign(timeLabel.style, {
- color: "rgba(255,255,255,.7)",
- fontSize: "12px",
- whiteSpace: "nowrap",
- paddingRight: "8px"
- });
- timeLabel.textContent = "0:00 / 0:00 |";
- centerGroup.append(timeLabel, nameLabel);
- btnBar.append(leftGroup, centerGroup, rightGroup);
- root.append(timeline, btnBar);
- host.appendChild(root);
- const parentKey = host.dataset.msParentKey;
- if (parentKey) {
- const notePath = this.getActiveNote()?.path ?? "";
- const savedState = this.notes[notePath]?.clones?.[host.dataset.msKey];
- const savedFileName = savedState?.fileName;
- const formatTime2 = (seconds) => {
- if (!isFinite(seconds)) return "0:00";
- const m = Math.floor(seconds / 60);
- const s = Math.floor(seconds % 60).toString().padStart(2, "0");
- return `${m}:${s}`;
- };
- if (savedFileName) {
- nameLabel.textContent = savedFileName;
- nameLabel.title = savedFileName;
- host.dataset.msFileName = savedFileName;
- } else {
- const originalHost = this.findHostByKey(parentKey);
- if (originalHost) {
- const mediaEl2 = originalHost.querySelector("video, audio, img");
- if (mediaEl2) {
- const { base } = this.getSrcAndBase(originalHost, mediaEl2);
- nameLabel.textContent = base || "Untitled";
- nameLabel.title = base || "Untitled";
- host.dataset.msFileName = base || "Untitled";
- }
- }
- }
- }
- const refresh = (isSeeking = false, formatTime2) => {
- btnPlay.innerHTML = media.paused ? ICONS.play : ICONS.pause;
- if (media.muted || media.volume === 0) {
- btnMute.innerHTML = ICONS.volumeOff;
- } else if (media.volume <= 0.5) {
- btnMute.innerHTML = ICONS.volumeLow;
- } else {
- btnMute.innerHTML = ICONS.volumeHigh;
- }
- volumeSlider.value = String(media.muted ? 0 : media.volume);
- const dur = Number.isFinite(media.duration) ? media.duration : 0;
- const frac = dur ? media.currentTime / dur : 0;
- progress.style.width = Math.round(Math.max(0, Math.min(1, frac)) * 100) + "%";
- thumb.style.left = progress.style.width;
- if (!isSeeking) {
- timeLabel.textContent = `${formatTime2(media.currentTime)} / ${formatTime2(dur)} | `;
- }
- };
- const formatTime = (seconds) => {
- if (!isFinite(seconds)) return "0:00";
- const m = Math.floor(seconds / 60);
- const s = Math.floor(seconds % 60).toString().padStart(2, "0");
- return `${m}:${s}`;
- };
- refresh(false, formatTime);
- btnPlay.addEventListener("mousedown", (e) => smartClickHandler(e, (evt) => {
- if (media.paused) media.play().catch(() => void 0);
- else media.pause();
- }));
- btnMute.addEventListener("mousedown", (e) => smartClickHandler(e, (evt) => {
- media.muted = !media.muted;
- }));
- volumeSlider.addEventListener("input", () => {
- media.volume = Number(volumeSlider.value);
- if (media.volume > 0) media.muted = false;
- const notePath = this.getActiveNote()?.path ?? "";
- const key = host.dataset.msKey;
- if (notePath && key) {
- this.saveState(notePath, key, { volume: media.volume });
- }
- });
- let seeking = false;
- const seekTo = (x) => {
- const r = timeline.getBoundingClientRect();
- const frac = r.width ? Math.min(1, Math.max(0, (x - r.left) / r.width)) : 0;
- if (isFinite(media.duration) && media.duration > 0) {
- media.currentTime = media.duration * frac;
- refresh(true, formatTime);
- }
- };
- timeline.addEventListener("pointerdown", (e) => {
- e.stopPropagation();
- seeking = true;
- e.currentTarget.setPointerCapture?.(e.pointerId);
- seekTo(e.clientX);
- }, { capture: true });
- timeline.addEventListener("pointerenter", () => {
- thumb.style.opacity = "1";
- });
- timeline.addEventListener("pointerleave", () => {
- if (!seeking) thumb.style.opacity = "0";
- });
- const onMove = (e) => {
- if (seeking) seekTo(e.clientX);
- };
- const onUp = (e) => {
- if (seeking) {
- seeking = false;
- e.currentTarget.releasePointerCapture?.(e.pointerId);
- }
- if (!timeline.matches(":hover")) thumb.style.opacity = "0";
- };
- window.addEventListener("pointermove", onMove, true);
- window.addEventListener("pointerup", onUp, true);
- media.addEventListener("timeupdate", () => refresh(false, formatTime));
- media.addEventListener("play", () => refresh(false, formatTime));
- media.addEventListener("pause", () => refresh(false, formatTime));
- media.addEventListener("volumechange", () => refresh(false, formatTime));
- const mo = new MutationObserver(() => {
- if (!document.contains(host)) {
- window.removeEventListener("pointermove", onMove, true);
- window.removeEventListener("mousemove", onFsMouseMove, true);
- window.removeEventListener("pointerup", onUp, true);
- ro.disconnect();
- mo.disconnect();
- }
- });
- mo.observe(host.parentElement, { childList: true });
- }
- installClickToggleDebug(host, mediaEl) {
- const media = mediaEl;
- if (!(media instanceof HTMLMediaElement)) return;
- const onMouseDown = (e) => {
- if (e.target.closest(".ms-clone-controls")) return;
- if (e.button === 2) {
- return;
- }
- const onMouseUp = (upEvent) => {
- window.removeEventListener("mouseup", onMouseUp, true);
- const dx = upEvent.clientX - e.clientX;
- const dy = upEvent.clientY - e.clientY;
- if (Math.hypot(dx, dy) < 4) {
- if (media.paused) {
- media.play().catch((err) => console.warn("[MS] clone play() rejected:", err));
- } else {
- media.pause();
- }
- }
- };
- window.addEventListener("mouseup", onMouseUp, true);
- };
- host.addEventListener("mousedown", onMouseDown, false);
- }
- applyLinkOverlay(host, link) {
- let overlay = host.querySelector(".ms-link-overlay");
- if (!link) {
- overlay?.remove();
- return;
- }
- if (!overlay) {
- overlay = document.createElement("div");
- overlay.className = "ms-link-overlay";
- Object.assign(overlay.style, {
- position: "absolute",
- inset: "3px",
- zIndex: "2500",
- cursor: "pointer",
- pointerEvents: "auto"
- });
- host.appendChild(overlay);
- }
- overlay.__ms_clickHandler?.();
- const onMouseDown = (e) => {
- if (e.button !== 0) return;
- const onMouseUp = (upEvent) => {
- window.removeEventListener("mouseup", onMouseUp, true);
- const dx = upEvent.clientX - e.clientX;
- const dy = upEvent.clientY - e.clientY;
- if (Math.hypot(dx, dy) < 4) {
- window.open(link, "_blank");
- }
- };
- window.addEventListener("mouseup", onMouseUp, true);
- };
- overlay.addEventListener("mousedown", onMouseDown);
- overlay.__ms_clickHandler = () => overlay.removeEventListener("mousedown", onMouseDown);
- }
- applyCloneSettingsToAll() {
- const root = this.getActiveContainerEl();
- if (!root) return;
- root.querySelectorAll(".ms-host[data-ms-parent-key]").forEach((cloneHost) => {
- cloneHost.dataset.msAnimation = this.cloneAnimation;
- const bgs = cloneHost.querySelectorAll(".ms-media-bg, .ms-local-bg");
- if (bgs.length > 0) {
- bgs.forEach((bg) => {
- bg.style.opacity = String(this.cloneBgOpacity);
- });
- } else if (cloneHost.dataset.msKind === "video" || cloneHost.dataset.msKind === "audio") {
- }
- const controls = cloneHost.querySelector(".ms-clone-controls");
- if (controls) {
- controls.style.backgroundColor = `rgba(0,0,0,${this.cloneControlsOpacity})`;
- }
- if (cloneHost.dataset.msKind === "audio") {
- cloneHost.dataset.msShowHandles = this.cloneShowAudioHandles ? "true" : "false";
- }
- });
- }
-};