Twitter/X 媒体批量下载器

批量下载某个 Twitter/X 账号的所有图片和视频(包括受限账号),原始质量。

// ==UserScript==
// @name                Twitter/X Media Batch Downloader
// @name:en             Twitter/X Media Batch Downloader
// @name:zh-CN          Twitter/X 媒体批量下载器
// @name:zh-TW          Twitter/X 媒體批量下載器
// @name:ja             Twitter/X メディア一括ダウンローダー
// @description         Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
// @description:en      Batch download all images and videos from a Twitter/X account, including withheld accounts, in original quality.
// @description:zh-CN   批量下载某个 Twitter/X 账号的所有图片和视频(包括受限账号),原始质量。
// @description:zh-TW   批量下載某個 Twitter/X 帳號的所有圖片和影片(包括受限帳號),原始品質。
// @description:ja      Twitter/X アカウントからすべての画像と動画(制限付きアカウントを含む)を元の品質で一括ダウンロードします。
// @antifeature         payment Unlock access to the Twitter/X Media Batch Downloader script by becoming a paid member! Join the membership to receive your Patreon auth code.
// @antifeature:en      payment Unlock access to the Twitter/X Media Batch Downloader script by becoming a paid member! Join the membership to receive your Patreon auth code.
// @antifeature:zh-CN   payment 解锁 Twitter/X 媒体批量下载器脚本需成为付费会员!加入会员以获取 Patreon 授权码。
// @antifeature:zh-TW   payment 解鎖 Twitter/X 媒體批量下載器腳本需成為付費會員!加入會員以取得 Patreon 授權碼。
// @antifeature:ja      payment Twitter/X メディア一括ダウンローダースクリプトを利用するには有料メンバーになる必要があります。Patreon 会員になって認証コードを取得してください。
// @icon                data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHZlcnNpb249IjEuMSIKICAgIGlkPSJMYXllcl8xIiB3aWR0aD0iNTEycHgiIGhlaWdodD0iNTEycHgiIHZpZXdCb3g9IjAgMCAyNCAyNCIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgMjQgMjQ7IgogICAgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+CiAgICA8ZyBmaWxsPSIjMWRhMGYxIj4KICAgICAgICA8cG9seWdvbgogICAgICAgICAgICBwb2ludHM9IjEyLjE1Mzk5MiwxMC43Mjk1NTMgOC4wODg2ODQsNS4wNDExOTkgNS45MjA0MSw1LjA0MTE5OSAxMC45NTYyOTksMTIuMDg3MDk3IDExLjU5MDIxLDEyLjk3MzQ1IDE1LjkwMDYzNSwxOS4wMDk1ODMgMTguMDY4OTA5LDE5LjAwOTU4MyAxMi43ODUyMTcsMTEuNjE1OTA2ICAiIC8+CiAgICAgICAgPHBhdGgKICAgICAgICAgICAgZD0iTTIxLjE1OTc5LDFIMi44NDAyMUMxLjgyMzg1MywxLDEsMS44MjM4NTMsMSwyLjg0MDIxdjE4LjMxOTU4QzEsMjIuMTc2MTQ3LDEuODIzODUzLDIzLDIuODQwMjEsMjNoMTguMzE5NTggICBDMjIuMTc2MTQ3LDIzLDIzLDIyLjE3NjE0NywyMywyMS4xNTk3OVYyLjg0MDIxQzIzLDEuODIzODUzLDIyLjE3NjE0NywxLDIxLjE1OTc5LDF6IE0xNS4yMzUzNTIsMjBsLTQuMzYyNTQ5LTYuMjEzMDEzICAgTDUuNDExNDM4LDIwSDRsNi4yNDY4ODctNy4xMDQ2NzVMNCw0aDQuNzY0NjQ4bDQuMTMwMTI3LDUuODgxOTU4TDE4LjA2OTU4LDRoMS40MTEzNzdsLTUuOTU2OTcsNi43NzU2MzVMMjAsMjBIMTUuMjM1MzUyeiIgLz4KICAgIDwvZz4KPC9zdmc+Cg==
// @namespace           https://xbatch.online
// @supportURL          https://www.patreon.com/exyezed
// @homepageURL         https://www.patreon.com/exyezed
// @version             5.6
// @author              afkarxyz
// @license             MIT
// @match               https://twitter.com/*
// @match               https://x.com/*
// @grant               GM_xmlhttpRequest
// @grant               GM_addStyle
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dayjs.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/umd/index.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/preact.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/hooks/dist/hooks.umd.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/FileSaver.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/umd/lucide-preact.min.js
// @require             https://cdn.jsdelivr.net/npm/@preact/[email protected]/dist/signals-core.min.js
// @require             https://cdn.jsdelivr.net/npm/[email protected]/dist/dexie-export-import.min.js
// @connect             api.xbatch.online
// @connect             backup.xbatch.online
// @connect             pbs.twimg.com
// @connect             video.twimg.com
// ==/UserScript==

(function() {
    'use strict';

    const { h, render } = preact;
    const { useState, useEffect, useRef } = preactHooks;
    const { signal, effect } = preactSignalsCore;

    const Icon = (iconName, { size = 16, color = 'currentColor', className = '', ...props } = {}) => {
        const IconComponent = LucidePreact[iconName];
        if (!IconComponent) {
            console.warn(`Lucide icon "${iconName}" not found`);
            return null;
        }
        return h(IconComponent, {
            size,
            color,
            class: className,
            ...props
        });
    };

    const renderIcon = (iconName, options = {}) => {
        const tempDiv = document.createElement('div');
        render(Icon(iconName, options), tempDiv);
        return tempDiv.innerHTML;
    };

    const db = new Dexie('TwitterXMediaBatchDownloader');
    db.version(1).stores({
        settings: 'key',
        mediaData: 'username, data, timestamp'
    });

    db.version(2).stores({
        settings: 'key',
        mediaData: 'cacheKey, username, timelineType, mediaType, data, timestamp'
    }).upgrade(tx => {
        return tx.mediaData.toCollection().modify(item => {
            item.cacheKey = `${item.username}_media_all`;
            item.timelineType = 'media';
            item.mediaType = 'all';
        });
    });

    const state = {
        isModalOpen: signal(false),
        activeTab: signal('dashboard'),
        authToken: signal(''),
        patreonAuth: signal(''),
        isVerified: signal(false),
        isLoading: signal(false),
        mediaData: signal(null),
        error: signal(null),
        errorType: signal('general'),
        success: signal(null),
        theme: signal('light'),
        downloadProgress: signal(0),
        currentUsername: signal(''),
        downloadedFiles: signal(0),
        totalFileSize: signal(0),
        selectedApi: signal('default'),
        fetchMode: signal('fresh'),
        selectedCacheUser: signal(null),
        cacheMediaPage: signal(1),
        mediaType: signal('all'),
        timelineType: signal('media'),
        isDownloading: signal(false),
        isDownloadingCurrent: signal(false),
        fetchType: signal('single'),
        batchSize: signal(100),
        startingBatch: signal(0),
        currentBatchPage: signal(0),
        isAutoBatch: signal(false),
        batchedMediaData: signal([]),
        currentBatchData: signal([]),
        loadingDirection: signal(null),
        concurrentLimit: signal(20),
        showBatchDatabase: signal(false),
        loadedFromDatabase: signal(false),
        loadedDatabaseConfig: signal(null)
    };

    async function loadSettings() {
        try {
            const authTokenDoc = await db.settings.get('authToken');
            const patreonAuthDoc = await db.settings.get('patreonAuth');
            const isVerifiedDoc = await db.settings.get('isVerified');
            const themeDoc = await db.settings.get('theme');
            const selectedApiDoc = await db.settings.get('selectedApi');
            const mediaTypeDoc = await db.settings.get('mediaType');
            const timelineTypeDoc = await db.settings.get('timelineType');
            const batchSizeDoc = await db.settings.get('batchSize');
            const startingBatchDoc = await db.settings.get('startingBatch');
            const concurrentLimitDoc = await db.settings.get('concurrentLimit');
            const showBatchDatabaseDoc = await db.settings.get('showBatchDatabase');

            if (authTokenDoc) state.authToken.value = authTokenDoc.value;
            if (patreonAuthDoc) state.patreonAuth.value = patreonAuthDoc.value;
            if (isVerifiedDoc) state.isVerified.value = isVerifiedDoc.value;
            if (themeDoc) state.theme.value = themeDoc.value;
            if (selectedApiDoc) state.selectedApi.value = selectedApiDoc.value;
            if (mediaTypeDoc) state.mediaType.value = mediaTypeDoc.value;
            if (timelineTypeDoc) state.timelineType.value = timelineTypeDoc.value;
            if (batchSizeDoc) state.batchSize.value = batchSizeDoc.value;
            if (startingBatchDoc) state.startingBatch.value = startingBatchDoc.value;
            if (concurrentLimitDoc) state.concurrentLimit.value = concurrentLimitDoc.value;
            if (showBatchDatabaseDoc) state.showBatchDatabase.value = showBatchDatabaseDoc.value;
        } catch (error) {
            console.error('Failed to load settings:', error);
        }
    }

    async function saveSettings() {
        try {
            await db.settings.bulkPut([
                { key: 'authToken', value: state.authToken.value },
                { key: 'patreonAuth', value: state.patreonAuth.value },
                { key: 'isVerified', value: state.isVerified.value },
                { key: 'theme', value: state.theme.value },
                { key: 'selectedApi', value: state.selectedApi.value },
                { key: 'mediaType', value: state.mediaType.value },
                { key: 'timelineType', value: state.timelineType.value },
                { key: 'batchSize', value: state.batchSize.value },
                { key: 'startingBatch', value: state.startingBatch.value },
                { key: 'concurrentLimit', value: state.concurrentLimit.value },
                { key: 'showBatchDatabase', value: state.showBatchDatabase.value }
            ]);
        } catch (error) {
            console.error('Failed to save settings:', error);
        }
    }

    const styles = `
    .tmd-modal-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:9999;display:flex;align-items:center;justify-content:center;animation:fadeIn .2s ease-out;}
    @keyframes fadeIn{from{opacity:0}to{opacity:1}}
    @keyframes slideUp{from{transform:translateY(20px);opacity:0}to{transform:translateY(0);opacity:1}}
    @keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}
    .tmd-modal{width:90%;max-width:600px;max-height:80vh;border-radius:12px;overflow:hidden;animation:slideUp .3s ease-out;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);}
    .tmd-modal.dark{background:hsl(240 5.9% 10%);color:hsl(240 4.8% 95.9%);border:1px solid hsl(240 5% 40% / .5);box-shadow:0 0 0 1px hsl(240 5% 35% / .2),0 20px 25px -5px rgba(0,0,0,.3),0 10px 10px -5px rgba(0,0,0,.2);}
    .tmd-modal.light{background:#fff;color:hsl(240 5.9% 10%);border:1px solid hsl(240 5.9% 90%);}
    .tmd-header{padding:20px;border-bottom:1px solid;display:flex;justify-content:space-between;align-items:center;}
    .dark .tmd-header{border-color:hsl(240 3.7% 15.9%);}
    .light .tmd-header{border-color:hsl(240 5.9% 90%);}
    .tmd-header-title{font-size:18px;font-weight:600;color:hsl(204.17deg 87.55% 52.75%);}
    .tmd-header-controls{display:flex;gap:8px;align-items:center;}
    .tmd-theme-toggle{padding:8px;border-radius:8px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;}
    .dark .tmd-theme-toggle{background:hsl(240 3.7% 15.9%);}
    .dark .tmd-theme-toggle:hover{background:hsl(240 5.3% 26.1%);}
    .light .tmd-theme-toggle{background:hsl(240 5.9% 95%);}
    .light .tmd-theme-toggle:hover{background:hsl(240 5.9% 90%);}
    .tmd-reset-toggle{color:inherit;}
    .dark .tmd-reset-toggle:hover{background:hsl(37.7deg 92.1% 50.2% / .2);color:hsl(37.7deg 92.1% 50.2%);}
    .light .tmd-reset-toggle:hover{background:hsl(37.7deg 92.1% 50.2% / .1);color:hsl(37.7deg 92.1% 50.2%);}
    .tmd-close-btn{padding:8px;border-radius:8px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;}
    .dark .tmd-close-btn{background:hsl(240 3.7% 15.9%);}
    .dark .tmd-close-btn:hover{background:hsl(0deg 84.2% 60.2% / .2);}
    .dark .tmd-close-btn:hover svg{stroke:hsl(0deg 84.2% 60.2%);}
    .light .tmd-close-btn{background:hsl(240 5.9% 95%);}
    .light .tmd-close-btn:hover{background:hsl(0deg 84.2% 60.2% / .1);}
    .light .tmd-close-btn:hover svg{stroke:hsl(0deg 84.2% 60.2%);}
    .tmd-tabs{display:flex;padding:0 20px;gap:16px;border-bottom:1px solid;}
    .dark .tmd-tabs{border-color:hsl(240 3.7% 15.9%);}
    .light .tmd-tabs{border-color:hsl(240 5.9% 90%);}
    .tmd-tab{padding:12px 0;cursor:pointer;border-bottom:2px solid transparent;transition:all .2s;font-weight:500;}
    .dark .tmd-tab{color:hsl(240 5% 64.9%);}
    .light .tmd-tab{color:hsl(240 3.8% 46.1%);}
    .tmd-tab:hover{color:hsl(204.17deg 87.55% 52.75%);}
    .tmd-tab.active{color:hsl(204.17deg 87.55% 52.75%);border-bottom-color:hsl(204.17deg 87.55% 52.75%);}
    .tmd-content{padding:20px;min-height:150px;max-height:calc(80vh - 150px);overflow-y:auto;display:flex;flex-direction:column;}
    .tmd-input-group{margin-bottom:20px;}
    .tmd-label{display:flex;align-items:center;gap:8px;margin-bottom:8px;font-weight:500;}
    .tmd-input{width:100%;padding:10px 12px;padding-right:40px;border-radius:8px;border:1px solid;font-size:14px;transition:all .2s;font-family:monospace;box-sizing:border-box;}
    .tmd-input-wrapper{position:relative;width:100%;}
    .tmd-input-toggle{position:absolute;right:10px;top:50%;transform:translateY(-50%);cursor:pointer;padding:4px;display:flex;align-items:center;justify-content:center;opacity:.5;transition:opacity .2s;}
    .tmd-input-toggle:hover{opacity:1;}
    .dark .tmd-input{background:hsl(240 3.7% 15.9%);border-color:hsl(240 5.3% 26.1%);color:hsl(240 4.8% 95.9%);}
    .dark .tmd-input:focus{border-color:hsl(204.17deg 87.55% 52.75%);outline:none;}
    .light .tmd-input{background:#fff;border-color:hsl(240 5.9% 90%);color:hsl(240 5.9% 10%);}
    .light .tmd-input:focus{border-color:hsl(204.17deg 87.55% 52.75%);outline:none;}
    .tmd-button{padding:10px 20px;border-radius:8px;font-weight:500;cursor:pointer;transition:all .2s;border:none;display:inline-flex;align-items:center;justify-content:center;gap:8px;margin:0;}
    .tmd-button-container{display:flex;justify-content:center;margin-top:15px;}
    .tmd-button-primary{background:hsl(204.17deg 87.55% 52.75%);color:#fff;}
    .tmd-button-primary:hover{background:hsl(204.17deg 87.55% 45%);}
    .tmd-button-primary:disabled{opacity:.5;cursor:not-allowed;}
    .tmd-button-secondary{background:hsl(142.1deg 76.2% 36.3%);color:#fff;}
    .tmd-button-secondary:hover{background:hsl(142.1deg 76.2% 30%);}
    .tmd-button-secondary:disabled{opacity:.5;cursor:not-allowed;}
    .tmd-button-outline{background:transparent;border:1px solid;}
    .dark .tmd-button-outline{border-color:hsl(240 5.3% 26.1%);color:hsl(240 4.8% 95.9%);}
    .dark .tmd-button-outline:hover{background:hsl(240 3.7% 15.9%);}
    .light .tmd-button-outline{border-color:hsl(240 5.9% 85%);color:hsl(240 5.9% 10%);}
    .light .tmd-button-outline:hover{background:hsl(240 5.9% 95%);}
    .tmd-button-outline:disabled{opacity:.5;cursor:not-allowed;}
    .tmd-button-outline:not(:disabled):hover{background:hsl(240 3.7% 15.9%);}
    .dark .tmd-button-outline:not(:disabled):hover{background:hsl(240 5.3% 26.1%);border-color:hsl(240 5.3% 35%);}
    .light .tmd-button-outline:not(:disabled):hover{background:hsl(240 5.9% 90%);border-color:hsl(240 5.9% 70%);}
    .tmd-spinner{animation:spin 1s linear infinite;}
    .tmd-error{padding:12px;border-radius:8px;margin-bottom:20px;display:flex;align-items:center;gap:8px;}
    .tmd-error.auth{background:hsl(45deg 100% 51% / .1);color:hsl(45deg 100% 45%);}
    .tmd-error.api,.tmd-error.username{background:hsl(45deg 100% 51% / .1);color:hsl(45deg 100% 45%);}
    .tmd-error.general{background:hsl(0deg 84.2% 60.2% / .1);color:hsl(0deg 84.2% 60.2%);}
    .tmd-error.failed{background:hsl(0deg 84.2% 60.2% / .1);color:hsl(0deg 84.2% 60.2%);}
    .tmd-error-icon{flex-shrink:0;display:flex;align-items:center;}
    .tmd-success{padding:12px;border-radius:8px;background:hsl(142.1deg 76.2% 36.3% / .1);color:hsl(142.1deg 76.2% 36.3%);margin-bottom:20px;display:flex;align-items:flex-start;gap:8px;}
    .tmd-success-icon{flex-shrink:0;display:flex;align-items:center;margin-top:2px;}
    .tmd-info-card{padding:16px;border-radius:8px;margin-bottom:20px;}
    .dark .tmd-info-card{background:hsl(240 3.7% 15.9%);border:1px solid hsl(240 5.3% 26.1%);}
    .light .tmd-info-card{background:hsl(240 4.8% 95.9%);border:1px solid hsl(240 5.9% 90%);}
    .tmd-info-card.clickable{transition:all .2s ease;cursor:default;position:relative;z-index:1;}
    .tmd-info-card.clickable:hover{border-color:hsl(204.17deg 87.55% 52.75%)!important;}
    .tmd-info-row{display:flex;justify-content:space-between;margin-bottom:8px;}
    .tmd-info-row:last-child{margin-bottom:0;}
    .tmd-info-label{font-weight:500;}
    .tmd-progress-bar{width:100%;height:8px;border-radius:4px;overflow:hidden;margin-bottom:0;}
    .dark .tmd-progress-bar{background:hsl(240 3.7% 15.9%);}
    .light .tmd-progress-bar{background:hsl(240 5.9% 90%);}
    .tmd-progress-fill{height:100%;background:linear-gradient(90deg,hsl(204.17deg 87.55% 45%),hsl(204.17deg 87.55% 52.75%));transition:width .3s ease;}
    .tmd-progress-info{display:flex;justify-content:space-between;margin-bottom:20px;font-size:14px;}
    .dark .tmd-progress-info{color:hsl(240 4.8% 95.9%);}
    .light .tmd-progress-info{color:hsl(240 5.9% 10%);}
    .dl-icon{display:inline-flex;margin-left:6px;padding:4px;border-radius:4px;transition:all .2s;cursor:pointer;}
    .tmd-radio-group{display:flex;gap:20px;margin-top:8px;}
    .tmd-radio-item{display:flex;align-items:center;gap:8px;cursor:pointer;}
    .tmd-radio{width:20px;height:20px;border-radius:50%;border:2px solid;position:relative;transition:all .2s;}
    .dark .tmd-radio{border-color:hsl(240 5.3% 26.1%);background:hsl(240 3.7% 15.9%);}
    .light .tmd-radio{border-color:hsl(240 5.9% 85%);background:#fff;}
    .tmd-radio.checked{border-color:hsl(204.17deg 87.55% 52.75%);}
    .tmd-radio.checked::after{content:'';position:absolute;width:10px;height:10px;border-radius:50%;background:hsl(204.17deg 87.55% 52.75%);top:50%;left:50%;transform:translate(-50%,-50%);}
    .tmd-radio-label{font-size:14px;user-select:none;}
    .tmd-button-square{width:40px;height:40px;padding:0;display:flex;align-items:center;justify-content:center;border-radius:8px;flex-shrink:0;}
    .tmd-icon-button{background:transparent;border:none;padding:6px;cursor:pointer;border-radius:6px;transition:all .3s ease;display:inline-flex;align-items:center;justify-content:center;opacity:.7;}
    .tmd-icon-button:hover{opacity:1;background:hsl(0deg 84.2% 60.2% / .1);}
    .tmd-icon-button:hover svg{stroke:hsl(0deg 84.2% 60.2%);transition:stroke .3s ease;}
    .tmd-delete-button{transition:all .3s ease;}
    .tmd-delete-button:hover{background:hsl(0deg 84.2% 60.2% / .1)!important;border-color:hsl(0deg 84.2% 60.2%)!important;}
    .tmd-delete-button:hover svg{stroke:hsl(0deg 84.2% 60.2%);transition:stroke .3s ease;}
    .tmd-load-button{transition:all .3s ease;}
    .tmd-load-button:hover{background:hsl(142.1deg 76.2% 36.3% / .1)!important;border-color:hsl(142.1deg 76.2% 36.3%)!important;color:hsl(142.1deg 76.2% 36.3%)!important;}
    .tmd-load-button:hover svg{stroke:hsl(142.1deg 76.2% 36.3%);transition:stroke .3s ease;}
    .tmd-download-current-button{transition:all .3s ease;}
    .tmd-download-current-button:hover{background:hsl(142.1deg 76.2% 36.3% / .1)!important;border-color:hsl(142.1deg 76.2% 36.3%)!important;}
    .tmd-download-current-button:hover svg{stroke:hsl(142.1deg 76.2% 36.3%);transition:stroke .3s ease;}
    .tmd-shred-button{transition:all .3s ease;}
    .tmd-shred-button:hover{color:hsl(0deg 84.2% 60.2%)!important;border-color:hsl(0deg 84.2% 60.2%)!important;background:hsl(0deg 84.2% 60.2% / .1)!important;}
    .tmd-shred-button:hover svg{stroke:hsl(0deg 84.2% 60.2%);transition:stroke .3s ease;}
    .tmd-download-single-button{transition:all .3s ease;}
    .tmd-download-single-button:hover{background:hsl(142.1deg 76.2% 36.3% / .1)!important;border-color:hsl(142.1deg 76.2% 36.3%)!important;}
    .tmd-download-single-button:hover svg{stroke:hsl(142.1deg 76.2% 36.3%);transition:stroke .3s ease;}
    .tmd-batch-controls{display:flex;flex-direction:column;gap:10px;margin-bottom:15px;}
    .tmd-batch-controls-row{display:flex;gap:8px;justify-content:center;}
    .tmd-button-stop:not(:disabled):hover{background:hsl(0deg 84.2% 60.2% / .1)!important;border-color:hsl(0deg 84.2% 60.2%)!important;color:hsl(0deg 84.2% 60.2%)!important;}
    .tmd-button-stop:not(:disabled):hover svg{stroke:hsl(0deg 84.2% 60.2%);}
    .tmd-button-start:not(:disabled):hover{background:hsl(142.1deg 76.2% 36.3% / .1)!important;border-color:hsl(142.1deg 76.2% 36.3%)!important;color:hsl(142.1deg 76.2% 36.3%)!important;}
    .tmd-button-start:not(:disabled):hover svg{stroke:hsl(142.1deg 76.2% 36.3%);}
    .tmd-tweet-link{text-decoration:none;cursor:pointer;transition:all .2s;}
    .tmd-tweet-link:hover{opacity:.8;text-decoration:underline;filter:brightness(1.2);}
    .tmd-filter-button{transition:all .3s ease;display:flex;align-items:center;justify-content:center;}
    .tmd-filter-button.tmd-filter-photo:hover{background:hsl(142.1deg 76.2% 36.3% / .1)!important;border-color:hsl(142.1deg 76.2% 36.3%)!important;}
    .tmd-filter-button.tmd-filter-photo:hover svg{stroke:hsl(142.1deg 76.2% 36.3%);}
    .tmd-filter-button.tmd-filter-video:hover{background:hsl(37.7deg 92.1% 50.2% / .1)!important;border-color:hsl(37.7deg 92.1% 50.2%)!important;}
    .tmd-filter-button.tmd-filter-video:hover svg{stroke:hsl(37.7deg 92.1% 50.2%);}
    .tmd-filter-button.tmd-filter-gif:hover{background:hsl(270deg 60% 50% / .1)!important;border-color:hsl(270deg 60% 50%)!important;}
    .tmd-filter-button.tmd-filter-gif:hover svg{stroke:hsl(270deg 60% 50%);}
    .tmd-alert-overlay{position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,.5);backdrop-filter:blur(4px);z-index:10000;display:flex;align-items:center;justify-content:center;animation:fadeIn .2s ease-out;}
    .tmd-alert{background:#fff;color:hsl(240 5.9% 10%);border-radius:12px;padding:24px;max-width:400px;width:90%;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 10px 10px -5px rgba(0,0,0,.04);animation:slideUp .3s ease-out;}
    .tmd-alert.dark{background:hsl(240 5.9% 10%);color:hsl(240 4.8% 95.9%);border:1px solid hsl(240 3.7% 15.9%);}
    .dark .tmd-alert{background:hsl(240 5.9% 10%);color:hsl(240 4.8% 95.9%);border:1px solid hsl(240 3.7% 15.9%);}
    .tmd-alert-title{font-size:18px;font-weight:600;margin-bottom:12px;}
    .tmd-alert-message{margin-bottom:20px;opacity:.9;}
    .tmd-alert-buttons{display:flex;gap:12px;justify-content:flex-end;}
    .tmd-preview-overlay{position:fixed;inset:0;background:rgba(0,0,0,.92);z-index:10001;display:flex;flex-direction:column;}
    .tmd-preview-header{display:flex;flex-direction:column;align-items:stretch;gap:16px;padding:20px;border-bottom:1px solid rgba(255,255,255,.15);color:#fff;position:relative;}
    .tmd-preview-id{display:flex;align-items:center;gap:12px;width:100%;}
    .tmd-preview-id-text{display:flex;flex-direction:column;min-width:0;}
    .tmd-preview-id-text .tmd-acc-name{font-weight:700;line-height:1.2;}
    .tmd-preview-id-text .tmd-acc-username{opacity:.8;font-size:13px;}
    .tmd-preview-stats{width:100%;color:#fff;opacity:.9;margin-top:8px;display:flex;gap:20px;flex-wrap:wrap;font-size:13px;}
    .tmd-preview-stats span{white-space:nowrap;}
    .tmd-preview-stats strong{font-weight:700;}
    .tmd-preview-toolbar{display:flex;align-items:center;gap:12px;justify-content:center;margin-top:12px;width:100%;max-width:600px;margin-left:auto;margin-right:auto;}
    .tmd-preview-toolbar .tmd-filter-chip{display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:10px 20px;border-radius:20px;border:1px solid rgba(255,255,255,.25);color:#fff;cursor:pointer;opacity:.85;user-select:none;transition:all .2s;flex:1;font-size:14px;min-width:120px;}
    .tmd-preview-toolbar .tmd-filter-chip.active{background:rgba(255,255,255,.12);opacity:1;}
    .tmd-preview-content{flex:1;display:flex;align-items:center;justify-content:center;position:relative;overflow:hidden;touch-action:pan-y;padding:80px 40px 90px 40px;}
    .tmd-preview-media{max-width:calc(100vw - 100px);max-height:calc(100vh - 340px);object-fit:contain;border-radius:8px;box-shadow:0 10px 30px rgba(0,0,0,.5);}
    .tmd-preview-nav{position:absolute;top:50%;transform:translateY(-50%);width:48px;height:48px;border-radius:50%;background:rgba(255,255,255,.12);border:1px solid rgba(255,255,255,.3);color:#fff;display:flex;align-items:center;justify-content:center;cursor:pointer;user-select:none;}
    .tmd-preview-nav:hover{background:rgba(255,255,255,.18);}
    .tmd-preview-nav.prev{left:16px;}
    .tmd-preview-nav.next{right:16px;}
    .tmd-preview-close{position:absolute;top:20px;right:20px;padding:8px;border-radius:8px;cursor:pointer;transition:all .2s;display:flex;align-items:center;justify-content:center;background:hsl(240 3.7% 15.9%);color:#fff;z-index:10;}
    .tmd-preview-close:hover{background:hsl(0deg 84.2% 60.2% / .2);}
    .tmd-preview-close:hover svg{stroke:hsl(0deg 84.2% 60.2%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo:not(.active){border-color:rgba(255,255,255,.25);background:transparent;}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo:not(.active) svg{stroke:hsl(142.1deg 76.2% 36.3%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo:not(.active):hover{border-color:hsl(142.1deg 76.2% 36.3%);background:hsl(142.1deg 76.2% 36.3% / .1);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo.active{border-color:hsl(142.1deg 76.2% 36.3%);background:hsl(142.1deg 76.2% 36.3% / .15);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo.active svg{stroke:hsl(142.1deg 76.2% 36.3%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-photo.active:hover{background:hsl(142.1deg 76.2% 36.3% / .25);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video:not(.active){border-color:rgba(255,255,255,.25);background:transparent;}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video:not(.active) svg{stroke:hsl(37.7deg 92.1% 50.2%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video:not(.active):hover{border-color:hsl(37.7deg 92.1% 50.2%);background:hsl(37.7deg 92.1% 50.2% / .1);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video.active{border-color:hsl(37.7deg 92.1% 50.2%);background:hsl(37.7deg 92.1% 50.2% / .15);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video.active svg{stroke:hsl(37.7deg 92.1% 50.2%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-video.active:hover{background:hsl(37.7deg 92.1% 50.2% / .25);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif:not(.active){border-color:rgba(255,255,255,.25);background:transparent;}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif:not(.active) svg{stroke:hsl(270deg 60% 50%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif:not(.active):hover{border-color:hsl(270deg 60% 50%);background:hsl(270deg 60% 50% / .1);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif.active{border-color:hsl(270deg 60% 50%);background:hsl(270deg 60% 50% / .15);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif.active svg{stroke:hsl(270deg 60% 50%);}
    .tmd-preview-toolbar .tmd-filter-chip.tmd-filter-gif.active:hover{background:hsl(270deg 60% 50% / .25);}
    .tmd-preview-counter{position:absolute;top:15px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.8);color:#fff;padding:6px 12px;border-radius:16px;font-size:13px;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.2);font-weight:500;}
    .tmd-preview-date{position:absolute;bottom:15px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,.8);color:#fff;padding:8px 14px;border-radius:20px;font-size:14px;backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.2);}
    .tmd-alert-button{padding:8px 16px;border-radius:8px;font-weight:500;cursor:pointer;transition:all .2s;border:none;}
    .tmd-alert-button-cancel{background:transparent;border:1px solid;}
    .dark .tmd-alert-button-cancel{border-color:hsl(240 5.3% 26.1%);color:hsl(240 4.8% 95.9%);}
    .dark .tmd-alert-button-cancel:hover{background:hsl(240 3.7% 15.9%);}
    .light .tmd-alert-button-cancel{border-color:hsl(240 5.9% 85%);color:hsl(240 5.9% 10%);}
    .light .tmd-alert-button-cancel:hover{background:hsl(240 5.9% 95%);}
    .tmd-alert-button-confirm{background:hsl(0deg 84.2% 60.2%);color:#fff;}
    .tmd-alert-button-confirm:hover{background:hsl(0deg 84.2% 50%);}
    input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{-webkit-appearance:none;margin:0;}
    input[type="number"]{-moz-appearance:textfield;}
    .tmd-media-list-container{flex:1;overflow-y:auto;overflow-x:hidden;margin-bottom:16px;padding:2px;position:relative;}
    .tmd-database-content{display:flex;flex-direction:column;height:100%;}
    .tmd-media-list-wrapper{flex:1;display:flex;flex-direction:column;min-height:0;}
    .tmd-database-buttons{display:flex;align-items:center;justify-content:space-between;gap:8px;}
    .tmd-database-buttons .tmd-button-outline{height:40px;padding:6px 12px;white-space:nowrap;}
    .tmd-database-buttons-right{display:flex;align-items:center;gap:8px;}
    @media (max-width:480px){.tmd-service-data-row{flex-direction:column!important;gap:20px!important}.tmd-service-data-row>div{flex:none!important;width:100%!important}.tmd-content{max-height:calc(100vh - 180px);padding:15px}.tmd-modal{max-height:90vh}.tmd-media-list-container{max-height:calc(100vh - 380px)}.tmd-download-current-button,.tmd-button-secondary{padding:8px 12px!important;font-size:13px!important;white-space:nowrap!important;min-width:auto!important}.tmd-download-current-button span,.tmd-button-secondary span{font-size:13px!important}.tmd-download-current-button svg,.tmd-button-secondary svg{width:16px!important;height:16px!important}.tmd-button-container .tmd-button-primary,.tmd-button-container .tmd-button-secondary{padding:8px 12px!important;font-size:13px!important;white-space:nowrap!important;min-width:auto!important;flex:1!important;max-width:150px!important}.tmd-button-container{display:flex!important;gap:8px!important;justify-content:center!important}.tmd-button-primary span,.tmd-button-secondary span{font-size:13px!important}.tmd-button-primary svg,.tmd-button-secondary svg{width:16px!important;height:16px!important}.tmd-database-buttons{display:flex!important;flex-wrap:wrap!important;gap:4px!important;width:100%!important;justify-content:stretch!important}.tmd-database-buttons .tmd-button-outline{height:40px!important;padding:6px 8px!important;font-size:12px!important;min-width:0!important;flex:1!important;white-space:nowrap!important;display:flex!important;align-items:center!important;justify-content:center!important;gap:4px!important}.tmd-database-buttons .tmd-button-outline span{font-size:12px!important}.tmd-database-buttons .tmd-button-outline svg{width:14px!important;height:14px!important}.tmd-database-buttons-right{display:contents!important}.tmd-database-content input[type="number"]{width:45px!important;padding:4px 6px!important}.tmd-preview-media{max-width:calc(100vw - 32px)!important;max-height:calc(100vh - 200px)!important}.tmd-preview-counter{top:12px!important;padding:4px 8px!important;font-size:12px!important;border-radius:12px!important}.tmd-preview-date{bottom:12px!important;padding:6px 10px!important;font-size:13px!important;border-radius:16px!important}.tmd-preview-nav{width:40px!important;height:40px!important}.tmd-preview-nav.prev{left:8px!important}.tmd-preview-nav.next{right:8px!important}.tmd-preview-toolbar .tmd-filter-chip{padding:6px 10px!important;font-size:12px!important;height:28px!important;min-width:90px!important}.tmd-preview-toolbar .tmd-filter-chip svg{width:12px!important;height:12px!important}}
    `;

    GM_addStyle(styles);

    function Modal() {
        const modalRef = useRef(null);
        const [showResetConfirm, setShowResetConfirm] = useState(false);

        useEffect(() => {
            function handleEscape(e) {
                const activeElement = document.activeElement;
                const isTyping = activeElement && (
                    activeElement.tagName === 'INPUT' ||
                    activeElement.tagName === 'TEXTAREA' ||
                    activeElement.tagName === 'SELECT' ||
                    activeElement.contentEditable === 'true'
                );

                if (e.key === 'Escape' && state.isModalOpen.value && !isTyping) {
                    state.isModalOpen.value = false;
                }
            }

            document.addEventListener('keydown', handleEscape);
            return () => document.removeEventListener('keydown', handleEscape);
        }, []);

        const handleOverlayClick = (e) => {
            if (e.target === e.currentTarget) {
                const activeElement = document.activeElement;
                const isInputFocused = activeElement && (
                    activeElement.tagName === 'INPUT' ||
                    activeElement.tagName === 'TEXTAREA' ||
                    activeElement.tagName === 'SELECT'
                );

                if (!isInputFocused) {
                    state.isModalOpen.value = false;
                }
            }
        };

        const toggleTheme = () => {
            state.theme.value = state.theme.value === 'dark' ? 'light' : 'dark';
            saveSettings();
        };

        const handleFactoryReset = async () => {
            try {
                await db.settings.clear();
                
                state.authToken.value = '';
                state.patreonAuth.value = '';
                state.isVerified.value = false;
                state.theme.value = 'light';
                state.selectedApi.value = 'default';
                state.mediaType.value = 'all';
                state.timelineType.value = 'media';
                state.batchSize.value = 100;
                state.startingBatch.value = 0;
                state.concurrentLimit.value = 20;
                state.showBatchDatabase.value = false;
                state.mediaData.value = null;
                state.error.value = null;
                state.success.value = null;
                state.downloadProgress.value = 0;
                state.currentUsername.value = '';
                state.downloadedFiles.value = 0;
                state.totalFileSize.value = 0;
                state.fetchMode.value = 'fresh';
                state.selectedCacheUser.value = null;
                state.cacheMediaPage.value = 1;
                state.isDownloading.value = false;
                state.isDownloadingCurrent.value = false;
                state.fetchType.value = 'single';
                state.currentBatchPage.value = 0;
                state.isAutoBatch.value = false;
                state.batchedMediaData.value = [];
                state.currentBatchData.value = [];
                state.loadingDirection.value = null;
                
                setShowResetConfirm(false);
                
                state.success.value = 'Settings reset completed successfully!';
                setTimeout(() => {
                    if (state.success.value === 'Settings reset completed successfully!') {
                        state.success.value = null;
                    }
                }, 2000);
                
                state.activeTab.value = 'auth';
            } catch (error) {
                console.error('Failed to reset settings:', error);
                state.error.value = 'Failed to reset settings. Please try again.';
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value === 'Failed to reset settings. Please try again.') {
                        state.error.value = null;
                    }
                }, 2000);
            }
        };

        if (!state.isModalOpen.value) return null;

        return h('div', null,
            showResetConfirm && h(AlertDialog, {
                title: 'Reset Settings',
                message: 'This will reset all settings to default values. Database will be preserved. Are you sure?',
                onConfirm: handleFactoryReset,
                onCancel: () => setShowResetConfirm(false),
                confirmLabel: 'Reset'
            }),
            h('div', {
                className: 'tmd-modal-overlay',
                onClick: handleOverlayClick
            },
                h('div', {
                    className: `tmd-modal ${state.theme.value}`,
                    ref: modalRef
                },
                    h('div', { className: 'tmd-header' },
                        h('div', { className: 'tmd-header-title' },
                            state.currentUsername.value ? `@${state.currentUsername.value}` : 'No User Detected'
                        ),
                        h('div', { className: 'tmd-header-controls' },
                            h('div', {
                                className: 'tmd-theme-toggle',
                                onClick: toggleTheme,
                                dangerouslySetInnerHTML: { __html: renderIcon(state.theme.value === 'dark' ? 'Sun' : 'Moon') },
                                title: 'Toggle theme'
                            }),
                            h('div', {
                                className: 'tmd-theme-toggle tmd-reset-toggle',
                                onClick: () => setShowResetConfirm(true),
                                dangerouslySetInnerHTML: { __html: renderIcon('RotateCcw') },
                                title: 'Reset Settings - Reset all settings to default values'
                            }),
                            h('div', {
                                className: 'tmd-close-btn',
                                onClick: () => state.isModalOpen.value = false,
                                dangerouslySetInnerHTML: { __html: renderIcon('X') }
                            })
                        )
                    ),
                    h('div', { className: 'tmd-tabs' },
                        h('div', {
                            className: `tmd-tab ${state.activeTab.value === 'dashboard' ? 'active' : ''}`,
                            onClick: () => state.activeTab.value = 'dashboard'
                        }, 'Dashboard'),
                        h('div', {
                            className: `tmd-tab ${state.activeTab.value === 'database' ? 'active' : ''}`,
                            onClick: () => state.activeTab.value = 'database'
                        }, 'Database'),
                        h('div', {
                            className: `tmd-tab ${state.activeTab.value === 'settings' ? 'active' : ''}`,
                            onClick: () => state.activeTab.value = 'settings'
                        }, 'Settings'),
                        h('div', {
                            className: `tmd-tab ${state.activeTab.value === 'auth' ? 'active' : ''}`,
                            onClick: () => state.activeTab.value = 'auth'
                        }, 'Auth')
                    ),
                    h('div', { className: 'tmd-content' },
                        state.success.value && h('div', { className: 'tmd-success' },
                            h('span', {
                                className: 'tmd-success-icon',
                                dangerouslySetInnerHTML: { __html: renderIcon('CheckCircle') }
                            }),
                            h('span', null, state.success.value)
                        ),
                        state.error.value && (state.errorType.value === 'general' || state.errorType.value === 'username' || state.errorType.value === 'api' || state.errorType.value === 'failed') && h('div', { 
                            className: `tmd-error ${state.errorType.value}` 
                        },
                            h('span', {
                                className: 'tmd-error-icon',
                                dangerouslySetInnerHTML: { __html: 
                                    state.errorType.value === 'username' || state.errorType.value === 'api' ? renderIcon('TriangleAlert') : 
                                    state.errorType.value === 'failed' ? renderIcon('XCircle') : 
                                    renderIcon('AlertCircle') 
                                }
                            }),
                            h('span', null, state.error.value)
                        ),
                        state.activeTab.value === 'dashboard' ? h(DashboardTab) :
                        state.activeTab.value === 'database' ? h(DatabaseTab) :
                        state.activeTab.value === 'settings' ? h(SettingsTab) :
                        h(AuthTab)
                    )
                )
            )
        );
    }

    function DashboardTab() {
        const [currentFiles, setCurrentFiles] = useState(state.downloadedFiles.value);
        const [currentSize, setCurrentSize] = useState(state.totalFileSize.value);
        const [isVerified, setIsVerified] = useState(state.isVerified.value);

        useEffect(() => {
            const cleanupDownloadedFiles = effect(() => {
                setCurrentFiles(state.downloadedFiles.value);
            });

            const cleanupTotalFileSize = effect(() => {
                setCurrentSize(state.totalFileSize.value);
            });

            const cleanupIsVerified = effect(() => {
                setIsVerified(state.isVerified.value);
            });

            return () => {
                if (typeof cleanupDownloadedFiles === 'function') {
                    cleanupDownloadedFiles();
                }
                if (typeof cleanupTotalFileSize === 'function') {
                    cleanupTotalFileSize();
                }
                if (typeof cleanupIsVerified === 'function') {
                    cleanupIsVerified();
                }
            };
        }, []);

        const fetchBatchMediaData = async (page = 0, _isRetry = false) => {
            if (state.fetchMode.value === 'cache') {

                state.isLoading.value = true;
                state.error.value = null;
                state.errorType.value = 'general';

                try {
                    const normalizedUsername = state.currentUsername.value.toLowerCase();
                    const cacheKey = `${normalizedUsername}_${state.timelineType.value}_${state.mediaType.value}`;
                    const cachedData = await db.mediaData.get(cacheKey);

                    if (cachedData) {
                        const startIdx = page * state.batchSize.value;
                        const endIdx = startIdx + state.batchSize.value;
                        const batchTimeline = cachedData.data.timeline.slice(startIdx, endIdx);

                        if (page === 0 || page === state.startingBatch.value) {
                            state.batchedMediaData.value = batchTimeline;
                            state.currentBatchData.value = batchTimeline;
                            state.mediaData.value = {
                                account_info: cachedData.data.account_info,
                                timeline: batchTimeline,
                                metadata: {
                                    has_more: endIdx < cachedData.data.timeline.length
                                }
                            };
                        } else {
                            const updatedTimeline = [...state.batchedMediaData.value, ...batchTimeline];
                            state.batchedMediaData.value = updatedTimeline;
                            state.currentBatchData.value = batchTimeline;
                            state.mediaData.value = {
                                ...state.mediaData.value,
                                timeline: updatedTimeline,
                                metadata: {
                                    has_more: endIdx < cachedData.data.timeline.length
                                }
                            };
                        }

                        state.currentBatchPage.value = page;
                        state.isLoading.value = false;
                        return state.mediaData.value.metadata;
                    } else {
                        state.isLoading.value = false;
                        state.error.value = `No cached data found for @${state.currentUsername.value}. Please fetch fresh data first.`;
                        state.errorType.value = 'general';
                        setTimeout(() => {
                            if (state.error.value === `No cached data found for @${state.currentUsername.value}. Please fetch fresh data first.`) {
                                state.error.value = null;
                            }
                        }, 2000);
                        return null;
                    }
                } catch (error) {
                    console.error('Failed to load cached data:', error);
                    state.isLoading.value = false;
                    state.error.value = `Failed to load cached data: ${error.message || 'Unknown error'}`;
                    state.errorType.value = 'general';
                    setTimeout(() => {
                        if (state.error.value && state.error.value.startsWith('Failed to load cached data:')) {
                            state.error.value = null;
                        }
                    }, 2000);
                    return null;
                }
            }

            if (!state.authToken.value || !state.currentUsername.value || !state.patreonAuth.value) {
                state.error.value = 'Please configure Auth Token in the Auth tab';
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value === 'Please configure Auth Token in the Auth tab') {
                        state.error.value = null;
                    }
                }, 2000);
                return null;
            }

            state.isLoading.value = true;
            state.error.value = null;

            const api = state.selectedApi.value === 'backup'
                ? 'https://backup.xbatch.online'
                : 'https://api.xbatch.online';

            try {
                const url = `${api}/metadata/${state.timelineType.value}/${state.batchSize.value}/${page}/${state.mediaType.value}/${state.currentUsername.value}/${state.authToken.value}/${state.patreonAuth.value}`;

                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 60000,
                        onload: (res) => {
                            if (res.status === 200) {
                                try {
                                    const data = JSON.parse(res.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    reject(new Error('Invalid response format'));
                                }
                            } else {
                                reject(new Error(`API error (${res.status})`));
                            }
                        },
                        onerror: () => reject(new Error('Network error')),
                        ontimeout: () => reject(new Error('Request timeout'))
                    });
                });

                if (!response || !response.account_info || !response.timeline) {
                    throw new Error('Invalid response format');
                }

                if (response.timeline.length === 0) {
                    state.isLoading.value = false;
                    const mediaTypeText = state.mediaType.value === 'gif' ? 'GIFs' :
                                         state.mediaType.value === 'image' ? 'images' :
                                         state.mediaType.value === 'video' ? 'videos' : 'media';
                    state.error.value = `@${state.currentUsername.value} doesn't have any ${mediaTypeText}. No data was cached.`;
                    state.errorType.value = 'username';
                    setTimeout(() => {
                        if (state.error.value && state.error.value.includes("doesn't have any")) {
                            state.error.value = null;
                        }
                    }, 2000);
                    return null;
                }

                if (page === 0 || page === state.startingBatch.value) {
                    state.batchedMediaData.value = response.timeline;
                    state.currentBatchData.value = response.timeline;
                    state.mediaData.value = {
                        account_info: response.account_info,
                        timeline: response.timeline,
                        metadata: response.metadata
                    };
                } else {
                    const updatedTimeline = [...state.batchedMediaData.value, ...response.timeline];
                    state.batchedMediaData.value = updatedTimeline;
                    state.currentBatchData.value = response.timeline;
                    state.mediaData.value = {
                        ...state.mediaData.value,
                        timeline: updatedTimeline,
                        metadata: response.metadata
                    };
                }

                state.currentBatchPage.value = page;
                state.isLoading.value = false;

                if (response.timeline.length > 0) {
                    const normalizedUsername = state.currentUsername.value.toLowerCase();
                    const isBatch = state.fetchType.value === 'batch' || state.fetchType.value === 'autoBatch';
                    const cacheKey = isBatch
                        ? `${normalizedUsername}_${state.timelineType.value}_${state.mediaType.value}_batch`
                        : `${normalizedUsername}_${state.timelineType.value}_${state.mediaType.value}`;

                    if (isBatch) {
                        const existingCache = await db.mediaData.get(cacheKey);

                        if (page === 0 || page === state.startingBatch.value || !existingCache) {
                            await db.mediaData.put({
                                cacheKey: cacheKey,
                                username: normalizedUsername,
                                timelineType: state.timelineType.value,
                                mediaType: state.mediaType.value,
                                data: response,
                                timestamp: Date.now(),
                                isBatch: true
                            });
                        } else {
                            const combinedTimeline = [...existingCache.data.timeline, ...response.timeline];
                            await db.mediaData.put({
                                cacheKey: cacheKey,
                                username: normalizedUsername,
                                timelineType: state.timelineType.value,
                                mediaType: state.mediaType.value,
                                data: {
                                    ...response,
                                    timeline: combinedTimeline
                                },
                                timestamp: Date.now(),
                                isBatch: true
                            });
                        }
                    } else {
                        await db.mediaData.put({
                            cacheKey: cacheKey,
                            username: normalizedUsername,
                            timelineType: state.timelineType.value,
                            mediaType: state.mediaType.value,
                            data: response,
                            timestamp: Date.now(),
                            isBatch: false
                        });
                    }
                }

                return response.metadata;

            } catch (error) {
                state.isLoading.value = false;
                state.error.value = error.message;
                state.errorType.value = 'api';
                setTimeout(() => {
                    if (state.error.value === error.message) {
                        state.error.value = null;
                    }
                }, 2000);
                return null;
            }
        };

        const handleNextBatch = async () => {
            state.loadingDirection.value = 'next';
            const metadata = await fetchBatchMediaData(state.currentBatchPage.value + 1);
            if (metadata && !metadata.has_more) {
                state.success.value = 'All batches fetched successfully!';
                setTimeout(() => {
                    if (state.success.value === 'All batches fetched successfully!') {
                        state.success.value = null;
                    }
                }, 2000);
            }
            state.loadingDirection.value = null;
        };

        const handlePreviousBatch = async () => {
            if (state.currentBatchPage.value > state.startingBatch.value) {
                state.loadingDirection.value = 'prev';
                await fetchBatchMediaData(state.currentBatchPage.value - 1);
                state.loadingDirection.value = null;
            }
        };

        const startAutoBatch = async () => {
            state.isAutoBatch.value = true;
            let currentPage = state.currentBatchPage.value || state.startingBatch.value;

            while (state.isAutoBatch.value) {
                const metadata = await fetchBatchMediaData(currentPage);

                if (!metadata || !metadata.has_more) {
                    state.isAutoBatch.value = false;
                    state.success.value = 'Auto batch completed!';
                    setTimeout(() => {
                        if (state.success.value === 'Auto batch completed!') {
                            state.success.value = null;
                        }
                    }, 2000);
                    break;
                }

                currentPage++;

                await new Promise(resolve => setTimeout(resolve, 500));
            }
        };

        const stopAutoBatch = () => {
            state.isAutoBatch.value = false;
        };

        const downloadCurrentBatch = async () => {
            if (!state.currentBatchData.value || state.currentBatchData.value.length === 0) {
                state.error.value = 'No current batch data available';
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value === 'No current batch data available') {
                        state.error.value = null;
                    }
                }, 2000);
                return;
            }

            if (state.isDownloadingCurrent.value) return;

            state.isDownloadingCurrent.value = true;

            const tempMediaData = state.mediaData.value;
            state.mediaData.value = {
                ...state.mediaData.value,
                timeline: state.currentBatchData.value
            };

            await downloadMedia();

            state.mediaData.value = tempMediaData;
            state.isDownloadingCurrent.value = false;
        };


        const updateDatabase = async () => {
            if (!state.mediaData.value || !state.loadedFromDatabase.value) return;
            
            const originalFetchType = state.fetchType.value;
            const originalFetchMode = state.fetchMode.value;
            
            state.fetchMode.value = 'fresh';
            
            if (state.loadedDatabaseConfig.value) {
                const { isBatch, timelineType, mediaType } = state.loadedDatabaseConfig.value;
                if (isBatch) {
                    state.fetchType.value = 'autoBatch';
                } else {
                    state.fetchType.value = 'single';
                }
                state.timelineType.value = timelineType;
                state.mediaType.value = mediaType;
            }
            
            state.isLoading.value = true;
            state.error.value = null;
            
            try {
                if (state.loadedDatabaseConfig.value && state.loadedDatabaseConfig.value.isBatch) {
                    state.currentBatchPage.value = 0;
                    state.batchedMediaData.value = [];
                    
                    state.isAutoBatch.value = true;
                    let currentPage = 0;
                    let allData = [];
                    let accountInfo = null;
                    
                    while (state.isAutoBatch.value) {
                        const metadata = await fetchBatchMediaData(currentPage);
                        
                        if (state.mediaData.value && state.mediaData.value.account_info) {
                            accountInfo = state.mediaData.value.account_info;
                        }
                        
                        if (state.currentBatchData.value && state.currentBatchData.value.length > 0) {
                            allData = [...allData, ...state.currentBatchData.value];
                        }
                        
                        if (!metadata || !metadata.has_more) {
                            state.isAutoBatch.value = false;
                            break;
                        }
                        
                        currentPage++;
                        await new Promise(resolve => setTimeout(resolve, 500));
                    }
                    
                    if (allData.length > 0 && accountInfo) {
                        const combinedResponse = {
                            account_info: accountInfo,
                            timeline: allData,
                            metadata: { has_more: false }
                        };
                        
                        state.mediaData.value = combinedResponse;
                        state.batchedMediaData.value = allData;
                        
                        if (state.loadedDatabaseConfig.value && state.loadedDatabaseConfig.value.cacheKey) {
                            const { cacheKey } = state.loadedDatabaseConfig.value;
                            await db.mediaData.put({
                                cacheKey: cacheKey,
                                username: state.currentUsername.value.toLowerCase(),
                                timelineType: state.timelineType.value,
                                mediaType: state.mediaType.value,
                                data: combinedResponse,
                                timestamp: Date.now(),
                                isBatch: true
                            });
                        }
                    }
                    
                } else {
                const api = state.selectedApi.value === 'backup'
                    ? 'https://backup.xbatch.online'
                    : 'https://api.xbatch.online';
                
                const url = `${api}/metadata/${state.timelineType.value}/${state.mediaType.value}/${state.currentUsername.value}/${state.authToken.value}/${state.patreonAuth.value}`;
                
                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 60000,
                        onload: (res) => {
                            if (res.status === 200) {
                                try {
                                    const data = JSON.parse(res.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    reject(new Error('Invalid response format'));
                                }
                            } else {
                                reject(new Error(`API error (${res.status})`));
                            }
                        },
                        onerror: () => reject(new Error('Network error')),
                        ontimeout: () => reject(new Error('Request timeout'))
                    });
                });
                
                if (response && response.timeline && response.timeline.length > 0) {
                    state.mediaData.value = response;
                    
                    if (state.loadedDatabaseConfig.value && state.loadedDatabaseConfig.value.cacheKey) {
                            const { cacheKey } = state.loadedDatabaseConfig.value;
                        await db.mediaData.put({
                            cacheKey: cacheKey,
                            username: state.currentUsername.value.toLowerCase(),
                            timelineType: state.timelineType.value,
                            mediaType: state.mediaType.value,
                            data: response,
                            timestamp: Date.now(),
                                isBatch: false
                        });
                        }
                    } else {
                        throw new Error('No data received from server');
                    }
                    }
                    
                    state.success.value = 'Database updated successfully!';
                    setTimeout(() => {
                        if (state.success.value === 'Database updated successfully!') {
                            state.success.value = null;
                        }
                    }, 2000);
                
            } catch (error) {
                console.error('Failed to update database:', error);
                state.error.value = `Failed to update: ${error.message}`;
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value && state.error.value.startsWith('Failed to update:')) {
                        state.error.value = null;
                    }
                }, 2000);
            } finally {
                state.isLoading.value = false;
                state.fetchType.value = originalFetchType;
                state.fetchMode.value = originalFetchMode;
            }
        };

        const fetchMediaData = async (_isRetry = false) => {
            if (state.fetchMode.value === 'cache') {

                state.isLoading.value = true;
                state.error.value = null;
                state.errorType.value = 'general';

                try {
                    const normalizedUsername = state.currentUsername.value.toLowerCase();
                    const cacheKey = `${normalizedUsername}_${state.timelineType.value}_${state.mediaType.value}`;
                    const cachedData = await db.mediaData.get(cacheKey);

                    if (cachedData) {
                        state.mediaData.value = cachedData.data;
                        state.isLoading.value = false;
                    } else {
                        state.isLoading.value = false;
                        state.error.value = `No cached data found for @${state.currentUsername.value} with ${state.mediaType.value} media from ${state.timelineType.value} timeline. Please fetch fresh data first.`;
                        state.errorType.value = 'general';
                        setTimeout(() => {
                            if (state.error.value && state.error.value.includes('No cached data found for')) {
                                state.error.value = null;
                            }
                        }, 2000);
                    }
                        } catch (error) {
                            console.error('Failed to load cached data:', error);
                            state.isLoading.value = false;
                            state.error.value = `Failed to load cached data: ${error.message || 'Unknown error'}. Please try fresh fetch.`;
                            state.errorType.value = 'general';
                            setTimeout(() => {
                                if (state.error.value && state.error.value.startsWith('Failed to load cached data:')) {
                                    state.error.value = null;
                                }
                            }, 2000);
                        }
                return;
            }

            if (!state.authToken.value || !state.currentUsername.value || !state.patreonAuth.value) {
                state.error.value = 'Please configure Auth Token in the Auth tab';
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value === 'Please configure Auth Token in the Auth tab') {
                        state.error.value = null;
                    }
                }, 2000);
                return;
            }

            state.isLoading.value = true;
            state.error.value = null;
            state.errorType.value = 'general';

            const api = state.selectedApi.value === 'backup'
                ? 'https://backup.xbatch.online'
                : 'https://api.xbatch.online';

            try {
                const url = state.patreonAuth.value === 'xbatchdemo' && state.currentUsername.value === 'xbatchdemo'
                    ? `${api}/demo/media/all/xbatchdemo/${state.authToken.value}/xbatchdemo`
                    : `${api}/metadata/${state.timelineType.value}/${state.mediaType.value}/${state.currentUsername.value}/${state.authToken.value}/${state.patreonAuth.value}`;

                let timeoutId;
                const response = await new Promise((resolve, reject) => {
                    timeoutId = setTimeout(() => {
                        state.isLoading.value = false;
                        reject(new Error('Request timeout - API took too long to respond'));
                    }, 60000);

                    try {
                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: url,
                            timeout: 60000,
                            onload: (res) => {
                                clearTimeout(timeoutId);
                                if (res.status === 200) {
                                    try {
                                        const data = JSON.parse(res.responseText);
                                        if (data.error || data.message) {
                                            state.isLoading.value = false;
                                            reject(new Error(data.error || data.message));
                                        } else {
                                            resolve(data);
                                        }
                                    } catch (parseError) {
                                        state.isLoading.value = false;
                                        if (res.responseText && res.responseText.includes('<') && res.responseText.includes('>')) {
                                            reject(new Error('Invalid authentication - API returned HTML instead of JSON'));
                                        } else {
                                            reject(new Error('Invalid JSON response from API'));
                                        }
                                    }
                                } else if (res.status === 401) {
                                    state.isLoading.value = false;
                                    reject(new Error('Invalid authentication tokens'));
                                } else if (res.status === 403) {
                                    state.isLoading.value = false;
                                    reject(new Error('Access forbidden - check your Patreon auth'));
                                } else if (res.status === 404) {
                                    state.isLoading.value = false;
                                    reject(new Error('User not found or no media available'));
                                } else if (res.status === 429) {
                                    state.isLoading.value = false;
                                    reject(new Error('Rate limit exceeded - please try again later'));
                                } else if (res.status >= 500) {
                                    state.isLoading.value = false;
                                    reject(new Error('Server error - please try backup API'));
                                } else {
                                    state.isLoading.value = false;
                                    reject(new Error(`API error (${res.status}): ${res.responseText || 'Unknown error'}`.substring(0, 200)));
                                }
                            },
                            onerror: () => {
                                clearTimeout(timeoutId);
                                state.isLoading.value = false;
                                reject(new Error('Network error - please check your connection'));
                            },
                            ontimeout: () => {
                                clearTimeout(timeoutId);
                                state.isLoading.value = false;
                                reject(new Error('Request timeout - API took too long to respond'));
                            }
                        });
                    } catch (err) {
                        clearTimeout(timeoutId);
                        state.isLoading.value = false;
                        reject(new Error('Failed to make request'));
                    }
                });

                if (timeoutId) clearTimeout(timeoutId);

                if (!response || !response.account_info || !response.timeline) {
                    state.isLoading.value = false;
                    if (!response) {
                        state.error.value = 'No data received from API. Please check your Auth Token.';
                    } else if (!response.account_info && !response.timeline) {
                        state.error.value = 'Invalid Auth Token or Patreon Auth. Please check your credentials.';
                    } else if (!response.account_info) {
                        state.error.value = 'Cannot retrieve account info. Please verify your Auth Token.';
                    } else {
                        state.error.value = 'Cannot retrieve media timeline. Please try again.';
                    }
                    state.errorType.value = 'api';
                    setTimeout(() => {
                        state.error.value = null;
                    }, 2000);
                    return;
                }

                if (response.timeline.length === 0) {
                    state.isLoading.value = false;
                    const mediaTypeText = state.mediaType.value === 'gif' ? 'GIFs' :
                                         state.mediaType.value === 'image' ? 'images' :
                                         state.mediaType.value === 'video' ? 'videos' : 'media';
                    state.error.value = `@${state.currentUsername.value} doesn't have any ${mediaTypeText}. No data was cached.`;
                    state.errorType.value = 'username';
                    setTimeout(() => {
                        if (state.error.value && state.error.value.includes("doesn't have any")) {
                            state.error.value = null;
                        }
                    }, 2000);
                    return;
                }

                state.mediaData.value = response;

                if (response.timeline.length > 0) {
                    const normalizedUsername = state.currentUsername.value.toLowerCase();
                    const cacheKey = `${normalizedUsername}_${state.timelineType.value}_${state.mediaType.value}`;
                    await db.mediaData.put({
                        cacheKey: cacheKey,
                        username: normalizedUsername,
                        timelineType: state.timelineType.value,
                        mediaType: state.mediaType.value,
                        data: response,
                        timestamp: Date.now()
                    });
                }

                state.isLoading.value = false;
            } catch (error) {
                console.error(`Failed with ${api}:`, error);

                state.isLoading.value = false;

                if (error.message.includes('Invalid authentication')) {
                    state.error.value = 'Invalid Auth Token. Please generate a new one in the Auth tab.';
                    state.errorType.value = 'general';
                } else if (error.message.includes('Invalid JSON response')) {
                    state.error.value = 'API returned invalid data. Please check your Auth Token.';
                    state.errorType.value = 'api';
                } else if (error.message.includes('Access forbidden')) {
                    state.error.value = 'Access denied. Please verify your Patreon Auth and regenerate Auth Token.';
                    state.errorType.value = 'general';
                } else if (error.message.includes('User not found')) {
                    state.error.value = `User @${state.currentUsername.value} not found or has no media.`;
                    state.errorType.value = 'username';
                } else if (error.message.includes('Rate limit')) {
                    state.error.value = 'Rate limit exceeded. Please wait a moment and try again.';
                    state.errorType.value = 'api';
                } else if (error.message.includes('Server error')) {
                    state.error.value = 'Server error. Please try using the Backup API service.';
                    state.errorType.value = 'api';
                } else if (error.message.includes('timeout')) {
                    state.error.value = 'Request timed out. The API is taking too long to respond. Please try again.';
                    state.errorType.value = 'api';
                } else if (error.message.includes('Network error')) {
                    state.error.value = 'Network error. Please check your internet connection.';
                    state.errorType.value = 'api';
                } else {
                    state.error.value = error.message || 'Failed to fetch media data. Please check your settings and try again.';
                    state.errorType.value = 'api';
                }

                setTimeout(() => {
                    state.error.value = null;
                }, 2000);
            }
        };

        const downloadMedia = async () => {
            if (!state.mediaData.value) return;
            if (state.isDownloading.value) return;

            state.isDownloading.value = true;
            state.downloadProgress.value = 0;
            state.downloadedFiles.value = 0;
            state.totalFileSize.value = 0;

            if (!state.mediaData.value?.timeline || !Array.isArray(state.mediaData.value.timeline)) {
                state.error.value = 'Invalid media data structure. Please refetch the data.';
                state.errorType.value = 'general';
                state.isDownloading.value = false;
                setTimeout(() => {
                    if (state.error.value === 'Invalid media data structure. Please refetch the data.') {
                        state.error.value = null;
                    }
                }, 2000);
                return;
            }

            const { timeline } = state.mediaData.value;
            const totalItems = timeline.length;
            const zipFiles = {};
            let successCount = 0;
            let failedCount = 0;
            let totalSize = 0;
            let processedCount = 0;

            const CONCURRENT_LIMIT = state.concurrentLimit.value || 20;
            const BATCH_DELAY = 500;

            console.log(`Starting parallel download of ${totalItems} media files...`);
            console.log(`Concurrent limit: ${CONCURRENT_LIMIT} files`);

            const tweetGroups = {};
            timeline.forEach((item, idx) => {
                if (!tweetGroups[item.tweet_id]) {
                    tweetGroups[item.tweet_id] = [];
                }
                tweetGroups[item.tweet_id].push({ item, originalIndex: idx });
            });

            const indexToFileNumber = {};
            Object.values(tweetGroups).forEach(group => {
                group.forEach((entry, fileIndex) => {
                    indexToFileNumber[entry.originalIndex] = group.length > 1 ? (fileIndex + 1) : null;
                });
            });

            const downloadFile = async (item, index) => {
                try {
                    const date = dayjs(item.date).format('YYYY-MM-DD_HHmmss');
                    const ext = item.type === 'video' ? 'mp4' : item.type === 'animated_gif' ? 'mp4' : 'jpg';
                    const fileNumber = indexToFileNumber[index];
                    const actualUsername = state.mediaData.value?.account_info?.name || state.currentUsername.value;
                    const baseFilename = fileNumber !== null
                        ? `${date}_${actualUsername}_${item.tweet_id}_${fileNumber}.${ext}`
                        : `${date}_${actualUsername}_${item.tweet_id}.${ext}`;

                    let filename = baseFilename;
                    if (state.mediaType.value === 'all') {
                        let subfolder = '';
                        if (item.type === 'photo') {
                            subfolder = 'images/';
                        } else if (item.type === 'video') {
                            subfolder = 'videos/';
                        } else if (item.type === 'animated_gif') {
                            subfolder = 'gif/';
                        }
                        filename = subfolder + baseFilename;
                    }

                    console.log(`[${index + 1}/${totalItems}] Starting download: ${item.url}`);

                    const response = await new Promise((resolve, reject) => {
                        const timeout = setTimeout(() => {
                            console.warn(`Timeout for file ${index + 1}`);
                            reject(new Error('Download timeout'));
                        }, 60000);

                        GM_xmlhttpRequest({
                            method: 'GET',
                            url: item.url,
                            responseType: 'arraybuffer',
                            onload: (res) => {
                                clearTimeout(timeout);
                                if (res.status === 200 && res.response) {
                                    resolve(res);
                                } else {
                                    reject(new Error(`HTTP ${res.status}`));
                                }
                            },
                            onerror: (err) => {
                                clearTimeout(timeout);
                                reject(err);
                            },
                            ontimeout: () => {
                                clearTimeout(timeout);
                                reject(new Error('Request timeout'));
                            }
                        });
                    });

                    if (!response.response || response.response.byteLength === 0) {
                        throw new Error('Empty response');
                    }

                    const fileData = new Uint8Array(response.response);

                    zipFiles[filename] = fileData;

                    successCount++;
                    totalSize += fileData.length;

                    console.log(`✓ [${index + 1}/${totalItems}] Downloaded: ${filename} (${(fileData.length / 1024).toFixed(2)} KB)`);

                    return { success: true, size: fileData.length };

                } catch (error) {
                    failedCount++;
                    console.error(`✗ [${index + 1}/${totalItems}] Failed:`, item.url, error.message);
                    return { success: false, error: error.message };
                } finally {
                    processedCount++;
                    state.downloadedFiles.value = successCount;
                    state.totalFileSize.value = totalSize;
                    state.downloadProgress.value = Math.round((processedCount / totalItems) * 100);
                }
            };

            const processBatch = async (batch) => {
                const promises = batch.map(({ item, index }) => downloadFile(item, index));
                const results = await Promise.allSettled(promises);

                const batchSuccess = results.filter(r => r.status === 'fulfilled' && r.value?.success).length;
                const batchFailed = results.filter(r => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value?.success)).length;
                console.log(`Batch complete: ${batchSuccess} success, ${batchFailed} failed`);

                return results;
            };

            const batches = [];
            for (let i = 0; i < totalItems; i += CONCURRENT_LIMIT) {
                const batch = timeline.slice(i, Math.min(i + CONCURRENT_LIMIT, totalItems))
                    .map((item, batchIndex) => ({ item, index: i + batchIndex }));
                batches.push(batch);
            }

            console.log(`Processing ${batches.length} batches...`);

            for (let batchIndex = 0; batchIndex < batches.length; batchIndex++) {
                console.log(`\nProcessing batch ${batchIndex + 1}/${batches.length}...`);
                await processBatch(batches[batchIndex]);

                if (batchIndex < batches.length - 1) {
                    console.log(`Waiting ${BATCH_DELAY}ms before next batch...`);
                    await new Promise(resolve => setTimeout(resolve, BATCH_DELAY));
                }

                console.log(`Overall progress: ${processedCount}/${totalItems} files, ${(totalSize / (1024 * 1024)).toFixed(2)} MB`);
            }

            console.log(`\n=== Download Summary ===`);
            console.log(`Total: ${totalItems} files`);
            console.log(`Success: ${successCount} files`);
            console.log(`Failed: ${failedCount} files`);
            console.log(`Total size: ${(totalSize / (1024 * 1024)).toFixed(2)} MB`);
            console.log(`Files in ZIP object: ${Object.keys(zipFiles).length}`);

            if (successCount > 0) {
                const SAFETY_CONFIG = {
                    maxSizePerZip: 500 * 1024 * 1024,
                    maxFilesPerZip: 500,
                    warnThreshold: 300 * 1024 * 1024,
                };

                const needsSplit = totalSize > SAFETY_CONFIG.maxSizePerZip ||
                                  Object.keys(zipFiles).length > SAFETY_CONFIG.maxFilesPerZip;

                if (totalSize > SAFETY_CONFIG.warnThreshold) {
                    console.warn(`⚠️ Large download detected: ${(totalSize / (1024 * 1024)).toFixed(2)} MB`);
                }

                try {
                    if (needsSplit) {
                        console.log('📦 File size/count exceeds safe limits. Creating multiple ZIP files...');

                        const chunks = [];
                        let currentChunk = {};
                        let currentSize = 0;
                        let currentCount = 0;

                        for (const [filename, data] of Object.entries(zipFiles)) {
                            if ((currentSize + data.length > SAFETY_CONFIG.maxSizePerZip ||
                                 currentCount >= SAFETY_CONFIG.maxFilesPerZip) &&
                                currentCount > 0) {
                                chunks.push({
                                    files: currentChunk,
                                    size: currentSize,
                                    count: currentCount
                                });
                                currentChunk = {};
                                currentSize = 0;
                                currentCount = 0;
                            }

                            currentChunk[filename] = data;
                            currentSize += data.length;
                            currentCount++;
                        }

                        if (currentCount > 0) {
                            chunks.push({
                                files: currentChunk,
                                size: currentSize,
                                count: currentCount
                            });
                        }

                        console.log(`Creating ${chunks.length} ZIP files...`);

                        for (let i = 0; i < chunks.length; i++) {
                            const chunk = chunks[i];
                            const partNumber = i + 1;
                            const totalParts = chunks.length;

                            console.log(`Creating ZIP part ${partNumber}/${totalParts} (${chunk.count} files, ${(chunk.size / (1024 * 1024)).toFixed(2)} MB)...`);

                            const compressed = await new Promise((resolve, reject) => {
                                fflate.zip(chunk.files, { level: 1 }, (err, data) => {
                                    if (err) reject(err);
                                    else resolve(data);
                                });
                            });

                            const blob = new Blob([compressed], { type: 'application/zip' });
                            const actualUsername = state.mediaData.value?.account_info?.name || state.currentUsername.value;
                            const zipFilename = totalParts > 1
                                ? `${actualUsername}_${dayjs().format('YYYY-MM-DD_HHmmss')}_part${partNumber}of${totalParts}.zip`
                                : `${actualUsername}_${dayjs().format('YYYY-MM-DD_HHmmss')}.zip`;

                            console.log(`ZIP part ${partNumber} created: ${(blob.size / (1024 * 1024)).toFixed(2)} MB`);

                            if (i > 0) {
                                await new Promise(resolve => setTimeout(resolve, 500));
                            }

                            saveAs(blob, zipFilename);
                            console.log(`✓ ZIP file saved: ${zipFilename}`);
                        }

                        console.log(`✅ All ${chunks.length} ZIP files created successfully!`);

                        if (failedCount > 0) {
                            state.error.value = `Downloaded ${successCount.toLocaleString()}/${totalItems.toLocaleString()} files into ${chunks.length} ZIP files. ${failedCount.toLocaleString()} files failed.`;
                            state.errorType.value = 'failed';
                            setTimeout(() => {
                                if (state.error.value && state.error.value.includes('files failed')) {
                                    state.error.value = null;
                                }
                            }, 2000);
                        } else {
                            state.success.value = `Successfully downloaded ${successCount.toLocaleString()} files into ${chunks.length} ZIP files.`;
                            state.error.value = null;
                            setTimeout(() => {
                                if (state.success.value && state.success.value.includes('Successfully downloaded')) {
                                    state.success.value = null;
                                }
                            }, 2000);
                        }

                    } else {
                        console.log('Creating single ZIP file...');

                        const fileList = Object.keys(zipFiles);
                        console.log(`Zipping ${fileList.length} files...`);

                        const compressed = await new Promise((resolve, reject) => {
                            fflate.zip(zipFiles, { level: 1 }, (err, data) => {
                                if (err) reject(err);
                                else resolve(data);
                            });
                        });

                        const blob = new Blob([compressed], { type: 'application/zip' });
                        const actualUsername = state.mediaData.value?.account_info?.name || state.currentUsername.value;
                        const zipFilename = `${actualUsername}_${dayjs().format('YYYY-MM-DD_HHmmss')}.zip`;

                        console.log(`ZIP created: ${(blob.size / (1024 * 1024)).toFixed(2)} MB`);

                        if (blob.size > 2 * 1024 * 1024 * 1024) {
                            console.error('⚠️ ZIP file exceeds 2GB browser limit!');
                            state.error.value = 'ZIP file is too large for browser. Please try downloading fewer files.';
                            state.errorType.value = 'general';
                            setTimeout(() => {
                                if (state.error.value === 'ZIP file is too large for browser. Please try downloading fewer files.') {
                                    state.error.value = null;
                                }
                            }, 2000);
                            return;
                        }

                        saveAs(blob, zipFilename);
                        console.log(`✓ ZIP file saved: ${zipFilename}`);

                        if (failedCount > 0) {
                            state.error.value = `Downloaded ${successCount.toLocaleString()}/${totalItems.toLocaleString()} files. ${failedCount.toLocaleString()} files failed.`;
                            state.errorType.value = 'failed';
                            setTimeout(() => {
                                if (state.error.value && state.error.value.includes('files failed')) {
                                    state.error.value = null;
                                }
                            }, 2000);
                        } else {
                            state.success.value = `Successfully downloaded ${successCount.toLocaleString()} files.`;
                            state.error.value = null;
                            setTimeout(() => {
                                if (state.success.value && state.success.value.includes('Successfully downloaded')) {
                                    state.success.value = null;
                                }
                            }, 2000);
                        }
                    }

                } catch (error) {
                    console.error('Failed to create ZIP file:', error);

                    if (error.message?.includes('memory')) {
                        state.error.value = 'Out of memory. Try downloading fewer files or use a device with more RAM.';
                        state.errorType.value = 'general';
                    } else if (error.message?.includes('quota')) {
                        state.error.value = 'Storage quota exceeded. Please free up some space and try again.';
                        state.errorType.value = 'general';
                    } else {
                        state.error.value = `Failed to create ZIP file: ${error.message || 'Unknown error'}`;
                        state.errorType.value = 'general';
                    }
                    setTimeout(() => {
                        if (state.error.value) {
                            state.error.value = null;
                        }
                    }, 2000);
                }
            } else {
                state.error.value = 'No files were successfully downloaded. Please check your connection and try again.';
                state.errorType.value = 'general';
                setTimeout(() => {
                    if (state.error.value === 'No files were successfully downloaded. Please check your connection and try again.') {
                        state.error.value = null;
                    }
                }, 2000);
            }

            state.isDownloading.value = false;
            state.downloadProgress.value = 0;
            state.downloadedFiles.value = 0;
            state.totalFileSize.value = 0;
        };

        return h('div', null,
            state.mediaData.value && h('div', {
                style: 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;'
            },
                h('div', { style: 'display: flex; gap: 8px;' },
                    h('button', {
                        className: 'tmd-button tmd-button-outline',
                        style: 'padding: 6px 12px;',
                        onClick: () => {
                            state.mediaData.value = null;
                            state.error.value = null;
                            state.success.value = null;
                            state.loadedFromDatabase.value = false;
                            state.loadedDatabaseConfig.value = null;
                        }
                    },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Undo2') } }),
                        'Back'
                    ),
                    
                    state.loadedFromDatabase.value && h('button', {
                        className: 'tmd-button tmd-button-outline',
                        style: 'padding: 6px 12px;',
                        onClick: updateDatabase,
                        disabled: state.isLoading.value || !state.authToken.value || !state.patreonAuth.value || !isVerified,
                        title: (
                            state.loadedDatabaseConfig.value && state.loadedDatabaseConfig.value.isBatch
                                ? 'Update database with fresh data using auto batch system'
                                : 'Update database with fresh data using single fetch'
                        )
                    },
                        state.isLoading.value ?
                            h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2') } }) :
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('CloudUpload') } }),
                        state.isLoading.value ? 'Updating...' : 'Update'
                    )
                ),

                state.fetchType.value !== 'single' && h('div', {
                    style: 'display: flex; gap: 8px; align-items: center;'
                },
                    state.fetchType.value === 'autoBatch' ? (
                        !state.isAutoBatch.value ? h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-start',
                            onClick: startAutoBatch,
                            disabled: state.isLoading.value || (state.mediaData.value?.metadata && !state.mediaData.value.metadata.has_more),
                            style: 'padding: 6px 12px;'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Play') } }),
                            'Start'
                        ) : h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-stop',
                            onClick: stopAutoBatch,
                            style: 'padding: 6px 12px;'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Square') } }),
                            'Stop'
                        )
                    ) : [
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            onClick: handlePreviousBatch,
                            disabled: state.currentBatchPage.value <= state.startingBatch.value || state.isLoading.value || !isVerified,
                            title: 'Previous batch',
                            dangerouslySetInnerHTML: {
                                __html: state.loadingDirection.value === 'prev' ?
                                    renderIcon('Loader2', { className: 'tmd-spinner' }) :
                                    renderIcon('ChevronLeft')
                            }
                        }),
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            onClick: handleNextBatch,
                            disabled: state.isLoading.value || (state.mediaData.value?.metadata && !state.mediaData.value.metadata.has_more) || !isVerified,
                            title: 'Next batch',
                            dangerouslySetInnerHTML: {
                                __html: state.loadingDirection.value === 'next' ?
                                    renderIcon('Loader2', { className: 'tmd-spinner' }) :
                                    renderIcon('ChevronRight')
                            }
                        })
                    ]
                )
            ),

            !state.mediaData.value && h('div', {
                className: 'tmd-service-data-row',
                style: 'display: flex; gap: 20px; margin-bottom: 20px; flex-wrap: wrap;'
            },
                h('div', { style: 'flex: 1; min-width: 280px;' },
                    h('label', { className: 'tmd-label' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Send') } }),
                        'Fetch'
                    ),
                    h('div', { className: 'tmd-radio-group', style: 'white-space: nowrap; flex-wrap: nowrap;' },
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.fetchType.value = 'single';
                                state.batchedMediaData.value = [];
                                state.currentBatchPage.value = state.startingBatch.value;
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.fetchType.value === 'single' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Single')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.fetchType.value = 'batch';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.fetchType.value === 'batch' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Batch')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.fetchType.value = 'autoBatch';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.fetchType.value === 'autoBatch' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Auto Batch')
                        )
                    )
                ),

                h('div', { style: 'flex: 1; min-width: 200px;' },
                    h('label', { className: 'tmd-label' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Database') } }),
                        'Source'
                    ),
                    h('div', { className: 'tmd-radio-group' },
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => state.fetchMode.value = 'fresh'
                        },
                            h('div', {
                                className: `tmd-radio ${state.fetchMode.value === 'fresh' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Fresh')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => state.fetchMode.value = 'cache'
                        },
                            h('div', {
                                className: `tmd-radio ${state.fetchMode.value === 'cache' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Cache')
                        )
                    )
                ),

                h('div', { style: 'flex: 1; min-width: 200px;' },
                    h('label', { className: 'tmd-label' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Images') } }),
                        'Media'
                    ),
                    h('div', { className: 'tmd-radio-group' },
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.mediaType.value = 'all';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.mediaType.value === 'all' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'All')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.mediaType.value = 'image';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.mediaType.value === 'image' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Image')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.mediaType.value = 'video';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.mediaType.value === 'video' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'Video')
                        ),
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.mediaType.value = 'gif';
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.mediaType.value === 'gif' ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, 'GIF')
                        )
                    )
                )
            ),


            (!isVerified && state.fetchMode.value === 'fresh' && !state.mediaData.value) && h('div', {
                className: 'tmd-error auth'
            },
                h('span', {
                    className: 'tmd-error-icon',
                    dangerouslySetInnerHTML: { __html: renderIcon('TriangleAlert') }
                }),
                h('span', null, 'Please verify your Patreon Auth in the Auth tab to unlock fetch and convert features')
            ),


            !state.mediaData.value && (
                h('div', { className: 'tmd-button-container', style: 'padding-top: 10px; gap: 10px;' },
                    h('button', {
                        className: 'tmd-button tmd-button-primary',
                        onClick: () => {
                            if (state.fetchType.value === 'single') {
                                fetchMediaData();
                            } else if (state.fetchType.value === 'batch') {
                                fetchBatchMediaData(state.startingBatch.value || 0);
                            } else if (state.fetchType.value === 'autoBatch') {
                                fetchBatchMediaData(state.startingBatch.value || 0).then(() => {
                                    if (state.mediaData.value?.metadata?.has_more) {
                                        startAutoBatch();
                                    }
                                });
                            }
                        },
                        disabled: state.isLoading.value || !state.currentUsername.value || !isVerified,
                        style: 'font-size: 16px; padding: 12px 24px;'
                    },
                        state.isLoading.value ?
                            h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2', { size: 20 }) } }) :
                            (!isVerified ?
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Lock', { size: 20 }) } }) :
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('CloudDownload', { size: 20 }) } })
                            ),
                        state.isLoading.value ? 'Fetching...' : 'Fetch Media'
                    ),
                    h('button', {
                        className: 'tmd-button tmd-button-secondary',
                        onClick: () => {
                            if (state.patreonAuth.value && state.authToken.value && state.currentUsername.value) {
                                const url = `https://gif.xbatch.online/${state.patreonAuth.value}/${state.authToken.value}/${state.currentUsername.value}`;
                                window.open(url, '_blank');
                            } else {
                                state.error.value = 'Please configure Auth Token in the Auth tab';
                                state.errorType.value = 'general';
                                setTimeout(() => {
                                    if (state.error.value === 'Please configure Auth Token in the Auth tab') {
                                        state.error.value = null;
                                    }
                                }, 2000);
                            }
                        },
                        disabled: !state.currentUsername.value || !isVerified,
                        style: 'font-size: 16px; padding: 12px 24px;'
                    },
                        !isVerified ?
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Lock', { size: 20 }) } }) :
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('ImagePlay', { size: 20, color: 'white' }) } }),
                        'Convert to GIF'
                    )
                )
            ),

            state.mediaData.value && (
                h('div', null,
                    h('div', { className: 'tmd-info-card' },
                        h('div', { className: 'tmd-info-row' },
                            h('span', { className: 'tmd-info-label' }, 'Username:'),
                            h('span', null, state.mediaData.value?.account_info?.name || 'N/A')
                        ),
                        h('div', { className: 'tmd-info-row' },
                            h('span', { className: 'tmd-info-label' }, 'Display Name:'),
                            h('span', null, state.mediaData.value?.account_info?.nick || 'N/A')
                        ),
                        h('div', { className: 'tmd-info-row' },
                            h('span', { className: 'tmd-info-label' }, 'Joined:'),
                            h('span', null, state.mediaData.value?.account_info?.date ? dayjs(state.mediaData.value.account_info.date).format('DD MMM YYYY - HH:mm:ss') : 'N/A')
                        ),
                        h('div', { className: 'tmd-info-row' },
                            h('span', { className: 'tmd-info-label' }, 'Total Media:'),
                            h('span', null, (() => {
                                if (state.fetchType.value === 'single') {
                                    return state.mediaData.value?.timeline?.length?.toLocaleString() || '0';
                                } else {
                                    return state.batchedMediaData.value?.length?.toLocaleString() || '0';
                                }
                            })()
                            )
                        ),

                        state.fetchType.value !== 'single' && [
                            h('hr', { style: 'margin: 12px 0; border: none; border-top: 1px solid; opacity: 0.2;' }),
                            h('div', { className: 'tmd-info-row' },
                                h('span', { className: 'tmd-info-label' }, 'Batch:'),
                                h('span', null, `${state.currentBatchPage.value + 1}`)
                            ),
                            h('div', { className: 'tmd-info-row' },
                                h('span', { className: 'tmd-info-label' }, 'Current Batch:'),
                                h('span', null, (() => {
                                    const currentBatchLength = state.currentBatchData.value?.length || 0;
                                    return currentBatchLength.toLocaleString();
                                })()
                                )
                            )
                        ]
                    ),


                    state.isDownloading.value && h('div', null,
                        h('div', { style: 'display: flex; align-items: center; gap: 8px; margin-bottom: 8px;' },
                            h('div', { className: 'tmd-progress-bar', style: 'flex: 1;' },
                                h('div', {
                                    className: 'tmd-progress-fill',
                                    style: `width: ${state.downloadProgress.value}%`
                                })
                            ),
                            h('span', { style: 'font-weight: 500; min-width: 45px; text-align: right;' },
                                `${Math.round(state.downloadProgress.value)}%`
                            )
                        ),
                        h('div', { className: 'tmd-progress-info' },
                            h('span', null,
                                `Files: ${currentFiles.toLocaleString()}/${state.mediaData.value.timeline.length.toLocaleString()}`
                            ),
                            h('span', null,
                                `Size: ${(currentSize / (1024 * 1024)).toFixed(2)} MB`
                            )
                        )
                    ),

                    h('div', { className: 'tmd-button-container', style: 'gap: 8px; justify-content: center;' },
                        (state.fetchType.value === 'batch' || state.fetchType.value === 'autoBatch') && h('button', {
                            className: 'tmd-button tmd-button-outline tmd-download-current-button',
                            onClick: downloadCurrentBatch,
                            disabled: state.isDownloadingCurrent.value || state.isDownloading.value || !state.mediaData.value || !state.currentBatchData.value || !isVerified,
                            style: 'padding: 10px 20px;'
                        },
                            state.isDownloadingCurrent.value ?
                                h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2', { size: 20 }) } }) :
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Download', { size: 20 }) } }),
                            state.isDownloadingCurrent.value ? 'Downloading...' : 'Download Current'
                        ),
                        h('button', {
                            className: 'tmd-button tmd-button-secondary',
                            onClick: downloadMedia,
                            disabled: state.isDownloading.value || state.isDownloadingCurrent.value
                        },
                            state.isDownloading.value && !state.isDownloadingCurrent.value ?
                                h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2', { size: 20 }) } }) :
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Download', { size: 20 }) } }),
                            state.isDownloading.value && !state.isDownloadingCurrent.value ? 'Downloading...' : 'Download All'
                        )
                    )
                )
            )
        );
    }

    function AlertDialog({ title, message, onConfirm, onCancel, confirmLabel = 'Delete' }) {
        return h('div', { className: 'tmd-alert-overlay', onClick: onCancel },
            h('div', {
                className: `tmd-alert ${state.theme.value}`,
                onClick: (e) => e.stopPropagation()
            },
                h('div', { className: 'tmd-alert-title' }, title),
                h('div', { className: 'tmd-alert-message' }, message),
                h('div', { className: 'tmd-alert-buttons' },
                    h('button', {
                        className: 'tmd-alert-button tmd-alert-button-cancel',
                        onClick: onCancel
                    }, 'Cancel'),
                    h('button', {
                        className: 'tmd-alert-button tmd-alert-button-confirm',
                        onClick: onConfirm
                    }, confirmLabel)
                )
            )
        );
    }

    function DatabaseTab() {
        const [cachedUsers, setCachedUsers] = useState([]);
        const [selectedUser, setSelectedUser] = useState(null);
        const [currentPage, setCurrentPage] = useState(1);
        const [currentAccountPage, setCurrentAccountPage] = useState(1);
        const [showDeleteAlert, setShowDeleteAlert] = useState(false);
        const [deleteTarget, setDeleteTarget] = useState(null);
        const [showClearAllAlert, setShowClearAllAlert] = useState(false);
        const [showShredListAlert, setShowShredListAlert] = useState(false);
        const [hasBatchDatabases, setHasBatchDatabases] = useState(false);
        const [hasAnyDatabase, setHasAnyDatabase] = useState(false);
        const [showPreviewModal, setShowPreviewModal] = useState(false);
        const [previewData, setPreviewData] = useState(null);
        const [previewIndex, setPreviewIndex] = useState(0);
        const [previewFilters, setPreviewFilters] = useState({ photo: true, video: true, animated_gif: true });
        const [mediaFilters, setMediaFilters] = useState({
            photo: false,
            video: false,
            animated_gif: false
        });
        const itemsPerPage = 5;
        const accountsPerPage = 3;

        useEffect(() => {
            loadCachedUsers();
        }, []);

        const loadCachedUsers = async () => {
            try {
                const allCaches = await db.mediaData.toArray();

                const batchDatabases = allCaches.filter(cache =>
                    cache.cacheKey && cache.cacheKey.endsWith('_batch')
                );
                setHasBatchDatabases(batchDatabases.length > 0);
                setHasAnyDatabase(allCaches.length > 0);

                const userMap = new Map();

                allCaches.forEach(cache => {
                    const isBatchCache = cache.cacheKey && cache.cacheKey.endsWith('_batch');
                    if (state.showBatchDatabase.value && !isBatchCache) return;

                    const mapKey = isBatchCache ? `${cache.username}_batch` : `${cache.username}_regular`;

                    if (!userMap.has(mapKey)) {
                        userMap.set(mapKey, {
                            username: cache.username,
                            configs: [],
                            latestTimestamp: cache.timestamp,
                            totalMedia: 0,
                            data: cache.data,
                            isBatchGroup: isBatchCache
                        });
                    }

                    const user = userMap.get(mapKey);
                    user.configs.push({
                        timelineType: cache.timelineType,
                        mediaType: cache.mediaType,
                        timestamp: cache.timestamp,
                        mediaCount: cache.data.timeline.length,
                        cacheKey: cache.cacheKey,
                        isBatch: cache.isBatch || isBatchCache
                    });

                    if (cache.timestamp > user.latestTimestamp) {
                        user.latestTimestamp = cache.timestamp;
                        user.data = cache.data;
                    }
                    user.totalMedia += cache.data.timeline.length;
                });

                const users = Array.from(userMap.values());
                setCachedUsers(users.sort((a, b) => b.latestTimestamp - a.latestTimestamp));
            } catch (error) {
                console.error('Failed to load cached users:', error);
            }
        };

        const handleDeleteClick = (type, target) => {
            if (type === 'media') {
                handleDirectMediaDelete(target);
            } else {
                setDeleteTarget({ type, target });
                setShowDeleteAlert(true);
            }
        };

        const handleDirectMediaDelete = async (target) => {
            try {
                const { cacheKey, index } = target;

                if (!cacheKey) {
                    console.error('No cacheKey provided for delete operation');
                    return;
                }

                if (index === undefined || index === null || index < 0) {
                    console.error('Invalid index provided for delete operation');
                    return;
                }

                const userData = await db.mediaData.get(cacheKey);
                if (userData) {
                    userData.data.timeline.splice(index, 1);
                    await db.mediaData.put(userData);

                    await loadCachedUsers();

                    if (selectedUser?.cacheKey === cacheKey) {
                        const updatedUser = await db.mediaData.get(cacheKey);
                        setSelectedUser(updatedUser);

                        const activeFilters = Object.values(mediaFilters).some(v => v);
                        const timelineToCheck = activeFilters ?
                            updatedUser.data.timeline.filter(media => {
                                if (mediaFilters.photo && media.type === 'photo') return true;
                                if (mediaFilters.video && media.type === 'video') return true;
                                if (mediaFilters.animated_gif && media.type === 'animated_gif') return true;
                                return false;
                            }) : updatedUser.data.timeline;

                        const newTotalPages = Math.ceil(timelineToCheck.length / itemsPerPage);
                        if (currentPage > newTotalPages && newTotalPages > 0) {
                            setCurrentPage(newTotalPages);
                        }
                    }
                }
            } catch (error) {
                console.error('Failed to delete media:', error);
            }
        };

        const handleDeleteConfirm = async () => {
            if (!deleteTarget) return;

            try {
                if (deleteTarget.type === 'user') {
                    const targetUsername = typeof deleteTarget.target === 'string' ?
                        deleteTarget.target : deleteTarget.target.username;
                    const targetIsBatch = typeof deleteTarget.target === 'object' ?
                        deleteTarget.target.isBatchGroup : undefined;

                    const allCaches = await db.mediaData.where('username').equals(targetUsername).toArray();

                    const cachesToDelete = allCaches.filter(cache => {
                        const isBatchCache = cache.cacheKey && cache.cacheKey.endsWith('_batch');
                        return targetIsBatch === undefined || targetIsBatch === isBatchCache;
                    });

                    for (const cache of cachesToDelete) {
                        await db.mediaData.delete(cache.cacheKey);
                    }
                    await loadCachedUsers();
                    if (selectedUser?.username === targetUsername) {
                        setSelectedUser(null);
                        setCurrentPage(1);
                    }
                } else if (deleteTarget.type === 'config') {
                    await db.mediaData.delete(deleteTarget.target);
                    await loadCachedUsers();
                    setSelectedUser(null);
                    setCurrentPage(1);
                } else if (deleteTarget.type === 'media') {
                    const { cacheKey, index } = deleteTarget.target;
                    const userData = await db.mediaData.get(cacheKey);
                    if (userData) {
                        userData.data.timeline.splice(index, 1);
                        await db.mediaData.put(userData);

                        await loadCachedUsers();

                        if (selectedUser?.cacheKey === cacheKey) {
                            const updatedUser = await db.mediaData.get(cacheKey);
                            setSelectedUser(updatedUser);

                            const activeFilters = Object.values(mediaFilters).some(v => v);
                            const timelineToCheck = activeFilters ?
                                updatedUser.data.timeline.filter(media => {
                                    if (mediaFilters.photo && media.type === 'photo') return true;
                                    if (mediaFilters.video && media.type === 'video') return true;
                                    if (mediaFilters.animated_gif && media.type === 'animated_gif') return true;
                                    return false;
                                }) : updatedUser.data.timeline;

                            const newTotalPages = Math.ceil(timelineToCheck.length / itemsPerPage);
                            if (currentPage > newTotalPages && newTotalPages > 0) {
                                setCurrentPage(newTotalPages);
                            }
                        }
                    }
                }
            } catch (error) {
                console.error('Failed to delete:', error);
            }

            setShowDeleteAlert(false);
            setDeleteTarget(null);
        };

        const handleClearAllConfirm = async () => {
            try {
                await db.mediaData.clear();
                await loadCachedUsers();
                setSelectedUser(null);
                setCurrentPage(1);
                setCurrentAccountPage(1);
                state.mediaData.value = null;
                state.selectedCacheUser.value = null;
            } catch (error) {
                console.error('Failed to clear cached media data:', error);
            }
            setShowClearAllAlert(false);
        };

        const handleShredListConfirm = async () => {
            try {
                if (selectedUser && selectedUser.cacheKey) {
                    const cacheKey = selectedUser.cacheKey;
                    
                    await db.mediaData.delete(cacheKey);

                    await loadCachedUsers();
                    setSelectedUser(null);
                    setCurrentPage(1);
                }
            } catch (error) {
                console.error('Failed to shred media list:', error);
            }
            setShowShredListAlert(false);
        };

        const handleDeleteCancel = () => {
            setShowDeleteAlert(false);
            setDeleteTarget(null);
        };

        const downloadSingleMedia = async (media, index) => {
            try {
                const date = dayjs(media.date).format('YYYY-MM-DD_HHmmss');
                const ext = media.type === 'video' ? 'mp4' : media.type === 'animated_gif' ? 'mp4' : 'jpg';
                const actualUsername = selectedUser.data?.account_info?.name ||
                                     selectedUser.username ||
                                     'unknown';
                const filename = `${date}_${actualUsername}_${media.tweet_id}_${index}.${ext}`;

                console.log(`Downloading single file: ${filename}`);

                const response = await new Promise((resolve, reject) => {
                    const timeout = setTimeout(() => {
                        reject(new Error('Download timeout'));
                    }, 60000);

                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: media.url,
                        responseType: 'blob',
                        onload: (res) => {
                            clearTimeout(timeout);
                            if (res.status === 200 && res.response) {
                                resolve(res.response);
                            } else {
                                reject(new Error(`HTTP ${res.status}`));
                            }
                        },
                        onerror: (err) => {
                            clearTimeout(timeout);
                            reject(err);
                        },
                        ontimeout: () => {
                            clearTimeout(timeout);
                            reject(new Error('Request timeout'));
                        }
                    });
                });

                saveAs(response, filename);
                console.log(`✓ Downloaded: ${filename}`);

            } catch (error) {
                console.error('Failed to download single media:', error);
                alert(`Failed to download media: ${error.message}`);
            }
        };

        const formatNumber = (num) => {
            return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        };

        const getTimeAgo = (timestamp) => {
            const now = Date.now();
            const diff = now - timestamp;
            const seconds = Math.floor(diff / 1000);
            const minutes = Math.floor(seconds / 60);
            const hours = Math.floor(minutes / 60);
            const days = Math.floor(hours / 24);

            if (days > 0) {
                const remainingHours = hours % 24;
                return `${days}d ${remainingHours}h ago`;
            } else if (hours > 0) {
                const remainingMinutes = minutes % 60;
                return `${hours}h ${remainingMinutes}m ago`;
            } else if (minutes > 0) {
                return `${minutes}m ago`;
            } else {
                return 'just now';
            }
        };

        const getMediaIcon = (type) => {
            switch(type) {
                case 'photo':
                    return renderIcon('Image', { color: 'hsl(142.1deg 76.2% 36.3%)' });
                case 'video':
                    return renderIcon('Video', { color: 'hsl(37.7deg 92.1% 50.2%)' });
                case 'animated_gif':
                    return renderIcon('ImagePlay', { color: 'hsl(270deg 60% 50%)' });
                default:
                    return renderIcon('Image', { color: 'hsl(142.1deg 76.2% 36.3%)' });
            }
        };

        const filteredTimeline = selectedUser ? (() => {
            const hasActiveFilter = Object.values(mediaFilters).some(v => v);

            if (!hasActiveFilter) {
                return selectedUser.data.timeline;
            }

            return selectedUser.data.timeline.filter(media => {
                if (mediaFilters.photo && media.type === 'photo') return true;
                if (mediaFilters.video && media.type === 'video') return true;
                if (mediaFilters.animated_gif && media.type === 'animated_gif') return true;
                return false;
            });
        })() : [];

        const paginatedMedia = filteredTimeline.slice(
            (currentPage - 1) * itemsPerPage,
            currentPage * itemsPerPage
        );

        const totalPages = Math.ceil(filteredTimeline.length / itemsPerPage) || 0;

        const toggleFilter = (type) => {
            setMediaFilters(prev => ({
                ...prev,
                [type]: !prev[type]
            }));
            setCurrentPage(1);
        };

        return h('div', null,
            showDeleteAlert && h(AlertDialog, {
                title: deleteTarget?.type === 'user' ?
                    'Delete User Cache' : 'Delete Media Entry',
                message: deleteTarget?.type === 'user' ?
                    (() => {
                        const targetUsername = typeof deleteTarget.target === 'string' ? deleteTarget.target : deleteTarget.target?.username || '';
                        return h('span', null,
                            'Are you sure you want to delete all cached data for @',
                            h('strong', null, targetUsername),
                            '?'
                        );
                    })() :
                    'Are you sure you want to delete this media entry?',
                onConfirm: handleDeleteConfirm,
                onCancel: handleDeleteCancel
            }),

            showClearAllAlert && h(AlertDialog, {
                title: 'Shred All Cache',
                message: h('span', null,
                    h('strong', null, 'WARNING:'),
                    ' This will permanently delete ALL cached media data. This action cannot be undone. Are you absolutely sure?'
                ),
                onConfirm: handleClearAllConfirm,
                onCancel: () => setShowClearAllAlert(false)
            }),

            showShredListAlert && h(AlertDialog, {
                title: 'Shred Media List',
                message: h('span', null,
                    h('strong', null, 'WARNING:'),
                    ' This will permanently delete ALL media items in this cached list. This action cannot be undone. Are you absolutely sure?'
                ),
                onConfirm: handleShredListConfirm,
                onCancel: () => setShowShredListAlert(false)
            }),

            !selectedUser ? (
                h('div', null,
                    h('div', { style: 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px;' },
                        h('div', { style: 'display: flex; align-items: center; gap: 8px;' },
                        h('button', {
                            className: 'tmd-button tmd-button-outline',
                            style: 'padding: 6px 12px;',
                            onClick: async (e) => {
                                e.stopPropagation();
                                const input = document.createElement('input');
                                input.type = 'file';
                                input.accept = '.json,application/json';
                                input.multiple = true;
                                input.style.display = 'none';
                                input.onchange = async () => {
                                    const files = Array.from(input.files || []);
                                    if (files.length === 0) return;
                                    try {
                                        for (const f of files) {
                                            const text = await f.text();
                                            let importData;
                                            try {
                                                importData = JSON.parse(text);
                                            } catch (parseErr) {
                                                throw new Error(`Invalid JSON file: ${f.name}`);
                                            }
                                            if (importData.tables) {
                                                if (Array.isArray(importData.tables.mediaData)) {
                                                    await db.mediaData.bulkPut(importData.tables.mediaData);
                                                }
                                                if (Array.isArray(importData.tables.settings)) {
                                                    await db.settings.bulkPut(importData.tables.settings);
                                                }
                                            } else if (importData.data && importData.data.data) {
                                                for (const table of importData.data.data) {
                                                    if (table.tableName === 'mediaData' && table.rows) {
                                                        await db.mediaData.bulkPut(table.rows.map(row => row.data || row));
                                                    } else if (table.tableName === 'settings' && table.rows) {
                                                        await db.settings.bulkPut(table.rows.map(row => row.data || row));
                                                    }
                                                }
                                            } else if (importData.mediaData || importData.settings) {
                                                    if (Array.isArray(importData.mediaData)) {
                                                        await db.mediaData.bulkPut(importData.mediaData);
                                                    }
                                                    if (Array.isArray(importData.settings)) {
                                                        await db.settings.bulkPut(importData.settings);
                                                    }
                                            } else {
                                                throw new Error(`Unsupported backup format in file: ${f.name}`);
                                            }
                                        }
                                        await loadCachedUsers();
                                        state.success.value = `Imported ${files.length} file(s) successfully!`;
                                        setTimeout(() => {
                                            if (state.success.value && state.success.value.startsWith('Imported ')) {
                                                state.success.value = null;
                                            }
                                        }, 2000);
                                    } catch (err) {
                                        console.error('Import failed:', err);
                                        state.error.value = `Import failed: ${err.message || 'Unknown error'}`;
                                        state.errorType.value = 'general';
                                        setTimeout(() => {
                                            if (state.error.value && state.error.value.startsWith('Import failed:')) {
                                                state.error.value = null;
                                            }
                                        }, 3000);
                                    } finally {
                                        input.remove();
                                    }
                                };
                                document.body.appendChild(input);
                                input.click();
                            },
                            title: 'Import database backup(s)'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('FileInput') } }),
                            'Import'
                        ),

                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-shred-button',
                            style: 'padding: 6px 12px;',
                            onClick: () => setShowClearAllAlert(true),
                            title: 'Shred all cached data',
                            disabled: !hasAnyDatabase
                        },
                            h('span', {
                                dangerouslySetInnerHTML: {
                                    __html: renderIcon('Shredder')
                                }
                            }),
                            'Shred'
                            )
                        ),

                        h('button', {
                            className: `tmd-button tmd-button-outline ${
                                state.showBatchDatabase.value ? 'tmd-batch-toggle-active' : ''
                            }`,
                            style: `padding: 6px 12px; ${
                                state.showBatchDatabase.value ?
                                'background: hsl(270deg 60% 50% / 0.15); border-color: hsl(270deg 60% 50%); color: hsl(270deg 60% 50%);' :
                                ''
                            }${
                                !hasBatchDatabases ? ' opacity: 0.5; cursor: not-allowed;' : ''
                            }`,
                            onClick: () => {
                                if (hasBatchDatabases) {
                                    state.showBatchDatabase.value = !state.showBatchDatabase.value;
                                    saveSettings();
                                    loadCachedUsers();
                                }
                            },
                            disabled: !hasBatchDatabases,
                            title: !hasBatchDatabases ?
                                'No batch databases available' :
                                state.showBatchDatabase.value ?
                                'Filter enabled: Showing only batch databases. Click to show all databases' :
                                'Filter disabled: Showing all databases. Click to filter batch databases only'
                        },
                            h('span', {
                                dangerouslySetInnerHTML: {
                                    __html: renderIcon('Layers')
                                }
                            }),
                            'Batch'
                        )
                    ),

                    cachedUsers.length === 0 ? (
                        h('div', {
                            style: 'text-align: center; padding: 40px 20px; opacity: 0.6;'
                        },
                            h('div', {
                                dangerouslySetInnerHTML: { __html: renderIcon('Frown', { size: 32 }) },
                                style: 'display: flex; justify-content: center; margin-bottom: 16px; opacity: 0.5;'
                            }),
                            h('p', { style: 'font-size: 16px;' },
                                state.showBatchDatabase.value ?
                                'No batch databases available' :
                                'No cached data available'
                            )
                        )
                    ) : (() => {
                    const paginatedAccounts = cachedUsers.slice(
                        (currentAccountPage - 1) * accountsPerPage,
                        currentAccountPage * accountsPerPage
                    );
                    const totalAccountPages = Math.ceil(cachedUsers.length / accountsPerPage);

                    return h('div', null,

                        h('div', { style: 'margin-bottom: 20px;' },
                            paginatedAccounts.map(user =>
                                h('div', {
                                    className: 'tmd-info-card clickable',
                                    style: 'margin-bottom: 12px;'
                                },
                                h('div', { style: 'display: flex; align-items: center; gap: 4px;' },
                                    user.data.account_info.profile_image && h('img', {
                                        src: user.data.account_info.profile_image,
                                        style: 'width: 56px; height: 56px; border-radius: 50%; object-fit: cover;'
                                    }),

                                    h('div', { style: 'flex: 1;' },
                                        h('div', { style: 'font-weight: 600; display: flex; align-items: center; gap: 8px;' },
                                            user.data.account_info.nick,
                                            user.isBatchGroup && h('span', {
                                                style: 'background: hsl(270deg 60% 50% / 0.2); color: hsl(270deg 60% 50%); padding: 2px 8px; border-radius: 4px; font-weight: 500; font-size: 11px;'
                                            }, 'BATCH')
                                        ),
                                        h('a', {
                                            href: `https://x.com/${user.username}`,
                                            target: '_blank',
                                            rel: 'noopener noreferrer',
                                            style: 'font-size: 14px; opacity: 0.7; color: inherit; text-decoration: none; display: inline-block; transition: all 0.2s;',
                                            onMouseEnter: (e) => {
                                                e.target.style.opacity = '1';
                                                e.target.style.color = 'hsl(204.17deg 87.55% 52.75%)';
                                                e.target.style.textDecoration = 'underline';
                                            },
                                            onMouseLeave: (e) => {
                                                e.target.style.opacity = '0.7';
                                                e.target.style.color = 'inherit';
                                                e.target.style.textDecoration = 'none';
                                            },
                                            onClick: (e) => e.stopPropagation()
                                        }, `@${user.username}`),
                                        h('div', { style: 'font-size: 12px; opacity: 0.5; margin-top: 4px;' },
                                            `Cached: ${dayjs(user.latestTimestamp).format('DD MMM YYYY HH:mm')} • ${getTimeAgo(user.latestTimestamp)}`
                                        ),
                                        h('div', { style: 'display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px;' },
                                            user.configs.map(config =>
                                                h('span', {
                                                    style: `
                                                        display: inline-flex;
                                                        align-items: center;
                                                        gap: 3px;
                                                        padding: 2px 6px;
                                                        font-size: 11px;
                                                        font-weight: 500;
                                                        border-radius: 4px;
                                                        background: ${
                                                            config.timelineType === 'media' ? 'hsl(204.17deg 87.55% 52.75% / 0.15)' :
                                                            config.timelineType === 'timeline' ? 'hsl(142.1deg 76.2% 36.3% / 0.15)' :
                                                            config.timelineType === 'tweets' ? 'hsl(37.7deg 92.1% 50.2% / 0.15)' :
                                                            'hsl(270deg 60% 50% / 0.15)'
                                                        };
                                                        color: ${
                                                            config.timelineType === 'media' ? 'hsl(204.17deg 87.55% 52.75%)' :
                                                            config.timelineType === 'timeline' ? 'hsl(142.1deg 76.2% 36.3%)' :
                                                            config.timelineType === 'tweets' ? 'hsl(37.7deg 92.1% 50.2%)' :
                                                            'hsl(270deg 60% 50%)'
                                                        };
                                                        border: 1px solid ${
                                                            config.timelineType === 'media' ? 'hsl(204.17deg 87.55% 52.75% / 0.3)' :
                                                            config.timelineType === 'timeline' ? 'hsl(142.1deg 76.2% 36.3% / 0.3)' :
                                                            config.timelineType === 'tweets' ? 'hsl(37.7deg 92.1% 50.2% / 0.3)' :
                                                            'hsl(270deg 60% 50% / 0.3)'
                                                        };
                                                        cursor: pointer;
                                                        transition: all 0.2s;
                                                    `,
                                                    onClick: async (e) => {
                                                        e.stopPropagation();
                                                        const cacheData = await db.mediaData.get(config.cacheKey);
                                                        if (cacheData) {
                                                            setSelectedUser(cacheData);
                                                            setCurrentPage(1);
                                                        }
                                                    },
                                                    onMouseEnter: (e) => {
                                                        e.target.style.opacity = '0.8';
                                                    },
                                                    onMouseLeave: (e) => {
                                                        e.target.style.opacity = '1';
                                                    },
                                                    title: `Load ${config.timelineType} with ${config.mediaType} media (${config.mediaCount} items)${config.isBatch ? ' - Batch' : ''}`
                                                },
                                                    h('span', null,
                                                        config.timelineType === 'media' ? 'Media' :
                                                        config.timelineType === 'timeline' ? 'Posts' :
                                                        config.timelineType === 'tweets' ? 'Tweets' :
                                                        'Replies'
                                                    ),
                                                    config.mediaType !== 'all' && h('span', {
                                                        style: 'opacity: 0.8; font-weight: 400;'
                                                    },
                                                        config.mediaType === 'image' ? '[IMG]' :
                                                        config.mediaType === 'video' ? '[VID]' :
                                                        '[GIF]'
                                                    ),
                                                    h('span', {
                                                        style: 'opacity: 0.9; font-weight: 600;'
                                                    }, `(${formatNumber(config.mediaCount)})`)
                                                )
                                            )
                                        )
                                    ),

                                    h('button', {
                                        className: 'tmd-button tmd-button-outline tmd-button-square',
                                        style: 'height: 40px;',
                                        title: 'Preview media gallery',
                                        onClick: async (e) => {
                                            e.stopPropagation();
                                            let dataToUse = user.data;
                                            if (!dataToUse?.timeline) {
                                                const firstConfig = user.configs?.[0];
                                                if (firstConfig?.cacheKey) {
                                                    const cacheData = await db.mediaData.get(firstConfig.cacheKey);
                                                    if (cacheData) dataToUse = cacheData.data;
                                                }
                                            }
                                            setPreviewData({
                                                account_info: dataToUse?.account_info || user.data?.account_info || {},
                                                timeline: dataToUse?.timeline || []
                                            });
                                            setPreviewIndex(0);
                                            setPreviewFilters({ photo: true, video: true, animated_gif: true });
                                            setShowPreviewModal(true);
                                        },
                                        dangerouslySetInnerHTML: { __html: renderIcon('Eye') }
                                    }),
                                    h('button', {
                                        className: 'tmd-button tmd-button-outline tmd-button-square tmd-delete-button',
                                        style: 'height: 40px;',
                                        title: 'Delete cache',
                                        onClick: (e) => {
                                            e.stopPropagation();
                                            handleDeleteClick('user', { username: user.username, isBatchGroup: user.isBatchGroup });
                                        },
                                        dangerouslySetInnerHTML: { __html: renderIcon('Trash2') }
                                    })
                                )
                            ))
                        ),

                        totalAccountPages > 1 && h('div', {
                            style: 'display: flex; justify-content: center; gap: 8px; align-items: center;'
                        },
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-button-square',
                                disabled: currentAccountPage === 1,
                                onClick: () => setCurrentAccountPage(1),
                                title: 'First page',
                                dangerouslySetInnerHTML: { __html: renderIcon('ChevronsLeft') }
                            }),
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-button-square',
                                disabled: currentAccountPage === 1,
                                onClick: () => setCurrentAccountPage(currentAccountPage - 1),
                                title: 'Previous page',
                                dangerouslySetInnerHTML: { __html: renderIcon('ChevronLeft') }
                            }),
                            h('span', {
                                style: 'padding: 0 12px; display: flex; align-items: center; font-weight: 500;'
                            }, `${currentAccountPage} / ${totalAccountPages}`),
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-button-square',
                                disabled: currentAccountPage === totalAccountPages,
                                onClick: () => setCurrentAccountPage(currentAccountPage + 1),
                                title: 'Next page',
                                dangerouslySetInnerHTML: { __html: renderIcon('ChevronRight') }
                            }),
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-button-square',
                                disabled: currentAccountPage === totalAccountPages,
                                onClick: () => setCurrentAccountPage(totalAccountPages),
                                title: 'Last page',
                                dangerouslySetInnerHTML: { __html: renderIcon('ChevronsRight') }
                            })
                        )
                    );
                    })()
                )
            ) : (
                h('div', { className: 'tmd-database-content' },
                    h('div', { className: 'tmd-database-buttons', style: 'margin-bottom: 16px;' },
                        h('button', {
                            className: 'tmd-button tmd-button-outline',
                            onClick: () => {
                                setSelectedUser(null);
                                setCurrentPage(1);
                            },
                            title: 'Back to user list'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Undo2') } }),
                            'Back'
                        ),
                        h('div', { className: 'tmd-database-buttons-right' },
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-load-button',
                                title: 'Load this cached data to Dashboard',
                                onClick: async () => {
                                    if (selectedUser) {
                                        state.mediaData.value = selectedUser.data;
                                        state.currentUsername.value = selectedUser.username;
                                        
                                        state.loadedFromDatabase.value = true;
                                        state.loadedDatabaseConfig.value = {
                                            cacheKey: selectedUser.cacheKey,
                                            isBatch: selectedUser.isBatch || (selectedUser.cacheKey && selectedUser.cacheKey.endsWith('_batch')),
                                            timelineType: selectedUser.timelineType || parts[1],
                                            mediaType: selectedUser.mediaType || parts[2]
                                        };

                                        if (selectedUser.cacheKey) {
                                            const parts = selectedUser.cacheKey.split('_');
                                            if (parts.length >= 3) {
                                                state.timelineType.value = parts[1];
                                                state.mediaType.value = parts[2];
                                                
                                                state.loadedDatabaseConfig.value.timelineType = parts[1];
                                                state.loadedDatabaseConfig.value.mediaType = parts[2] === 'batch' ? parts[2 - 1] : parts[2];
                                            }
                                        } else if (selectedUser.timelineType && selectedUser.mediaType) {
                                            state.timelineType.value = selectedUser.timelineType;
                                            state.mediaType.value = selectedUser.mediaType;
                                        }

                                        state.activeTab.value = 'dashboard';
                                    }
                                }
                            },
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Upload') } }),
                                'Load'
                            ),
                            h('button', {
                                className: 'tmd-button tmd-button-outline',
                                title: 'Export entire database to a JSON backup',
                                onClick: async () => {
                                    try {
                                        const exportData = {
                                            databaseName: db.name,
                                            version: db.verno,
                                            exportDate: new Date().toISOString(),
                                            tables: {}
                                        };
                                        
                                        const settings = await db.settings.toArray();
                                        exportData.tables.settings = settings;
                                        
                                        if (selectedUser && selectedUser.cacheKey) {
                                            const userData = await db.mediaData.get(selectedUser.cacheKey);
                                            exportData.tables.mediaData = userData ? [userData] : [];
                                        } else {
                                            const mediaData = await db.mediaData.toArray();
                                            exportData.tables.mediaData = mediaData;
                                        }
                                        const jsonString = JSON.stringify(exportData, null, 2);
                                        const blob = new Blob([jsonString], { type: 'application/json' });
                                        const uname = (typeof selectedUser?.username === 'string' && selectedUser.username) ? selectedUser.username : (state.currentUsername.value || 'database');
                                        const ts = dayjs().format('YYYYMMDD_HHmmss');
                                        
                                        const isBatch = selectedUser?.isBatch || (selectedUser?.cacheKey && selectedUser.cacheKey.endsWith('_batch'));
                                        
                                        let mediaType = 'all';
                                        if (selectedUser?.mediaType) {
                                            mediaType = selectedUser.mediaType;
                                        } else if (selectedUser?.cacheKey) {
                                            const parts = selectedUser.cacheKey.split('_');
                                            if (parts.length >= 3) {
                                                mediaType = parts[2] === 'batch' ? (parts.length >= 4 ? parts[2] : 'all') : parts[2];
                                            }
                                        }
                                        
                                        let fileName = `${uname}`;
                                        if (isBatch) {
                                            fileName += '_batch';
                                        }
                                        fileName += `_${mediaType}_${ts}.json`;
                                        saveAs(blob, fileName);
                                        state.success.value = `Exported database as ${fileName}`;
                                        setTimeout(() => {
                                            if (state.success.value && state.success.value.startsWith('Exported database')) {
                                                state.success.value = null;
                                            }
                                        }, 2000);
                                    } catch (err) {
                                        console.error('Export failed:', err);
                                        state.error.value = `Export failed: ${err.message || 'Unknown error'}`;
                                        state.errorType.value = 'general';
                                        setTimeout(() => {
                                            if (state.error.value && state.error.value.startsWith('Export failed:')) {
                                                state.error.value = null;
                                            }
                                        }, 3000);
                                    }
                                }
                            },
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('FileOutput') } }),
                                'Export'
                            ),
                            h('button', {
                                className: 'tmd-button tmd-button-outline tmd-shred-button',
                                title: 'Shred: delete all media in this cached list',
                                onClick: () => setShowShredListAlert(true)
                            },
                                h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Shredder') } }),
                                'Shred'
                            )
                        )
                    ),
                    h('div', {
                        className: 'tmd-info-card',
                        style: 'margin-bottom: 8px; background: hsl(204.17deg 87.55% 52.75% / 0.1);'
                    },
                        h('div', { style: 'display: flex; align-items: center; justify-content: space-between;' },
                            h('div', { style: 'display: flex; align-items: center; gap: 8px;' },
                                h('span', {
                                    dangerouslySetInnerHTML: { __html: renderIcon('Database', { color: 'hsl(204.17deg 87.55% 52.75%)' }) },
                                    style: 'opacity: 0.8;'
                                }),
                                h('div', null,
                                    h('div', { style: 'font-size: 14px; font-weight: 600;' },
                                        `${selectedUser.data.account_info.nick}'s ${(() => {
                                            if (selectedUser.mediaType) {
                                                if (selectedUser.mediaType === 'image') return 'Images';
                                                if (selectedUser.mediaType === 'video') return 'Videos';
                                                if (selectedUser.mediaType === 'gif') return 'GIFs';
                                            }
                                            if (selectedUser.cacheKey) {
                                                const parts = selectedUser.cacheKey.split('_');
                                                if (parts.length >= 3) {
                                                    const mediaType = parts[2];
                                                    if (mediaType === 'image') return 'Images';
                                                    if (mediaType === 'video') return 'Videos';
                                                    if (mediaType === 'gif') return 'GIFs';
                                                }
                                            }
                                            return 'Media';
                                        })()}`),
                                    h('div', { style: 'font-size: 12px; opacity: 0.6;' },
                                        filteredTimeline.length === 0 ?
                                            'No items match the selected filters' :
                                            `Showing ${formatNumber(Math.min((currentPage - 1) * itemsPerPage + 1, filteredTimeline.length))}-${
                                                formatNumber(Math.min(currentPage * itemsPerPage, filteredTimeline.length))
                                            } of ${formatNumber(filteredTimeline.length)} items${
                                                Object.values(mediaFilters).some(v => v) ? ' (filtered)' : ''
                                            }`
                                    )
                                )
                            ),

                            (selectedUser.mediaType === 'all' || !selectedUser.mediaType) && h('div', { style: 'display: flex; gap: 4px;' },
                                h('button', {
                                    className: `tmd-button tmd-button-outline tmd-button-square tmd-filter-button tmd-filter-photo`,
                                    style: `width: 32px; height: 32px; ${
                                        mediaFilters.photo ?
                                        'background: hsl(142.1deg 76.2% 36.3% / 0.15); border-color: hsl(142.1deg 76.2% 36.3%);' :
                                        ''
                                    }`,
                                    onClick: () => toggleFilter('photo'),
                                    title: 'Filter photos',
                                    dangerouslySetInnerHTML: {
                                        __html: renderIcon('Image', {
                                            color: mediaFilters.photo ? 'hsl(142.1deg 76.2% 36.3%)' : 'currentColor'
                                        })
                                    }
                                }),

                                h('button', {
                                    className: `tmd-button tmd-button-outline tmd-button-square tmd-filter-button tmd-filter-video`,
                                    style: `width: 32px; height: 32px; ${
                                        mediaFilters.video ?
                                        'background: hsl(37.7deg 92.1% 50.2% / 0.15); border-color: hsl(37.7deg 92.1% 50.2%);' :
                                        ''
                                    }`,
                                    onClick: () => toggleFilter('video'),
                                    title: 'Filter videos',
                                    dangerouslySetInnerHTML: {
                                        __html: renderIcon('Video', {
                                            color: mediaFilters.video ? 'hsl(37.7deg 92.1% 50.2%)' : 'currentColor'
                                        })
                                    }
                                }),

                                h('button', {
                                    className: `tmd-button tmd-button-outline tmd-button-square tmd-filter-button tmd-filter-gif`,
                                    style: `width: 32px; height: 32px; ${
                                        mediaFilters.animated_gif ?
                                        'background: hsl(270deg 60% 50% / 0.15); border-color: hsl(270deg 60% 50%);' :
                                        ''
                                    }`,
                                    onClick: () => toggleFilter('animated_gif'),
                                    title: 'Filter animated GIFs',
                                    dangerouslySetInnerHTML: {
                                        __html: renderIcon('ImagePlay', {
                                            color: mediaFilters.animated_gif ? 'hsl(270deg 60% 50%)' : 'currentColor'
                                        })
                                    }
                                })
                            )
                        )
                    ),

                        h('div', { className: 'tmd-media-list-wrapper' },
                            h('div', { className: 'tmd-media-list-container' },
                                filteredTimeline.length === 0 ?
                                    h('div', {
                                        style: 'text-align: center; padding: 40px 20px; opacity: 0.6;'
                                    },
                                        h('div', {
                                            dangerouslySetInnerHTML: { __html: renderIcon('Frown', { size: 32 }) },
                                            style: 'display: flex; justify-content: center; margin-bottom: 12px; opacity: 0.5;'
                                        }),
                                        h('p', { style: 'font-size: 14px;' },
                                            Object.values(mediaFilters).some(v => v) ?
                                                'No media matches the selected filters' :
                                                'No media available'
                                        )
                                    ) :
                                paginatedMedia.map((media) => {
                                const originalIndex = selectedUser.data.timeline.indexOf(media);
                                return h('div', {
                                    className: 'tmd-info-card clickable',
                                    style: 'margin-bottom: 8px;'
                                },
                                    h('div', { style: 'display: flex; align-items: center; justify-content: space-between;' },
                                        h('div', { style: 'display: flex; align-items: center; gap: 8px;' },
                                            h('span', {
                                                dangerouslySetInnerHTML: { __html: getMediaIcon(media.type) },
                                                style: 'opacity: 0.7;'
                                            }),
                                            h('div', null,
                                                h('a', {
                                                    className: 'tmd-tweet-link',
                                                    href: `https://x.com/${selectedUser.username}/status/${media.tweet_id}`,
                                                    target: '_blank',
                                                    rel: 'noopener noreferrer',
                                                    style: `font-size: 14px; display: block; color: ${
                                                        media.type === 'photo' ? 'hsl(142.1deg 76.2% 36.3%)' :
                                                        media.type === 'video' ? 'hsl(37.7deg 92.1% 50.2%)' :
                                                        media.type === 'animated_gif' ? 'hsl(270deg 60% 50%)' :
                                                        'hsl(204.17deg 87.55% 52.75%)'
                                                    };`,
                                                    title: `View tweet ${media.tweet_id}`
                                                }, media.tweet_id),
                                                h('div', { style: 'font-size: 12px; opacity: 0.6;' },
                                                    dayjs(media.date).format('DD MMM YYYY HH:mm'))
                                            )
                                        ),
                                        h('div', { style: 'display: flex; gap: 4px;' },
                                            h('button', {
                                                className: 'tmd-button tmd-button-outline tmd-button-square tmd-preview-button',
                                                style: 'width: 32px; height: 32px;',
                                                title: 'Preview media',
                                                onClick: () => window.open(media.url, '_blank'),
                                                dangerouslySetInnerHTML: { __html: renderIcon('Eye') }
                                            }),
                                            h('button', {
                                                className: 'tmd-button tmd-button-outline tmd-button-square tmd-download-single-button',
                                                style: 'width: 32px; height: 32px;',
                                                title: 'Download media',
                                                onClick: () => downloadSingleMedia(media, originalIndex),
                                                dangerouslySetInnerHTML: { __html: renderIcon('Download') }
                                            }),
                                            h('button', {
                                                className: 'tmd-button tmd-button-outline tmd-button-square tmd-delete-button',
                                                style: 'width: 32px; height: 32px;',
                                                title: 'Delete media',
                                                onClick: () => handleDeleteClick('media', { cacheKey: selectedUser.cacheKey, index: originalIndex }),
                                                dangerouslySetInnerHTML: { __html: renderIcon('Trash2') }
                                            })
                                        )
                                    )
                                );
                                })
                            )
                        ),

                        totalPages > 1 && h('div', {
                        style: 'display: flex; justify-content: center; gap: 8px; align-items: center;'
                    },
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            disabled: currentPage === 1,
                            onClick: () => setCurrentPage(1),
                            title: 'First page',
                            dangerouslySetInnerHTML: { __html: renderIcon('ChevronsLeft') }
                        }),
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            disabled: currentPage === 1,
                            onClick: () => setCurrentPage(currentPage - 1),
                            title: 'Previous page',
                            dangerouslySetInnerHTML: { __html: renderIcon('ChevronLeft') }
                        }),
                        h('span', {
                            style: 'padding: 0 12px; display: flex; align-items: center; font-weight: 500;'
                        }, `${currentPage} / ${totalPages}`),
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            disabled: currentPage === totalPages,
                            onClick: () => setCurrentPage(currentPage + 1),
                            title: 'Next page',
                            dangerouslySetInnerHTML: { __html: renderIcon('ChevronRight') }
                        }),
                        h('button', {
                            className: 'tmd-button tmd-button-outline tmd-button-square',
                            disabled: currentPage === totalPages,
                            onClick: () => setCurrentPage(totalPages),
                            title: 'Last page',
                            dangerouslySetInnerHTML: { __html: renderIcon('ChevronsRight') }
                        })
                    )
                )
            ),

            showPreviewModal && previewData && h(PreviewModal, {
                data: previewData,
                index: previewIndex,
                onClose: () => setShowPreviewModal(false),
                onIndexChange: (i) => setPreviewIndex(i),
                filters: previewFilters,
                setFilters: setPreviewFilters
            })
        );
    }

    function PreviewModal({ data, index, onClose, onIndexChange, filters, setFilters }) {
        const containerRef = useRef(null);
        const [touchStartX, setTouchStartX] = useState(null);
        const [, setTouchEndX] = useState(null);
        
        const formatNumber = (num) => {
            return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
        };

        const allTimeline = Array.isArray(data?.timeline) ? data.timeline : [];
        const filtered = allTimeline.filter(m =>
            (filters.photo && m.type === 'photo') ||
            (filters.video && m.type === 'video') ||
            (filters.animated_gif && m.type === 'animated_gif')
        );

        const counts = allTimeline.reduce((acc, m) => {
            acc.total++;
            if (m.type === 'photo') acc.photo++;
            else if (m.type === 'video') acc.video++;
            else if (m.type === 'animated_gif') acc.animated_gif++;
            return acc;
        }, { photo: 0, video: 0, animated_gif: 0, total: 0 });

        const current = filtered[Math.max(0, Math.min(index, filtered.length - 1))];

        useEffect(() => {
            function onKey(e) {
                if (e.key === 'Escape') onClose();
                if (e.key === 'ArrowRight') next();
                if (e.key === 'ArrowLeft') prev();
            }
            window.addEventListener('keydown', onKey);
            return () => window.removeEventListener('keydown', onKey);
        }, [index, filtered.length]);

        const onTouchStart = (e) => setTouchStartX(e.changedTouches[0].clientX);
        const onTouchEnd = (e) => {
            setTouchEndX(e.changedTouches[0].clientX);
            const delta = e.changedTouches[0].clientX - (touchStartX ?? e.changedTouches[0].clientX);
            const threshold = 40;
            if (delta < -threshold) next();
            if (delta > threshold) prev();
        };

        const next = () => onIndexChange(Math.min(index + 1, filtered.length - 1));
        const prev = () => onIndexChange(Math.max(index - 1, 0));

        const toggleFilter = (key) => setFilters(prev => ({ ...prev, [key]: !prev[key] }));

        const acc = data?.account_info || {};
        const profileImg = acc.profile_image || acc.profile_image_url || '';
        const displayName = acc.name || acc.nick || '';
        const username = acc.nick || acc.name || '';
        const followers = acc.followers_count ?? acc.followers ?? 0;
        const following = acc.friends_count ?? acc.following ?? 0;

        return h('div', { className: 'tmd-preview-overlay' },
            h('div', { className: 'tmd-preview-header' },
                h('div', { className: 'tmd-preview-id' },
                    profileImg && h('img', { src: profileImg, style: 'width:36px;height:36px;border-radius:50%;object-fit:cover;flex-shrink:0;' }),
                    h('div', { className: 'tmd-preview-id-text' },
                        h('div', { className: 'tmd-acc-name' }, displayName || username || 'Unknown'),
                        h('div', { className: 'tmd-acc-username' }, `@${username}`)
                    )
                ),
                h('div', { className: 'tmd-preview-stats' },
                    h('span', null, 'Media: ', h('strong', null, formatNumber(counts.total))),
                    h('span', null, 'Followers: ', h('strong', null, formatNumber(followers))),
                    h('span', null, 'Following: ', h('strong', null, formatNumber(following)))
                ),
                h('div', { className: 'tmd-preview-toolbar' },
                        h('div', {
                            className: `tmd-filter-chip ${filters.photo ? 'active' : ''} tmd-filter-photo`,
                            onClick: () => toggleFilter('photo'),
                            title: 'Show photos'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Image') } }),
                            h('span', null, formatNumber(counts.photo))
                        ),
                        h('div', {
                            className: `tmd-filter-chip ${filters.video ? 'active' : ''} tmd-filter-video`,
                            onClick: () => toggleFilter('video'),
                            title: 'Show videos'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Video') } }),
                            h('span', null, formatNumber(counts.video))
                        ),
                        h('div', {
                            className: `tmd-filter-chip ${filters.animated_gif ? 'active' : ''} tmd-filter-gif`,
                            onClick: () => toggleFilter('animated_gif'),
                            title: 'Show GIFs'
                        },
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('ImagePlay') } }),
                            h('span', null, formatNumber(counts.animated_gif))
                        )
                    ),
                h('button', {
                    className: 'tmd-preview-close',
                    onClick: onClose,
                    title: 'Close preview',
                    dangerouslySetInnerHTML: { __html: renderIcon('X') }
                })
            ),
            h('div', {
                className: 'tmd-preview-content',
                ref: containerRef,
                onTouchStart,
                onTouchEnd
            },
                filtered.length === 0 ? h('div', { style: 'color:#fff; opacity:0.8; text-align: center; display: flex; flex-direction: column; align-items: center; gap: 16px;' },
                    h('div', {
                        dangerouslySetInnerHTML: { __html: renderIcon('Frown', { size: 32 }) },
                        style: 'opacity: 0.5;'
                    }),
                    h('div', { style: 'font-size: 16px;' }, 'No media for selected filters')
                ) : (
                    current?.type === 'photo' ?
                        h('img', { className: 'tmd-preview-media', src: current.url, alt: 'photo' }) :
                        h('video', {
                            className: 'tmd-preview-media',
                            src: current.url,
                            controls: true,
                            autoplay: false,
                            loop: current.type === 'animated_gif',
                            muted: current.type === 'animated_gif'
                        })
                ),
                filtered.length > 1 && h('div', { className: 'tmd-preview-nav prev', onClick: prev, dangerouslySetInnerHTML: { __html: renderIcon('ChevronLeft') } }),
                filtered.length > 1 && h('div', { className: 'tmd-preview-nav next', onClick: next, dangerouslySetInnerHTML: { __html: renderIcon('ChevronRight') } }),
                filtered.length > 0 && h('div', { className: 'tmd-preview-counter' },
                    `${Math.max(1, index + 1)}/${filtered.length}`
                ),
                current && h('div', { className: 'tmd-preview-date' },
                    dayjs(current.date).format('MMM DD, YYYY • HH:mm')
                )
            )
        );
    }

    function SettingsTab() {
        return h('div', null,
            h('div', {
                style: 'display: flex; gap: 20px; margin-bottom: 20px;'
            },
                h('div', { className: 'tmd-input-group', style: 'width: 180px; margin-bottom: 0;' },
                    h('label', { className: 'tmd-label' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Layers') } }),
                        'Batch Size'
                    ),
                    h('input', {
                        type: 'number',
                        className: 'tmd-input',
                        value: state.batchSize.value,
                        onInput: (e) => {
                            const value = parseInt(e.target.value);
                            if (value > 0 && value <= 200) {
                                state.batchSize.value = value;
                                saveSettings();
                            }
                        },
                        placeholder: '1-200',
                        min: 1,
                        max: 200,
                        style: 'padding-right: 12px; width: 100%;'
                    })
                ),

                h('div', { className: 'tmd-input-group', style: 'width: 180px; margin-bottom: 0;' },
                    h('label', { className: 'tmd-label' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('BetweenHorizontalStart') } }),
                        'Starting Batch'
                    ),
                    h('input', {
                        type: 'number',
                        className: 'tmd-input',
                        value: state.startingBatch.value,
                        onInput: (e) => {
                            const value = parseInt(e.target.value);
                            if (value >= 0) {
                                state.startingBatch.value = value;
                                state.currentBatchPage.value = value;
                                saveSettings();
                            }
                        },
                        placeholder: '0-based',
                        min: 0,
                        style: 'padding-right: 12px; width: 100%;'
                    })
                )
            ),

            h('div', { className: 'tmd-input-group' },
                h('label', { className: 'tmd-label' },
                    h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Twitter') } }),
                    'Timeline'
                ),
                h('div', { className: 'tmd-radio-group', style: 'display: flex; gap: 20px; margin-top: 12px; flex-wrap: wrap;' },
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.timelineType.value = 'media';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.timelineType.value === 'media' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Media')
                    ),
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.timelineType.value = 'timeline';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.timelineType.value === 'timeline' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Posts')
                    ),
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.timelineType.value = 'tweets';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.timelineType.value === 'tweets' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Tweets')
                    ),
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.timelineType.value = 'with_replies';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.timelineType.value === 'with_replies' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Replies')
                    )
                )
            ),

            h('div', { className: 'tmd-input-group' },
                h('label', { className: 'tmd-label' },
                    h('span', { dangerouslySetInnerHTML: { __html: renderIcon('HardDriveDownload') } }),
                    'Concurrent Downloads'
                ),
                h('div', { className: 'tmd-radio-group', style: 'display: flex; gap: 20px; margin-top: 12px; flex-wrap: wrap;' },
                    [5, 10, 20, 50, 100].map(limit =>
                        h('div', {
                            className: 'tmd-radio-item',
                            onClick: () => {
                                state.concurrentLimit.value = limit;
                                saveSettings();
                            }
                        },
                            h('div', {
                                className: `tmd-radio ${state.concurrentLimit.value === limit ? 'checked' : ''}`
                            }),
                            h('span', { className: 'tmd-radio-label' }, limit.toString())
                        )
                    )
                )
            ),

            h('div', { className: 'tmd-input-group' },
                h('label', { className: 'tmd-label' },
                    h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Server') } }),
                    'Service'
                ),
                h('div', { className: 'tmd-radio-group', style: 'display: flex; gap: 20px; margin-top: 12px; flex-wrap: wrap;' },
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.selectedApi.value = 'default';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.selectedApi.value === 'default' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Default')
                    ),
                    h('div', {
                        className: 'tmd-radio-item',
                        onClick: () => {
                            state.selectedApi.value = 'backup';
                            saveSettings();
                        }
                    },
                        h('div', {
                            className: `tmd-radio ${state.selectedApi.value === 'backup' ? 'checked' : ''}`
                        }),
                        h('span', { className: 'tmd-radio-label' }, 'Backup')
                    )
                )
            ),

            h('div', { className: 'tmd-success' },
                h('div', null,
                    h('div', { style: 'margin-bottom: 8px;' },
                        '• For accounts with thousands of media: Use ',
                        h('strong', { style: 'color: hsl(204.17deg 87.55% 52.75%);' }, 'Batch/Auto Batch'),
                        ' mode if single fetch fails.'
                    ),
                    h('div', { style: 'margin-bottom: 8px;' },
                        '• If Default service fails: Switch to ',
                        h('strong', { style: 'color: hsl(142.1deg 76.2% 36.3%);' }, 'Backup'),
                        ' service.'
                    ),
                    h('div', null,
                        h('strong', { style: 'color: hsl(0deg 84.2% 60.2%);' }, '• Warning:'),
                        ' Using more than 20 concurrent downloads may cause some files to fail. Use ',
                        h('strong', { style: 'color: hsl(142.1deg 76.2% 36.3%);' }, '20 or below'),
                        ' for better reliability.'
                    )
                )
            )
        );
    }

    function AuthTab() {
        const [showAuthToken, setShowAuthToken] = useState(false);
        const [showPatreonAuth, setShowPatreonAuth] = useState(false);
        const [generateStatus, setGenerateStatus] = useState('idle');
        const [verifyStatus, setVerifyStatus] = useState('idle');

        const handleAuthTokenChange = (e) => {
            state.authToken.value = e.target.value;
            saveSettings();
        };

        const handlePatreonAuthChange = (e) => {
            state.patreonAuth.value = e.target.value;
            state.isVerified.value = false;
            saveSettings();
        };

        const verifyPatreonAuth = async () => {
            if (!state.patreonAuth.value) {
                return;
            }

            if (state.patreonAuth.value === 'xbatchdemo') {
                state.isVerified.value = true;
                setVerifyStatus('success');
                await saveSettings();
                setTimeout(() => {
                    setVerifyStatus('idle');
                }, 1000);
                return;
            }

            setVerifyStatus('loading');
            state.error.value = null;

            const api = state.selectedApi.value === 'backup'
                ? 'https://backup.xbatch.online'
                : 'https://api.xbatch.online';

            const url = `${api}/verify/${state.patreonAuth.value}`;

            try {
                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 10000,
                        onload: (res) => {
                            if (res.status === 200) {
                                try {
                                    const data = JSON.parse(res.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    reject(new Error('Invalid response format'));
                                }
                            } else {
                                reject(new Error(`Verification failed: ${res.status}`));
                            }
                        },
                        onerror: () => reject(new Error('Network error')),
                        ontimeout: () => reject(new Error('Request timeout'))
                    });
                });

                if (response.valid === true) {
                    state.isVerified.value = true;
                    await saveSettings();
                    setVerifyStatus('success');
                    setTimeout(() => {
                        setVerifyStatus('idle');
                    }, 1000);
                } else {
                    state.isVerified.value = false;
                    await saveSettings();
                    setVerifyStatus('error');
                    state.error.value = 'Invalid Patreon Auth code';
                    state.errorType.value = 'auth';
                    setTimeout(() => {
                        if (state.error.value === 'Invalid Patreon Auth code') {
                            state.error.value = null;
                        }
                        setVerifyStatus('idle');
                    }, 2000);
                }
            } catch (error) {
                console.error('Verification failed:', error);
                state.isVerified.value = false;
                await saveSettings();
                setVerifyStatus('error');
                state.error.value = 'Verification failed. Please try again.';
                state.errorType.value = 'auth';
                setTimeout(() => {
                    if (state.error.value === 'Verification failed. Please try again.') {
                        state.error.value = null;
                    }
                    setVerifyStatus('idle');
                }, 2000);
            }
        };

        const generateAuthToken = async () => {
            if (!state.patreonAuth.value) {
                return;
            }

            if (state.patreonAuth.value === 'xbatchdemo') {
                return;
            }

            setGenerateStatus('loading');
            state.error.value = null;

            const api = state.selectedApi.value === 'backup'
                ? 'https://backup.xbatch.online'
                : 'https://api.xbatch.online';

            const url = `${api}/token/${state.patreonAuth.value}`;

            try {
                const response = await new Promise((resolve, reject) => {
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: url,
                        timeout: 60000,
                        onload: (res) => {
                            if (res.status === 200) {
                                try {
                                    const data = JSON.parse(res.responseText);
                                    resolve(data);
                                } catch (parseError) {
                                    reject(new Error('Invalid response format'));
                                }
                            } else if (res.status === 401) {
                                reject(new Error('Invalid Patreon Auth'));
                            } else if (res.status === 403) {
                                reject(new Error('Access forbidden'));
                            } else if (res.status === 429) {
                                reject(new Error('Rate limit exceeded'));
                            } else {
                                reject(new Error(`API error: ${res.status}`));
                            }
                        },
                        onerror: () => {
                            reject(new Error('Network error'));
                        },
                        ontimeout: () => {
                            reject(new Error('Request timeout'));
                        }
                    });
                });

                if (response.auth_token) {
                    state.authToken.value = response.auth_token;
                    await saveSettings();
                    setGenerateStatus('success');

                    setTimeout(() => {
                        setGenerateStatus('idle');
                    }, 1000);
                } else {
                    throw new Error('No auth token in response');
                }

            } catch (error) {
                console.error('Failed to generate auth token:', error);
                state.error.value = error.message || 'Failed to generate auth token';
                state.errorType.value = 'auth';
                setGenerateStatus('error');

                setTimeout(() => {
                    if (state.error.value) {
                        state.error.value = null;
                    }
                }, 2000);

                setTimeout(() => {
                    setGenerateStatus('idle');
                }, 2000);
            }
        };

        return h('div', null,
            state.error.value && state.errorType.value === 'auth' && h('div', { className: 'tmd-error auth' },
                h('span', { className: 'tmd-error-icon', dangerouslySetInnerHTML: { __html: renderIcon('TriangleAlert') } }),
                h('span', null, state.error.value)
            ),

            h('div', { className: 'tmd-input-group' },
                h('div', { style: 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;' },
                    h('label', { className: 'tmd-label', style: 'margin-bottom: 0;' },
                        h('span', { dangerouslySetInnerHTML: { __html: state.isVerified.value ? renderIcon('LockOpen') : renderIcon('Lock') } }),
                        'Patreon Auth'
                    ),
                    h('button', {
                        className: `tmd-button tmd-button-outline`,
                        onClick: verifyPatreonAuth,
                        disabled: verifyStatus === 'loading' || !state.patreonAuth.value || state.patreonAuth.value.trim() === '',
                        style: `padding: 6px 12px; font-size: 13px; ${
                            verifyStatus === 'success' ? 'background: hsl(142.1deg 76.2% 36.3% / 0.15); border-color: hsl(142.1deg 76.2% 36.3%); color: hsl(142.1deg 76.2% 36.3%);' :
                            verifyStatus === 'error' ? 'background: hsl(0deg 84.2% 60.2% / 0.15); border-color: hsl(0deg 84.2% 60.2%); color: hsl(0deg 84.2% 60.2%);' :
                            !state.patreonAuth.value || state.patreonAuth.value.trim() === '' ? 'opacity: 0.5; cursor: not-allowed;' :
                            ''
                        }`,
                        title: state.patreonAuth.value && state.patreonAuth.value.trim() !== '' ? 'Verify your Patreon Auth' : 'Enter Patreon Auth first'
                    },
                        verifyStatus === 'loading' ?
                            h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2') } }) :
                        verifyStatus === 'success' ?
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('CircleCheckBig', { color: 'hsl(142.1deg 76.2% 36.3%)' }) } }) :
                        verifyStatus === 'error' ?
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('ShieldX', { color: 'hsl(0deg 84.2% 60.2%)' }) } }) :
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('ShieldCheck') } }),
                        verifyStatus === 'loading' ? 'Verifying...' :
                        verifyStatus === 'success' ? 'Verified' :
                        verifyStatus === 'error' ? 'Failed' :
                        'Verify'
                    )
                ),
                h('div', { className: 'tmd-input-wrapper' },
                    h('input', {
                        type: showPatreonAuth ? 'text' : 'password',
                        className: 'tmd-input',
                        value: state.patreonAuth.value,
                        onInput: handlePatreonAuthChange,
                        placeholder: 'Enter your Patreon auth'
                    }),
                    h('div', {
                        className: 'tmd-input-toggle',
                        onClick: () => setShowPatreonAuth(!showPatreonAuth),
                        dangerouslySetInnerHTML: { __html: showPatreonAuth ? renderIcon('EyeOff') : renderIcon('Eye') }
                    })
                )
            ),

            h('div', { className: 'tmd-input-group' },
                h('div', { style: 'display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;' },
                    h('label', { className: 'tmd-label', style: 'margin-bottom: 0;' },
                        h('span', { dangerouslySetInnerHTML: { __html: renderIcon('Key') } }),
                        'Auth Token'
                    ),
                    h('button', {
                        className: `tmd-button tmd-button-outline`,
                        onClick: generateAuthToken,
                        disabled: generateStatus === 'loading' || !state.patreonAuth.value || state.patreonAuth.value.trim() === '' || !state.isVerified.value || state.patreonAuth.value === 'xbatchdemo',
                        style: `padding: 6px 12px; font-size: 13px; ${
                            generateStatus === 'success' ? 'background: hsl(142.1deg 76.2% 36.3% / 0.15); border-color: hsl(142.1deg 76.2% 36.3%); color: hsl(142.1deg 76.2% 36.3%);' :
                            generateStatus === 'error' ? 'background: hsl(0deg 84.2% 60.2% / 0.15); border-color: hsl(0deg 84.2% 60.2%); color: hsl(0deg 84.2% 60.2%);' :
                            !state.patreonAuth.value || state.patreonAuth.value.trim() === '' || !state.isVerified.value || state.patreonAuth.value === 'xbatchdemo' ? 'opacity: 0.5; cursor: not-allowed;' :
                            ''
                        }`,
                        title: !state.patreonAuth.value || state.patreonAuth.value.trim() === '' ? 'Enter Patreon Auth first' : state.patreonAuth.value === 'xbatchdemo' ? 'Demo code cannot generate auth tokens' : !state.isVerified.value ? 'Please verify Patreon Auth first' : 'Generate new auth token'
                    },
                        generateStatus === 'loading' ?
                            h('span', { className: 'tmd-spinner', dangerouslySetInnerHTML: { __html: renderIcon('Loader2') } }) :
                        generateStatus === 'success' ?
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('CircleCheckBig', { color: 'hsl(142.1deg 76.2% 36.3%)' }) } }) :
                        generateStatus === 'error' ?
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('CircleX', { color: 'hsl(0deg 84.2% 60.2%)' }) } }) :
                            h('span', { dangerouslySetInnerHTML: { __html: renderIcon('RotateCcwKey') } }),
                        generateStatus === 'loading' ? 'Generating...' :
                        generateStatus === 'success' ? 'Generated' :
                        generateStatus === 'error' ? 'Failed' :
                        'Generate'
                    )
                ),
                h('div', { className: 'tmd-input-wrapper' },
                    h('input', {
                        type: showAuthToken ? 'text' : 'password',
                        className: 'tmd-input',
                        value: state.authToken.value,
                        onInput: handleAuthTokenChange,
                        placeholder: 'Enter your auth token'
                    }),
                    h('div', {
                        className: 'tmd-input-toggle',
                        onClick: () => setShowAuthToken(!showAuthToken),
                        dangerouslySetInnerHTML: { __html: showAuthToken ? renderIcon('EyeOff') : renderIcon('Eye') }
                    })
                )
            ),

            h('div', { className: 'tmd-success' },
                h('div', null,
                    h('div', { style: 'margin-bottom: 8px;' },
                        '• Use code ',
                        h('code', { 
                            style: 'background: hsl(142.1deg 70% 29% / 0.2); color: hsl(142.1deg 76.2% 36.3%); padding: 2px 6px; border-radius: 4px; cursor: pointer; transition: all 0.2s;',
                            onClick: () => {
                                state.patreonAuth.value = 'xbatchdemo';
                                state.isVerified.value = false;
                                saveSettings();
                            },
                            onMouseEnter: (e) => {
                                e.target.style.background = 'hsl(142.1deg 76.2% 36.3% / 0.3)';
                                e.target.style.transform = 'scale(1.05)';
                            },
                            onMouseLeave: (e) => {
                                e.target.style.background = 'hsl(142.1deg 70% 29% / 0.2)';
                                e.target.style.transform = 'scale(1)';
                            },
                            title: 'Click to auto-fill Patreon Auth'
                        }, 'xbatchdemo'),
                        ' for Patreon Auth, then click ',
                        h('strong', { style: 'color: hsl(142.1deg 76.2% 36.3%);' }, 'Verify'),
                        ' to unlock demo access. Visit ',
                        h('a', {
                            href: 'https://x.com/xbatchdemo',
                            target: '_blank',
                            rel: 'noopener noreferrer',
                            style: 'color: hsl(204.17deg 87.55% 52.75%); text-decoration: none;',
                            onMouseEnter: (e) => e.target.style.textDecoration = 'underline',
                            onMouseLeave: (e) => e.target.style.textDecoration = 'none'
                        }, '@xbatchdemo'),
                        ' to test.'
                    ),
                    h('div', { style: 'margin-bottom: 8px;' },
                        '• Need help getting Auth Token? ',
                        h('a', {
                            href: 'https://www.patreon.com/posts/how-to-obtain-127206894',
                            target: '_blank',
                            rel: 'noopener noreferrer',
                            style: 'color: hsl(204.17deg 87.55% 52.75%); text-decoration: none;',
                            onMouseEnter: (e) => e.target.style.textDecoration = 'underline',
                            onMouseLeave: (e) => e.target.style.textDecoration = 'none'
                        }, 'View the guide here.')
                    ),
                    h('div', { style: 'margin-bottom: 8px;' },
                        h('a', {
                            href: 'https://www.patreon.com/exyezed/membership',
                            target: '_blank',
                            rel: 'noopener noreferrer',
                            style: 'color: hsl(204.17deg 87.55% 52.75%); text-decoration: none;',
                            onMouseEnter: (e) => e.target.style.textDecoration = 'underline',
                            onMouseLeave: (e) => e.target.style.textDecoration = 'none'
                        }, '• Subscribe now'),
                        ' to receive your Patreon auth code and start downloading with ease!'
                    ),
                    h('div', null,
                        '• To report bugs or request features, please contact us at ',
                        h('a', {
                            href: 'mailto:[email protected]',
                            style: 'color: hsl(204.17deg 87.55% 52.75%); text-decoration: none;',
                            onMouseEnter: (e) => e.target.style.textDecoration = 'underline',
                            onMouseLeave: (e) => e.target.style.textDecoration = 'none'
                        }, '[email protected]')
                    )
                )
            )
        );
    }

    function createIcon() {
        const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
        svg.setAttribute("width", "18");
        svg.setAttribute("height", "18");
        svg.setAttribute("viewBox", "0 0 24 24");
        svg.setAttribute("fill", "none");
        svg.setAttribute("stroke", "currentColor");
        svg.setAttribute("stroke-width", "2");
        svg.style.cursor = "pointer";
        svg.style.transition = "color 0.2s";

        const paths = ["M12 15V3", "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", "m7 10 5 5 5-5"];
        paths.forEach(d => {
            const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
            path.setAttribute("d", d);
            svg.appendChild(path);
        });

        return svg;
    }

    function insertIcons() {
        document.querySelectorAll('[data-testid="UserName"]').forEach(div => {
            if (!div.querySelector(".dl-icon")) {
                const target = div.querySelector('[aria-label*="verified"]')?.closest("button")?.parentElement
                            || div.querySelector(".css-1jxf684")?.closest("span");
                if (target) {
                    const icon = createIcon();
                    const wrapper = document.createElement("div");
                    wrapper.className = "dl-icon";
                    wrapper.style.cssText = `
                        display:inline-flex;
                        margin-left:6px;
                        padding:4px;
                        background:hsl(240 3.7% 15.9%);
                        border-radius:4px;
                        transition:background 0.2s;
                    `;
                    wrapper.appendChild(icon);
                    wrapper.onmouseenter = () => {
                        icon.style.color = "hsl(204.17deg 87.55% 52.75%)";
                        wrapper.style.background = "hsl(240 3.7% 20%)";
                    };
                    wrapper.onmouseleave = () => {
                        icon.style.color = "";
                        wrapper.style.background = "hsl(240 3.7% 15.9%)";
                    };
                    wrapper.onclick = (e) => {
                        e.stopPropagation();
                        e.preventDefault();

                        let username = null;

                        const urlMatch = window.location.pathname.match(/^\/([^\/?]+)(?:\/|\?|$)/);
                        if (urlMatch && !['home', 'explore', 'notifications', 'messages', 'bookmarks', 'lists', 'communities', 'premium', 'verified-orgs-signup', 'settings', 'search', 'compose', 'i'].includes(urlMatch[1])) {
                            username = urlMatch[1];
                        }

                        if (!username) {
                            const profileLink = div.closest('a[href*="/"]');
                            if (profileLink) {
                                const usernameMatch = profileLink.href.match(/(?:twitter\.com|x\.com)\/([^\/\?]+)/);
                                if (usernameMatch && !['home', 'explore', 'notifications', 'messages', 'bookmarks', 'lists', 'communities', 'premium', 'verified-orgs-signup', 'settings', 'search', 'compose', 'i'].includes(usernameMatch[1])) {
                                    username = usernameMatch[1];
                                }
                            }
                        }

                        if (username) {
                            state.currentUsername.value = username;
                        }

                        state.isModalOpen.value = true;
                    };

                    target.parentNode.insertBefore(wrapper, target.nextSibling);
                }
            }
        });
    }

    async function init() {
        await loadSettings();

        const modalContainer = document.createElement('div');
        modalContainer.id = 'tmd-modal-root';
        document.body.appendChild(modalContainer);

        const renderModal = () => {
            render(h(Modal), modalContainer);
        };

        effect(() => {
            renderModal();
        });

        const floatingButton = document.createElement('div');
        floatingButton.id = 'tmd-floating-button';
        floatingButton.style.cssText = `
            position: fixed;
            top: 50%;
            left: -20px;
            width: 48px;
            height: 48px;
            cursor: pointer;
            z-index: 9998;
            transition: all 0.3s ease;
            opacity: 0.5;
        `;
        
        floatingButton.innerHTML = renderIcon('Twitter', { size: 48, color: 'hsl(204.17deg 87.55% 52.75%)' })
            .replace('<svg', '<svg style="transform-origin: bottom center; transition: all 0.3s ease;"');
        
        floatingButton.onmouseenter = () => {
            floatingButton.style.transform = 'translateX(25px) rotate(20deg)';
            floatingButton.style.opacity = '0.9';
            const svg = floatingButton.querySelector('svg');
            if (svg) {
                svg.style.transform = 'scale(1.1)';
            }
        };
        
        floatingButton.onmouseleave = () => {
            floatingButton.style.transform = 'translateX(0) rotate(0)';
            floatingButton.style.opacity = '0.5';
            const svg = floatingButton.querySelector('svg');
            if (svg) {
                svg.style.transform = 'scale(1)';
            }
        };
        
        floatingButton.onclick = (e) => {
            e.stopPropagation();
            e.preventDefault();
            
            let username = null;
            const urlMatch = window.location.pathname.match(/^\/([^\/?]+)(?:\/|\?|$)/);
            if (urlMatch && !['home', 'explore', 'notifications', 'messages', 'bookmarks', 'lists', 'communities', 'premium', 'verified-orgs-signup', 'settings', 'search', 'compose', 'i'].includes(urlMatch[1])) {
                username = urlMatch[1];
            }
            
            if (username) {
                state.currentUsername.value = username;
            }
            
            state.isModalOpen.value = true;
        };
        
        document.body.appendChild(floatingButton);

        insertIcons();
        const observer = new MutationObserver(insertIcons);
        observer.observe(document.body, { childList: true, subtree: true });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址