Time Control

Script allowing you to control time.

当前为 2025-10-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Time Control
// @description  Script allowing you to control time.
// @icon         https://parsefiles.back4app.com/JPaQcFfEEQ1ePBxbf6wvzkPMEqKYHhPYv8boI1Rc/ce262758ff44d053136358dcd892979d_low_res_Time_Machine.png
// @namespace    mailto:[email protected]
// @version      1.4.2
// @author       lucaszheng
// @license      MIT
//
// @match        *://*/*
// @grant        unsafeWindow
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue

// @inject-into  page
// @run-at       document-start
// ==/UserScript==
/*globals unsafeWindow,GM_setValue,GM_getValue,GM_deleteValue*/

(function (window) {
  'use strict';
  let scale = 1, pristine = true;
  /** @type {null | number} */
  let timeJump = null;

  let timeReset = false;
  let debug = false;

  const {
    Reflect: {
      apply, construct,
      setPrototypeOf,
      getPrototypeOf,
      getOwnPropertyDescriptor
    },
    Object: {
      defineProperty,
      freeze
    },
    Event,
    Number: {
      isFinite
    },
    Symbol: {
      toPrimitive,
      toStringTag
    },
    console: {
      trace: log
    }
  } = window;

  function update() {
    for (let idx = 0; idx < updaters.length; idx++) {
      updaters[idx]();
    }
  }

  /**
   * @this { { toString: () => string; now: number }}
   * @param {'string' | 'number' | 'default'} type
   */
  function timeToPrimitive(type) {
    switch (type) {
      case 'string': return this.toString();
      default: return this.now;
    }
  }

  /**
   * @this { { now: number } }
   */
  function timeToString() {
    return apply(date.toString, construct(DateConstructor, [this.now]), []);
  }

  let profile_id = '';
  /** @param {string} name */
  function get_var_name(name) {
    if (profile_id != '') name = name + '_profile_' + profile_id;
    return name;
  }
  /** @type {typeof GM_getValue} */
  function getValue(name, defaultValue) {
    return GM_getValue(get_var_name(name), defaultValue);
  }
  /** @type {typeof GM_setValue} */
  function setValue(name, value) {
    return GM_setValue(get_var_name(name), value);
  }
  /** @type {typeof GM_deleteValue} */
  function deleteValue(name) {
    return GM_deleteValue(get_var_name(name));
  }

  const time = {
    [toStringTag]: 'time',
    [toPrimitive]: timeToPrimitive,
    toString: timeToString,
    /**
     * @param {number | null} [newTime]
     */
    jump(newTime) {
      if (!newTime && newTime !== 0) return;
      pristine = false;
      try {
        timeJump = +newTime;
        update();
      } finally {
        timeJump = null;
      }
    },

    /**
     * @param {number | null} [shiftTime]
     */
    shift(shiftTime) {
      if (!shiftTime) return;
      shiftTime = +shiftTime;
      if (!shiftTime) return;
      time.jump(time.now + shiftTime);
    },

    reset(resetTime = true, resetScale = true, resetDebug = true) {
      if (resetDebug) debug = false;
      if (pristine) return;

      if (resetScale) scale = 1;

      if (!resetTime) return;
      timeReset = true;
      update();
      timeReset = false;
      pristine = scale === 1;
    },

    storage: {
      [toStringTag]: 'storage',
      [toPrimitive]: timeToPrimitive,
      toString: timeToString,

      get profile() {
        return profile_id || null;
      },
      set profile(val) {
        profile_id = (val ?? '') + '';
      },

      /**
       * @param {number} newTime
       */
      jump(newTime) {
        setValue('baseTime', time.real);
        setValue('contTime', +newTime);
      },

      save(saveTime = true, saveScale = true, saveDebug = true) {
        if (saveDebug) {
          if (debug === false) time.storage.reset(false, false, true);
          else time.storage.debug = debug;
        }
        if (saveTime) {
          if (pristine) time.storage.reset(true, false, false);
          else time.storage.now = time.now;
        }
        if (saveScale) {
          if (scale === 1) time.storage.reset(false, true, false);
          else time.storage.scale = scale;
        }
      },

      load(loadTime = true, loadScale = true, loadDebug = true) {
        if (loadDebug) time.debug = time.storage.debug;
        if (time.storage.pristine) return time.reset(true, true, false);

        if (loadTime) {
          let baseTime = getValue('baseTime', null);
          let contTime = getValue('contTime', null);
          if (baseTime != null && contTime != null)
            time.jump((time.real - baseTime) + contTime);
        }
        if (loadScale) time.scale = time.storage.scale;
      },

      reset(resetTime = true, resetScale = true, resetDebug = true) {
        if (resetTime) {
          deleteValue('baseTime');
          deleteValue('contTime');
        }
        if (resetScale) deleteValue('scale');
        if (resetDebug) deleteValue('debug');
      },

      get debug() { return getValue('debug', false); },
      set debug(value) { setValue('debug', !!value); },

      get now() {
        let baseTime = getValue('baseTime', null);
        let contTime = getValue('contTime', null);
        if (baseTime != null && contTime != null)
          return (time.real - baseTime) + contTime;
        return time.real;
      },
      set now(value) { time.storage.jump(value); },

      get pristine() {
        let baseTime = getValue('baseTime', null);
        let contTime = getValue('contTime', null);
        let scale = getValue('scale', null);
        return (baseTime == null || contTime == null) && scale == null;
      },
      set pristine(value) {
        if (!value) return;
        time.storage.reset(true, true, false);
      },

      get real() { return date.realTime(); },

      get scale() {
        let scale = getValue('scale', null);
        if (scale != null) return scale;
        return 1;
      },
      set scale(value) {
        if (value === time.storage.scale) return;
        setValue('scale', +value);
      }
    },

    get debug() { return debug; },
    set debug(value) { debug = !!value; },

    get now() { return date.now(); },
    set now(value) { time.jump(value); },

    get pristine() { return pristine; },
    set pristine(value) { if (value) time.reset(); },

    get real() { return date.realTime(); },

    get scale() { return scale; },
    set scale(value) {
      value = +value;
      if (value === scale) return;
      pristine = false; update(); scale = value;
    }
  };

  freeze(time.storage);
  defineProperty(getPrototypeOf(window), 'time', {
    value: freeze(time),
    writable: true,
    enumerable: false,
    configurable: true
  });

  /** @type {(() => void)[]} */
  const updaters = [];

  /**
   * @template {() => number | null | undefined} T
   * @param {T} func
   * @param {object} self
   * @param {object | null} req_self
   * @param {(func: T) => number} offset
   */
  function wrap_now(func, self, offset = () => 0, req_self = null) {
    let baseTime = 0;
    let contTime = baseTime;

    /** @type {ProxyHandler<typeof func>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        let time = apply(target, self, args);
        // pristine check necessary due to handler.apply(func, self, [])
        if (pristine || !isFinite(time) || (req_self !== null && self !== req_self)) return time;
        return ((time - baseTime) * scale) + contTime;
      }
    };
    setPrototypeOf(handler, null);

    updaters[updaters.length] =
      function update() {
        if (!handler.apply) return;
        contTime = timeJump == null ? handler.apply(func, self, []) : timeJump + offset(func);
        baseTime = apply(func, self, []) ?? baseTime;
        if (timeReset) contTime = baseTime;
      };

    return new Proxy(func, wrapHandler(handler));
  }

  /**
   * @template {object} O
   * @template {keyof O} P
   * @template {(this: O) => Extract<O[P], number | null | undefined>} T
   * @param {O} obj
   * @param {P} prop
   * @param {() => object} getSelf
   * @param {null | ((getter: (...args: unknown[]) => O[P]) => T)} getFunc
   * @param {object | null} req_self
   * @param {(func: T) => number} offset
   */
  function wrap_getter(obj, prop, getSelf, getFunc = null, offset = () => 0, req_self = null) {
    const propDesc = getOwnPropertyDescriptor(obj, prop);
    if (propDesc?.get) {
      const func = getFunc?.(propDesc.get) ?? propDesc.get, real_func = propDesc.get;
      let baseTime = 0;
      let contTime = baseTime;

      /** @type {ProxyHandler<typeof real_func>} */
      const handler = {
        apply(_target, self, args) {
          // cannot show `self`, it results in infinite loop from Chrome Devtools automatically expanding document.timeline
          if (debug) log('apply(%o, self, %o)', func, args);
          let time = apply(func, self, args);
          // pristine check necessary due to handler.apply(func, self, [])
          if (pristine || !isFinite(time) || (req_self !== null && self !== req_self)) return time;
          return ((time - baseTime) * scale) + contTime;
        }
      };
      setPrototypeOf(handler, null);

      updaters[updaters.length] =
        function update() {
          if (!handler.apply) return;
          contTime = timeJump == null ? handler.apply(real_func, getSelf(), []) : timeJump + offset(/** @type {T} */(func));
          baseTime = apply(func, getSelf(), []) ?? baseTime;
          if (timeReset) contTime = baseTime;
        };

      const wrappedGetter = new Proxy(real_func, wrapHandler(handler));

      defineProperty(obj, prop, {
        configurable: propDesc.configurable,
        enumerable: propDesc.enumerable,
        get: wrappedGetter,
        set: propDesc.set
      });
      return /** @type {T} */(wrappedGetter);
    }
    return null;
  }

  const DateConstructor = window.Date;
  /** @type {{ realTime: typeof Date.now, now: typeof Date.now, real_perfNow: typeof performance.now, toString: typeof Date.prototype.toString, handler: ProxyHandler<DateConstructor> }} */
  const date = {
    realTime: window.Date.now,
    now: wrap_now(window.Date.now, window.Date),
    real_perfNow: window.performance.now.bind(performance),
    toString: DateConstructor.prototype.toString,
    handler: {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        return time.toString();
      },
      construct(target, args, newTarget) {
        if (debug) log('construct(%o, %o, %o)', target, args, newTarget);
        if (args.length < 1) {
          args[0] = time.now;
        }
        return construct(DateConstructor, args, newTarget);
      }
    }
  };
  setPrototypeOf(date, null);
  setPrototypeOf(date.handler, null);
  DateConstructor.now = date.now;

  window.Date = new Proxy(DateConstructor, wrapHandler(date.handler));
  window.Date.prototype.constructor = window.Date;

  window.Performance.prototype.now = wrap_now(
    window.Performance.prototype.now,
    window.performance,
    () => date.real_perfNow() - date.realTime(),
    window.performance
  );

  function noop() { }

  /**
   * @param {(handler: TimerHandler, timeout?: number | undefined, ...args: any[]) => number} func
   */
  function wrap_timer(func) {
    /** @type {ProxyHandler<typeof func>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        if (args.length > 1) {
          args[1] = +args[1];
          if (args[1] && scale === 0)
            args[0] = noop;
          else if (args[1] && isFinite(args[1]))
            args[1] /= scale;
        }
        return apply(target, self, args);
      }
    };
    setPrototypeOf(handler, null);
    return new Proxy(func, wrapHandler(handler));
  }

  window.setTimeout = wrap_timer(window.setTimeout);
  window.setInterval = wrap_timer(window.setInterval);

  const docTimeline = window.document.timeline;
  const wrappedGetAnimTime = wrap_getter(
    window.AnimationTimeline.prototype, 'currentTime', () => docTimeline,
    (func) => function () {
      const time = apply(func, this, arguments);
      if (this !== docTimeline) return time;
      return typeof time === 'number' ? time : null;
    },
    getAnimTime =>
      (apply(getAnimTime, docTimeline, []) ?? date.real_perfNow()) - date.realTime(),
    docTimeline
  );
  if (wrappedGetAnimTime) {
    /** @type {ProxyHandler<typeof requestAnimationFrame>} */
    const handler = {
      apply(target, self, args) {
        if (debug) log('apply(%o, %o, %o)', target, self, args);
        if (typeof args[0] === 'function') {
          const cb = args[0];
          args[0] = function () {
            if (!pristine)
              arguments[0] = apply(wrappedGetAnimTime, docTimeline, []);
            return apply(cb, this, arguments);
          }
        }
        return apply(target, self, args);
      }
    };
    setPrototypeOf(handler, null);
    window.requestAnimationFrame = new Proxy(window.requestAnimationFrame, wrapHandler(handler));
  }
  wrap_getter(
    window.Event.prototype, 'timeStamp', () => new Event(''), null,
    getTimeStamp =>
      (apply(getTimeStamp, new Event(''), []) ?? date.real_perfNow()) - date.realTime()
  );

  /**
   * @param {ProxyHandler<any>} handler
   */
  function wrapHandler(handler) {
    /** @type {ProxyHandler<ProxyHandler<any>>} */
    const internalHandler = {
      get(target, prop) {
        if (pristine) return undefined;
        return Reflect.get(target, prop);
      }
    };
    setPrototypeOf(internalHandler, null);
    return new Proxy(handler, internalHandler);
  }

  time.storage.load();
})(/** @type {typeof window} */(unsafeWindow));