Telegraph Auto Next

Enhanced Telegraph image viewer with auto-navigation, state memory, page jump, and UI controls. Keyboard shortcuts: 'g' to start/stop auto-next, 'f' to set interval. Based on SparkZhang's script: https://greasyfork.org/zh-CN/scripts/481178

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name         Telegraph Auto Next
// @license      MIT
// @namespace    http://tampermonkey.net/
// @version      1.3.5
// @description  Enhanced Telegraph image viewer with auto-navigation, state memory, page jump, and UI controls. Keyboard shortcuts: 'g' to start/stop auto-next, 'f' to set interval. Based on SparkZhang's script: https://greasyfork.org/zh-CN/scripts/481178
// @author       CurssedCoffin (perfected with gemini) https://github.com/CurssedCoffin. Original by SparkZhang (https://greasyfork.org/zh-CN/users/1226354-sparkzhang)
// @match        https://telegra.ph/*
// @match        https://reader.960173.xyz/*
// @match        https://graph.org/*
// @exclude      https://telegra.ph/
// @exclude      https://graph.org/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=telegra.ph
// @resource     fa https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css
// @require      https://unpkg.com/vue@3/dist/vue.global.js
// @grant        unsafeWindow
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @run-at       document-idle
// ==/UserScript==

/* global Vue */

if (location.hostname === 'graph.org') {
    location.replace(location.href.replace('graph.org', 'telegra.ph'));
}

unsafeWindow.Vue = Vue;

init();

function init() {
    if (location.pathname === '/' || location.pathname === '') return;

    var urls = getImageUrls();
    if (urls.length > 0) {
        injectStyle();
        createWrapper();
        createVue(urls, getTitle());
    }
}

function injectStyle() {
    var fa = GM_getResourceText("fa");
    if (fa) {
        fa = fa.replaceAll("../webfonts", "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/webfonts");
        GM_addStyle(fa);
    }
    GM_addStyle(`
  .tl_page_wrap, header, main, nav.main-nav, .content-wrapper, #rwl-iqxin { display:none !important; }
  #app { left: 0; top: 0; z-index: 9999; position: absolute; width:100%; min-height: 100vh; background-color: #000; }
  .photo-viewer { background-color: #000; height: 100vh; display: flex; flex-direction: column; overflow: hidden; }
  .photo-viewer-header, .photo-viewer-footer {
    height: 40px; display: flex; align-items: center; justify-content: center; background-color: #383838; position: relative; flex-shrink: 0;
  }
  .photo-viewer-header, .photo-viewer-content, .photo-viewer-footer { color: #fff; text-align: center; }
  .photo-viewer-click-left, .photo-viewer-click-right {
    top: 0; left: 0; width:50%; height: 100%; z-index: 999; position: absolute; cursor: pointer;
  }
  .photo-viewer-click-right{ left: 50%; }
  .photo-viewer-content {
    font-size: 0; position: relative; flex-grow: 1; display: flex; align-items: center; justify-content: center; overflow: hidden;
  }
  .photo-viewer-content img { max-width: 100%; max-height: 100%; object-fit: contain; }
  .photo-viewer-btn { width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; cursor: pointer; transition: 0.2s; }
  .photo-viewer-btn:hover { background-color: #525252; }
  .photo-viewer-counter { padding: 0 10px; display: flex; align-items: center; gap: 5px; }
  .photo-viewer-footer-controls { position: absolute; right: 10px; display: flex; align-items: center; gap: 10px; height: 100%; }
  .ui-input {
    background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2);
    color: #fff; text-align: center; border-radius: 4px; outline: none; font-size: 14px;
  }
  .ui-input:focus { border-color: #00ff00; background: rgba(255, 255, 255, 0.2); }
  .page-jump-input { width: 45px; height: 24px; }
  .interval-input { width: 50px; height: 24px; }
  .ui-input::-webkit-inner-spin-button, .ui-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
  .auto-active { color: #00ff00; }
`);
}

function createWrapper() {
    var app = document.createElement("div");
    app.id = "app";
    document.body.appendChild(app);
}

function getTitle() {
    return document.querySelector(".tl_article_header h1, header h1, title")?.innerText || "Untitled";
}

function getImageUrls() {
    var urls =[];
    var imgs = document.querySelectorAll(".figure_wrapper img, article img, .image-item, .content-wrapper img, main img");
    imgs.forEach(img => { if (img.src) urls.push(img.src); });
    return urls;
}

function createVue(urls, title) {
    const { createApp, ref, computed, watch, onMounted } = Vue;
    const app = createApp({
        template: `<photo-viewer :urls="urls" :title="title"/>`,
        setup() { return { urls: ref(urls), title: ref(title) }; }
    });

    app.component("PhotoViewer", {
        props:["urls", "title"],
        template: `
        <div class="photo-viewer">
          <div class="photo-viewer-header">{{title}}</div>
          <div class="photo-viewer-content" ref="content">
            <div class="photo-viewer-click-left" @click.stop="clickLeft"></div>
            <div class="photo-viewer-click-right" @click.stop="clickRight"></div>
          </div>
          <div class="photo-viewer-footer">
              <div class="photo-viewer-btn" @click="first"><i class="fa fa-chevron-left"></i><i class="fa fa-chevron-left"></i></div>
              <div class="photo-viewer-btn" @click="prev"><i class="fa fa-chevron-left"></i></div>
              <div class="photo-viewer-counter">
                  <input type="number" class="ui-input page-jump-input" v-model.lazy="inputPage" @focus="isPageInputActive = true" @blur="handlePageBlur" @keyup.enter="$event.target.blur()"/>
                  <span>/ {{total}}</span>
              </div>
              <div class="photo-viewer-btn" @click="next"><i class="fa fa-chevron-right"></i></div>
              <div class="photo-viewer-btn" @click="last"><i class="fa fa-chevron-right"></i><i class="fa fa-chevron-right"></i></div>
              <div class="photo-viewer-footer-controls">
                  <div class="photo-viewer-btn" :class="{'auto-active': !!autoTimer}" @click="toggleAutoPlay">
                      <i class="fa" :class="autoTimer ? 'fa-pause' : 'fa-play'"></i>
                  </div>
                  <div style="display:flex; align-items:center; gap:5px; font-size:13px;">
                      <input type="number" step="0.1" class="ui-input interval-input" v-model="inputInterval" @keyup.enter="$event.target.blur()"/>
                      <span>s</span>
                  </div>
              </div>
          </div>
        </div>`,
        setup(props) {
            const currentIndex = ref(0);
            const content = ref(null);
            const autoTimer = ref(null);
            const autoInterval = ref(parseFloat(localStorage.getItem('tg_auto_interval')) || 3);
            const inputPage = ref(1);
            const isPageInputActive = ref(false);
            const inputInterval = ref(autoInterval.value);
            const imgs = ref(props.urls.map(url => {
                var img = document.createElement("img");
                img.src = url;
                return img;
            }));
            const img = computed(() => imgs.value[currentIndex.value]);
            const total = computed(() => props.urls.length);

            watch(img, (newImg, oldImg) => {
                if (oldImg) oldImg.remove();
                if (content.value) content.value.appendChild(newImg);
                if (!isPageInputActive.value) inputPage.value = currentIndex.value + 1;
            });

            watch(inputInterval, (newVal) => {
                let val = parseFloat(newVal);
                if (!isNaN(val) && val > 0) {
                    autoInterval.value = val;
                    localStorage.setItem('tg_auto_interval', val);
                    if (autoTimer.value) startAutoPlay(); 
                }
            });

            const handlePageBlur = () => {
                isPageInputActive.value = false;
                let page = parseInt(inputPage.value);
                if (!isNaN(page)) {
                    if (page < 1) page = 1;
                    if (page > total.value) page = total.value;
                    currentIndex.value = page - 1;
                }
                inputPage.value = currentIndex.value + 1;
            };

            const stopAutoPlay = (clearMemory = true) => {
                if (autoTimer.value) { clearInterval(autoTimer.value); autoTimer.value = null; }
                if (clearMemory) localStorage.setItem('tg_auto_enabled', 'false');
            };

            const startAutoPlay = () => {
                stopAutoPlay(false);
                if (autoInterval.value > 0) {
                    autoTimer.value = setInterval(next, autoInterval.value * 1000);
                    localStorage.setItem('tg_auto_enabled', 'true');
                }
            };

            const toggleAutoPlay = () => { autoTimer.value ? stopAutoPlay(true) : startAutoPlay(); };
            const first = () => { currentIndex.value = 0; };
            const last = () => { if (total.value > 0) currentIndex.value = total.value - 1; };
            const prev = () => { if (currentIndex.value > 0) currentIndex.value--; };
            
            // 【修改点】在这里:判断如果已经到了最后一张,调用 stopAutoPlay(false),停止计时器但不清空 global 记忆。
            const next = () => {
                if (currentIndex.value < total.value - 1) currentIndex.value++;
                else stopAutoPlay(false); 
            };

            onMounted(() => {
                document.addEventListener("keydown", e => {
                    if (e.target.tagName === 'INPUT') return;
                    if (e.key === "ArrowRight") next();
                    else if (e.key === "ArrowLeft") prev();
                    else if (e.key === "g") toggleAutoPlay();
                    else if (e.key === "f") {
                        let input = prompt("Enter auto-next interval (seconds):", autoInterval.value);
                        if (input !== null) inputInterval.value = input;
                    }
                });
                if (content.value) content.value.appendChild(img.value);
                if (localStorage.getItem('tg_auto_enabled') === 'true') startAutoPlay();
            })
            return { 
                imgs, img, currentIndex, total, inputPage, inputInterval, isPageInputActive,
                first, last, prev, next, content, autoTimer, toggleAutoPlay, handlePageBlur,
                clickLeft: prev, clickRight: next
            };
        }
    });
    app.mount("#app");
}