---
name: 使用默认应用打开附件
description: 提取选中元素中的附件文件,并使用系统默认应用程序独立打开,配备高颜值 HTML UI。
author: ymjr
version: 1.0.0
license: MIT
usage: 选中画板中带有附件的图形(如图片、嵌入的MD等),运行此脚本即可在外部默认应用中打开。
features:
- 采用 Obsidian 原生 openWithDefaultApp 接口,完美兼容 Win/Mac/Linux
- 自动过滤无效元素,支持批量打开,提供友好的异常捕获
- 采用独立无污染的 HTML 悬浮窗(Toast)进行美观的 UI 交互
dependencies:
- 无
autorun: false
---
/*
```javascript
*/
const locales = {
zh: {
warn_no_selection: "未选中任何有效附件",
warn_no_selection_desc: "请先选中画板中的图片或嵌入文件节点。",
warn_file_not_found: "找不到源文件",
success_open: "已在外部打开",
success_open_desc: "正在唤起系统默认应用打开: ",
err_open: "打开失败",
},
en: {
warn_no_selection: "No valid attachment selected",
warn_no_selection_desc: "Please select images or embedded files first.",
warn_file_not_found: "Source file not found",
success_open: "Opened externally",
success_open_desc: "Launching system default app for: ",
err_open: "Failed to open",
}
};
/**
* ==========================================
* [UI 模块] 悬浮通知管理器 (不污染 window)
* ==========================================
*/
class YMJR_UIManager {
constructor(plugin) {
this.plugin = plugin;
this.containerId = "ymjr-toast-container";
this.initContainer();
}
initContainer() {
if (!document.getElementById(this.containerId)) {
const container = document.createElement("div");
container.id = this.containerId;
// 使用 Obsidian 的 CSS 变量以保证和主题完美融合
Object.assign(container.style, {
position: "fixed",
top: "30px",
right: "30px",
zIndex: "99999",
display: "flex",
flexDirection: "column",
gap: "10px",
pointerEvents: "none" // 不阻挡鼠标事件
});
document.body.appendChild(container);
}
this.container = document.getElementById(this.containerId);
}
/**
* @param {string} title 标题
* @param {string} message 描述信息
* @param {"success"|"warning"|"error"} type 状态类型
*/
showToast(title, message, type = "success") {
const toast = document.createElement("div");
// 状态颜色配置
const colors = {
success: "var(--interactive-success)",
warning: "var(--color-orange)",
error: "var(--color-red)"
};
const iconColor = colors[type] || colors.success;
// 构建高颜值 HTML UI (毛玻璃效果、圆角、阴影)
toast.innerHTML = `
`;
Object.assign(toast.style, {
position: "relative",
minWidth: "280px",
maxWidth: "350px",
padding: "12px 16px 12px 24px",
backgroundColor: "var(--background-secondary-alt)",
border: "1px solid var(--background-modifier-border)",
borderRadius: "8px",
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.2)",
backdropFilter: "blur(8px)",
WebkitBackdropFilter: "blur(8px)",
transform: "translateX(120%)",
opacity: "0",
transition: "all 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275)",
overflow: "hidden"
});
this.container.appendChild(toast);
// 触发进入动画
requestAnimationFrame(() => {
toast.style.transform = "translateX(0)";
toast.style.opacity = "1";
});
// 定时移除动画
setTimeout(() => {
toast.style.transform = "translateX(120%)";
toast.style.opacity = "0";
setTimeout(() => {
if (toast.parentNode) {
toast.parentNode.removeChild(toast);
}
}, 400); // 等待动画结束
}, 3500);
}
}
// 单例模式挂载到 ExcalidrawAutomate.plugin,避免全局污染和重复创建
if (!ExcalidrawAutomate.plugin._ymjr_ui_manager) {
ExcalidrawAutomate.plugin._ymjr_ui_manager = new YMJR_UIManager(ExcalidrawAutomate.plugin);
}
const ui = ExcalidrawAutomate.plugin._ymjr_ui_manager;
/**
* ==========================================
* [核心逻辑] 处理附件打开
* ==========================================
*/
async function handleOpenAttachments() {
// 确保画布元素已同步
await ea.addElementsToView();
// 获取当前选中的所有元素
const selectedEls = ea.getViewSelectedElements();
// 过滤出真正含有 fileId 的附件/图片元素
const attachmentEls = selectedEls.filter(el => el.fileId);
if (attachmentEls.length === 0) {
ui.showToast(t("warn_no_selection"), t("warn_no_selection_desc"), "warning");
return;
}
// 遍历打开
for (const el of attachmentEls) {
try {
const embeddedFile = ea.targetView.excalidrawData.getFile(el.fileId);
if (!embeddedFile || !embeddedFile.file) {
ui.showToast(t("warn_file_not_found"), `FileID: ${el.fileId}`, "warning");
continue;
}
const tfile = embeddedFile.file;
// [优化] 使用 Obsidian 原生的 openWithDefaultApp 代替 child_process
// 这种方式天然支持跨平台 (Win/Mac/Linux),容错率极高
app.openWithDefaultApp(tfile.path);
ui.showToast(t("success_open"), `${t("success_open_desc")} ${tfile.name}`, "success");
} catch (error) {
console.error("[YMJR Open Attachment Error]: ", error);
ui.showToast(t("err_open"), error.message || String(error), "error");
}
}
}
// 执行核心功能
handleOpenAttachments();