mirror of
https://github.com/hanblade67-cloud/media-layout.git
synced 2026-07-22 06:41:50 +00:00
6406 lines
286 KiB
JavaScript
6406 lines
286 KiB
JavaScript
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 = `<svg xmlns='http://www.w3.org/2000/svg' width='72' height='72' viewBox='0 0 72 72'><g transform='translate(36,36) rotate(${svgRotation}) translate(-9,-9)'><image href='data:image/png;base64,${baseCursor}' x='0' y='0' width='18' height='18'/></g></svg>`;
|
||
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: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>`,
|
||
pause: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/></svg>`,
|
||
mute: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M16.5 12A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>`,
|
||
volumeMute: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M16.5 12A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM5 9v6h4l5 5V4L9 9H5z"/></svg>`,
|
||
volumeLow: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M7 9v6h4l5 5V4l-5 5H7zm7 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/></svg>`,
|
||
volumeHigh: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3A4.5 4.5 0 0014 7.97v8.05c1.48-.73 2.5-2.25 2.5-4.02zM14 3.23v2.06c2.89.86 5 3.54 5 6.71s-2.11 5.85-5 6.71v2.06c4.01-.91 7-4.49 7-8.77s-2.99-7.86-7-8.77z"/></svg>`,
|
||
volumeOff: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M3.63 3.63c-.39.39-.39 1.02 0 1.41L7.29 8.7 7 9H3v6h4l5 5v-6.59l4.18 4.18c-.65.49-1.38.88-2.18 1.11v2.06c1.34-.3 2.57-.92 3.61-1.75l2.05 2.05c.39.39 1.02.39 1.41 0 .39-.39.39-1.02 0-1.41L5.05 3.63c-.39-.39-1.02-.39-1.42 0zM19 12c0 .82-.15 1.61-.41 2.34l1.53 1.53c.56-1.17.88-2.48.88-3.87 0-4.28-2.99-7.86-7-8.77v2.06c2.89.86 5 3.54 5 6.71zM14 8.97V7.97c0-1.77-1.02-3.29-2.5-4.03v1.79l2.5 2.5zM9.29 9l-2-2H9v2.18L7.82 10H9v2h.29l-2-2z"/></svg>`,
|
||
rewind: `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAtxJREFUWIXtlk2IHFUUhe99r6rxB0UXohiMWbgQEVdGJv5EjEZEwaUBYxZugpEsBZe6c5aJGCERQRCRKPiLoigiA0rQheLSf4kaQYIgkmTqfbeOi1RLd2e6u3oguJmzKqreeee8qnNvXbMNbOB/hi9KkHQVsN/MVFXV8+5+oidvM7AvpXQmpfScu59c2G0pZRvwB7AKNMAPki7owbsbOAmcAUpEfCMpLyQOPAKcjoivJW0BHgJUStk+ixcRjwMF+ELSJmA3oNXV1Zt6CUvyUsrTQAu8L+nS7lR3dAbum8LLpZRlQMBrki7qeHcBappmqY/4xcAbndBBSWn4bJYBSZcDHwFtKWV5gjdmoJohvqlt27fN7EYze7Su65fmOj7Luw541923mNmeuq5fmbV+TQNN0yxFxJtmVkm6t67rlT7ipZSdEXHU3U+3bbt9MBh8OY+TJm8Au1JKn7j7nznnrX3FI2Kvu7/n7r/knJf6iI8ZGIbNzF41s09TSre7+8/zNnD3DDwr6bCZvZVSus3dj/cRHwNwqAvVWGimYRhC4DjQRsRTkuY2tqlVACginuhreMSAImLfArwxA5MnnVoV5wujBo5IeqaUcmDBNvmrpEN9P8FUTHS8DyVdNmv9yCd4sJRysLt+fdjxZvBmd0JgF3CqlPKtpOt7GHjA7GwZAk1EfCXpmr4Gzkl7VVVH27bd4e6XRMTnpZR7Zp1oiJzzEUn3S7o2Io41TbO1D2/NchsMBsdyzje7+4/u/kFEPNlns7quP8453yLp75TSCrB7XQbMzNz9t5TSnWb2jqRl4LCket6G7v59VVW3mtlnZvZy374yFRPhXJF0hdm5GViDV42Ec32/41EMBxLgO0k3AHu6zrljFi8i9o8MJFcDDy80kIyiG8lOdG9DwE+SLuzB2wn8BZzqKuW/kWw9Q+mVbds+ZmZtSulFd/+9J28zsDel9E9K6YV1DaUb2MD5wL8gzcwDK9O2AQAAAABJRU5ErkJggg==" style="width:20px; height:20px;">`,
|
||
forward: `<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7DAAAOwwHHb6hkAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAt1JREFUWIXtljGIXFUUhv/z7n2juEFQEEKMimBhp4jEkIUVo4JgIYgBg1iIoBCJbVoRC7GLomHtBCUGLZagEIQ0QlSCRRAUQQvZxIgiFoLszjv/ub+Fd2QcZnbeDEKa/Zt58Oa757/v/O+dC+xqV9dYNrqQdAvJlwAMcs6nzOxynwUk7SN5DEDJOb9tZr8u7EJS6+7fkexIbpP83d0f6sHdQPInksPKXu267sDCBrquu4+kSD4t6baI+JpkFxEv7sS5++HKPSnpzoj4huQWyaMLGXD3VZJy97W6s+tJflAXX5fUTuNIPl65Q5XbQ3KDZHH31yU1Sxmoi1lEnCBZSH4m6aZ5Bkacu79SzX8saWUpA2NFjpD8i+QPku6eZ2Ds3lGSWxFxSdLts2rPfUQ5549KKasArouIL9z9kbk7+oc7LemwpL0R8dWscPbq0WAwuJRSOgjgRzM7FxEv9+Hatv0ypXS/mf3SNM3nJJ9dygAAmNnVlNIagDOSTpJcBzA1nBPclaZpHgRwDsB7U8O5UwYmVUP2ag3n5VkZmMI17v5GDeebSxsYKSKO18V6GRjjTpCUpH3AAi34H5Xrry1lYNQCSScBXFmAyyTfkvQagHfM7Od/b/ZtweQXkuRTfVog6UaSn8z8QvYxUKfeRZIeEccBgOQT8wxIuqsOuu1pr2GeBk2q67p7I+IsgD2SHss5n+/DufujEXHGzLZLKWuDweDi5H/mZoDkkaZpLgAYppQOtW3bq3hEvGBmn5rZZkrp4LTiOxoYDSMAHwK4kFI6YGbfzys8FrZ1AGebplk1s80doRnj+P0e4/g/GZB0M8nzi4zjXMFtM4OZ7ZW0v5SyAeAeMzuWUjq1w263KnfHcDj8MyI2ANwK4Jm2bU/PKz6+UHb3b+uxaovkH+7+cA9uheRmfVKlHske6F0YE4fSUsrzpZSVnPO7CxxK95dSngOApmnWzey3RQzsalfXXH8DZoHVpJT60DoAAAAASUVORK5CYII=" style="width:20px; height:20px;">`,
|
||
fullscreen: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/></svg>`,
|
||
edit: `<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor"><path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/></svg>`,
|
||
more: `<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor"><path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/></svg>`
|
||
};
|
||
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";
|
||
}
|
||
});
|
||
}
|
||
};
|