Show Last Modified Timestamp for Non-HTML Documents

Display the Last-Modified timestamp for non-HTML documents

// ==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
// @version     2025.04.27.001
// ==/UserScript==
(function () {
  // 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;

  // 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;

  // 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: rgba(0, 0, 0, 0.6);
    color: white;
    z-index: 10;
    font-size: 12px;
    padding: 8px;
    font-family: sans-serif;
    cursor: pointer;
  }
  .last-modified-overlay:focus::after {
    content: attr(data-content);
    position: absolute;
    top: 100%;
    left: 0;
    background: #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 {
    margin: 0 6px;
    cursor: pointer;
  }
  .drag-button {
    font: inherit;
    font-size: 12px;
    cursor: move;
  }
  .copy-button {
    font: inherit;
    font-size: 12px;
    cursor: copy;
  }
  [data-tooltip] {
    font: inherit;
    font-size: 12px;
    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: #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);

})();