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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

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

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Advertisement:

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

Advertisement:

// ==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");
}