Greasy Fork 支持简体中文。

Gooboo存档同步(WebDAV)

游戏存档WebDAV 上传和下载,适用于使用 localStorage 存储存档的网页游戏。

// ==UserScript==
// @name         Gooboo存档同步(WebDAV)
// @version      1.1
// @description  游戏存档WebDAV 上传和下载,适用于使用 localStorage 存储存档的网页游戏。
// @author       zding
// @match        *://*/gooboo/
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        GM_registerMenuCommand
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @run-at       document-start
// @license      MIT
// @namespace https://greasyfork.org/users/762887
// ==/UserScript==

(function() {
    'use strict';

    // 默认值
    const defaultUrl = 'https://bora.teracloud.jp/dav'; //示例用的是infini-cloud.net的免费webdav,可以注册验证后,在个人页面的Refferal Code填写邀请码GR52W获得额外的5g
    const defaultUsername = 'Username';
    const defaultPassword = 'password';
    const defaultBackupCount = 5;
    const defaultShowFloatingTopButton = true;
    const defaultShowFloatingMenu = true;
    const defaultBlacklistedKeyPatterns = [/Hm_lvt/i, /_ga/i, /_gid/i, /HMACCOUNT/i, /Hm_lpvt/i];
    // 定义需要屏蔽的 localStorage key 关键词,使用正则表达式,忽略大小写(屏蔽这个的原因是因为有些手机浏览器会有如百度联盟的影响项,而电脑没有,会导致两者同步内容不一致。)
    const domain = window.location.hostname;
    const webdavDirectory = domain + '_saves'; // WebDAV 上的目录,模式使用当前域名_saves


    // 从 GM_getValue 中读取设置,如果不存在则使用默认值
    let url = GM_getValue('url', defaultUrl);
    let username = GM_getValue('username', defaultUsername);
    let password = GM_getValue('password', defaultPassword);
    let backupCount = GM_getValue('backupCount', defaultBackupCount);
    let showFloatingTopButton = GM_getValue('showFloatingTopButton', defaultShowFloatingTopButton);
    showFloatingTopButton = (typeof showFloatingTopButton === 'string') ? (showFloatingTopButton === 'true') : showFloatingTopButton;
    let showFloatingMenu = GM_getValue('showFloatingMenu', defaultShowFloatingMenu);
    showFloatingMenu = (typeof showFloatingMenu === 'string') ? (showFloatingMenu === 'true') : showFloatingMenu;
    let blacklistedKeyPatterns = GM_getValue('blacklistedKeyPatterns', defaultBlacklistedKeyPatterns.map(regex => regex.source));
    blacklistedKeyPatterns = blacklistedKeyPatterns.map(source => new RegExp(source, 'i'));

    GM_addStyle(`
        .custom-modal {
            display: none;
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0, 0, 0, 0.2);
            backdrop-filter: blur(5px);
            z-index: 1000;
            animation: fadeIn 0.3s ease-in-out;
        }

        .custom-modal-content {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background-color: rgba(255, 255, 255, 0.8);
            padding: 20px;
            border-radius: 12px;
            text-align: center;
            width: 70%;
            max-width: 450px;
        }

        .custom-modal-button {
            background-color: #66b3ff;
            border: none;
            color: white;
            padding: 10px 20px;
            text-align: center;
            text-decoration: none;
            display: inline-block;
            font-size: 16px;
            margin: 6px 3px;
            cursor: pointer;
            border-radius: 8px;
            transition: background-color 0.3s ease;
        }

        .custom-modal-button:hover {
            background-color: #3385ff;
        }

        .custom-select {
            width: 100%;
            padding: 10px;
            margin: 6px 0;
            border: 1px solid #ccc;
            border-radius: 6px;
            box-sizing: border-box;
            font-size: 14px;
        }

        .custom-select:focus {
            outline: none;
            border-color: #66b3ff;
            box-shadow: 0 0 5px rgba(102, 179, 255, 0.5);
        }

        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }

        /* 顶部按钮栏样式 */
        #topButtonContainer {
            position: fixed;
            top: -1000px;
            left: 0;
            width: 100%;
            background: linear-gradient(to right, rgba(255, 255, 255, 0) 30%, rgba(255, 255, 255, 0.8) 50%, rgba(255, 255, 255, 0) 70%);
            padding: 10px;
            text-align: center;
            z-index: 9999;
            transition: top 0.3s ease;
            border: none;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        #topButtonContainer.show {
            top: 0;
        }

        #topButtonContainer button {
            padding: 6px 10px;
            border: none;
            background-color: #66b3ff;
            color: white;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin: 0 3px;
            font-size: 17px;
        }

        #topButtonContainer button:hover {
            background-color: #3385ff;
        }

        @media (max-width: 768px) {
            #topButtonContainer {
                padding: 5px;
            }

            #topButtonContainer button {
                padding: 4px 8px;
                margin: 0 2px;
                font-size: 12px;
            }
        }

        /* 向下箭头样式 */
        #arrowIndicator {
            position: fixed;
            top: 0;
            left: 50%;
            transform: translateX(-50%);
            font-size: 20px;
            color: rgba(0, 0, 0, 0.3);
            z-index: 10001;
            cursor: pointer;
            height: 30px;
            line-height: 30px;
        }

        #arrowIndicator.hidden {
            opacity: 0;
        }

        /* 悬浮按钮样式 */
        #floatingButtonContainer {
            position: fixed;
            left: 10px;
            bottom: 10px;
            z-index: 5000;
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            transform: scale(0.8);
        }

        #floatingButtonContainer button {
            padding: 6px 10px;
            border: none;
            background-color: #66b3ff;
            color: white;
            border-radius: 6px;
            cursor: pointer;
            transition: background-color 0.3s ease;
            margin: 3px 0;
            width: auto;
            display: flex;
            align-items: center;
            font-size: 14px;
        }

        #floatingButtonContainer button:hover {
            background-color: #3385ff;
        }

        #floatingButtonContainer button img {
            width: 16px;
            height: 16px;
            margin-right: 4px;
        }
    `);

    function isValidLocalStorageKey(key) {
        if (!key) {
            return false;
        }

        const trimmedKey = key.trim();
        if (trimmedKey === "") {
            return false;
        }

        for (const pattern of blacklistedKeyPatterns) {
            if (pattern.test(key)) {
                return false;
            }
        }

        return true;
    }

    // 弹窗
    function createModal(message, content, onConfirm, onCancel, showCancel) {
        const modal = document.createElement('div');
        modal.className = 'custom-modal';

        const modalContent = document.createElement('div');
        modalContent.className = 'custom-modal-content';

        const messageElement = document.createElement('p');
        messageElement.textContent = message;

        modalContent.appendChild(messageElement);

        if (content) {
            modalContent.appendChild(content);
        }

        const confirmButton = document.createElement('button');
        confirmButton.className = 'custom-modal-button';
        confirmButton.textContent = '确定';
        confirmButton.addEventListener('click', () => {
            modal.style.display = 'none';
            if (onConfirm) {
                onConfirm();
            }
        });

        modalContent.appendChild(confirmButton);

        if (showCancel) {
            const cancelButton = document.createElement('button');
            cancelButton.className = 'custom-modal-button';
            cancelButton.textContent = '取消';
            cancelButton.addEventListener('click', () => {
                modal.style.display = 'none';
                if (onCancel) {
                    onCancel();
                }
            });
            modalContent.appendChild(cancelButton);
        }


        modal.appendChild(modalContent);
        document.body.appendChild(modal);

        return modal;
    }

    // 显示弹窗
    function showModal(message, content, onConfirm, onCancel, showCancel = false) {
        const modal = createModal(message, content, onConfirm, onCancel, showCancel);
        modal.style.display = 'block';
    }

    // 请求构造
    function request({method, path='', headers, data}) {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: method,
                url: url + '/' + webdavDirectory + '/' + path,
                headers: {
                    authorization: 'Basic ' + btoa(`${username}:${password}`),
                    ...headers
                },
                data: data,
                onload: response => {
                    if (response.status >= 200 && response.status < 300) {
                        resolve(response);
                    } else {
                        console.error(`WebDAV 请求失败!状态码: ${response.status} ${response.statusText}`);
                        reject(response);
                    }
                },
                onerror: (error) => {
                    console.error("GM_xmlhttpRequest error:", error);
                    reject(error);
                }
            });
        });
    }

    // 获取文件列表
    async function getFileList() {
        try {
            const res = await request({
                method: 'PROPFIND',
                headers: {depth: 1}
            });

            let files = [];
            let path = res.responseText.match(/(?<=<d:href>).*?(?=<\/d:href>)/gi);
            if (path) {
                path.forEach(p => {
                    const filename = p.split('/').pop();
                    if (filename && filename.endsWith('.txt')) {
                        files.push(filename);
                    }
                });
            }

            files.sort().reverse();
            return files;
        } catch (error) {
            console.error("Error getting file list:", error);
            return [];
        }
    }

    // 删除文件
    function deleteFile(filename) {
        return request({
            method: 'DELETE',
            path: filename
        }).then(() => {
            console.log(`Deleted file: ${filename}`);
        }).catch(error => {
            console.error(`Error deleting file: ${filename}`, error);
            throw error;
        });
    }

    // 清理旧存档
    async function cleanupOldBackups() {
        try {

            for (let i = 0; i < localStorage.length; i++) {
                const localStorageKey = localStorage.key(i);
                const filenamePrefix = localStorageKey + "_";
                const files = await getFileList();


                const relevantFiles = files.filter(file => file.startsWith(filenamePrefix));
                relevantFiles.sort().reverse();

                const filesToDelete = relevantFiles.slice(backupCount);

                console.log(`需要删除的旧存档数量 (${localStorageKey}): ${filesToDelete.length}`);

                for (const filename of filesToDelete) {
                    await deleteFile(filename);
                }

                if (filesToDelete.length > 0) {
                    // showModal(`成功删除 ${filesToDelete.length} 个旧存档。`);
                } else {
                    console.log(`没有需要删除的旧存档 (${localStorageKey})。`);
                }
            }
        } catch (error) {
            console.error("清理旧存档出错:", error);
            showModal("清理旧存档出错! 请检查控制台。");
        }
    }

    // 下载配置
    function downloadConfig(localStorageKey, filename) {
        return request({
            method: 'GET',
            path: filename
        }).then(res => {
            const configData = res.responseText;
            localStorage.setItem(localStorageKey, configData);
            return configData;
        }).catch(error => {
            console.error("Error downloading config:", error);
            throw error;
        });
    }

    // 导入所有配置
    async function downloadLatestConfig() {
        try {
            const matchingDates = await getMatchingDates();

            if (matchingDates.length === 0) {
                showModal("没有找到匹配的存档日期!");
                return;
            }
            const latestDate = matchingDates.sort((a, b) => {
                const dateA = new Date(a.replace(/_/g, '-')); // 将 _ 替换为 -,以便 Date 对象可以正确解析
                const dateB = new Date(b.replace(/_/g, '-'));
                return dateB - dateA;
            })[0];

            if (!latestDate) {
                showModal("没有找到最新的存档日期!");
                return;
            }

            const failedDownloads = [];
            const downloadedKeys = [];

            for (let i = 0; i < localStorage.length; i++) {
                const localStorageKey = localStorage.key(i);
                if (!localStorageKey) {
                    continue;
                }
                if (isValidLocalStorageKey(localStorageKey)) {
                    const filename = localStorageKey + "_" + latestDate + ".txt";
                    try {
                        const res = await request({
                            method: 'GET',
                            path: filename
                        });
                        const configData = res.responseText;
                        localStorage.setItem(localStorageKey, configData);
                        console.log(`配置文件 [${filename}] 下载成功并保存到 localStorageKey [${localStorageKey}]!`);
                        downloadedKeys.push(localStorageKey);
                    } catch (error) {
                        console.warn(`下载配置文件 [${filename}] 出错 (localStorageKey: ${localStorageKey}):`, error);
                        failedDownloads.push(filename);
                    }
                }
            }

            if (failedDownloads.length > 0) {
                showModal(`以下配置文件下载失败: ${failedDownloads.join(', ')}。请检查控制台。`, null, () => {
                    window.location.reload();
                });
            } else if (downloadedKeys.length > 0) {
                showModal(`${latestDate} 存档下载成功!`, null, () => {
                    window.location.reload();
                });
            } else {
                showModal("没有找到可下载的存档!", null, () => {
                    window.location.reload();
                });
            }

        } catch (error) {
            console.error("下载最新配置文件出错:", error);
            showModal("下载最新配置文件出错! 请检查控制台。");
        }
    }


    function getCurrentDate() {
        const now = new Date();
        const utc = now.getTime() + (now.getTimezoneOffset() * 60000);
        const chinaTime = new Date(utc + (3600000 * 8));

        const year = chinaTime.getFullYear();
        const month = (chinaTime.getMonth() + 1).toString().padStart(2, '0');
        const day = chinaTime.getDate().toString().padStart(2, '0');
        const hours = chinaTime.getHours().toString().padStart(2, '0');
        const minutes = chinaTime.getMinutes().toString().padStart(2, '0');
        const seconds = chinaTime.getSeconds().toString().padStart(2, '0');

        return `${year}_${month}_${day}_${hours}_${minutes}_${seconds}`;
    }

    // 创建目录
    function createDirectory() {
        return request({
            method: 'PROPFIND',
            headers: {depth: 0}
        }).catch(error => {
            if (error && error.status === 404) {
                console.log("目录不存在,尝试创建它。");
                return request({
                    method: 'MKCOL'
                }).then(() => {
                    console.log(`创建目录: ${webdavDirectory}`);
                }).catch(createError => {
                    console.error(`创建目录出错: ${webdavDirectory}`, createError);
                    throw createError;
                });
            } else {
                console.error("PROPFIND 请求失败:", error);
                throw error;
            }
        });
    }

    // 上传存档
    async function uploadSave() {
        try {
            try {
                await createDirectory();
            } catch (createError) {
                console.error("创建目录失败:", createError);
                if (createError.status === 401 || createError.status === 403) {
                    showModal("创建目录失败: 账户密码错误或无权限。请检查你的 WebDAV 账户和密码。");
                } else if (createError.status === 0) {
                    showModal("创建目录失败: 无法连接到 WebDAV 服务器。请检查网络连接或服务器地址。");
                } else {
                    showModal(`创建目录失败: 服务器错误 (状态码 ${createError.status})。请检查服务器状态。`);
                }
                return;
            }

            const currentDate = getCurrentDate();

            for (let i = 0; i < localStorage.length; i++) {
                const localStorageKey = localStorage.key(i);
                if (isValidLocalStorageKey(localStorageKey)) {
                    const filename = localStorageKey + "_" + currentDate + ".txt";
                    const data = localStorage.getItem(localStorageKey);

                    if (!data) {
                        console.warn(`localStorageKey [${localStorageKey}] 没有数据!`);
                        continue;
                    }

                    try {
                        await request({
                            method: 'PUT',
                            path: filename,
                            data: data,
                            headers: {
                                'Content-Type': 'text/plain'
                            }
                        });

                        console.log(`游戏存档文件 [${filename}] 上传成功 (localStorageKey: ${localStorageKey})。`);
                    } catch (uploadError) {
                        console.error(`上传文件 ${filename} 失败:`, uploadError);

                        if (uploadError.status === 401 || uploadError.status === 403) {
                            showModal(`上传文件 ${filename} 失败: 账户密码错误或无权限。请检查你的 WebDAV 账户和密码。`);
                            return;
                        } else if (uploadError.status === 0) {
                            showModal(`上传文件 ${filename} 失败: 无法连接到 WebDAV 服务器。请检查网络连接或服务器地址。`);
                            return;
                        } else {
                            showModal(`上传文件 ${filename} 失败: 服务器错误 (状态码 ${uploadError.status})。请检查服务器状态。`);
                            return;
                        }
                    }
                }
            }

            showModal("所有存档上传完成!", null, async () => {
                await cleanupOldBackups();
            });

        } catch (error) {
            console.error("上传游戏存档文件出错:", error);
            showModal("上传游戏存档文件出错! 请检查控制台。");
        }
    }

    // 获取所有匹配的日期
    async function getMatchingDates() {
        const localStorageKeys = [];
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (isValidLocalStorageKey(key)) {
                localStorageKeys.push(key);
            }
        }
        const files = await getFileList();
        console.log("getFileList() 返回的文件列表:", files);
        const dateToKeys = {};

        files.forEach(file => {
            const parts = file.split('_');
            if (parts.length > 6) {
                const key = parts.slice(0, parts.length - 6).join('_');
                let date = parts.slice(parts.length - 6).slice(0, 6).join('_');

                if (date.endsWith(".txt")) {
                    date = date.slice(0, -4);
                }

                if (!dateToKeys[date]) {
                    dateToKeys[date] = new Set();
                }
                dateToKeys[date].add(key);

            }
        });

        console.log("dateToKeys:", dateToKeys);
        console.log("localStorageKeys:", localStorageKeys);

        const matchingDates = Object.entries(dateToKeys)
            .filter(([date, keys]) => {
                const allKeysPresent = localStorageKeys.every(localStorageKey => keys.has(localStorageKey));
                return allKeysPresent;
            })
            .map(([date, ]) => date);

        return matchingDates;
    }


    // 下载指定日期的配置
    async function downloadSpecificConfig() {
        const matchingDates = await getMatchingDates();

        if (matchingDates.length === 0) {
            showModal("没有找到匹配的存档日期!");
            return;
        }

        const select = document.createElement('select');
        select.className = 'custom-select';
        matchingDates.forEach(date => {
            const option = document.createElement('option');
            option.value = date;
            option.textContent = date;
            select.appendChild(option);
        });

        showModal("选择要下载的存档日期:", select, async () => {
            const selectedDate = select.value;
            const failedDownloads = [];
            const downloadedKeys = [];

            try {
                for (let i = 0; i < localStorage.length; i++) {
                    const localStorageKey = localStorage.key(i);
                    if (isValidLocalStorageKey(localStorageKey)) {
                        if (!localStorageKey) {
                            continue;
                        }
                        const filename = localStorageKey + "_" + selectedDate + ".txt";
                        try {
                            const res = await request({
                                method: 'GET',
                                path: filename
                            });
                            const configData = res.responseText;
                            localStorage.setItem(localStorageKey, configData);
                            console.log(`配置文件 [${filename}] 下载成功并保存到 localStorageKey [${localStorageKey}]!`);
                            downloadedKeys.push(localStorageKey);
                        } catch (error) {
                            console.warn(`下载配置文件 [${filename}] 出错 (localStorageKey: ${localStorageKey}):`, error);
                            failedDownloads.push(filename);
                        }
                    }
                }

                if (failedDownloads.length > 0) {
                    showModal(`以下配置文件下载失败: ${failedDownloads.join(', ')}。请检查控制台。`, null, () => {
                        window.location.reload();
                    });
                } else if (downloadedKeys.length > 0) {
                    showModal(`${selectedDate} 存档下载成功!`, null, () => {
                        window.location.reload();
                    });
                } else {
                    showModal("没有找到可下载的存档!", null, () => {
                        window.location.reload();
                    });
                }
            } catch (error) {
                console.error("下载指定日期的配置文件出错:", error);
                showModal("下载指定日期的配置文件出错! 请检查控制台。");
            }
        }, () => {}, true);
    }



    // 初始化
    function initialize() {
            console.log("WebDAV 初始化完成。");

            unsafeWindow.webdavDemo = {
                downloadLatest: downloadLatestConfig,
                list: getFileList,
                upload: uploadSave,
                downloadSpecific: downloadSpecificConfig
            };
    }

    initialize();

    let topButtonContainer;
    let arrowIndicator;

    function createTopButtonContainer() {
        topButtonContainer = document.createElement('div');
        topButtonContainer.id = 'topButtonContainer';

        const downloadLatestButton = document.createElement('button');
        downloadLatestButton.textContent = '下载最新配置';
        downloadLatestButton.addEventListener('click', downloadLatestConfig);
        topButtonContainer.appendChild(downloadLatestButton);

        const uploadButton = document.createElement('button');
        uploadButton.textContent = '上传所有存档';
        uploadButton.addEventListener('click', uploadSave);
        topButtonContainer.appendChild(uploadButton);

        const downloadSpecificButton = document.createElement('button');
        downloadSpecificButton.textContent = '下载指定配置';
        downloadSpecificButton.addEventListener('click', downloadSpecificConfig);
        topButtonContainer.appendChild(downloadSpecificButton);

        document.body.insertBefore(topButtonContainer, document.body.firstChild);

        topButtonContainer.addEventListener('mouseenter', () => {
            topButtonContainer.classList.add('show');
            arrowIndicator.classList.add('hidden'); // 鼠标进入按钮栏时隐藏箭头
        });

        topButtonContainer.addEventListener('mouseleave', () => {
            topButtonContainer.classList.remove('show');
            arrowIndicator.classList.remove('hidden');
        });
    }

    function createArrowIndicator() {
        arrowIndicator = document.createElement('div');
        arrowIndicator.id = 'arrowIndicator';
        arrowIndicator.textContent = '▼';
        document.body.insertBefore(arrowIndicator, document.body.firstChild);

        arrowIndicator.addEventListener('mouseenter', () => {
            topButtonContainer.classList.add('show');
            arrowIndicator.classList.add('hidden');
        });

        arrowIndicator.addEventListener('mouseleave', () => {
            topButtonContainer.classList.remove('show');
            arrowIndicator.classList.remove('hidden');
        });

        arrowIndicator.addEventListener('touchstart', (event) => {
            event.preventDefault();
            topButtonContainer.classList.toggle('show');
            arrowIndicator.classList.toggle('hidden');

            if (topButtonContainer.classList.contains('show')) {
                document.addEventListener('touchstart', hideOnOutsideClick);
            }
        });
    }

    function hideOnOutsideClick(event) {
        if (!topButtonContainer.contains(event.target) && event.target !== arrowIndicator) {
            topButtonContainer.classList.remove('show');
            arrowIndicator.classList.remove('hidden');
            document.removeEventListener('touchstart', hideOnOutsideClick);
        }
    }

 let floatingButtonContainer;

    function createFloatingButton() {
        floatingButtonContainer = document.createElement('div');
        floatingButtonContainer.id = 'floatingButtonContainer';

        // 上传按钮
        const uploadButton = document.createElement('button');
        uploadButton.addEventListener('click', uploadSave);
        uploadButton.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 20px; height: 20px; margin-right: 5px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
        </svg>
        <span>上传</span>
    `;
        floatingButtonContainer.appendChild(uploadButton);

        // 下载最新配置按钮
        const downloadLatestButton = document.createElement('button');
        downloadLatestButton.addEventListener('click', downloadLatestConfig);
        downloadLatestButton.innerHTML = `
        <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 20px; height: 20px; margin-right: 5px;">
          <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
        </svg>
        <span>最新</span>
    `;
        floatingButtonContainer.appendChild(downloadLatestButton);

        // 下载指定配置按钮
        const downloadSpecificButton = document.createElement('button');
        downloadSpecificButton.addEventListener('click', downloadSpecificConfig);
        downloadSpecificButton.innerHTML = `
       <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" style="width: 20px; height: 20px; margin-right: 5px;">
         <path stroke-linecap="round" stroke-linejoin="round" d="M7.5 7.5h-.75A2.25 2.25 0 0 0 4.5 9.75v7.5a2.25 2.25 0 0 0 2.25 2.25h7.5a2.25 2.25 0 0 0 2.25-2.25v-7.5a2.25 2.25 0 0 0-2.25-2.25h-.75m-6 3.75 3 3m0 0 3-3m-3 3V1.5m6 9h.75a2.25 2.25 0 0 1 2.25 2.25v7.5a2.25 2.25 0 0 1-2.25 2.25h-7.5a2.25 2.25 0 0 1-2.25-2.25v-.75" />
       </svg>
        <span>指定</span>
    `;
        floatingButtonContainer.appendChild(downloadSpecificButton);

        document.body.insertBefore(floatingButtonContainer, document.body.firstChild);
    }

    // 保存设置函数
    function promptAndSave(key, message, defaultValue, type = 'text') {
        let promptMessage = message + ' (默认为: ' + defaultValue + ')';
        let value = prompt(promptMessage, GM_getValue(key, defaultValue));

        if (value !== null) {
            if (type === 'number') {
                value = parseInt(value, 10);
                if (isNaN(value)) {
                    alert('无效的数字,请重新设置。');
                    return;
                }
            } else if (type === 'boolean') {
                value = (value.toLowerCase() === 'true');
            } else if (type === 'regex') {
                try {
                    new RegExp(value); // 验证正则表达式
                } catch (e) {
                    alert('无效的正则表达式,请重新设置。');
                    return;
                }
            }

            GM_setValue(key, value);
            // 根据key更新变量的值
            if (key === 'url') {
                url = value;
            } else if (key === 'username') {
                username = value;
            } else if (key === 'password') {
                password = value;
            } else if (key === 'backupCount') {
                backupCount = value;
            } else if (key === 'showFloatingTopButton') {
                showFloatingTopButton = value;
            } else if (key === 'showFloatingMenu') {
                showFloatingMenu = value;
            } else if (key === 'blacklistedKeyPatterns') {
                 try {
                    blacklistedKeyPatterns = value.split(',').map(s => s.trim()).map(source => new RegExp(source, 'i'));
                    GM_setValue(key, value.split(',').map(s => s.trim())); // 保存为字符串数组
                 } catch (e) {
                    alert("无效的正则表达式列表,请使用逗号分隔。");
                    return;
                 }

            }
            alert(message + ' 已保存: ' + value);
        }
    }

    window.addEventListener('load', () => {
        if (showFloatingTopButton) {
            createTopButtonContainer();
            createArrowIndicator();
        }
        if (showFloatingMenu) {
            createFloatingButton();
        }
    });


    GM_registerMenuCommand("上传所有存档", uploadSave);
    GM_registerMenuCommand("下载最新配置", downloadLatestConfig);
    GM_registerMenuCommand("下载指定配置", downloadSpecificConfig);
    GM_registerMenuCommand("设置 WebDAV URL", () => promptAndSave('url', '请输入 WebDAV URL', defaultUrl));
    GM_registerMenuCommand("设置 WebDAV 用户名", () => promptAndSave('username', '请输入 WebDAV 用户名', defaultUsername));
    GM_registerMenuCommand("设置 WebDAV 密码", () => promptAndSave('password', '请输入 WebDAV 密码', defaultPassword));
    GM_registerMenuCommand("设置 备份数量", () => promptAndSave('backupCount', '请输入 备份数量', defaultBackupCount, 'number'));
    GM_registerMenuCommand("设置 显示顶部悬浮按钮", () => promptAndSave('showFloatingTopButton', '是否显示顶部悬浮按钮 (true/false)', defaultShowFloatingTopButton, 'boolean'));
    GM_registerMenuCommand("设置 显示悬浮菜单", () => promptAndSave('showFloatingMenu', '是否显示悬浮菜单 (true/false)', defaultShowFloatingMenu, 'boolean'));
    GM_registerMenuCommand("设置 屏蔽的 Key 关键词 (逗号分隔)", () => {
        const defaultPatternsString = defaultBlacklistedKeyPatterns.map(regex => regex.source).join(', ');
        promptAndSave('blacklistedKeyPatterns', '请输入需要屏蔽的 localStorage Key 关键词 (使用逗号分隔, 忽略大小写)', defaultPatternsString, 'regex');
    });

})();