Twitter image viewing enhancement

Make Twitter photo viewing more humane

Install this script?
Author's suggested script

You may also like Twitter remove content warning.

Install this script
// ==UserScript==
// @name         Twitter image viewing enhancement
// @name:zh-CN   Twitter 图片查看增强
// @name:zh-TW   Twitter 圖像查看增強
// @icon         https://twitter.com/favicon.ico
// @namespace    https://moe.best/
// @version      1.4.0
// @description        Make Twitter photo viewing more humane
// @description:zh-CN  让推特图片浏览更加人性化
// @description:zh-TW  讓 Twitter 照片瀏覽更人性化
// @author       Jindai Kirin
// @include      https://x.com/*
// @include      https://twitter.com/*
// @license      MIT
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// ==/UserScript==

(() => {
  'use strict';

  // 滑动切换图片
  let enableDragToSwitch = GM_getValue('enableDragToSwitch', false);
  GM_registerMenuCommand('Drag to switch images', () => {
    enableDragToSwitch = confirm(`Do you want to enable drag to switch images?
Current: ${enableDragToSwitch ? 'Enabled' : 'Disabled'}

Please refresh to take effect after modification.`);
    GM_setValue('enableDragToSwitch', enableDragToSwitch);
  });

  if (enableDragToSwitch) GM_addStyle('img{-webkit-user-drag:none}');

  const labels = {};

  const setLabels = labelsStr => {
    const list = (labelsStr || '').split(',');
    if (list.length !== 3) return;
    labels.close = list[0];
    labels.prev = list[1];
    labels.next = list[2];
  };

  setLabels(GM_getValue('labels_v1', ''));

  // 手动设置 aria-label
  GM_registerMenuCommand('Set aria-label', () => {
    let input, list;
    let error = false;
    do {
      const current = GM_getValue('labels_v1', '');
      input = prompt(
        `Please input the aria-label of Close, Previous, Next button and join them by commas(,). Submit an empty string will disable it.${
          error ? '\n\nINPUT ERROR' : ''
        }`,
        input || current
      );
      if (input === null) return;
      input = input.trim();
      if (input.length === 0) {
        GM_setValue('labels_v1', '');
        return;
      } else list = input.split(',').map(label => label.trim());
      error = list.length !== 3;
    } while (error);
    const value = list.join(',');
    setLabels(value);
    GM_setValue('labels_v1', value);
  });

  if (!Object.values(labels).length) {
    try {
      const kv = {
        af8fa2ad: 'close',
        af8fa2ae: 'close',
        c4d53ba2: 'prev',
        d70740d9: 'next',
        d70740da: 'next',
      };
      const i18nModule = webpackChunk_twitter_responsive_web.find(([[name]]) =>
        name.startsWith('i18n')
      );
      Object.values(i18nModule[1]).forEach(fn => {
        if (fn.length < 3) return;
        try {
          fn(undefined, undefined, () => ({
            _register: () => (k, v) => {
              if (k in kv) labels[kv[k]] = v;
            },
          }));
        } catch (e) {}
      });
    } catch (error) {
      console.error(error);
    }
  }

  const getBtnByLabel = label => document.querySelector(`[aria-label="${label}"]`);
  const clickBtn = name => {
    const $btn = getBtnByLabel(labels[name]);
    if ($btn) {
      $btn.click();
      return true;
    }
    return false;
  };

  const closeImgView = () => clickBtn('close');
  const prevImg = () => clickBtn('prev');
  const nextImg = () => clickBtn('next');

  /**
   * @param {HTMLElement} el
   */
  const isTwitterImg = el => el.tagName == 'IMG' && el.baseURI.includes('/photo/');

  window.addEventListener(
    'wheel',
    ({ deltaY, target }) => {
      if (isTwitterImg(target) || target.dataset.testid === 'swipe-to-dismiss') {
        if (deltaY < 0) prevImg();
        else if (deltaY > 0) nextImg();
      }
    },
    { passive: true }
  );

  if (enableDragToSwitch) {
    let x = 0;
    let y = 0;
    window.addEventListener('mousedown', ({ clientX, clientY }) => {
      x = clientX;
      y = clientY;
    });
    window.addEventListener(
      'mouseup',
      ({ button, clientX, clientY, target }) => {
        if (button !== 0 || !isTwitterImg(target)) return;
        const [sx, sy] = [clientX - x, clientY - y].map(Math.abs);
        const mx = clientX - x;
        if (sx <= 10 && sy <= 10) closeImgView();
        if (sy <= sx) {
          if (mx > 0) prevImg();
          else if (mx < 0) nextImg();
        }
      },
      { passive: true }
    );
  } else {
    document.addEventListener(
      'click',
      e => {
        if (!isTwitterImg(e.target)) return;
        closeImgView();
        e.stopPropagation();
      },
      { capture: true, passive: true }
    );
  }
})();