Show Last Modified Timestamp for Non-HTML Documents

Display the Last-Modified timestamp for non-HTML documents

  1. // ==UserScript==
  2. // @name Show Last Modified Timestamp for Non-HTML Documents
  3. // @namespace https://greasyfork.org/users/1310758
  4. // @description Display the Last-Modified timestamp for non-HTML documents
  5. // @match *://*/*
  6. // @license MIT License
  7. // @author pachimonta
  8. // @grant GM.setValue
  9. // @grant GM_getValue
  10. // @grant GM_setClipboard
  11. // @noframes
  12. // @version 2025.05.03.001
  13. // ==/UserScript==
  14. (function () {
  15. // Only proceed for non-HTML documents with required properties
  16. if (
  17. !document ||
  18. !document.lastModified ||
  19. !document.contentType ||
  20. /^text\/html(?:$|;)/.test(document.contentType) ||
  21. !document.head ||
  22. !document.body
  23. ) return;
  24.  
  25. // Information note shown on overlay focus
  26. const NOTE = String.raw`Content last modified time.
  27.  
  28. Note: The 'lastModified' value may differ from the
  29. server's 'Last-Modified' header. If not available,
  30. the 'Date' header is used, showing when the server
  31. sent the response. Timestamp formats may also vary
  32. by environment, which can cause display errors.
  33.  
  34. 📋: Copy, 📌: Drag overlay, ✅: Toggle display.
  35. Press CTRL+C to copy the last modified time.
  36. Press CTRL+V to toggle display.
  37. When unchecked, it hides on blur.`;
  38.  
  39. const DEFAULT_OVERLAY_VISIBLE = true;
  40.  
  41. // Parse lastModified string into Date
  42. const lastModifiedDate = new Date(document.lastModified);
  43.  
  44. // Format date (YYYY-MM-DD)
  45. const dateFormatOptions = {
  46. year: 'numeric',
  47. month: '2-digit',
  48. day: '2-digit',
  49. };
  50. const formattedDate = new Intl.DateTimeFormat(
  51. 'sv-SE',
  52. dateFormatOptions
  53. ).format(lastModifiedDate);
  54.  
  55. // Format time with timezone
  56. const timeFormatOptions = {
  57. hour12: false,
  58. hour: '2-digit',
  59. minute: '2-digit',
  60. second: '2-digit',
  61. timeZoneName: 'short',
  62. };
  63. const formattedTime = new Intl.DateTimeFormat(
  64. 'sv-SE',
  65. timeFormatOptions
  66. ).format(lastModifiedDate);
  67.  
  68. // Get short weekday name (Mon, Tue, ...)
  69. const weekdayOptions = {
  70. weekday: 'short',
  71. };
  72. const dayOfWeek = new Intl.DateTimeFormat('en-US', weekdayOptions).format(
  73. lastModifiedDate
  74. );
  75.  
  76. // Final display string
  77. const lastModifiedText = `${formattedDate} (${dayOfWeek}) ${formattedTime}`;
  78.  
  79. // Get overlay visibility state from user settings
  80. const isOverlayVisible = GM_getValue(
  81. 'lastModifiedOverlayVisible',
  82. DEFAULT_OVERLAY_VISIBLE
  83. );
  84.  
  85. // Create overlay root element
  86. const overlay = document.createElement('span');
  87. overlay.classList.add('last-modified-overlay');
  88. if (!isOverlayVisible) overlay.classList.add('opacity-zero');
  89. overlay.setAttribute('tabindex', '0');
  90. overlay.dataset.content = NOTE;
  91.  
  92. // Last-modified info button
  93. const infoButton = document.createElement('button');
  94. infoButton.textContent = lastModifiedText;
  95. infoButton.classList.add('overlay-info-button');
  96. infoButton.setAttribute('tabindex', '-1');
  97. overlay.append(infoButton);
  98.  
  99. // When info button receives focus, move focus to overlay for accessibility/tooltips
  100. infoButton.addEventListener(
  101. 'focus',
  102. function () {
  103. overlay.focus();
  104. },
  105. true
  106. );
  107.  
  108. // Copy button
  109. const copyButton = document.createElement('button');
  110. copyButton.classList.add('copy-button');
  111. copyButton.textContent = '📋';
  112. copyButton.dataset.tooltip = 'Copy';
  113. overlay.append(copyButton);
  114.  
  115. // Copy to clipboard with double confirmation
  116. let copyTooltipTimeout;
  117. copyButton.addEventListener('click', () => {
  118. if (copyButton.dataset.tooltip === 'Copied!') return;
  119. function resetTooltip() {
  120. copyButton.dataset.tooltip = 'Copy';
  121. copyButton.blur();
  122. }
  123. if (copyTooltipTimeout) clearTimeout(copyTooltipTimeout);
  124.  
  125. if (copyButton.dataset.tooltip === 'Copy') {
  126. copyButton.dataset.tooltip = 'Copy?';
  127. copyTooltipTimeout = setTimeout(resetTooltip, 3000);
  128. return;
  129. }
  130. GM_setClipboard(lastModifiedText, 'text');
  131. copyButton.dataset.tooltip = 'Copied!';
  132. copyTooltipTimeout = setTimeout(resetTooltip, 1000);
  133. });
  134.  
  135. // Toggle label & checkbox for overlay visibility
  136. const toggleLabel = document.createElement('label');
  137. toggleLabel.setAttribute('tabindex', '0');
  138. toggleLabel.classList.add('toggle-label');
  139. toggleLabel.dataset.tooltip = 'Toggle display';
  140.  
  141. const toggleCheckbox = document.createElement('input');
  142. toggleCheckbox.type = 'checkbox';
  143. toggleCheckbox.classList.add('toggle-checkbox');
  144. toggleCheckbox.checked = isOverlayVisible;
  145. toggleCheckbox.addEventListener(
  146. 'change',
  147. function () {
  148. const checked = toggleCheckbox.checked;
  149. const prevValue = GM_getValue(
  150. 'lastModifiedOverlayVisible',
  151. DEFAULT_OVERLAY_VISIBLE
  152. );
  153. if (prevValue !== checked) {
  154. GM.setValue('lastModifiedOverlayVisible', checked);
  155. }
  156. overlay.classList.toggle('opacity-zero', !checked);
  157. },
  158. true
  159. );
  160. toggleLabel.append(toggleCheckbox);
  161. overlay.prepend(toggleLabel);
  162.  
  163. // Drag button
  164. const dragButton = document.createElement('button');
  165. dragButton.classList.add('drag-button');
  166. dragButton.textContent = '📌';
  167. dragButton.dataset.tooltip = 'Drag overlay';
  168. overlay.prepend(dragButton);
  169.  
  170. // Reset overlay position on double click of drag button
  171. dragButton.addEventListener('dblclick', function() {
  172. overlay.style.top = defaultTop;
  173. overlay.style.right = defaultRight;
  174. overlay.style.bottom = 'auto';
  175. overlay.style.left = 'auto';
  176. });
  177.  
  178. // Keyboard shortcuts: Ctrl+C to copy, Ctrl+V to toggle
  179. overlay.addEventListener('keydown', function(event) {
  180. if (event.ctrlKey && event.key === 'c') {
  181. GM_setClipboard(lastModifiedText, 'text');
  182. }
  183. if (event.ctrlKey && event.key === 'v') {
  184. toggleCheckbox.click();
  185. }
  186. });
  187.  
  188. // Sync overlay state when window/tab gets focus (cross-tab)
  189. window.addEventListener(
  190. 'focus',
  191. function () {
  192. const currentVisible = GM_getValue(
  193. 'lastModifiedOverlayVisible',
  194. DEFAULT_OVERLAY_VISIBLE
  195. );
  196. toggleCheckbox.checked = currentVisible;
  197. overlay.classList.toggle('opacity-zero', !currentVisible);
  198. },
  199. true
  200. );
  201.  
  202. // Overlay styles
  203. const style = document.createElement('style');
  204. style.textContent = String.raw`
  205. .last-modified-overlay {
  206. display: inline-block;
  207. position: fixed;
  208. top: 8px;
  209. right: 32px;
  210. background-color: rgba(0, 0, 0, 0.6);
  211. color: white;
  212. z-index: 10;
  213. font-size: 13px;
  214. padding: 8px;
  215. font-family: sans-serif;
  216. cursor: pointer;
  217. border: 1px solid transparent;
  218. border-radius: 6px;
  219. }
  220. .last-modified-overlay:focus::after {
  221. content: attr(data-content);
  222. position: absolute;
  223. top: 100%;
  224. left: 0;
  225. background-color: #ffe;
  226. color: #000;
  227. padding: 4px 8px;
  228. margin-top: 4px;
  229. border: 1px solid #000;
  230. border-radius: 6px;
  231. z-index: 10;
  232. white-space: break-spaces;
  233. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.19);
  234. opacity: 0.9;
  235. }
  236. .last-modified-overlay:focus,
  237. .last-modified-overlay:hover {
  238. opacity: 1;
  239. }
  240. .overlay-info-button {
  241. appearance: none;
  242. color: white;
  243. margin: 0 6px;
  244. background-color: transparent;
  245. border: 2px solid transparent;
  246. cursor: pointer;
  247. }
  248. .drag-button {
  249. cursor: move;
  250. }
  251. .copy-button {
  252. cursor: copy;
  253. }
  254. [data-tooltip] {
  255. font-family: inherit;
  256. position: relative;
  257. opacity: 0.6;
  258. z-index: 100;
  259. }
  260. [data-tooltip]:hover::after,
  261. [data-tooltip]:focus::after {
  262. content: attr(data-tooltip);
  263. display: inline-block;
  264. position: absolute;
  265. top: 100%;
  266. left: -26px;
  267. background-color: #ffe;
  268. color: #000;
  269. padding: 4px 8px;
  270. margin-top: 4px;
  271. border: 1px solid #000;
  272. border-radius: 6px;
  273. white-space: break-spaces;
  274. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.19);
  275. }
  276. [data-tooltip]:hover,
  277. [data-tooltip]:focus {
  278. opacity: 0.9;
  279. }
  280. .opacity-zero {
  281. opacity: 0;
  282. transition: opacity 0.25s;
  283. }
  284. `;
  285. document.head.append(style);
  286.  
  287. // Insert overlay after <body> for layout consistency
  288. document.body.after(overlay);
  289.  
  290. // ------- Drag and Drop Functionality -------
  291. // Utility: Get clientX and clientY from mouse or touch event
  292. function getEventClientXY(e) {
  293. if (e.touches && e.touches.length > 0) {
  294. return { x: e.touches[0].clientX, y: e.touches[0].clientY };
  295. } else if (typeof e.clientX === 'number' && typeof e.clientY === 'number') {
  296. return { x: e.clientX, y: e.clientY };
  297. }
  298. return null;
  299. }
  300.  
  301. let isDragging = false;
  302. let dragOffsetX = 0;
  303. let dragOffsetY = 0;
  304. // Store default position for reset
  305. const defaultTop = overlay.style.top;
  306. const defaultRight = overlay.style.right;
  307.  
  308. function dragStart(e) {
  309. // Only allow dragging from the drag button
  310. if (e.target !== dragButton) return;
  311. isDragging = true;
  312. const pos = getEventClientXY(e);
  313. if (!pos) return;
  314. const rect = overlay.getBoundingClientRect();
  315. dragOffsetX = pos.x - rect.left;
  316. dragOffsetY = pos.y - rect.top;
  317. e.preventDefault();
  318. }
  319.  
  320. function dragMove(e) {
  321. if (!isDragging) return;
  322. const pos = getEventClientXY(e);
  323. if (!pos) return;
  324. let newLeft = pos.x - dragOffsetX;
  325. let newTop = pos.y - dragOffsetY;
  326. // Keep overlay within viewport
  327. newLeft = Math.max(0, Math.min(window.innerWidth - overlay.offsetWidth, newLeft));
  328. newTop = Math.max(0, Math.min(window.innerHeight - overlay.offsetHeight, newTop));
  329. overlay.style.left = newLeft + 'px';
  330. overlay.style.top = newTop + 'px';
  331. overlay.style.right = 'auto';
  332. overlay.style.bottom = 'auto';
  333. e.preventDefault();
  334. }
  335.  
  336. function dragEnd() {
  337. isDragging = false;
  338. }
  339.  
  340. // Register drag event listeners for mouse and touch
  341. overlay.addEventListener('mousedown', dragStart);
  342. overlay.addEventListener('touchstart', dragStart, { passive: false });
  343.  
  344. document.addEventListener('mousemove', dragMove);
  345. document.addEventListener('touchmove', dragMove, { passive: false });
  346.  
  347. document.addEventListener('mouseup', dragEnd);
  348. document.addEventListener('touchend', dragEnd);
  349.  
  350. })();