Garmin Connect 训练计划批量清理 & 导出 - 增强版 V2.3

批量识别和删除 Garmin Connect 日历中指定月份的训练计划,新增:在日历页面批量导出运动记录(GPX);支持在“我的训练”页面批量删除训练。

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Garmin Connect 训练计划批量清理 & 导出 - 增强版 V2.3
// @namespace    http://tampermonkey.net/
// @version      2.3
// @license      MIT
// @description  批量识别和删除 Garmin Connect 日历中指定月份的训练计划,新增:在日历页面批量导出运动记录(GPX);支持在“我的训练”页面批量删除训练。
// @author       qinjian
// @match        https://connect.garmin.cn/modern/calendar*
// @match        https://connect.garmin.com/modern/calendar*
// @match        https://connect.garmin.cn/modern/workouts*
// @match        https://connect.garmin.com/modern/workouts*
// @grant        none
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // =================================================================================================
    // 1. 数据存储和状态
    // =================================================================================================
    let cachedCalendarItems = []; // 日历页所有数据(包括计划和活动)
    let currentContext = null;    // 日历页上下文
    let workoutCheckInterval = null;

    // =================================================================================================
    // 2. 样式注入
    // =================================================================================================
    const STYLES = `
        /* 批量删除按钮(日历页) */
        #garmin-batch-del-btn {
            background-color: #9c27b0; 
            color: white; 
            border: none; 
            padding: 6px 15px; 
            border-radius: 4px; 
            font-size: 14px; 
            font-weight: bold; 
            cursor: pointer; 
            display: inline-block;
            margin-left: 20px;
        }
        #garmin-batch-del-btn:disabled { background-color: #ccc; cursor: not-allowed; }

        /* 批量导出按钮(日历页 V2.3 新增) */
        #garmin-batch-export-btn {
            background-color: #007bff; /* 蓝色 */
            color: white; 
            border: none; 
            padding: 6px 15px; 
            border-radius: 4px; 
            font-size: 14px; 
            font-weight: bold; 
            cursor: pointer; 
            display: inline-block;
            margin-left: 10px;
        }
        #garmin-batch-export-btn:disabled { background-color: #ccc; cursor: not-allowed; }

        /* 批量删除按钮(训练列表页) */
        #garmin-batch-del-workouts-btn {
            background-color: #d9534f; 
            color: white; 
            border: none; 
            padding: 6px 15px; 
            border-radius: 4px; 
            font-size: 14px; 
            font-weight: bold; 
            cursor: pointer; 
            display: inline-block;
            margin-left: 10px;
        }

        /* 模态框样式 */
        #gc-modal-overlay {
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: rgba(0,0,0,0.6); z-index: 9999; display: flex;
            justify-content: center; align-items: center; font-family: 'Open Sans', sans-serif;
        }
        #gc-modal-box {
            background: #fff; width: 500px; max-height: 80vh; border-radius: 8px;
            box-shadow: 0 4px 20px rgba(0,0,0,0.3); display: flex; flex-direction: column;
            overflow: hidden;
        }
        #gc-modal-header {
            padding: 15px 20px; background: #f5f7fa; border-bottom: 1px solid #eee;
            display: flex; justify-content: space-between; align-items: center;
        }
        #gc-modal-title { font-size: 16px; font-weight: bold; color: #333; }
        #gc-close-btn { cursor: pointer; font-size: 20px; color: #999; }
        #gc-modal-body { padding: 0; overflow-y: auto; flex-grow: 1; background: #fff; }
        .gc-list-item {
            display: flex; align-items: center; padding: 10px 20px; border-bottom: 1px solid #eee;
            transition: background 0.2s; background: #fcf8ff;
        }
        .gc-list-item:hover { background-color: #f5f0ff; }
        .gc-checkbox { transform: scale(1.3); margin-right: 15px; cursor: pointer; }
        .gc-item-info { flex-grow: 1; }
        .gc-visual-tag {
            display: inline-block; width: 8px; height: 8px; border-radius: 50%; 
            margin-right: 6px; box-shadow: 0 0 4px #ccc;
        }
        /* 类型颜色标签 */
        .tag-delete { background-color: #9c27b0; box-shadow: 0 0 4px #ce93d8; } /* 计划/删除 */
        .tag-export { background-color: #007bff; box-shadow: 0 0 4px #80bdff; } /* 活动/导出 */

        .gc-item-title { font-size: 14px; font-weight: 600; color: #333; }
        .gc-item-detail { font-size: 12px; color: #888; margin-top: 3px; display: flex; justify-content: space-between;}
        #gc-modal-footer {
            padding: 15px 20px; border-top: 1px solid #eee; background: #fff;
            display: flex; justify-content: space-between; align-items: center;
        }
        .gc-btn { padding: 8px 20px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; }
        .gc-btn-cancel { background: #f0f0f0; color: #333; }
        
        /* 动态按钮颜色 */
        .gc-btn-action-delete { background: #d9534f; color: #fff; }
        .gc-btn-action-delete:disabled { background: #f0ad4e; opacity: 0.7; cursor: not-allowed;}
        
        .gc-btn-action-export { background: #28a745; color: #fff; } /* 绿色导出确认键 */
        .gc-btn-action-export:disabled { background: #8fd19e; opacity: 0.7; cursor: not-allowed;}

        #gc-select-all-area { font-size: 14px; display: flex; align-items: center;}
    `;

    const styleSheet = document.createElement("style");
    styleSheet.innerText = STYLES;
    document.head.appendChild(styleSheet);


    // =================================================================================================
    // 3. 通用辅助函数
    // =================================================================================================

    function getCsrfToken() {
        const metaTag = document.querySelector('meta[name="csrf-token"]');
        return metaTag ? metaTag.content : null;
    }

    // --- 删除逻辑 ---
    async function deleteItem(id, type = 'schedule') {
        const csrfToken = getCsrfToken();
        if (!csrfToken) return false;

        const domain = window.location.origin;
        const apiPath = type === 'workout' ? 'workout' : 'schedule';
        const deleteUrl = `${domain}/gc-api/workout-service/${apiPath}/${id}`;

        try {
            const response = await fetch(deleteUrl, {
                method: 'DELETE',
                headers: { 'connect-csrf-token': csrfToken, 'Content-Type': 'application/json' }
            });
            return response.status === 204;
        } catch (error) {
            console.error(`删除出错 ID: ${id}`, error);
            return false;
        }
    }

    // --- V2.3 新增:导出逻辑 ---
    async function exportItem(id) {
        const csrfToken = getCsrfToken();
        const domain = window.location.origin;
        // 目标 URL: /gc-api/download-service/export/gpx/activity/{id}
        const exportUrl = `${domain}/gc-api/download-service/export/gpx/activity/${id}`;

        try {
            const response = await fetch(exportUrl, {
                method: 'GET',
                headers: { 'connect-csrf-token': csrfToken }
            });

            if (response.status === 200) {
                // 将响应转为 Blob,然后创建下载链接
                const blob = await response.blob();
                const url = window.URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.style.display = 'none';
                a.href = url;
                a.download = `activity_${id}.gpx`; // 设置文件名
                document.body.appendChild(a);
                a.click();
                window.URL.revokeObjectURL(url);
                a.remove();
                return true;
            } else {
                console.error(`导出失败 ID ${id}, Status: ${response.status}`);
                return false;
            }
        } catch (error) {
            console.error(`导出出错 ID: ${id}`, error);
            return false;
        }
    }


    // =================================================================================================
    // 4. 日历页面逻辑
    // =================================================================================================

    function getContextFromUrl(url) {
        const match = url.match(/\/year\/(\d{4})\/month\/(\d{1,2})/);
        if (match) {
            let year = parseInt(match[1]);
            let urlMonth = parseInt(match[2]); 
            let monthIndex = urlMonth; 
            if (urlMonth === 12) {
                monthIndex = 0; 
                year += 1;
            }
            return {
                year: year,
                monthIndex: monthIndex, 
                display: `${year}年${monthIndex + 1}月`
            };
        }
        return null;
    }

    function getContextFromDom() {
        const header = document.querySelector('.calendar-header');
        if (!header) return null;
        const headerText = header.querySelector('.calendar-date-wrapper .calendar-date') || header;
        
        let cnDateMatch = headerText.innerText.match(/(\d{1,2})月\s*(\d{4})/);
        if (cnDateMatch) {
              return { 
                year: parseInt(cnDateMatch[2], 10), 
                monthIndex: parseInt(cnDateMatch[1], 10) - 1, 
                display: `${cnDateMatch[2]}年${cnDateMatch[1]}月` 
            };
        }
        cnDateMatch = headerText.innerText.match(/(\d{4}).*?(\d{1,2})月/);
        if (cnDateMatch) {
              return { 
                year: parseInt(cnDateMatch[1], 10), 
                monthIndex: parseInt(cnDateMatch[2], 10) - 1, 
                display: `${cnDateMatch[1]}年${cnDateMatch[2]}月` 
            };
        }
        return null;
    }
    
    function isItemInCurrentMonth(item, currentContext) {
        if (!currentContext || !item.date) return false;
        const itemDate = new Date(item.date);
        if (isNaN(itemDate.getTime())) return false;
        return (itemDate.getFullYear() === currentContext.year) &&
               (itemDate.getMonth() === currentContext.monthIndex);
    }

    // 收集目标项目 (V2.3 修改:增加 'activity' 类型)
    function collectTargetItems(calendarItems, context) {
        const targetItems = [];
        // workout/trainingPlan 用于删除, activity 用于导出
        const targetTypes = ['workout', 'trainingPlan', 'activity']; 

        calendarItems.forEach(item => {
            if (targetTypes.includes(item.itemType)) {
                if (isItemInCurrentMonth(item, context)) {
                    targetItems.push({
                        id: item.id,
                        title: item.title,
                        date: item.date || '未知日期',
                        itemType: item.itemType
                    });
                }
            }
        });

        targetItems.sort((a, b) => new Date(a.date) - new Date(b.date));
        return targetItems;
    }

    /**
     * 渲染模态框 (V2.3 升级:支持 Delete 和 Export 两种模式)
     * @param {Array} items - 待展示的数据
     * @param {String} monthTitle - 月份标题
     * @param {String} mode - 'delete' 或 'export'
     */
    function renderModal(items, monthTitle, mode = 'delete') {
        const old = document.getElementById('gc-modal-overlay');
        if (old) old.remove();

        // 根据模式设置文案和颜色
        const isDelete = mode === 'delete';
        const actionText = isDelete ? '删除' : '导出 GPX';
        const confirmBtnClass = isDelete ? 'gc-btn-action-delete' : 'gc-btn-action-export';
        const confirmBtnText = isDelete ? '确认删除' : '开始导出';
        const visualTagClass = isDelete ? 'tag-delete' : 'tag-export';
        
        const overlay = document.createElement('div');
        overlay.id = 'gc-modal-overlay';
        
        overlay.innerHTML = `
            <div id="gc-modal-box">
                <div id="gc-modal-header">
                    <div id="gc-modal-title">${actionText}: ${monthTitle} (共 ${items.length} 项)</div>
                    <div id="gc-close-btn">✕</div>
                </div>
                <div id="gc-modal-body"></div>
                <div id="gc-modal-footer">
                    <div id="gc-select-all-area">
                        <input type="checkbox" id="gc-select-all" checked class="gc-checkbox" style="margin-right:8px">
                        <label for="gc-select-all">全选 (${items.length}/${items.length})</label>
                    </div>
                    <div>
                        <button class="gc-btn gc-btn-cancel" id="gc-btn-cancel">取消</button>
                        <button class="gc-btn ${confirmBtnClass}" id="gc-btn-confirm">${confirmBtnText}</button>
                    </div>
                </div>
            </div>
        `;

        document.body.appendChild(overlay);

        const listContainer = overlay.querySelector('#gc-modal-body');
        
        items.forEach(item => {
            const row = document.createElement('div');
            row.className = 'gc-list-item';
            let dateStr = item.date.substring(5, 10) || '--/--'; 
            
            // 类型显示名称
            let typeName = '未知';
            if (item.itemType === 'trainingPlan') typeName = '计划';
            if (item.itemType === 'workout') typeName = '单次训练';
            if (item.itemType === 'activity') typeName = '已完成活动';

            row.innerHTML = `
                <input type="checkbox" class="gc-checkbox item-chk" data-id="${item.id}" checked>
                <div class="gc-item-info">
                    <div class="gc-item-title">
                        <span class="gc-visual-tag ${visualTagClass}"></span>
                        ${item.title || '无标题'}
                    </div>
                    <div class="gc-item-detail">
                        <span>日期: ${dateStr}</span>
                        <span style="font-style: italic;">类型: ${typeName} | ID: ${item.id}</span>
                    </div>
                </div>
            `;
            
            // 点击行也能切换checkbox
            row.onclick = (e) => {
                if (e.target.type !== 'checkbox') {
                    const chk = row.querySelector('.item-chk');
                    chk.checked = !chk.checked;
                    updateBtnState();
                }
            };
            row.querySelector('.item-chk').onchange = (e) => { e.stopPropagation(); updateBtnState(); };
            listContainer.appendChild(row);
        });

        const close = () => overlay.remove();
        overlay.querySelector('#gc-close-btn').onclick = close;
        overlay.querySelector('#gc-btn-cancel').onclick = close;

        const allChk = overlay.querySelector('#gc-select-all');
        const itemChks = overlay.querySelectorAll('.item-chk');
        
        allChk.onchange = () => {
            itemChks.forEach(chk => chk.checked = allChk.checked);
            updateBtnState();
        };

        function updateBtnState() {
            const checkedCount = overlay.querySelectorAll('.item-chk:checked').length;
            const delBtn = overlay.querySelector('#gc-btn-confirm');
            const allLabel = overlay.querySelector('#gc-select-all-area label');
            
            delBtn.innerText = checkedCount > 0 ? `${actionText}选中的 (${checkedCount})` : '请选择';
            delBtn.disabled = checkedCount === 0;
            allLabel.innerText = `全选 (${checkedCount}/${itemChks.length})`;
            
            if(checkedCount === itemChks.length) allChk.checked = true;
            else if(checkedCount === 0) allChk.checked = false;
            allChk.indeterminate = checkedCount > 0 && checkedCount < itemChks.length;
        }

        // 确认按钮逻辑
        overlay.querySelector('#gc-btn-confirm').onclick = async () => {
            const selectedChks = overlay.querySelectorAll('.item-chk:checked');
            const idsToProcess = Array.from(selectedChks).map(chk => chk.dataset.id);
            
            if (idsToProcess.length === 0) return;

            // 确认提示
            const msg = isDelete 
                ? `⚠️ 最终确认:\n真的要永久删除这 ${idsToProcess.length} 个训练计划吗?`
                : `📥 准备导出:\n即将开始下载 ${idsToProcess.length} 个 GPX 文件。`;
            
            if (!confirm(msg)) return;

            const confirmBtn = overlay.querySelector('#gc-btn-confirm');
            const cancelBtn = overlay.querySelector('#gc-btn-cancel');
            cancelBtn.style.display = 'none'; 
            allChk.disabled = true;
            itemChks.forEach(c => c.disabled = true);

            let successCount = 0;
            
            for (let i = 0; i < idsToProcess.length; i++) {
                const id = idsToProcess[i];
                confirmBtn.innerText = `处理中... ${i + 1}/${idsToProcess.length}`;
                
                let success = false;
                if (isDelete) {
                    success = await deleteItem(id, 'schedule');
                } else {
                    success = await exportItem(id);
                }

                if (success) {
                    successCount++;
                    const itemRow = overlay.querySelector(`[data-id="${id}"]`).closest('.gc-list-item');
                    if(itemRow) {
                        itemRow.style.opacity = '0.5';
                        itemRow.style.textDecoration = isDelete ? 'line-through' : 'none';
                        if (!isDelete) itemRow.style.background = '#e8f5e9'; // 导出成功变绿
                    }
                }
                // 导出时稍微增加间隔,防止浏览器阻止并发下载
                const delay = isDelete ? 100 : 800; 
                await new Promise(r => setTimeout(r, delay)); 
            }

            alert(`处理完成!\n成功${actionText}: ${successCount} 项。`);
            overlay.remove();
            if (isDelete) location.reload(); // 仅删除需要刷新,导出不需要
        };

        updateBtnState();
    }

    // 打开选择模态框 (入口函数,区分模式)
    function openSelectionModal(mode = 'delete') {
        let contextToUse = currentContext || getContextFromDom();
        
        if (!contextToUse) {
            alert('错误:无法确定当前的日历年月信息。请刷新页面重试。');
            return;
        }

        if (cachedCalendarItems.length === 0) {
            alert(`当前 ${contextToUse.display} 未检测到数据,请确保页面已完全加载或刷新。`);
            return;
        }

        // 筛选数据:删除模式只看计划,导出模式只看活动
        let filteredItems = [];
        if (mode === 'delete') {
            filteredItems = cachedCalendarItems.filter(i => i.itemType === 'workout' || i.itemType === 'trainingPlan');
        } else {
            filteredItems = cachedCalendarItems.filter(i => i.itemType === 'activity');
        }

        if (filteredItems.length === 0) {
            const msg = mode === 'delete' 
                ? `当前 ${contextToUse.display} 没有可删除的训练计划。` 
                : `当前 ${contextToUse.display} 没有可导出的已完成活动。`;
            alert(msg);
            return;
        }
        
        renderModal(filteredItems, contextToUse.display, mode);
    }

    // 检查视图并注入日历按钮 (V2.3 更新:注入两个按钮)
    function checkViewAndInject() {
        const url = location.href;
        const isCalendarView = url.includes('/modern/calendar');
        const isWeekView = url.includes('/week/');
        const isYearView = url.includes('/year/') && !url.includes('/month/');
        
        const headerContainer = document.querySelector('.calendar-header'); 
        let delBtn = document.getElementById('garmin-batch-del-btn');
        let exportBtn = document.getElementById('garmin-batch-export-btn');

        if (isCalendarView && !isWeekView && !isYearView && headerContainer) {
            const toolbar = headerContainer.querySelector('.calendar-header-toolbar > div:first-child') || headerContainer;

            // 1. 注入删除按钮
            if (!delBtn) {
                delBtn = document.createElement('button');
                delBtn.id = 'garmin-batch-del-btn';
                delBtn.innerText = '🗑️ 批量删除计划';
                delBtn.onclick = () => openSelectionModal('delete');
                toolbar.appendChild(delBtn);
            }
            // 2. 注入导出按钮 (V2.3 新增)
            if (!exportBtn) {
                exportBtn = document.createElement('button');
                exportBtn.id = 'garmin-batch-export-btn';
                exportBtn.innerText = '📥 批量导出记录 (GPX)';
                exportBtn.onclick = () => openSelectionModal('export');
                toolbar.appendChild(exportBtn);
            }

            delBtn.style.display = 'inline-block';
            exportBtn.style.display = 'inline-block';
            
            // 按钮启用状态简单控制(只要有上下文就启用,具体点击后再细分)
            const isDisabled = !currentContext;
            delBtn.disabled = isDisabled;
            exportBtn.disabled = isDisabled;

        } else {
            if (delBtn) delBtn.style.display = 'none';
            if (exportBtn) exportBtn.style.display = 'none';
        }
    }
    
    // API 响应数据处理 (日历页)
    function processCalendarResponse(url, data) {
        const context = getContextFromUrl(url); 
        if (context) {
            const items = data.calendarItems || [];
            // V2.3: collectTargetItems 现已包含 activity
            const targetItems = collectTargetItems(items, context);
            
            cachedCalendarItems = targetItems;
            currentContext = context;

            console.log(`V2.3: 日历数据捕获成功。总项数: ${targetItems.length}`);
            checkViewAndInject();
        }
    }


    // =================================================================================================
    // 5. 训练列表页面逻辑 (保持 V2.2 不变)
    // =================================================================================================
    
    function checkWorkoutsViewAndInject() {
        if (!location.href.includes('/modern/workouts')) return;
        const container = document.querySelector('.tab-pane.active'); 
        const listHeaderRow = document.querySelector('.sortable-header-row'); 
        if (!container || !listHeaderRow) return false;
        
        let delBtn = document.getElementById('garmin-batch-del-workouts-btn');
        let selectAllContainer = document.getElementById('garmin-select-all-workouts-container');
        
        const firstHeaderCell = listHeaderRow.querySelector('th:first-child');
        if (firstHeaderCell && !selectAllContainer) {
            selectAllContainer = document.createElement('div');
            selectAllContainer.id = 'garmin-select-all-workouts-container';
            selectAllContainer.style.cssText = 'display: flex; align-items: center; width: 100%; height: 100%; justify-content: center;';
            selectAllContainer.innerHTML = `<input type="checkbox" id="garmin-select-all-workouts" class="gc-checkbox" style="transform: scale(1.3); margin: 0;">`;
            firstHeaderCell.innerHTML = '';
            firstHeaderCell.appendChild(selectAllContainer);
        }
        
        const createWorkoutForm = container.querySelector('form.bottom-xs');
        if (!delBtn) {
            delBtn = document.createElement('button');
            delBtn.id = 'garmin-batch-del-workouts-btn';
            delBtn.className = 'gc-btn gc-btn-delete'; 
            delBtn.style.cssText = 'padding: 6px 15px; margin-left: 10px;'; 
            delBtn.innerText = '🗑️ 批量删除 (0 项)';
            delBtn.disabled = true;
            delBtn.onclick = confirmAndDeleteWorkouts;
        }
        
        if (createWorkoutForm && delBtn.parentElement !== createWorkoutForm) {
            createWorkoutForm.appendChild(delBtn); 
            const anchor = createWorkoutForm.querySelector('button.create-workout');
            if (anchor) anchor.style.marginRight = '10px';
        } else if (!createWorkoutForm && delBtn.parentNode !== container) {
            container.prepend(delBtn);
        }
        
        injectCheckboxesAndBindEvents();
        return true;
    }
    
    function injectCheckboxesAndBindEvents() {
        const rows = document.querySelectorAll('tbody tr[data-id]'); 
        const selectAllChk = document.getElementById('garmin-select-all-workouts');
        const delBtn = document.getElementById('garmin-batch-del-workouts-btn');
        if (!selectAllChk || !delBtn) return;

        rows.forEach(row => {
            let chk = row.querySelector('.gc-item-checkbox');
            if (!chk) {
                const workoutId = row.getAttribute('data-id');
                if (!workoutId) return; 
                chk = document.createElement('input');
                chk.type = 'checkbox';
                chk.className = 'gc-checkbox gc-item-checkbox';
                chk.setAttribute('data-id', workoutId);
                chk.style.cssText = 'transform: scale(1.3); margin: 0;';
                const firstCell = row.querySelector('td:first-child');
                if (firstCell) {
                    firstCell.innerHTML = ''; 
                    firstCell.style.textAlign = 'center';
                    firstCell.appendChild(chk);
                }
            }
            chk.onchange = updateWorkoutButtonState;
        });

        selectAllChk.onchange = () => {
            document.querySelectorAll('.gc-item-checkbox').forEach(chk => chk.checked = selectAllChk.checked);
            updateWorkoutButtonState();
        };
        updateWorkoutButtonState();
    }
    
    function updateWorkoutButtonState() {
        const itemChks = document.querySelectorAll('.gc-item-checkbox');
        const selectedCount = Array.from(itemChks).filter(chk => chk.checked).length;
        const delBtn = document.getElementById('garmin-batch-del-workouts-btn');
        const selectAllChk = document.getElementById('garmin-select-all-workouts');

        if (!delBtn) return;
        delBtn.innerText = selectedCount > 0 ? `🗑️ 批量删除 (${selectedCount} 项)` : '🗑️ 批量删除 (0 项)';
        delBtn.disabled = selectedCount === 0;
        if (selectAllChk) {
            selectAllChk.checked = itemChks.length > 0 && selectedCount === itemChks.length;
            selectAllChk.indeterminate = selectedCount > 0 && selectedCount < itemChks.length;
        }
    }
    
    async function confirmAndDeleteWorkouts() {
        const selectedChks = document.querySelectorAll('.gc-item-checkbox:checked');
        const idsToDelete = Array.from(selectedChks).map(chk => chk.getAttribute('data-id'));
        if (idsToDelete.length === 0) return;
        if (!confirm(`⚠️ 最终确认:\n真的要永久删除选中的 ${idsToDelete.length} 个训练吗?`)) return;

        const delBtn = document.getElementById('garmin-batch-del-workouts-btn');
        delBtn.disabled = true;
        document.querySelectorAll('.gc-item-checkbox').forEach(c => c.disabled = true);
        
        let successCount = 0;
        for (let i = 0; i < idsToDelete.length; i++) {
            const id = idsToDelete[i];
            delBtn.innerText = `处理中... ${i + 1}/${idsToDelete.length}`;
            const success = await deleteItem(id, 'workout'); 
            if (success) {
                successCount++;
                const itemRow = document.querySelector(`tbody tr[data-id="${id}"]`);
                if(itemRow) { itemRow.style.textDecoration = 'line-through'; itemRow.style.opacity = '0.5'; }
            }
            await new Promise(r => setTimeout(r, 100)); 
        }
        alert(`清理完成!\n成功删除: ${successCount} 项。请刷新页面。`);
        location.reload(); 
    }


    // =================================================================================================
    // 6. API 拦截 (V1.x 代码)
    // =================================================================================================
    
    function hookFetch() {
        if (window.fetch.isHooked) return;
        const originalFetch = window.fetch;
        window.fetch = async function(...args) {
            const response = await originalFetch.apply(this, args);
            const url = args[0] && (typeof args[0] === 'string' ? args[0] : args[0].url);
            
            if (url && url.includes('/gc-api/calendar-service/') && url.includes('/year/') && url.includes('/month/')) {
                if (response.status >= 200 && response.status < 300) {
                    try {
                        const cloned = response.clone();
                        const data = await cloned.json();
                        if (data && data.calendarItems) processCalendarResponse(url, data);
                    } catch (e) { console.error('JSON Error', e); }
                }
            }
            return response;
        };
        window.fetch.isHooked = true;
    }

    function hookXHR() {
        if (window.XMLHttpRequest.isHooked) return;
        const originalOpen = XMLHttpRequest.prototype.open;
        XMLHttpRequest.prototype.open = function(method, url) {
            this._url = url;
            originalOpen.apply(this, arguments);
        };
        const originalSend = XMLHttpRequest.prototype.send;
        XMLHttpRequest.prototype.send = function() {
            if (this._url && this._url.includes('/gc-api/calendar-service/') && this._url.includes('/year/') && this._url.includes('/month/')) {
                this.addEventListener('load', function() {
                    if (this.status >= 200 && this.status < 300) {
                        try {
                            const data = JSON.parse(this.responseText);
                            if (data && data.calendarItems) processCalendarResponse(this._url, data);
                        } catch (e) {}
                    }
                });
            }
            originalSend.apply(this, arguments);
        };
        window.XMLHttpRequest.isHooked = true;
    }


    // =================================================================================================
    // 7. 初始化和页面切换监听
    // =================================================================================================
    
    let lastUrl = location.href;
    const observer = new MutationObserver((mutationsList) => {
        const currentUrl = location.href;
        if (currentUrl !== lastUrl) {
            lastUrl = currentUrl;
            cachedCalendarItems = []; 
            currentContext = null; 
            if (currentUrl.includes('/modern/workouts')) setTimeout(checkWorkoutsViewAndInject, 5000); 
            else if (currentUrl.includes('/modern/calendar')) checkViewAndInject();
        }
        
        if (currentUrl.includes('/modern/workouts')) {
            const isRelevant = mutationsList.some(m => Array.from(m.addedNodes).some(n => n.tagName === 'TBODY'));
            if (isRelevant) checkWorkoutsViewAndInject();
        } else if (currentUrl.includes('/modern/calendar')) {
            checkViewAndInject();
        }
    });

    function initExtension() {
        hookFetch();
        hookXHR(); 
        observer.observe(document.body, { subtree: true, childList: true });
        
        if (location.href.includes('/modern/workouts')) {
            checkWorkoutsViewAndInject(); 
            if (!document.querySelector('.tab-pane.active') || !document.querySelector('.sortable-header-row')) {
                workoutCheckInterval = setInterval(() => {
                    if (checkWorkoutsViewAndInject()) {
                        clearInterval(workoutCheckInterval);
                        workoutCheckInterval = null;
                    }
                }, 100);
            }
        } else if (location.href.includes('/modern/calendar')) {
            checkViewAndInject();
        }
    }

    initExtension();
})();