有一个使用悬浮窗进行命令控制的想法,所以让gpt写了一个简单的quickaddjs 脚本,目前来说扩展性还可以(修改原始文本就行)。
关于执行命令的部分,可以参考 【技巧】超越快捷键!实现 Obsidian 内的「文本指令」 - 经验分享 - Obsidian 中文论坛 这篇如何通过 Cmd+Shift+i
得到指令id的,此处的代码只是一个可以拖动的窗口。仍然通过js 以macro
的方式被引入。
module.exports = async function (params) {
const { app, quickAddApi } = params;
// 添加在脚本开始处
const existingToolbar = document.querySelector('.float-toolbar');
if (existingToolbar) {
document.body.removeChild(existingToolbar);
return;
}
// 读取命令配置
let { commands, windowConfig = {} } = await loadCommands();
const useLastPosition = windowConfig.useLastPosition || false;
// 创建弹窗容器
const popupContainer = document.createElement('div');
popupContainer.classList.add('float-toolbar');
Object.assign(popupContainer.style, {
position: 'fixed',
width: windowConfig.width || '300px',
height: windowConfig.height || 'auto',
left: useLastPosition && windowConfig.left ? windowConfig.left : '50%',
top: useLastPosition && windowConfig.top ? windowConfig.top : '50%',
transform: useLastPosition ? 'none' : 'translate(-50%, -50%)',
backgroundColor: 'var(--background-primary)',
padding: '20px',
border: '1px solid var(--background-modifier-border)',
borderRadius: '10px',
boxShadow: '0 4px 6px rgba(0, 0, 0, 0.3)',
zIndex: '9999',
cursor: 'move',
userSelect: 'none',
resize: 'none',
overflow: 'hidden',
});
// 创建命令按钮容器
const commandButtonsContainer = document.createElement('div');
commandButtonsContainer.classList.add('command-buttons-container');
// 创建命令按钮
function createCommandButtons() {
commandButtonsContainer.innerHTML = ''; // 清空现有按钮
commands.forEach((cmd) => {
const button = document.createElement('button');
button.textContent = cmd.text;
button.addEventListener('click', async (e) => {
if (e.button === 0) {
// 左键点击
try {
await app.commands.executeCommandById(cmd.commandId);
} catch (error) {
console.error(`执行命令 ${cmd.text} 失败:`, error);
new Notice(`执行命令 ${cmd.text} 失败,打开控制台查看详情`, 1000);
}
}
});
// 右键点击删除按钮
button.addEventListener('contextmenu', (e) => {
e.preventDefault();
// 更新命令列表
commands = commands.filter((c) => c.text !== cmd.text); // 🚩 现在可以重新赋值
saveCommands(commands);
createCommandButtons();
});
commandButtonsContainer.appendChild(button);
});
}
createCommandButtons();
// 创建关闭按钮
const closeButton = document.createElement('button');
closeButton.textContent = '×';
Object.assign(closeButton.style, {
position: 'absolute',
top: '5px',
right: '5px',
width: '20px',
height: '20px',
padding: '0',
border: 'none',
background: 'transparent',
color: 'var(--text-muted)',
fontSize: '20px',
lineHeight: '20px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: '50%',
transition: 'background-color 0.2s, color 0.2s',
});
// 添加悬停效果
closeButton.addEventListener('mouseover', () => {
closeButton.style.backgroundColor = 'var(--background-modifier-hover)';
closeButton.style.color = 'var(--text-normal)';
});
closeButton.addEventListener('mouseout', () => {
closeButton.style.backgroundColor = 'transparent';
closeButton.style.color = 'var(--text-muted)';
});
// 定义事件处理函数
let isDragging = false;
let offsetX, offsetY;
const handleMouseMove = (e) => {
if (isDragging) {
const x = e.clientX - offsetX;
const y = e.clientY - offsetY;
const maxX = window.innerWidth - popupContainer.offsetWidth / 3;
const maxY = window.innerHeight - popupContainer.offsetHeight / 3;
popupContainer.style.left = `${Math.max(0, Math.min(x, maxX))}px`;
popupContainer.style.top = `${Math.max(0, Math.min(y, maxY))}px`;
popupContainer.style.transform = 'none';
}
};
const handleMouseUp = () => {
if (isDragging) {
isDragging = false;
// 保存新位置
saveWindowConfig({
left: popupContainer.style.left,
top: popupContainer.style.top,
width: popupContainer.style.width,
height: popupContainer.style.height,
useLastPosition: true, // 自动启用记忆位置
});
}
};
// 修改关闭按钮的点击处理程序
closeButton.addEventListener('click', () => {
closeToolbar(handleMouseMove, handleMouseUp, popupContainer);
});
// 创建新增命令按钮
const addCommandButton = document.createElement('button');
addCommandButton.textContent = '+ 新增命令';
addCommandButton.style.marginTop = '10px';
addCommandButton.addEventListener('click', async () => {
const allCommands = app.commands.listCommands();
const selectedCommand = await quickAddApi.suggester(
allCommands.map((cmd) => cmd.name),
allCommands
);
if (!selectedCommand) {
new Notice('未选择命令');
return;
}
// 检查中文字符数量
let displayText = selectedCommand.name;
const chineseCharCount = displayText.length;
if (chineseCharCount > 6) {
// 弹出输入框获取替代文本
const userInput = await quickAddApi.inputPrompt(
'输入替代文本',
'原命令名过长(超过6个汉字),请输入替代文本:',
displayText
);
if (userInput === null) {
new Notice('已取消添加命令');
return;
}
if (userInput.trim() === '') {
new Notice('替代文本不能为空');
return;
}
displayText = userInput;
}
const newCommand = {
text: displayText,
commandId: selectedCommand.id,
};
commands.push(newCommand);
saveCommands(commands);
createCommandButtons(); // 刷新按钮
});
// 创建调整大小的手柄
const resizeHandle = document.createElement('div');
Object.assign(resizeHandle.style, {
position: 'absolute',
bottom: '0',
right: '0',
width: '10px',
height: '10px',
backgroundColor: 'var(--background-modifier-border)',
cursor: 'se-resize',
});
// 拖拽调整大小的逻辑
let isResizing = false;
let startX, startY, startWidth, startHeight;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startY = e.clientY;
startWidth = parseInt(document.defaultView.getComputedStyle(popupContainer).width, 10);
startHeight = parseInt(document.defaultView.getComputedStyle(popupContainer).height, 10);
e.preventDefault(); // 防止文本选中
});
document.addEventListener('mousemove', (e) => {
if (isResizing) {
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
popupContainer.style.width = `${newWidth}px`;
popupContainer.style.height = `${newHeight}px`;
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
// 保存调整后的尺寸
saveWindowConfig({
width: popupContainer.style.width,
height: popupContainer.style.height,
left: popupContainer.style.left,
top: popupContainer.style.top,
});
}
});
// 将命令按钮、新增按钮、关闭按钮和调整手柄添加到弹窗容器中
popupContainer.appendChild(commandButtonsContainer);
popupContainer.appendChild(addCommandButton);
popupContainer.appendChild(closeButton);
popupContainer.appendChild(resizeHandle);
// 将弹窗添加到页面中
document.body.appendChild(popupContainer);
// 拖拽功能实现
popupContainer.addEventListener('mousedown', (e) => {
if (e.target === resizeHandle) return; // 如果点击的是调整手柄,则不触发拖拽
isDragging = true;
offsetX = e.clientX - popupContainer.getBoundingClientRect().left;
offsetY = e.clientY - popupContainer.getBoundingClientRect().top;
});
// 拖拽过程的具体处理
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
function closeToolbar(handleMouseMove, handleMouseUp, popupContainer) {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
// 移除弹窗
document.body.removeChild(popupContainer);
}
async function loadCommands() {
const path = '05-木板/JSetc脚本/commands.json';
try {
const content = await app.vault.adapter.read(path);
// 兼容旧格式(纯数组)和新格式(带配置的对象)
const result = JSON.parse(content);
return Array.isArray(result)
? { commands: result }
: {
commands: result.commands || [],
windowConfig: result.windowConfig || {},
};
} catch (error) {
console.error('读取命令文件失败:', error);
return { commands: [] };
}
}
async function saveWindowConfig(config) {
const path = '05-木板/JSetc脚本/commands.json';
try {
const existing = await loadCommands();
const newData = {
commands: existing.commands,
windowConfig: {
...existing.windowConfig,
...config,
},
};
await app.vault.adapter.write(path, JSON.stringify(newData, null, 2));
} catch (error) {
console.error('保存窗口配置失败:', error);
}
}
async function saveCommands(commands) {
const path = '05-木板/JSetc脚本/commands.json';
try {
const existing = await loadCommands();
const newData = {
commands,
windowConfig: existing.windowConfig || {},
};
await app.vault.adapter.write(path, JSON.stringify(newData, null, 2));
} catch (error) {
console.error('保存命令文件失败:', error);
}
}
效果大概是: