Greasy Fork

来自缓存

Greasy Fork is available in English.

flyhentai

在 Exhentai 画廊缩略图右上角添加按钮,点击跳转到本地应用;详情页添加查找中文版按钮

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @license MIT
// @name         flyhentai
// @namespace    http://tampermonkey.net/
// @version      1.1
// @description  在 Exhentai 画廊缩略图右上角添加按钮,点击跳转到本地应用;详情页添加查找中文版按钮
// @author       You
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    // URL 校验:只在 e-hentai.org 或 exhentai.org 上运行
    const hostname = window.location.hostname;
    if (hostname !== 'e-hentai.org' && hostname !== 'exhentai.org') {
        return; // 不是目标网站,直接退出
    }

    // 配置本地应用URL前缀
    const LOCAL_APP_BASE_URL = 'http://192.168.0.108:5173/g';

    // 创建跳转按钮样式
    const style = document.createElement('style');
    style.textContent = `
        .local-app-btn {
            position: absolute;
            top: 5px;
            right: 5px;
            background: rgba(0, 123, 255, 0.9);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 4px 8px;
            font-size: 12px;
            cursor: pointer;
            z-index: 1000;
            transition: all 0.2s ease;
            text-decoration: none;
            display: inline-block;
            font-weight: bold;
        }

        .local-app-btn:hover {
            background: rgba(0, 86, 179, 0.95);
            transform: scale(1.05);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
        }

        .local-app-btn-detail {
            background: rgba(0, 123, 255, 0.9);
            color: white;
            border: none;
            border-radius: 4px;
            padding: 6px 24px;
            font-size: 18px;
            cursor: pointer;
            text-decoration: none;
            display: block;
            font-weight: bold;
            margin: 10px auto;
            width: fit-content;
            line-height: 1.2;
        }

        .local-app-btn-detail:hover {
            background: rgba(0, 86, 179, 0.95);
            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
        }

        .gl3t {
            position: relative !important;
        }

        /* 确保按钮不会被图片遮挡 */
        .gl3t img {
            z-index: 1;
        }

        .local-app-btn {
            z-index: 10;
        }

        /* 下拉加载更多区域 */
        .pull-to-refresh-area {
            position: fixed;
            bottom: -100px;
            left: 0;
            right: 0;
            height: 100px;
            background: linear-gradient(to top, rgba(0, 123, 255, 0.1), transparent);
            z-index: 1001;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: bottom 0.3s ease, opacity 0.3s ease;
            opacity: 0;
        }

        .pull-to-refresh-area.visible {
            bottom: 0;
            opacity: 1;
        }

        .pull-to-refresh-area.loading {
            background: linear-gradient(to top, rgba(0, 123, 255, 0.3), transparent);
        }

        .pull-indicator {
            background: rgba(0, 123, 255, 0.9);
            color: white;
            padding: 15px 30px;
            border-radius: 30px;
            font-size: 14px;
            font-weight: bold;
            text-align: center;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
            backdrop-filter: blur(10px);
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .pull-indicator.loading::after {
            content: '';
            width: 16px;
            height: 16px;
            border: 2px solid white;
            border-top: 2px solid transparent;
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }

        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }

        .pull-hint {
            position: fixed;
            bottom: 120px;
            left: 50%;
            transform: translateX(-50%);
            background: rgba(0, 0, 0, 0.8);
            color: white;
            padding: 10px 20px;
            border-radius: 25px;
            font-size: 12px;
            z-index: 1002;
            opacity: 0;
            transition: opacity 0.3s ease;
            pointer-events: none;
        }

        .pull-hint.visible {
            opacity: 1;
        }

            `;
    document.head.appendChild(style);

    // 下拉加载更多相关变量
    let pullArea = null;
    let pullIndicator = null;
    let pullHint = null;
    let isPulling = false;
    let pullStartY = 0;
    let pullCurrentY = 0;
    let pullThreshold = 120; // 下拉阈值
    let holdTimer = null;
    let isLoading = false;

    // 检查是否在 Exhentai 顶级路径
    function isExhentaiTopLevel() {
        return window.location.hostname === 'exhentai.org' &&
            (window.location.pathname === '/' || window.location.pathname === '');
    }

    // 创建下拉加载更多区域
    function createPullToRefreshArea() {
        // 只在 Exhentai 顶级路径创建
        if (!isExhentaiTopLevel()) {
            return;
        }

        // 防止重复创建
        if (document.querySelector('.pull-to-refresh-area')) {
            return;
        }

        // 创建下拉区域容器
        pullArea = document.createElement('div');
        pullArea.className = 'pull-to-refresh-area';

        // 创建指示器
        pullIndicator = document.createElement('div');
        pullIndicator.className = 'pull-indicator';
        pullIndicator.textContent = '继续上拉加载下一页';

        // 创建提示
        pullHint = document.createElement('div');
        pullHint.className = 'pull-hint';
        pullHint.textContent = '拉到页面底部并持续上拉';

        // 组装元素
        pullArea.appendChild(pullIndicator);
        document.body.appendChild(pullArea);
        document.body.appendChild(pullHint);

        // 设置事件监听
        setupPullEvents();
    }

    // 设置下拉事件
    function setupPullEvents() {
        let isAtBottom = false;

        // 检查是否在页面底部
        function checkIfAtBottom() {
            const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
            const windowHeight = window.innerHeight;
            const documentHeight = document.documentElement.scrollHeight;

            // 距离底部50px内认为在底部
            isAtBottom = scrollTop + windowHeight >= documentHeight - 50;

            // 检查页面是否有数据
            const galleryContainers = document.querySelectorAll('div.gl3t');
            const hasData = galleryContainers.length > 0;

            return isAtBottom && hasData;
        }

        // 触摸开始
        document.addEventListener('touchstart', (e) => {
            if (isLoading) return;

            const touch = e.touches[0];
            pullStartY = touch.clientY;
            pullCurrentY = pullStartY;

            // 检查是否在页面底部
            if (checkIfAtBottom()) {
                isPulling = true;
                pullHint.classList.add('visible');
            }
        });

        // 触摸移动
        document.addEventListener('touchmove', (e) => {
            if (!isPulling || isLoading) return;

            const touch = e.touches[0];
            pullCurrentY = touch.clientY;
            const deltaY = pullStartY - pullCurrentY; // 向上为负值

            // 只处理向上拉的手势
            if (deltaY > pullThreshold) {
                pullArea.classList.add('visible');
                pullIndicator.textContent = '松开加载下一页';
                pullHint.classList.remove('visible');

                // 清除之前的定时器,改为准备松开触发
                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            } else if (deltaY > 30) {
                pullArea.classList.add('visible');
                pullIndicator.textContent = '继续上拉';
                pullHint.classList.remove('visible');

                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            } else {
                pullArea.classList.remove('visible');
                pullHint.classList.add('visible');

                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            }
        });

        // 触摸结束
        document.addEventListener('touchend', () => {
            if (!isPulling || isLoading) return;

            const deltaY = pullStartY - pullCurrentY;

            // 如果达到了阈值,松开时触发翻页
            if (deltaY > pullThreshold) {
                loadNextPage();
            } else {
                // 没达到阈值,直接隐藏
                isPulling = false;
                pullArea.classList.remove('visible');
                pullHint.classList.remove('visible');
            }

            if (holdTimer) {
                clearTimeout(holdTimer);
                holdTimer = null;
            }

            pullStartY = 0;
            pullCurrentY = 0;
        });

        // 鼠标事件支持(桌面端)
        document.addEventListener('mousedown', (e) => {
            if (isLoading) return;

            if (checkIfAtBottom()) {
                isPulling = true;
                pullStartY = e.clientY;
                pullCurrentY = pullStartY;
                pullHint.classList.add('visible');
                e.preventDefault();
            }
        });

        document.addEventListener('mousemove', (e) => {
            if (!isPulling || isLoading) return;

            pullCurrentY = e.clientY;
            const deltaY = pullStartY - pullCurrentY;

            if (deltaY > pullThreshold) {
                pullArea.classList.add('visible');
                pullIndicator.textContent = '松开加载下一页';
                pullHint.classList.remove('visible');

                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            } else if (deltaY > 30) {
                pullArea.classList.add('visible');
                pullIndicator.textContent = '继续上拉';
                pullHint.classList.remove('visible');

                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            } else {
                pullArea.classList.remove('visible');
                pullHint.classList.add('visible');

                if (holdTimer) {
                    clearTimeout(holdTimer);
                    holdTimer = null;
                }
            }
        });

        document.addEventListener('mouseup', () => {
            if (!isPulling || isLoading) return;

            const deltaY = pullStartY - pullCurrentY;

            // 如果达到了阈值,松开时触发翻页
            if (deltaY > pullThreshold) {
                loadNextPage();
            } else {
                // 没达到阈值,直接隐藏
                isPulling = false;
                pullArea.classList.remove('visible');
                pullHint.classList.remove('visible');
            }

            if (holdTimer) {
                clearTimeout(holdTimer);
                holdTimer = null;
            }

            pullStartY = 0;
            pullCurrentY = 0;
        });
    }

    // 加载下一页
    function loadNextPage() {
        if (isLoading) return;

        // 检查页面是否为空(没有画廊数据)
        const galleryContainers = document.querySelectorAll('div.gl3t');
        if (galleryContainers.length === 0) {
            pullIndicator.textContent = '当前页面无数据,无法翻页';
            setTimeout(() => {
                pullArea.classList.remove('visible');
                isPulling = false;
            }, 2000);
            return;
        }

        const nextLink = document.querySelector('a#dnext');
        if (!nextLink || !nextLink.href) {
            pullIndicator.textContent = '没有更多页面了';
            setTimeout(() => {
                pullArea.classList.remove('visible');
                isPulling = false;
            }, 2000);
            return;
        }

        // 检查是否是最后一页的指示
        const isLastPage = nextLink.classList.contains('inactive') ||
            nextLink.style.opacity === '0.5' ||
            !nextLink.href || nextLink.href === window.location.href;

        if (isLastPage) {
            pullIndicator.textContent = '已到达最后一页';
            setTimeout(() => {
                pullArea.classList.remove('visible');
                isPulling = false;
            }, 2000);
            return;
        }

        isLoading = true;
        isPulling = false;
        pullIndicator.classList.add('loading');
        pullIndicator.textContent = '正在加载...';
        pullHint.classList.remove('visible');

        // 模拟加载延迟
        setTimeout(() => {
            window.location.href = nextLink.href;
        }, 500);
    }


    // 添加按钮到所有画廊容器
    function addButtonsToGalleries() {
        const galleryContainers = document.querySelectorAll('div.gl3t');

        galleryContainers.forEach(container => {
            // 检查是否已经添加过按钮
            if (container.querySelector('.local-app-btn')) {
                return;
            }

            // 获取链接元素
            const link = container.querySelector('a');
            if (!link || !link.href) {
                return;
            }

            // 提取gallery ID和token
            const href = link.href;
            const match = href.match(/\/g\/([^\/]+)\/([^\/]+)\/?/);

            if (match) {
                const galleryId = match[1];
                const token = match[2];

                // 创建跳转按钮
                const button = document.createElement('a');
                button.href = `${LOCAL_APP_BASE_URL}/${galleryId}/${token}`;
                button.className = 'local-app-btn';
                button.textContent = '🚀';
                button.target = '_blank'; // 在新标签页打开
                button.title = '在本地应用中打开此画廊';

                // 阻止默认链接行为,只处理按钮点击
                button.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    window.open(button.href, '_blank');
                });

                // 将按钮添加到容器中
                container.appendChild(button);

                console.log(`已为画廊 ${galleryId}/${token} 添加本地应用按钮`);
            }
        });
    }

    // 处理详情页面的按钮添加
    function addButtonsToDetailPage() {
        // 检查是否在详情页
        const match = window.location.href.match(/\/g\/([^\/]+)\/([^\/]+)\/?/);
        if (!match) return;

        const galleryId = match[1];
        const token = match[2];
        const gd5 = document.querySelector('#gd5');

        if (gd5 && !gd5.querySelector('.local-app-btn-detail')) {
            // 创建本地应用按钮
            const localAppButton = document.createElement('a');
            localAppButton.href = `${LOCAL_APP_BASE_URL}/${galleryId}/${token}`;
            localAppButton.className = 'local-app-btn-detail';
            localAppButton.textContent = '🚀';
            localAppButton.target = '_blank';
            localAppButton.title = '在本地应用中打开此画廊';

            localAppButton.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                window.open(localAppButton.href, '_blank');
            });

            // 创建查找中文版按钮
            const chineseVersionButton = document.createElement('a');
            chineseVersionButton.href = '#';
            chineseVersionButton.className = 'local-app-btn-detail';
            chineseVersionButton.textContent = '🔍中文版';
            chineseVersionButton.target = '_blank';
            chineseVersionButton.title = '查找此画廊的中文版';

            chineseVersionButton.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();

                const searchUrl = generateChineseVersionSearchUrl();
                if (searchUrl) {
                    window.open(searchUrl, '_blank');
                } else {
                    alert('无法生成搜索链接,请稍后重试');
                }
            });

            // 添加按钮到页面
            const br = document.createElement('br');
            const br2 = document.createElement('br');
            gd5.appendChild(br);
            gd5.appendChild(localAppButton);
            gd5.appendChild(br2);
            gd5.appendChild(chineseVersionButton);

            console.log(`已为详情页 ${galleryId}/${token} 添加本地应用按钮和查找中文版按钮`);
        }
    }

    // 生成查找中文版的搜索URL
    function generateChineseVersionSearchUrl() {
        try {
            // 获取 #gd2 元素
            const gd2 = document.querySelector('#gd2');
            if (!gd2) {
                console.error('未找到 #gd2 元素');
                return null;
            }

            // 获取h1标题元素
            const h1Elements = gd2.querySelectorAll('h1');
            const gnElement = h1Elements[0]; // 第一个h1
            const gjElement = h1Elements[1]; // 第二个h1 (可能不存在)

            if (!gnElement) {
                console.error('未找到任何标题元素');
                return null;
            }

            const title1 = gnElement.textContent.trim();
            const title2 = gjElement ? gjElement.textContent.trim() : '';

            let selectedTitle;

            // 检查标题是否为空
            if (!title1 && !title2) {
                console.error('两个标题都为空');
                return null;
            } else if (!title2) {
                console.log('第二个标题为空,选择第一个标题');
                selectedTitle = title1;
            } else if (!title1) {
                console.log('第一个标题为空,选择第二个标题');
                selectedTitle = title2;
            } else {
                // 两个标题都有内容,选择英文占比较少的
                selectedTitle = selectTitleWithLessEnglish(title1, title2);
            }

            // 清洗标题
            const cleanedTitle = cleanTitle(selectedTitle);

            if (!cleanedTitle) {
                console.error('清洗后的标题为空');
                return null;
            }

            // 生成搜索URL
            const baseUrl = 'https://exhentai.org/?';
            const searchParams = new URLSearchParams();
            searchParams.set('f_search', `language:chinese ${cleanedTitle}`);

            console.log(`生成的搜索关键词: ${cleanedTitle}`);
            return baseUrl + searchParams.toString();

        } catch (error) {
            console.error('生成搜索URL时出错:', error);
            return null;
        }
    }

    // 选择英文占比较少的标题
    function selectTitleWithLessEnglish(title1, title2) {
        const englishRatio1 = calculateEnglishRatio(title1);
        const englishRatio2 = calculateEnglishRatio(title2);

        console.log(`标题1: "${title1}" 英文占比: ${englishRatio1.toFixed(2)}`);
        console.log(`标题2: "${title2}" 英文占比: ${englishRatio2.toFixed(2)}`);

        return englishRatio1 <= englishRatio2 ? title1 : title2;
    }

    // 计算英文占比
    function calculateEnglishRatio(text) {
        if (!text) return 1;

        // 统计英文字符数(包括英文字母、数字、空格和常见英文标点)
        const englishChars = text.match(/[a-zA-Z0-9\s\.,!?;:'"()\-]/g) || [];
        const totalChars = text.replace(/\s/g, '').length; // 不计算空格的总字符数

        return totalChars > 0 ? englishChars.length / totalChars : 0;
    }

    // 清洗标题
    function cleanTitle(title) {
        if (!title) return '';

        console.log(`原始标题: "${title}"`);

        // 使用正则表达式删除所有括号内容(包括全角和半角的方括号、圆括号)
        // 【xx】、[xx]、(xxx) 都会被删除
        let cleaned = title.replace(/【.*?】|\[.*?\]|\(.*?\)/g, '').trim();

        console.log(`清洗后标题: "${cleaned}"`);
        return cleaned;
    }


    // 创建下拉加载更多区域
    createPullToRefreshArea();

    // 初始添加按钮
    addButtonsToGalleries();
    addButtonsToDetailPage();

    // 监听DOM变化,为动态加载的内容添加按钮
    const observer = new MutationObserver((mutations) => {
        let shouldUpdate = false;

        mutations.forEach((mutation) => {
            if (mutation.type === 'childList') {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE) {
                        // 检查是否添加了新的画廊容器
                        if (node.classList && node.classList.contains('gl3t')) {
                            shouldUpdate = true;
                        } else if (node.querySelector && node.querySelector('.gl3t')) {
                            shouldUpdate = true;
                        }
                    }
                });
            }
        });

        if (shouldUpdate) {
            setTimeout(addButtonsToGalleries, 100); // 短暂延迟确保DOM更新完成
        }
    });

    // 开始观察整个文档
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // 定期检查(备用方案)
    setInterval(() => {
        addButtonsToGalleries();
        addButtonsToDetailPage();
    }, 2000);

    console.log('Exhentai Gallery Opener 脚本已加载');
})();