Show Last Modified Timestamp for Non-HTML Documents

Display the Last-Modified timestamp for non-HTML documents

Versión del día 3/5/2025. Echa un vistazo a la versión más reciente.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Necesitará instalar una extensión como Tampermonkey para instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión como Stylus para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

Necesitará instalar una extensión del gestor de estilos de usuario para instalar este estilo.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Show Last Modified Timestamp for Non-HTML Documents
// @namespace   https://greasyfork.org/users/1310758
// @description Display the Last-Modified timestamp for non-HTML documents
// @match       *://*/*
// @license     MIT License
// @author      pachimonta
// @grant       GM.setValue
// @grant       GM_getValue
// @grant       GM_setClipboard
// @noframes
// @version     2025.05.03.001
// ==/UserScript==
(function () {
  // Only proceed for non-HTML documents with required properties
  if (
    !document ||
    !document.lastModified ||
    !document.contentType ||
    /^text\/html(?:$|;)/.test(document.contentType) ||
    !document.head ||
    !document.body
  ) return;

  // Information note shown on overlay focus
  const NOTE = String.raw`Content last modified time.

Note: The 'lastModified' value may differ from the
server's 'Last-Modified' header. If not available,
the 'Date' header is used, showing when the server
sent the response. Timestamp formats may also vary
by environment, which can cause display errors.

📋: Copy, 📌: Drag overlay, ✅: Toggle display.
Press CTRL+C to copy the last modified time.
Press CTRL+V to toggle display.
When unchecked, it hides on blur.`;

  const DEFAULT_OVERLAY_VISIBLE = true;

  // Parse lastModified string into Date
  const lastModifiedDate = new Date(document.lastModified);

  // Format date (YYYY-MM-DD)
  const dateFormatOptions = {
    year: 'numeric',
    month: '2-digit',
    day: '2-digit',
  };
  const formattedDate = new Intl.DateTimeFormat(
    'sv-SE',
    dateFormatOptions
  ).format(lastModifiedDate);

  // Format time with timezone
  const timeFormatOptions = {
    hour12: false,
    hour: '2-digit',
    minute: '2-digit',
    second: '2-digit',
    timeZoneName: 'short',
  };
  const formattedTime = new Intl.DateTimeFormat(
    'sv-SE',
    timeFormatOptions
  ).format(lastModifiedDate);

  // Get short weekday name (Mon, Tue, ...)
  const weekdayOptions = {
    weekday: 'short',
  };
  const dayOfWeek = new Intl.DateTimeFormat('en-US', weekdayOptions).format(
    lastModifiedDate
  );

  // Final display string
  const lastModifiedText = `${formattedDate} (${dayOfWeek}) ${formattedTime}`;

  // Get overlay visibility state from user settings
  const isOverlayVisible = GM_getValue(
    'lastModifiedOverlayVisible',
    DEFAULT_OVERLAY_VISIBLE
  );

  // Create overlay root element
  const overlay = document.createElement('span');
  overlay.classList.add('last-modified-overlay');
  if (!isOverlayVisible) overlay.classList.add('opacity-zero');
  overlay.setAttribute('tabindex', '0');
  overlay.dataset.content = NOTE;

  // Last-modified info button
  const infoButton = document.createElement('button');
  infoButton.textContent = lastModifiedText;
  infoButton.classList.add('overlay-info-button');
  infoButton.setAttribute('tabindex', '-1');
  overlay.append(infoButton);

  // When info button receives focus, move focus to overlay for accessibility/tooltips
  infoButton.addEventListener(
    'focus',
    function () {
      overlay.focus();
    },
    true
  );

  // Copy button
  const copyButton = document.createElement('button');
  copyButton.classList.add('copy-button');
  copyButton.textContent = '📋';
  copyButton.dataset.tooltip = 'Copy';
  overlay.append(copyButton);

  // Copy to clipboard with double confirmation
  let copyTooltipTimeout;
  copyButton.addEventListener('click', () => {
    if (copyButton.dataset.tooltip === 'Copied!') return;
    function resetTooltip() {
      copyButton.dataset.tooltip = 'Copy';
      copyButton.blur();
    }
    if (copyTooltipTimeout) clearTimeout(copyTooltipTimeout);

    if (copyButton.dataset.tooltip === 'Copy') {
      copyButton.dataset.tooltip = 'Copy?';
      copyTooltipTimeout = setTimeout(resetTooltip, 3000);
      return;
    }
    GM_setClipboard(lastModifiedText, 'text');
    copyButton.dataset.tooltip = 'Copied!';
    copyTooltipTimeout = setTimeout(resetTooltip, 1000);
  });

  // Toggle label & checkbox for overlay visibility
  const toggleLabel = document.createElement('label');
  toggleLabel.setAttribute('tabindex', '0');
  toggleLabel.classList.add('toggle-label');
  toggleLabel.dataset.tooltip = 'Toggle display';

  const toggleCheckbox = document.createElement('input');
  toggleCheckbox.type = 'checkbox';
  toggleCheckbox.classList.add('toggle-checkbox');
  toggleCheckbox.checked = isOverlayVisible;
  toggleCheckbox.addEventListener(
    'change',
    function () {
      const checked = toggleCheckbox.checked;
      const prevValue = GM_getValue(
        'lastModifiedOverlayVisible',
        DEFAULT_OVERLAY_VISIBLE
      );
      if (prevValue !== checked) {
        GM.setValue('lastModifiedOverlayVisible', checked);
      }
      overlay.classList.toggle('opacity-zero', !checked);
    },
    true
  );
  toggleLabel.append(toggleCheckbox);
  overlay.prepend(toggleLabel);

  // Drag button
  const dragButton = document.createElement('button');
  dragButton.classList.add('drag-button');
  dragButton.textContent = '📌';
  dragButton.dataset.tooltip = 'Drag overlay';
  overlay.prepend(dragButton);

  // Reset overlay position on double click of drag button
  dragButton.addEventListener('dblclick', function() {
    overlay.style.top = defaultTop;
    overlay.style.right = defaultRight;
    overlay.style.bottom = 'auto';
    overlay.style.left = 'auto';
  });

  // Keyboard shortcuts: Ctrl+C to copy, Ctrl+V to toggle
  overlay.addEventListener('keydown', function(event) {
    if (event.ctrlKey && event.key === 'c') {
      GM_setClipboard(lastModifiedText, 'text');
    }
    if (event.ctrlKey && event.key === 'v') {
      toggleCheckbox.click();
    }
  });

  // Sync overlay state when window/tab gets focus (cross-tab)
  window.addEventListener(
    'focus',
    function () {
      const currentVisible = GM_getValue(
        'lastModifiedOverlayVisible',
        DEFAULT_OVERLAY_VISIBLE
      );
      toggleCheckbox.checked = currentVisible;
      overlay.classList.toggle('opacity-zero', !currentVisible);
    },
    true
  );

  // Overlay styles
  const style = document.createElement('style');
  style.textContent = String.raw`
  .last-modified-overlay {
    display: inline-block;
    position: fixed;
    top: 8px;
    right: 32px;
    background-color: rgba(0, 0, 0, 0.6);
    color: white;
    z-index: 10;
    font-size: 13px;
    padding: 8px;
    font-family: sans-serif;
    cursor: pointer;
    border: 1px solid transparent;
    border-radius: 6px;
  }
  .last-modified-overlay:focus::after {
    content: attr(data-content);
    position: absolute;
    top: 100%;
    left: 0;
    background-color: #ffe;
    color: #000;
    padding: 4px 8px;
    margin-top: 4px;
    border: 1px solid #000;
    border-radius: 6px;
    z-index: 10;
    white-space: break-spaces;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.19);
    opacity: 0.9;
  }
  .last-modified-overlay:focus,
  .last-modified-overlay:hover {
    opacity: 1;
  }
  .overlay-info-button {
    appearance: none;
    color: white;
    margin: 0 6px;
    background-color: transparent;
    border: 2px solid transparent;
    cursor: pointer;
  }
  .drag-button {
    cursor: move;
  }
  .copy-button {
    cursor: copy;
  }
  [data-tooltip] {
    font-family: inherit;
    position: relative;
    opacity: 0.6;
    z-index: 100;
  }
  [data-tooltip]:hover::after,
  [data-tooltip]:focus::after {
    content: attr(data-tooltip);
    display: inline-block;
    position: absolute;
    top: 100%;
    left: -26px;
    background-color: #ffe;
    color: #000;
    padding: 4px 8px;
    margin-top: 4px;
    border: 1px solid #000;
    border-radius: 6px;
    white-space: break-spaces;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.19);
  }
  [data-tooltip]:hover,
  [data-tooltip]:focus {
    opacity: 0.9;
  }
  .opacity-zero {
    opacity: 0;
    transition: opacity 0.25s;
  }
  `;
  document.head.append(style);

  // Insert overlay after <body> for layout consistency
  document.body.after(overlay);

  // ------- Drag and Drop Functionality -------
  // Utility: Get clientX and clientY from mouse or touch event
  function getEventClientXY(e) {
    if (e.touches && e.touches.length > 0) {
      return { x: e.touches[0].clientX, y: e.touches[0].clientY };
    } else if (typeof e.clientX === 'number' && typeof e.clientY === 'number') {
      return { x: e.clientX, y: e.clientY };
    }
    return null;
  }

  let isDragging = false;
  let dragOffsetX = 0;
  let dragOffsetY = 0;
  // Store default position for reset
  const defaultTop = overlay.style.top;
  const defaultRight = overlay.style.right;

  function dragStart(e) {
    // Only allow dragging from the drag button
    if (e.target !== dragButton) return;
    isDragging = true;
    const pos = getEventClientXY(e);
    if (!pos) return;
    const rect = overlay.getBoundingClientRect();
    dragOffsetX = pos.x - rect.left;
    dragOffsetY = pos.y - rect.top;
    e.preventDefault();
  }

  function dragMove(e) {
    if (!isDragging) return;
    const pos = getEventClientXY(e);
    if (!pos) return;
    let newLeft = pos.x - dragOffsetX;
    let newTop = pos.y - dragOffsetY;
    // Keep overlay within viewport
    newLeft = Math.max(0, Math.min(window.innerWidth - overlay.offsetWidth, newLeft));
    newTop = Math.max(0, Math.min(window.innerHeight - overlay.offsetHeight, newTop));
    overlay.style.left = newLeft + 'px';
    overlay.style.top = newTop + 'px';
    overlay.style.right = 'auto';
    overlay.style.bottom = 'auto';
    e.preventDefault();
  }

  function dragEnd() {
    isDragging = false;
  }

  // Register drag event listeners for mouse and touch
  overlay.addEventListener('mousedown', dragStart);
  overlay.addEventListener('touchstart', dragStart, { passive: false });

  document.addEventListener('mousemove', dragMove);
  document.addEventListener('touchmove', dragMove, { passive: false });

  document.addEventListener('mouseup', dragEnd);
  document.addEventListener('touchend', dragEnd);

})();