您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
将 GitHub 页面上的相对时间转换为绝对日期和时间
- // ==UserScript==
- // @name Github Time Format Converter
- // @name:zh-CN Github 时间格式转换
- // @name:zh-TW Github 時間格式轉換
- // @description Convert relative times on GitHub to absolute date and time
- // @description:zh-CN 将 GitHub 页面上的相对时间转换为绝对日期和时间
- // @description:zh-TW 將 GitHub 頁面上的相對時間轉換成絕對日期與時間
- // @version 1.2.0
- // @icon https://raw.githubusercontent.com/MiPoNianYou/UserScripts/main/Icons/Github-Time-Format-Converter-Icon.svg
- // @author 念柚
- // @namespace https://github.com/MiPoNianYou/UserScripts
- // @supportURL https://github.com/MiPoNianYou/UserScripts/issues
- // @license GPL-3.0
- // @match https://github.com/*
- // @exclude https://github.com/topics/*
- // @grant GM_addStyle
- // @run-at document-idle
- // ==/UserScript==
- (function () {
- "use strict";
- const Config = {
- SCRIPT_SETTINGS: {
- TOOLTIP_VERTICAL_OFFSET: 5,
- VIEWPORT_EDGE_MARGIN: 5,
- TRANSITION_DURATION_MS: 100,
- UI_FONT_STACK: "-apple-system, BlinkMacSystemFont, system-ui, sans-serif",
- UI_FONT_STACK_MONO: "ui-monospace, SFMono-Regular, Menlo, monospace",
- },
- ELEMENT_IDS: {
- TOOLTIP_CONTAINER: "TimeConverterTooltipContainer",
- },
- CSS_CLASSES: {
- PROCESSED_TIME_ELEMENT: "time-converter-processed-element",
- TOOLTIP_IS_VISIBLE: "time-converter-tooltip--is-visible",
- },
- UI_TEXTS: {
- "zh-CN": {
- INVALID_DATE_STRING: "无效日期",
- },
- "zh-TW": {
- INVALID_DATE_STRING: "無效日期",
- },
- "en-US": {
- INVALID_DATE_STRING: "Invalid Date",
- },
- },
- DOM_SELECTORS: {
- RELATIVE_TIME: `relative-time:not(.${"time-converter-processed-element"})`,
- PROCESSED_TIME_SPAN: `span.${"time-converter-processed-element"}[data-tooltip-time]`,
- },
- };
- Config.DOM_SELECTORS.RELATIVE_TIME = `relative-time:not(.${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT})`;
- Config.DOM_SELECTORS.PROCESSED_TIME_SPAN = `span.${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT}[data-tooltip-time]`;
- const State = {
- currentUserLocale: "en-US",
- localizedText: Config.UI_TEXTS["en-US"],
- shortDateFormatter: null,
- tooltipTimeFormatter: null,
- initialize() {
- this.currentUserLocale = this.detectBrowserLanguage();
- this.localizedText =
- Config.UI_TEXTS[this.currentUserLocale] ||
- Config.UI_TEXTS["en-US"] ||
- {};
- this.initializeDateTimeFormatters(this.currentUserLocale);
- },
- detectBrowserLanguage() {
- const languages = navigator.languages || [navigator.language];
- for (const lang of languages) {
- const langLower = lang.toLowerCase();
- if (langLower === "zh-cn") return "zh-CN";
- if (
- langLower === "zh-tw" ||
- langLower === "zh-hk" ||
- langLower === "zh-mo" ||
- langLower === "zh-hant"
- )
- return "zh-TW";
- if (langLower === "en-us") return "en-US";
- if (langLower.startsWith("zh-")) return "zh-CN";
- if (langLower.startsWith("en-")) return "en-US";
- }
- for (const lang of languages) {
- const langLower = lang.toLowerCase();
- if (langLower.startsWith("zh")) return "zh-CN";
- if (langLower.startsWith("en")) return "en-US";
- }
- return "en-US";
- },
- initializeDateTimeFormatters(locale) {
- try {
- this.shortDateFormatter = new Intl.DateTimeFormat(locale, {
- year: "2-digit",
- month: "2-digit",
- day: "2-digit",
- });
- this.tooltipTimeFormatter = new Intl.DateTimeFormat(locale, {
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: false,
- });
- } catch (e) {
- this.shortDateFormatter = null;
- this.tooltipTimeFormatter = null;
- }
- },
- getLocalizedText(key) {
- return (
- this.localizedText[key] ||
- (Config.UI_TEXTS["en-US"]
- ? Config.UI_TEXTS["en-US"][key]
- : undefined) ||
- key.replace(/_/g, " ")
- );
- },
- };
- const UserInterface = {
- tooltipContainerElement: null,
- injectStyles() {
- const appleEaseOutStandard = "cubic-bezier(0, 0, 0.58, 1)";
- const transitionDuration = Config.SCRIPT_SETTINGS.TRANSITION_DURATION_MS;
- const cssStyles = `
- :root {
- --ctp-frappe-rosewater: rgb(242, 213, 207);
- --ctp-frappe-flamingo: rgb(238, 190, 190);
- --ctp-frappe-pink: rgb(244, 184, 228);
- --ctp-frappe-mauve: rgb(202, 158, 230);
- --ctp-frappe-red: rgb(231, 130, 132);
- --ctp-frappe-maroon: rgb(234, 153, 156);
- --ctp-frappe-peach: rgb(239, 159, 118);
- --ctp-frappe-yellow: rgb(229, 200, 144);
- --ctp-frappe-green: rgb(166, 209, 137);
- --ctp-frappe-teal: rgb(129, 200, 190);
- --ctp-frappe-sky: rgb(153, 209, 219);
- --ctp-frappe-sapphire: rgb(133, 193, 220);
- --ctp-frappe-blue: rgb(140, 170, 238);
- --ctp-frappe-lavender: rgb(186, 187, 241);
- --ctp-frappe-text: rgb(198, 208, 245);
- --ctp-frappe-subtext1: rgb(181, 191, 226);
- --ctp-frappe-subtext0: rgb(165, 173, 206);
- --ctp-frappe-overlay2: rgb(148, 156, 187);
- --ctp-frappe-overlay1: rgb(131, 139, 167);
- --ctp-frappe-overlay0: rgb(115, 121, 148);
- --ctp-frappe-surface2: rgb(98, 104, 128);
- --ctp-frappe-surface1: rgb(81, 87, 109);
- --ctp-frappe-surface0: rgb(65, 69, 89);
- --ctp-frappe-base: rgb(48, 52, 70);
- --ctp-frappe-mantle: rgb(41, 44, 60);
- --ctp-frappe-crust: rgb(35, 38, 52);
- --ctp-latte-rosewater: rgb(220, 138, 120);
- --ctp-latte-flamingo: rgb(221, 120, 120);
- --ctp-latte-pink: rgb(234, 118, 203);
- --ctp-latte-mauve: rgb(136, 57, 239);
- --ctp-latte-red: rgb(210, 15, 57);
- --ctp-latte-maroon: rgb(230, 69, 83);
- --ctp-latte-peach: rgb(254, 100, 11);
- --ctp-latte-yellow: rgb(223, 142, 29);
- --ctp-latte-green: rgb(64, 160, 43);
- --ctp-latte-teal: rgb(23, 146, 153);
- --ctp-latte-sky: rgb(4, 165, 229);
- --ctp-latte-sapphire: rgb(32, 159, 181);
- --ctp-latte-blue: rgb(30, 102, 245);
- --ctp-latte-lavender: rgb(114, 135, 253);
- --ctp-latte-text: rgb(76, 79, 105);
- --ctp-latte-subtext1: rgb(92, 95, 119);
- --ctp-latte-subtext0: rgb(108, 111, 133);
- --ctp-latte-overlay2: rgb(124, 127, 147);
- --ctp-latte-overlay1: rgb(140, 143, 161);
- --ctp-latte-overlay0: rgb(156, 160, 176);
- --ctp-latte-surface2: rgb(172, 176, 190);
- --ctp-latte-surface1: rgb(188, 192, 204);
- --ctp-latte-surface0: rgb(204, 208, 218);
- --ctp-latte-base: rgb(239, 241, 245);
- --ctp-latte-mantle: rgb(230, 233, 239);
- --ctp-latte-crust: rgb(220, 224, 232);
- --ctp-tooltip-bg-dark: rgb(from var(--ctp-frappe-mantle) r g b / 0.92);
- --ctp-tooltip-text-dark: var(--ctp-frappe-text);
- --ctp-tooltip-border-dark: rgb(from var(--ctp-frappe-surface2) r g b / 0.25);
- --ctp-tooltip-shadow-dark: 0 1px 3px rgba(0, 0, 0, 0.15), 0 5px 10px rgba(0, 0, 0, 0.2);
- --ctp-tooltip-bg-light: rgb(from var(--ctp-latte-mantle) r g b / 0.92);
- --ctp-tooltip-text-light: var(--ctp-latte-text);
- --ctp-tooltip-border-light: rgb(from var(--ctp-latte-surface2) r g b / 0.3);
- --ctp-tooltip-shadow-light: 0 1px 3px rgba(90, 90, 90, 0.08), 0 5px 10px rgba(90, 90, 90, 0.12);
- }
- #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER} {
- position: fixed;
- padding: 6px 10px;
- border-radius: 8px;
- font-size: 12px;
- line-height: 1.4;
- z-index: 2147483647;
- pointer-events: none;
- white-space: pre;
- max-width: 350px;
- opacity: 0;
- visibility: hidden;
- font-family: ${Config.SCRIPT_SETTINGS.UI_FONT_STACK_MONO};
- backdrop-filter: blur(10px) saturate(180%);
- -webkit-backdrop-filter: blur(10px) saturate(180%);
- transition: opacity ${transitionDuration}ms ${appleEaseOutStandard},
- visibility ${transitionDuration}ms ${appleEaseOutStandard};
- background-color: var(--ctp-tooltip-bg-dark);
- color: var(--ctp-tooltip-text-dark);
- border: 1px solid var(--ctp-tooltip-border-dark);
- box-shadow: var(--ctp-tooltip-shadow-dark);
- }
- #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER}.${Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE} {
- opacity: 1;
- visibility: visible;
- }
- .${Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT}[data-tooltip-time] {
- display: inline-block;
- vertical-align: baseline;
- font-family: ${Config.SCRIPT_SETTINGS.UI_FONT_STACK_MONO};
- text-align: right;
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- cursor: help;
- color: inherit;
- background: none;
- border: none;
- }
- @media (prefers-color-scheme: light) {
- #${Config.ELEMENT_IDS.TOOLTIP_CONTAINER} {
- background-color: var(--ctp-tooltip-bg-light);
- color: var(--ctp-tooltip-text-light);
- border: 1px solid var(--ctp-tooltip-border-light);
- box-shadow: var(--ctp-tooltip-shadow-light);
- }
- }
- `;
- try {
- GM_addStyle(cssStyles);
- } catch (e) {}
- },
- ensureTooltipContainer() {
- this.tooltipContainerElement = document.getElementById(
- Config.ELEMENT_IDS.TOOLTIP_CONTAINER
- );
- if (!this.tooltipContainerElement && document.body) {
- this.tooltipContainerElement = document.createElement("div");
- this.tooltipContainerElement.id = Config.ELEMENT_IDS.TOOLTIP_CONTAINER;
- this.tooltipContainerElement.setAttribute("role", "tooltip");
- this.tooltipContainerElement.setAttribute("aria-hidden", "true");
- try {
- document.body.appendChild(this.tooltipContainerElement);
- } catch (e) {}
- }
- return this.tooltipContainerElement;
- },
- displayTooltip(targetElement) {
- const tooltipTime = targetElement.dataset.tooltipTime;
- this.ensureTooltipContainer();
- if (!tooltipTime || !this.tooltipContainerElement) return;
- this.tooltipContainerElement.textContent = tooltipTime;
- this.tooltipContainerElement.setAttribute("aria-hidden", "false");
- const targetRect = targetElement.getBoundingClientRect();
- this.tooltipContainerElement.classList.add(
- Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE
- );
- this.tooltipContainerElement.style.left = "-9999px";
- this.tooltipContainerElement.style.top = "-9999px";
- this.tooltipContainerElement.style.visibility = "hidden";
- requestAnimationFrame(() => {
- if (!this.tooltipContainerElement || !targetElement.isConnected) {
- this.hideTooltip();
- return;
- }
- const tooltipWidth = this.tooltipContainerElement.offsetWidth;
- const tooltipHeight = this.tooltipContainerElement.offsetHeight;
- const viewportWidth = window.innerWidth;
- const viewportHeight = window.innerHeight;
- const verticalOffset = Config.SCRIPT_SETTINGS.TOOLTIP_VERTICAL_OFFSET;
- const margin = Config.SCRIPT_SETTINGS.VIEWPORT_EDGE_MARGIN;
- let tooltipLeft =
- targetRect.left + targetRect.width / 2 - tooltipWidth / 2;
- tooltipLeft = Math.max(margin, tooltipLeft);
- tooltipLeft = Math.min(
- viewportWidth - tooltipWidth - margin,
- tooltipLeft
- );
- let tooltipTop;
- const spaceAbove = targetRect.top - verticalOffset;
- const spaceBelow = viewportHeight - targetRect.bottom - verticalOffset;
- if (spaceAbove >= tooltipHeight + margin) {
- tooltipTop = targetRect.top - tooltipHeight - verticalOffset;
- } else if (spaceBelow >= tooltipHeight + margin) {
- tooltipTop = targetRect.bottom + verticalOffset;
- } else {
- tooltipTop =
- spaceAbove > spaceBelow
- ? Math.max(
- margin,
- targetRect.top - tooltipHeight - verticalOffset
- )
- : Math.min(
- viewportHeight - tooltipHeight - margin,
- targetRect.bottom + verticalOffset
- );
- if (tooltipTop < margin) tooltipTop = margin;
- }
- this.tooltipContainerElement.style.left = `${tooltipLeft}px`;
- this.tooltipContainerElement.style.top = `${tooltipTop}px`;
- this.tooltipContainerElement.style.visibility = "visible";
- });
- },
- hideTooltip() {
- if (this.tooltipContainerElement) {
- this.tooltipContainerElement.classList.remove(
- Config.CSS_CLASSES.TOOLTIP_IS_VISIBLE
- );
- this.tooltipContainerElement.setAttribute("aria-hidden", "true");
- }
- },
- };
- const TimeConverter = {
- formatDateTime(dateSource, formatType) {
- const dateObject =
- typeof dateSource === "string" ? new Date(dateSource) : dateSource;
- if (isNaN(dateObject.getTime())) {
- return State.getLocalizedText("INVALID_DATE_STRING");
- }
- let formatter;
- if (formatType === "shortDate") {
- formatter = State.shortDateFormatter;
- } else if (formatType === "tooltipTime") {
- formatter = State.tooltipTimeFormatter;
- } else {
- formatter = State.shortDateFormatter;
- }
- if (!formatter) {
- const year = String(dateObject.getFullYear()).slice(-2);
- const month = String(dateObject.getMonth() + 1).padStart(2, "0");
- const day = String(dateObject.getDate()).padStart(2, "0");
- const hours = String(dateObject.getHours()).padStart(2, "0");
- const minutes = String(dateObject.getMinutes()).padStart(2, "0");
- const seconds = String(dateObject.getSeconds()).padStart(2, "0");
- if (formatType === "shortDate") {
- return `${year}-${month}-${day}`;
- } else if (formatType === "tooltipTime") {
- return `${hours}:${minutes}:${seconds}`;
- }
- return `${year}-${month}-${day}`;
- }
- try {
- let formattedString = formatter.format(dateObject);
- formattedString = formattedString.replace(/[/]/g, "-");
- if (formatType !== "shortDate" && formatType !== "tooltipTime") {
- formattedString = formattedString.replace(", ", " ");
- }
- return formattedString;
- } catch (e) {
- return State.getLocalizedText("INVALID_DATE_STRING");
- }
- },
- convertElement(element) {
- if (
- !element ||
- !(element instanceof Element) ||
- element.classList.contains(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT)
- ) {
- return;
- }
- const dateTimeAttribute = element.getAttribute("datetime");
- if (!dateTimeAttribute) {
- element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
- return;
- }
- try {
- const displayDateText = this.formatDateTime(
- dateTimeAttribute,
- "shortDate"
- );
- const tooltipTimeText = this.formatDateTime(
- dateTimeAttribute,
- "tooltipTime"
- );
- if (
- displayDateText === State.getLocalizedText("INVALID_DATE_STRING") ||
- tooltipTimeText === State.getLocalizedText("INVALID_DATE_STRING")
- ) {
- element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
- return;
- }
- const replacementSpan = document.createElement("span");
- replacementSpan.textContent = displayDateText;
- replacementSpan.dataset.tooltipTime = tooltipTimeText;
- replacementSpan.classList.add(
- Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT
- );
- if (element.parentNode) {
- element.parentNode.replaceChild(replacementSpan, element);
- } else {
- element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
- }
- } catch (error) {
- element.classList.add(Config.CSS_CLASSES.PROCESSED_TIME_ELEMENT);
- }
- },
- processAllInNode(targetNode = document.body) {
- if (!targetNode || typeof targetNode.querySelectorAll !== "function")
- return;
- try {
- const timeElements = targetNode.querySelectorAll(
- Config.DOM_SELECTORS.RELATIVE_TIME
- );
- timeElements.forEach(this.convertElement.bind(this));
- } catch (e) {}
- },
- };
- const EventManager = {
- domMutationObserver: null,
- init() {
- this.setupTooltipListeners();
- this.startDomObserver();
- },
- setupTooltipListeners() {
- document.body.addEventListener("mouseover", (event) => {
- const targetSpan = event.target.closest(
- Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
- );
- if (targetSpan) UserInterface.displayTooltip(targetSpan);
- });
- document.body.addEventListener("mouseout", (event) => {
- const targetSpan = event.target.closest(
- Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
- );
- if (
- targetSpan &&
- (!event.relatedTarget ||
- !UserInterface.tooltipContainerElement?.contains(
- event.relatedTarget
- ))
- ) {
- UserInterface.hideTooltip();
- }
- });
- document.body.addEventListener(
- "focusin",
- (event) => {
- const targetSpan = event.target.closest(
- Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
- );
- if (targetSpan) UserInterface.displayTooltip(targetSpan);
- },
- true
- );
- document.body.addEventListener(
- "focusout",
- (event) => {
- const targetSpan = event.target.closest(
- Config.DOM_SELECTORS.PROCESSED_TIME_SPAN
- );
- if (targetSpan) UserInterface.hideTooltip();
- },
- true
- );
- },
- handleDomMutation(mutations) {
- for (const mutation of mutations) {
- if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
- for (const addedNode of mutation.addedNodes) {
- if (addedNode.nodeType === Node.ELEMENT_NODE) {
- if (addedNode.matches(Config.DOM_SELECTORS.RELATIVE_TIME)) {
- TimeConverter.convertElement(addedNode);
- } else if (
- addedNode.querySelector(Config.DOM_SELECTORS.RELATIVE_TIME)
- ) {
- const descendantElements = addedNode.querySelectorAll(
- Config.DOM_SELECTORS.RELATIVE_TIME
- );
- descendantElements.forEach(
- TimeConverter.convertElement.bind(TimeConverter)
- );
- }
- }
- }
- }
- }
- },
- startDomObserver() {
- if (this.domMutationObserver) return;
- this.domMutationObserver = new MutationObserver(
- this.handleDomMutation.bind(this)
- );
- const observerConfiguration = { childList: true, subtree: true };
- if (document.body) {
- try {
- this.domMutationObserver.observe(
- document.body,
- observerConfiguration
- );
- } catch (e) {}
- } else {
- document.addEventListener(
- "DOMContentLoaded",
- () => {
- if (document.body) {
- try {
- this.domMutationObserver.observe(
- document.body,
- observerConfiguration
- );
- } catch (e) {}
- }
- },
- { once: true }
- );
- }
- },
- };
- const ScriptManager = {
- init() {
- try {
- State.initialize();
- UserInterface.injectStyles();
- UserInterface.ensureTooltipContainer();
- TimeConverter.processAllInNode(
- document.body || document.documentElement
- );
- EventManager.init();
- } catch (error) {}
- },
- };
- if (
- document.readyState === "complete" ||
- (document.readyState !== "loading" && !document.documentElement.doScroll)
- ) {
- ScriptManager.init();
- } else {
- document.addEventListener("DOMContentLoaded", () => ScriptManager.init(), {
- once: true,
- });
- }
- })();