【QuickAdd脚本】移动子笔记或附件到当前文件夹

脚本介绍

以复选框显示文档中不处于同一文件夹的嵌入文件,可筛选:page_facing_up:笔记或者:package:附件,复选框选择 true 提交 (Submit) 后则移动到当前笔记所在的文件夹,如果 false 或者不提交 (Submit) 则不移动。

QuickAdd Macro 脚本

/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-03-18 02:30:36 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-04-01 19:11:23
*/

// 导入所需模块
const path = require('path');
const fs = require('fs');
const quickAddApi = app.plugins.plugins.quickadd.api;

// 导出异步函数
module.exports = async (params) => {
    // 获取当前活动文件和缓存的元数据
    const file = app.workspace.getActiveFile();
    const cachedMetadata = app.metadataCache.getFileCache(file);
    let links = [];
    let embeds = [];

    // 提取链接和嵌入的文件
    if (cachedMetadata.links) {
        links = cachedMetadata.links.map(l => l.link);
    }

    if (cachedMetadata.embeds) {
        embeds = cachedMetadata.embeds.map(e => e.link);
    }

    let allLinks = [...links, ...embeds];
    console.log(allLinks);

    // // 如果没有链接和嵌入文件,则提示并返回
    // if (!allLinks.length) {
    //     new Notice("💬当前笔记没有检测到嵌入的笔记或附件");
    //     return;
    // };

    // 获取所有文件和链接文件路径
    const files = app.vault.getFiles();
    let linkFilePaths = [];
    for (let i = 0; i < allLinks.length; i++) {
        const link = allLinks[i];
        const filePath = getFilePath(files, link);
        if (filePath) {
            linkFilePaths.push(filePath);
        }
    };
    console.log(linkFilePaths);

    // 检查链接文件是否在同一文件夹中
    const activefile = app.workspace.getActiveFile();
    console.log(activefile);
    const check = checkLinkNotesInSameFolder(activefile.path, linkFilePaths);

    // 筛选可移动的文件类型
    const moveFiles = linkFilePaths.filter((filePath, index) => !check[index]);

    // 筛选附件和笔记
    const attachmentTypes = ['md', 'canvas', 'excalidraw'];
    const notes = moveFiles.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
    const attachments = moveFiles.filter(link => !notes.includes(link));

    // 提示用户选择移动类型
    const choices = [`📁移动当前文件夹至其他位置`, `📄移动笔记到当前文件夹(${notes.length})`, `📦移动附件到当前文件夹(${attachments.length})`];
    const choice = await quickAddApi.suggester(choices, choices);
    if (!choice) return;

    if (choice === choices[0]) {
        let listPaths = app.vault.getAllFolders().map(f => f.path);
        listPaths.unshift("/");
        const choice = await quickAddApi.suggester(listPaths, listPaths);
        if (!choice) return;
        const newFilePath = path.join(choice, path.basename(path.dirname(activefile.path)));
        const oldFolderPath = path.dirname(activefile.path);
        await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
        new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
        return;
    };

    // 根据用户选择筛选链接
    const filteredLinks = choice === choices[1] ? notes : attachments;

    // 用户选择具体文件
    const selectedItems = await quickAddApi.checkboxPrompt(filteredLinks, filteredLinks);
    if (!selectedItems) return;

    // 匹配选择的文件路径
    const matchedLinkFilePaths = selectedItems.map((filePath) => {
        return linkFilePaths.find((linkFilePath) => linkFilePath.replace(".md", "").endsWith(filePath.replace(".md", "")));
    });
    if (!matchedLinkFilePaths) return;

    console.log(matchedLinkFilePaths);

    // 移动文件到当前文件夹
    for (let i = 0; i < matchedLinkFilePaths.length; i++) {
        const oldFilePath = matchedLinkFilePaths[i];

        // 2024-03-22_11:41:如果存在FolderNote,则移动的是整个文件夹
        const fileName = path.basename(oldFilePath);
        const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");

        if (isFolderNote) {
            const newFilePath = path.join(path.dirname(activefile.path), fileName.replace(".md", "").replace(".canvas", ""));
            const oldFolderPath = path.dirname(oldFilePath);
            await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
            new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
        } else {
            const newFilePath = path.join(path.dirname(activefile.path), path.basename(oldFilePath));
            await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
            new Notice(`📄已移动${matchedLinkFilePaths[i]}`, 1000);
        }
    }
    new Notice("✅已移动文件到当前文件夹");
};

// 获取文件路径函数
function getFilePath(files, baseName) {
    let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === baseName.replace(".md", ""));
    let filePath = files2.map((f) => f.path);
    return filePath[0];
}

// 检查链接文件是否在同一文件夹中函数
function checkLinkNotesInSameFolder(activeFilePath, linkFilePaths) {
    const activeFileDir = path.dirname(activeFilePath);
    let check = [];
    for (let i = 0; i < linkFilePaths.length; i++) {
        const linkFileDir = path.dirname(linkFilePaths[i]);

        if (activeFileDir === linkFileDir) {
            check.push(true);
        } else {
            check.push(false);
        }
    }
    return check;
};

References

  1. 2024-03-03_QuickAdd脚本-Obsidian批量重命名(笔记-附件-文件夹)

ChangeLog

  • 2024-03-22_11:33:如果嵌入的是FolderNote则移动整个FolderNote文件夹。 :white_check_mark: 2024-03-22
  • 2024-04-01_19:09:添加移动当前文件夹至其他位置的选项,方便Folder笔记的移动
    • Pasted image 20240401190941
2 个赞

熊猫别熬夜…大佬

這腳本太棒了!!:raised_hands::raised_hands::raised_hands::raised_hands::raised_hands:
感謝開發分享:clap::clap::clap::clap::clap:

1 个赞

暂时还没有记录操作数据日志的功能,即操作之后不可撤回(之后有时间加上),下面的这个脚本也可以用于移动笔记,会记录操作数据方便撤回,

2 个赞

#1更新脚本:如果嵌入的是FolderNote则移动整个FolderNote文件夹。 :white_check_mark: 2024-03-22

:bulb:推荐快捷键设置:Ctrl + Shift + M

1 个赞

添加 📥归档当前文件到指定文件夹选项

JS代码
/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-03-18 02:30:36 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-04-10 16:27:00
*/

// 导入所需模块
const path = require('path');
const fs = require('fs');
const quickAddApi = app.plugins.plugins.quickadd.api;
const files = app.vault.getFiles();
// 获取当前活动文件和缓存的元数据
const file = app.workspace.getActiveFile();
const cachedMetadata = app.metadataCache.getFileCache(file);
let listPaths = app.vault.getAllFolders().map(f => f.path);
listPaths.unshift("/");

// 导出异步函数
module.exports = {
    entry: async (QuickAdd, settings, params) => {
        // 获取选中的文本
        const editor = app.workspace.activeEditor.editor;
        // 获取选中的文本否则自动获取当前行的文本
        const selection = editor.getSelection();
        let selectionEmbed = matchSelectionEmbed(selection);
        console.log(selectionEmbed);
        let wikiPath = "";
        if (selectionEmbed) {
            wikiPath = getFilePath(files, selectionEmbed); // 匹配Wiki链接
        }

        let links = [];
        let embeds = [];
        // 提取链接和嵌入的文件
        if (cachedMetadata.links) {
            links = cachedMetadata.links.map(l => l.link);
        }

        if (cachedMetadata.embeds) {
            embeds = cachedMetadata.embeds.map(e => e.link);
        }

        let allLinks = [...links, ...embeds];
        console.log(allLinks);

        // 获取所有文件和链接文件路径
        let linkFilePaths = [];
        for (let i = 0; i < allLinks.length; i++) {
            const link = allLinks[i];
            const filePath = getFilePath(files, link);
            if (filePath) {
                linkFilePaths.push(filePath);
            }
        };
        console.log(linkFilePaths);

        linkFilePaths = wikiPath ? [wikiPath] : linkFilePaths;

        // 检查链接文件是否在同一文件夹中
        const activefile = app.workspace.getActiveFile();
        console.log(activefile);
        const check = checkLinkNotesInSameFolder(activefile.path, linkFilePaths);

        // 筛选可移动的文件类型
        const moveFiles = linkFilePaths.filter((filePath, index) => !check[index]);

        // 筛选附件和笔记
        const attachmentTypes = ['md', 'canvas', 'excalidraw'];
        const notes = moveFiles.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
        const attachments = moveFiles.filter(link => !notes.includes(link));

        // 提示用户选择移动类型
        const choices = [`📁移动当前文件夹至其他位置`, `📄移动笔记到当前文件夹(${notes.length})`, `📦移动附件到当前文件夹(${attachments.length})`, `📥归档当前文件到指定文件夹`];
        const choice = await quickAddApi.suggester(choices, choices);
        if (!choice) return;

        if (choice === choices[0]) {
            const choice = await quickAddApi.suggester(listPaths, listPaths);
            if (!choice) return;
            const newFilePath = path.join(choice, path.basename(path.dirname(activefile.path)));
            const oldFolderPath = path.dirname(activefile.path);
            await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
            new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            return;
        } else if (choice === choices[3]) {
            // 2024-04-10_15:37:归档文件或者FolderNote
            const archiveFolder = settings["归档路径"] + (settings["归档格式"] ? "/" + quickAddApi.date.now(settings["归档格式"]) : "");
            if (!(await app.vault.getFolderByPath(archiveFolder))) {
                await app.vault.createFolder(archiveFolder);
            }
            const fileName = path.basename(activefile.path);
            const oldFilePath = activefile.path;
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");
            if (isFolderNote) {
                const newFilePath = path.join(archiveFolder, fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📥已归档📁${oldFolderPath}`, 1000);
            } else {
                const newFilePath = path.join(archiveFolder, fileName);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📥已归档📝${fileName}`, 1000);
            }
            return;
        }

        // 根据用户选择筛选链接
        const filteredLinks = choice === choices[1] ? notes : attachments;

        // 用户选择具体文件
        const selectedItems = await quickAddApi.checkboxPrompt(filteredLinks, filteredLinks);
        if (!selectedItems) return;

        // 匹配选择的文件路径
        const matchedLinkFilePaths = selectedItems.map((filePath) => {
            return linkFilePaths.find((linkFilePath) => linkFilePath.replace(".md", "").endsWith(filePath.replace(".md", "")));
        });
        if (!matchedLinkFilePaths) return;

        console.log(matchedLinkFilePaths);

        // 移动文件到当前文件夹
        for (let i = 0; i < matchedLinkFilePaths.length; i++) {
            const oldFilePath = matchedLinkFilePaths[i];

            // 2024-03-22_11:41:如果存在FolderNote,则移动的是整个文件夹
            const fileName = path.basename(oldFilePath);
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");

            if (isFolderNote) {
                const newFilePath = path.join(path.dirname(activefile.path), fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            } else {
                const newFilePath = path.join(path.dirname(activefile.path), path.basename(oldFilePath));
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📄已移动${matchedLinkFilePaths[i]}`, 1000);
            }
        }
        new Notice("✅已移动文件到当前文件夹");
    },
    settings: {
        name: "移动子笔记或附件",
        author: "熊猫别熬夜",
        options: {
            "归档路径": {
                type: "dropdown",
                defaultValue: "800【存档】Archive",
                options: listPaths,
            },
            "归档格式": {
                type: "text",
                defaultValue: "YYYY",
                description: "可以设置以时间格式划分子文件夹,YYYY(年),MM(月),DD(日)",
            },
        }
    }
};

// 获取文件路径函数
function getFilePath(files, baseName) {
    let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === baseName.replace(".md", ""));
    let filePath = files2.map((f) => f.path);
    return filePath[0];
}


// 检查链接文件是否在同一文件夹中函数
function checkLinkNotesInSameFolder(activeFilePath, linkFilePaths) {
    const activeFileDir = path.dirname(activeFilePath);
    let check = [];
    for (let i = 0; i < linkFilePaths.length; i++) {
        const linkFileDir = path.dirname(linkFilePaths[i]);

        if (activeFileDir === linkFileDir) {
            check.push(true);
        } else {
            check.push(false);
        }
    }
    return check;
};

function matchSelectionEmbed(text) {
    const regex = /\[\[?([^\]]*?)(\|.*)?\]\]?\(?([^)\n]*)\)?/;
    const matches = text.match(regex);
    if (!matches) return;
    if (matches[3]) return decodeURIComponent(matches[3]);
    if (matches[1]) return decodeURIComponent(matches[1]);
}

@熊猫别熬夜 熊猫佬,想知道如果想用这个脚本【把当前笔记以及它的附件一起移动到另外的文件夹】,是需要依次执行「移动笔记」和「移动附件」两个操作吗?

如果是单文件确实是这样的,如果是folder note可以直用移动文件夹选项吧
image

其实移动附件我用得不多,按理说最好给附件设置相对路径来着,我主要用来移动一些pdf之类的。

大佬,这个可以对这种链接也起作用吗? [[b#BBB]]
因为有时候重组后,链接为文件的首标题,希望这种也能够被一起识别移动


为啥我只有一个文件会被归档呢

估计只识别到第2个链接,其他的链接没识别到

没做适配,获取的文件名是 b#BBB,估计是提取路径的时候没做做预处理,我今晚改下…
同上,[[未命名/未命名]]也是没做文件名的预处理。

另外之前那个Excalidraw的网格方法,实际上我也没有啥好的方法,其实我也想有CAD那样的裁剪功能,hhh

话说这个是用的ob的移动吗,移动后似乎链接到标题的链接会断

用的Ob自带的API,量不是太大的话基本没问题,但个别插件会影响重命名的,比如我用过的一个附件管理的插件会影响ob的重命名。

抱歉,过了很久才给你回复。

PKMer_QuickAdd 脚本 - 移动子笔记或附件到当前文件夹 - #5,来自 Probe - Blog - PKMer里面有人提到了并给出了解决方案:

function getFilePath(files, baseName) {
    // 下一行的原先 baseName => 改成 path.basename(baseName)
    let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === path.basename(baseName).replace(".md", ""));
    let filePath = files2.map((f) => f.path);
    return filePath[0];
}
24.10.27 修改后的脚本
/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-03-18 02:30:36 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-10-27 14:57:40
*/

// 导入所需模块
const path = require('path');
const fs = require('fs');
const quickAddApi = app.plugins.plugins.quickadd.api;
const files = app.vault.getFiles();
// 获取当前活动文件和缓存的元数据
const file = app.workspace.getActiveFile();
const cachedMetadata = app.metadataCache.getFileCache(file);
let listPaths = app.vault.getAllFolders().map(f => f.path);
listPaths.unshift("/");

// 导出异步函数
module.exports = {
    entry: async (QuickAdd, settings, params) => {
        let editor;
        let selection;
        try {
            // 获取选中的文本
            editor = app.workspace.activeEditor.editor;
            // 获取选中的文本否则自动获取当前行的文本
            selection = editor.getSelection();
        } catch {

        }
        let selectionEmbed;
        if (selection) {
            selectionEmbed = matchSelectionEmbed(selection);
        }
        console.log(selectionEmbed);
        let wikiPath = "";
        if (selectionEmbed) {
            wikiPath = getFilePath(files, selectionEmbed); // 匹配Wiki链接
        }
        let links = [];
        let embeds = [];
        // 提取链接和嵌入的文件
        if (cachedMetadata.links) {
            links = cachedMetadata.links.map(l => l.link);
        }

        if (cachedMetadata.embeds) {
            embeds = cachedMetadata.embeds.map(e => e.link);
        }

        let allLinks = [...links, ...embeds];
        console.log(allLinks);

        // 获取所有文件和链接文件路径
        let linkFilePaths = [];
        for (let i = 0; i < allLinks.length; i++) {
            const link = allLinks[i];
            const filePath = getFilePath(files, link);
            if (filePath) {
                linkFilePaths.push(filePath);
            }
        };
        console.log(linkFilePaths);

        linkFilePaths = wikiPath ? [wikiPath] : linkFilePaths;

        // 检查链接文件是否在同一文件夹中
        const activefile = app.workspace.getActiveFile();
        console.log(activefile);
        const check = checkLinkNotesInSameFolder(activefile.path, linkFilePaths);

        // 筛选可移动的文件类型
        const moveFiles = linkFilePaths.filter((filePath, index) => !check[index]);

        // 筛选附件和笔记
        const attachmentTypes = ['md', 'canvas', 'excalidraw'];
        const notes = moveFiles.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
        const attachments = moveFiles.filter(link => !notes.includes(link));

        // 提示用户选择移动类型
        const choices = [`📁移动当前文件夹至其他位置`, `📄移动笔记到当前文件夹(${notes.length})`, `📦移动附件到当前文件夹(${attachments.length})`, `📥归档当前文件到指定文件夹`];
        const choice = await quickAddApi.suggester(choices, choices);
        if (!choice) return;

        if (choice === choices[0]) {
            const choice = await quickAddApi.suggester(listPaths, listPaths);
            if (!choice) return;
            const newFilePath = path.join(choice, path.basename(path.dirname(activefile.path)));
            const oldFolderPath = path.dirname(activefile.path);
            await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
            new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            return;
        } else if (choice === choices[3]) {
            // 2024-04-10_15:37:归档文件或者FolderNote
            const archiveFolder = settings["归档路径"] + (settings["归档格式"] ? "/" + quickAddApi.date.now(settings["归档格式"]) : "");
            if (!(await app.vault.getFolderByPath(archiveFolder))) {
                await app.vault.createFolder(archiveFolder);
            }
            const fileName = path.basename(activefile.path);
            const oldFilePath = activefile.path;
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");
            if (isFolderNote) {
                const newFilePath = path.join(archiveFolder, fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📥已归档📁${oldFolderPath}`, 1000);
            } else {
                const newFilePath = path.join(archiveFolder, fileName);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📥已归档📝${fileName}`, 1000);
            }
            return;
        }

        // 根据用户选择筛选链接
        const filteredLinks = choice === choices[1] ? notes : attachments;

        // 用户选择具体文件
        const selectedItems = await quickAddApi.checkboxPrompt(filteredLinks, filteredLinks);
        if (!selectedItems) return;

        // 匹配选择的文件路径
        const matchedLinkFilePaths = selectedItems.map((filePath) => {
            return linkFilePaths.find((linkFilePath) => linkFilePath.replace(".md", "").endsWith(filePath.replace(".md", "")));
        });
        if (!matchedLinkFilePaths) return;

        console.log(matchedLinkFilePaths);

        // 移动文件到当前文件夹
        for (let i = 0; i < matchedLinkFilePaths.length; i++) {
            const oldFilePath = matchedLinkFilePaths[i];

            // 2024-03-22_11:41:如果存在FolderNote,则移动的是整个文件夹
            const fileName = path.basename(oldFilePath);
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");

            if (isFolderNote) {
                const newFilePath = path.join(path.dirname(activefile.path), fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            } else {
                const newFilePath = path.join(path.dirname(activefile.path), path.basename(oldFilePath));
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📄已移动${matchedLinkFilePaths[i]}`, 1000);
            }
        }
        new Notice("✅已移动文件到当前文件夹");
    },
    settings: {
        name: "移动子笔记或附件",
        author: "熊猫别熬夜",
        options: {
            "归档路径": {
                type: "dropdown",
                defaultValue: "800【存档】Archive",
                options: listPaths,
            },
            "归档格式": {
                type: "text",
                defaultValue: "YYYY",
                description: "可以设置以时间格式划分子文件夹,YYYY(年),MM(月),DD(日)",
            },
        }
    }
};

// 获取文件路径函数
// [PKMer\_QuickAdd 脚本 - 移动子笔记或附件到当前文件夹 - #5,来自 Probe - Blog - PKMer](https://forum.pkmer.net/t/topic/1885/5)
function getFilePath(files, baseName) {
    // 下一行的原先 baseName => 改成 path.basename(baseName)
    let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === path.basename(baseName).replace(".md", ""));
    let filePath = files2.map((f) => f.path);
    return filePath[0];
}

// 检查链接文件是否在同一文件夹中函数
function checkLinkNotesInSameFolder(activeFilePath, linkFilePaths) {
    const activeFileDir = path.dirname(activeFilePath);
    let check = [];
    for (let i = 0; i < linkFilePaths.length; i++) {
        const linkFileDir = path.dirname(linkFilePaths[i]);

        if (activeFileDir === linkFileDir) {
            check.push(true);
        } else {
            check.push(false);
        }
    }
    return check;
};

function matchSelectionEmbed(text) {
    const regex = /\[\[?([^\]]*?)(\|.*)?\]\]?\(?([^)\n]*)\)?/;
    const matches = text.match(regex);
    if (!matches) return;
    if (matches[3]) return decodeURIComponent(matches[3]);
    if (matches[1]) return decodeURIComponent(matches[1]);
}
1 个赞

辛苦大佬!!!!

1 个赞

安卓端不支持require!

有了这个,another quick switcher是不是可以卸载了 :rofl:

安卓端提示require未定义,和require相关都要改写。

添加了可以移动file://格式的外部文件移动到当前文件夹,并修改内部链接格式的选项,适用于QQ信息内的图片移动。

/*
 * @Author: 熊猫别熬夜 
 * @Date: 2024-03-18 02:30:36 
 * @Last Modified by: 熊猫别熬夜
 * @Last Modified time: 2024-12-12 17:55:17
*/

// 导入所需模块
const path = require('path');
const fs = require('fs');
const quickAddApi = app.plugins.plugins.quickadd.api;
const files = app.vault.getFiles();
// 获取当前活动文件和缓存的元数据
const file = app.workspace.getActiveFile();
const cachedMetadata = app.metadataCache.getFileCache(file);
let listPaths = app.vault.getAllFolders().map(f => f.path);
listPaths.unshift("/");

// 导出异步函数
module.exports = {
    entry: async (QuickAdd, settings, params) => {
        let editor;
        let selection;
        try {
            // 获取选中的文本
            editor = app.workspace.activeEditor.editor;
            // // 获取选中的文本否则自动获取当前行的文本
            // selection = editor.getSelection();
            // 选择所在的一行
            const line = editor.getLine(editor.getCursor().line);
            // 获取选中的文本否则自动获取当前行的文本
            selection = editor.getSelection() ? editor.getSelection() : line;
        } catch {

        }
        let selectionEmbed;
        if (selection) {
            selectionEmbed = matchSelectionEmbed(selection);
        }
        console.log(selectionEmbed);
        let wikiPath = "";
        if (selectionEmbed) {
            wikiPath = getFilePath(files, selectionEmbed); // 匹配Wiki链接
        }
        let links = [];
        let embeds = [];
        // 提取链接和嵌入的文件
        if (cachedMetadata.links) {
            links = cachedMetadata.links.map(l => l.link);
        }

        if (cachedMetadata.embeds) {
            embeds = cachedMetadata.embeds.map(e => e.link);
        }

        let allLinks = [...links, ...embeds];
        console.log(allLinks);

        // 提取外部文件链接 (file://)
        let externalFiles = [];
        let fileContent = '';
        try {
            fileContent = await app.vault.read(file);
            externalFiles = extractExternalFileLinks(fileContent);
            console.log('外部文件:', externalFiles);
        } catch (error) {
            console.error('读取文件内容失败:', error);
        }

        // 获取所有文件和链接文件路径
        let linkFilePaths = [];
        for (let i = 0; i < allLinks.length; i++) {
            const link = allLinks[i];
            const filePath = getFilePath(files, link);
            if (filePath) {
                linkFilePaths.push(filePath);
            }
        };
        console.log(linkFilePaths);

        linkFilePaths = wikiPath ? [wikiPath] : linkFilePaths;

        // 检查链接文件是否在同一文件夹中
        const activefile = app.workspace.getActiveFile();
        console.log(activefile);
        const check = checkLinkNotesInSameFolder(activefile.path, linkFilePaths);

        // 筛选可移动的文件类型
        const moveFiles = linkFilePaths.filter((filePath, index) => !check[index]);

        // 筛选附件和笔记
        const attachmentTypes = ['md', 'canvas', 'excalidraw'];
        const notes = moveFiles.filter(link => attachmentTypes.some(type => link.endsWith('.' + type)));
        const attachments = moveFiles.filter(link => !notes.includes(link));

        // 提示用户选择移动类型
        const choices = [`📁移动当前文件夹至其他位置`, `📄移动笔记到当前文件夹(${notes.length})`, `📦移动附件到当前文件夹(${attachments.length})`, `🌐移动外部文件到当前文件夹(${externalFiles.length})`, `📥归档当前文件到指定文件夹`];
        const choice = await quickAddApi.suggester(choices, choices);
        if (!choice) return;

        if (choice === choices[0]) {
            const choice = await quickAddApi.suggester(listPaths, listPaths);
            if (!choice) return;
            const newFilePath = path.join(choice, path.basename(path.dirname(activefile.path)));
            const oldFolderPath = path.dirname(activefile.path);
            await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
            new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            return;
        } else if (choice === choices[4]) {
            // 2024-04-10_15:37:归档文件或者FolderNote
            const archiveFolder = settings["归档路径"] + (settings["归档格式"] ? "/" + quickAddApi.date.now(settings["归档格式"]) : "");
            if (!(await app.vault.getFolderByPath(archiveFolder))) {
                await app.vault.createFolder(archiveFolder);
            }
            const fileName = path.basename(activefile.path);
            const oldFilePath = activefile.path;
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");
            if (isFolderNote) {
                const newFilePath = path.join(archiveFolder, fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📥已归档📁${oldFolderPath}`, 1000);
            } else {
                const newFilePath = path.join(archiveFolder, fileName);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📥已归档📝${fileName}`, 1000);
            }
            return;
        } else if (choice === choices[3]) {
            // 移动外部文件
            if (externalFiles.length === 0) {
                new Notice('❌未找到外部文件链接', 2000);
                return;
            }

            // 用户选择具体的外部文件
            const selectedItems = await quickAddApi.checkboxPrompt(externalFiles, externalFiles);
            if (!selectedItems || selectedItems.length === 0) return;

            // 复制外部文件到当前文件夹
            const activefile = app.workspace.getActiveFile();
            const targetDir = path.dirname(activefile.path);
            const vaultBasePath = app.vault.adapter.basePath;

            // 记录需要替换的链接映射(原始 file:// URL 字符串 -> 新的 Obsidian 链接)
            const linkReplacements = new Map();
            let updatedContent = fileContent;

            for (let i = 0; i < selectedItems.length; i++) {
                const externalFilePath = selectedItems[i];
                try {
                    // 检查源文件是否存在
                    if (!fs.existsSync(externalFilePath)) {
                        new Notice(`❌文件不存在: ${path.basename(externalFilePath)}`, 2000);
                        continue;
                    }

                    // 目标文件路径(相对于 vault 根目录)
                    const targetFilePath = path.join(targetDir, path.basename(externalFilePath)).replace(/\\/g, '/');
                    const vaultTargetPath = path.join(vaultBasePath, targetFilePath);

                    // 检查目标文件是否已存在
                    if (fs.existsSync(vaultTargetPath)) {
                        new Notice(`⚠️文件已存在: ${path.basename(externalFilePath)}`, 2000);
                    } else {
                        // 确保目标目录存在
                        const targetDirPath = path.dirname(vaultTargetPath);
                        if (!fs.existsSync(targetDirPath)) {
                            fs.mkdirSync(targetDirPath, { recursive: true });
                        }

                        // 复制文件
                        fs.copyFileSync(externalFilePath, vaultTargetPath);
                        new Notice(`📋已复制 ${path.basename(externalFilePath)}`, 1000);
                    }

                    // 提取文件内容中匹配的原始 file:// URL 字符串
                    const fileName = path.basename(externalFilePath);
                    const newLink = `![[${fileName}]]`;
                    const originalUrl = findFileUrlInContent(fileContent, externalFilePath);
                    if (originalUrl) {
                        linkReplacements.set(originalUrl, newLink);
                    }
                } catch (error) {
                    console.error('复制外部文件失败:', error);
                    new Notice(`❌复制失败: ${path.basename(externalFilePath)}`, 2000);
                }
            }

            // 更新文件内容中的链接
            if (linkReplacements.size > 0) {
                try {
                    // 替换文件内容中的 file:// 链接
                    for (const [originalUrl, newLink] of linkReplacements.entries()) {
                        // 匹配 ![](file://...) 或 ![alt](file://...) 格式
                        // 需要转义原始 URL 中的特殊字符,但保留正则表达式元字符的转义
                        const escapedUrl = escapeRegExp(originalUrl);
                        const imgPattern = new RegExp(`!\\[([^\\]]*)\\]\\(${escapedUrl}\\)`, 'g');
                        updatedContent = updatedContent.replace(imgPattern, (match, altText) => {
                            return altText ? `![${altText}]${newLink}` : newLink;
                        });
                    }

                    // 保存更新后的内容
                    if (updatedContent !== fileContent) {
                        await app.vault.modify(activefile, updatedContent);
                        new Notice("✅已更新文件中的链接格式", 2000);
                    }
                } catch (error) {
                    console.error('更新文件链接失败:', error);
                    new Notice('⚠️文件已复制,但更新链接失败', 2000);
                }
            }

            new Notice("✅已复制外部文件到当前文件夹");
            return;
        }

        // 根据用户选择筛选链接
        const filteredLinks = choice === choices[1] ? notes : attachments;

        // 用户选择具体文件
        const selectedItems = await quickAddApi.checkboxPrompt(filteredLinks, filteredLinks);
        if (!selectedItems) return;

        // 匹配选择的文件路径
        const matchedLinkFilePaths = selectedItems.map((filePath) => {
            return linkFilePaths.find((linkFilePath) => linkFilePath.replace(".md", "").endsWith(filePath.replace(".md", "")));
        });
        if (!matchedLinkFilePaths) return;

        console.log(matchedLinkFilePaths);

        // 移动文件到当前文件夹
        for (let i = 0; i < matchedLinkFilePaths.length; i++) {
            const oldFilePath = matchedLinkFilePaths[i];

            // 2024-03-22_11:41:如果存在FolderNote,则移动的是整个文件夹
            const fileName = path.basename(oldFilePath);
            const isFolderNote = path.basename(path.dirname(oldFilePath)) === fileName.replace(".md", "").replace(".canvas", "");

            if (isFolderNote) {
                const newFilePath = path.join(path.dirname(activefile.path), fileName.replace(".md", "").replace(".canvas", ""));
                const oldFolderPath = path.dirname(oldFilePath);
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFolderPath), newFilePath);
                new Notice(`📁已移动${oldFolderPath}文件夹至${newFilePath}`, 1000);
            } else {
                const newFilePath = path.join(path.dirname(activefile.path), path.basename(oldFilePath));
                await app.fileManager.renameFile(app.vault.getAbstractFileByPath(oldFilePath), newFilePath);
                new Notice(`📄已移动${matchedLinkFilePaths[i]}`, 1000);
            }
        }
        new Notice("✅已移动文件到当前文件夹");
    },
    settings: {
        name: "移动子笔记或附件",
        author: "熊猫别熬夜",
        options: {
            "归档路径": {
                type: "dropdown",
                defaultValue: "800【存档】Archive",
                options: listPaths,
            },
            "归档格式": {
                type: "text",
                defaultValue: "YYYY",
                description: "可以设置以时间格式划分子文件夹,YYYY(年),MM(月),DD(日)",
            },
        }
    }
};

// 获取文件路径函数
// [PKMer\_QuickAdd 脚本 - 移动子笔记或附件到当前文件夹 - #5,来自 Probe - Blog - PKMer](https://forum.pkmer.net/t/topic/1885/5)
function getFilePath(files, baseName) {
    // 下一行的原先 baseName => 改成 path.basename(baseName)
    let files2 = files.filter(f => path.basename(f.path).replace(".md", "") === path.basename(baseName).replace(".md", ""));
    let filePath = files2.map((f) => f.path);
    return filePath[0];
}

// 检查链接文件是否在同一文件夹中函数
function checkLinkNotesInSameFolder(activeFilePath, linkFilePaths) {
    const activeFileDir = path.dirname(activeFilePath);
    let check = [];
    for (let i = 0; i < linkFilePaths.length; i++) {
        const linkFileDir = path.dirname(linkFilePaths[i]);

        if (activeFileDir === linkFileDir) {
            check.push(true);
        } else {
            check.push(false);
        }
    }
    return check;
};

function matchSelectionEmbed(text) {
    const regex = /\[\[?([^\]]*?)(\|.*)?\]\]?\(?([^)\n]*)\)?/;
    const matches = text.match(regex);
    if (!matches) return;
    if (matches[3]) return decodeURIComponent(matches[3]);
    if (matches[1]) return decodeURIComponent(matches[1]);
}

// 提取外部文件链接 (file://) 函数
function extractExternalFileLinks(content) {
    // 匹配 file:// 协议链接,支持 Markdown 图片语法 ![](file://...)
    const fileProtocolRegex = /file:\/\/[\/]?([^\s\)"\]]+)/g;
    const matches = [];
    let match;

    while ((match = fileProtocolRegex.exec(content)) !== null) {
        try {
            // 解码 URL 编码的路径
            let filePath = decodeURIComponent(match[1]);
            // 处理 Windows 路径 (file:///D:/... 或 file:///D:\... 转换为 D:/... 或 D:\...)
            if (filePath.startsWith('/')) {
                filePath = filePath.substring(1);
            }
            // 如果路径使用正斜杠,转换为系统路径分隔符
            filePath = filePath.replace(/\//g, path.sep);

            // 检查文件是否存在
            if (fs.existsSync(filePath)) {
                matches.push(filePath);
            }
        } catch (error) {
            console.error('处理外部文件路径失败:', match[1], error);
        }
    }

    // 去重
    return [...new Set(matches)];
}

// 转义正则表达式特殊字符
function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// 在文件内容中查找匹配的 file:// URL 字符串
function findFileUrlInContent(content, filePath) {
    // 匹配 file:// 链接格式
    const fileUrlPattern = /file:\/\/[\/]?([^\s\)"\]]+)/g;
    let match;

    while ((match = fileUrlPattern.exec(content)) !== null) {
        try {
            let decodedPath = decodeURIComponent(match[1]);
            if (decodedPath.startsWith('/')) {
                decodedPath = decodedPath.substring(1);
            }
            decodedPath = decodedPath.replace(/\//g, path.sep);

            // 规范化路径进行比较
            const normalizedFilePath = path.normalize(filePath);
            const normalizedDecodedPath = path.normalize(decodedPath);

            if (normalizedFilePath === normalizedDecodedPath ||
                normalizedFilePath.toLowerCase() === normalizedDecodedPath.toLowerCase()) {
                return match[0]; // 返回完整的 file:// URL
            }
        } catch (error) {
            // 继续尝试下一个匹配
        }
    }

    return null;
}
1 个赞