GitHub Static Time

A userscript that replaces relative times with a static time formatted as you like it

  1. // ==UserScript==
  2. // @name GitHub Static Time
  3. // @version 1.1.0
  4. // @description A userscript that replaces relative times with a static time formatted as you like it
  5. // @license MIT
  6. // @author Rob Garrison
  7. // @namespace https://github.com/Mottie
  8. // @match https://github.com/*
  9. // @run-at document-end
  10. // @grant GM_addStyle
  11. // @grant GM_getValue
  12. // @grant GM_setValue
  13. // @grant GM_registerMenuCommand
  14. // @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.4/moment-with-locales.min.js
  15. // @require https://greasyfork.org/scripts/28721-mutations/code/mutations.js?version=1108163
  16. // @require https://greasyfork.org/scripts/398877-utils-js/code/utilsjs.js?version=1079637
  17. // @icon https://github.githubassets.com/pinned-octocat.svg
  18. // @supportURL https://github.com/Mottie/GitHub-userscripts/issues
  19. // ==/UserScript==
  20.  
  21. (() => {
  22. /* global moment $ $$ on */
  23. "use strict";
  24.  
  25. let busy = false;
  26. let timeFormat = GM_getValue("ghst-format", "LLL");
  27. let locale = GM_getValue("ghst-locale", "en");
  28. let useUTC = GM_getValue("ghst-utc", "false");
  29.  
  30. // list copied from
  31. // https://github.com/moment/momentjs.com/blob/master/data/locale.js
  32. const locales = [
  33. { "abbr": "af", "name": "Afrikaans" },
  34. { "abbr": "sq", "name": "Albanian" },
  35. { "abbr": "ar", "name": "Arabic" },
  36. { "abbr": "ar-dz", "name": "Arabic (Algeria)" },
  37. { "abbr": "ar-kw", "name": "Arabic (Kuwait)" },
  38. { "abbr": "ar-ly", "name": "Arabic (Libya)" },
  39. { "abbr": "ar-ma", "name": "Arabic (Morocco)" },
  40. { "abbr": "ar-sa", "name": "Arabic (Saudi Arabia)" },
  41. { "abbr": "ar-tn", "name": "Arabic (Tunisia)" },
  42. { "abbr": "hy-am", "name": "Armenian" },
  43. { "abbr": "az", "name": "Azerbaijani" },
  44. { "abbr": "bm", "name": "Bambara" },
  45. { "abbr": "eu", "name": "Basque" },
  46. { "abbr": "be", "name": "Belarusian" },
  47. { "abbr": "bn", "name": "Bengali" },
  48. { "abbr": "bn-bd", "name": "Bengali (Bangladesh)" },
  49. { "abbr": "bs", "name": "Bosnian" },
  50. { "abbr": "br", "name": "Breton" },
  51. { "abbr": "bg", "name": "Bulgarian" },
  52. { "abbr": "my", "name": "Burmese" },
  53. { "abbr": "km", "name": "Cambodian" },
  54. { "abbr": "ca", "name": "Catalan" },
  55. { "abbr": "tzm", "name": "Central Atlas Tamazight" },
  56. { "abbr": "tzm-latn", "name": "Central Atlas Tamazight Latin" },
  57. { "abbr": "zh-cn", "name": "Chinese (China)" },
  58. { "abbr": "zh-hk", "name": "Chinese (Hong Kong)" },
  59. { "abbr": "zh-mo", "name": "Chinese (Macau)" },
  60. { "abbr": "zh-tw", "name": "Chinese (Taiwan)" },
  61. { "abbr": "cv", "name": "Chuvash" },
  62. { "abbr": "hr", "name": "Croatian" },
  63. { "abbr": "cs", "name": "Czech" },
  64. { "abbr": "da", "name": "Danish" },
  65. { "abbr": "nl", "name": "Dutch" },
  66. { "abbr": "nl-be", "name": "Dutch (Belgium)" },
  67. { "abbr": "en-au", "name": "English (Australia)" },
  68. { "abbr": "en-ca", "name": "English (Canada)" },
  69. { "abbr": "en-in", "name": "English (India)" },
  70. { "abbr": "en-ie", "name": "English (Ireland)" },
  71. { "abbr": "en-il", "name": "English (Israel)" },
  72. { "abbr": "en-nz", "name": "English (New Zealand)" },
  73. { "abbr": "en-sg", "name": "English (Singapore)" },
  74. { "abbr": "en-gb", "name": "English (United Kingdom)" },
  75. { "abbr": "en", "name": "English (United States)" },
  76. { "abbr": "eo", "name": "Esperanto" },
  77. { "abbr": "et", "name": "Estonian" },
  78. { "abbr": "fo", "name": "Faroese" },
  79. { "abbr": "fil", "name": "Filipino" },
  80. { "abbr": "fi", "name": "Finnish" },
  81. { "abbr": "fr", "name": "French" },
  82. { "abbr": "fr-ca", "name": "French (Canada)" },
  83. { "abbr": "fr-ch", "name": "French (Switzerland)" },
  84. { "abbr": "fy", "name": "Frisian" },
  85. { "abbr": "gl", "name": "Galician" },
  86. { "abbr": "ka", "name": "Georgian" },
  87. { "abbr": "de", "name": "German" },
  88. { "abbr": "de-at", "name": "German (Austria)" },
  89. { "abbr": "de-ch", "name": "German (Switzerland)" },
  90. { "abbr": "el", "name": "Greek" },
  91. { "abbr": "gu", "name": "Gujarati" },
  92. { "abbr": "he", "name": "Hebrew" },
  93. { "abbr": "hi", "name": "Hindi" },
  94. { "abbr": "hu", "name": "Hungarian" },
  95. { "abbr": "is", "name": "Icelandic" },
  96. { "abbr": "id", "name": "Indonesian" },
  97. { "abbr": "ga", "name": "Irish or Irish Gaelic" },
  98. { "abbr": "it", "name": "Italian" },
  99. { "abbr": "it-ch", "name": "Italian (Switzerland)" },
  100. { "abbr": "ja", "name": "Japanese" },
  101. { "abbr": "jv", "name": "Javanese" },
  102. { "abbr": "kn", "name": "Kannada" },
  103. { "abbr": "kk", "name": "Kazakh" },
  104. { "abbr": "tlh", "name": "Klingon" },
  105. { "abbr": "gom-deva", "name": "Konkani Devanagari script" },
  106. { "abbr": "gom-latn", "name": "Konkani Latin script" },
  107. { "abbr": "ko", "name": "Korean" },
  108. { "abbr": "ku", "name": "Kurdish" },
  109. { "abbr": "ky", "name": "Kyrgyz" },
  110. { "abbr": "lo", "name": "Lao" },
  111. { "abbr": "lv", "name": "Latvian" },
  112. { "abbr": "lt", "name": "Lithuanian" },
  113. { "abbr": "lb", "name": "Luxembourgish" },
  114. { "abbr": "mk", "name": "Macedonian" },
  115. { "abbr": "ms-my", "name": "Malay" },
  116. { "abbr": "ms", "name": "Malay" },
  117. { "abbr": "ml", "name": "Malayalam" },
  118. { "abbr": "dv", "name": "Maldivian" },
  119. { "abbr": "mt", "name": "Maltese (Malta)" },
  120. { "abbr": "mi", "name": "Maori" },
  121. { "abbr": "mr", "name": "Marathi" },
  122. { "abbr": "mn", "name": "Mongolian" },
  123. { "abbr": "me", "name": "Montenegrin" },
  124. { "abbr": "ne", "name": "Nepalese" },
  125. { "abbr": "se", "name": "Northern Sami" },
  126. { "abbr": "nb", "name": "Norwegian Bokmål" },
  127. { "abbr": "nn", "name": "Nynorsk" },
  128. { "abbr": "oc-lnc", "name": "Occitan, lengadocian dialecte" },
  129. { "abbr": "fa", "name": "Persian" },
  130. { "abbr": "pl", "name": "Polish" },
  131. { "abbr": "pt", "name": "Portuguese" },
  132. { "abbr": "pt-br", "name": "Portuguese (Brazil)" },
  133. { "abbr": "x-pseudo", "name": "Pseudo" },
  134. { "abbr": "pa-in", "name": "Punjabi (India)" },
  135. { "abbr": "ro", "name": "Romanian" },
  136. { "abbr": "ru", "name": "Russian" },
  137. { "abbr": "gd", "name": "Scottish Gaelic" },
  138. { "abbr": "sr", "name": "Serbian" },
  139. { "abbr": "sr-cyrl", "name": "Serbian Cyrillic" },
  140. { "abbr": "sd", "name": "Sindhi" },
  141. { "abbr": "si", "name": "Sinhalese" },
  142. { "abbr": "sk", "name": "Slovak" },
  143. { "abbr": "sl", "name": "Slovenian" },
  144. { "abbr": "es", "name": "Spanish" },
  145. { "abbr": "es-do", "name": "Spanish (Dominican Republic)" },
  146. { "abbr": "es-mx", "name": "Spanish (Mexico)" },
  147. { "abbr": "es-us", "name": "Spanish (United States)" },
  148. { "abbr": "sw", "name": "Swahili" },
  149. { "abbr": "sv", "name": "Swedish" },
  150. { "abbr": "tl-ph", "name": "Tagalog (Philippines)" },
  151. { "abbr": "tg", "name": "Tajik" },
  152. { "abbr": "tzl", "name": "Talossan" },
  153. { "abbr": "ta", "name": "Tamil" },
  154. { "abbr": "te", "name": "Telugu" },
  155. { "abbr": "tet", "name": "Tetun Dili (East Timor)" },
  156. { "abbr": "th", "name": "Thai" },
  157. { "abbr": "bo", "name": "Tibetan" },
  158. { "abbr": "tr", "name": "Turkish" },
  159. { "abbr": "tk", "name": "Turkmen" },
  160. { "abbr": "uk", "name": "Ukrainian" },
  161. { "abbr": "ur", "name": "Urdu" },
  162. { "abbr": "ug-cn", "name": "Uyghur (China)" },
  163. { "abbr": "uz", "name": "Uzbek" },
  164. { "abbr": "uz-latn", "name": "Uzbek Latin" },
  165. { "abbr": "vi", "name": "Vietnamese" },
  166. { "abbr": "cy", "name": "Welsh" },
  167. { "abbr": "yo", "name": "Yoruba Nigeria" },
  168. { "abbr": "ss", "name": "siSwati" }
  169. ];
  170. const block = document.createElement("span");
  171. block.className = "ghst-time time";
  172.  
  173. function staticTime(tempFormat) {
  174. if (busy) {
  175. return;
  176. }
  177. busy = true;
  178. let selector = typeof tempFormat === "string"
  179. // update existing timestamps
  180. ? ".ghst-time"
  181. // process html elements
  182. : "relative-time, time-ago";
  183. if ($(selector)) {
  184. let indx = 0;
  185. const els = $$(selector);
  186. const len = els.length;
  187.  
  188. // loop with delay to allow user interaction
  189. const loop = () => {
  190. let el, time, node, formatted,
  191. // max number of DOM insertions per loop
  192. max = 0;
  193. while (max < 20 && indx < len) {
  194. if (indx >= len) {
  195. return;
  196. }
  197. el = els[indx];
  198. time = moment(el.getAttribute("datetime") || "");
  199. if (el && time.isValid()) {
  200. if (useUTC === "true") {
  201. time = time.utc();
  202. }
  203. if (tempFormat) {
  204. formatted = time.format(tempFormat);
  205. el.textContent = formatted;
  206. el.title = formatted;
  207. } else {
  208. formatted = time.format(timeFormat);
  209. node = block.cloneNode(true);
  210. node.setAttribute("datetime", time);
  211. node.textContent = formatted;
  212. node.title = formatted;
  213. // el.parentElement may be null sometimes when using browser
  214. // back arrow
  215. if (el.parentElement) {
  216. // replace relative-time/time-ago element
  217. el.parentElement.replaceChild(node, el);
  218. }
  219. }
  220. max++;
  221. }
  222. indx++;
  223. }
  224. if (indx < len) {
  225. requestAnimationFrame(loop);
  226. }
  227. };
  228. loop();
  229. }
  230. busy = false;
  231. }
  232.  
  233. function addPanel() {
  234. const div = document.createElement("div");
  235. GM_addStyle(`
  236. #ghst-settings { opacity:0; visibility:hidden; }
  237. #ghst-settings.ghst-open { position:fixed; z-index:65535; top:0; bottom:0;
  238. left:0; right:0; opacity:1; visibility:visible;
  239. background:rgba(0, 0, 0, .5); }
  240. #ghst-settings-inner { position:fixed; left:50%; top:50%; width:25rem;
  241. transform:translate(-50%,-50%); box-shadow:0 .5rem 1rem #111;
  242. color:#c0c0c0 }
  243. #ghst-settings-inner .boxed-group-inner { height: 255px; }
  244. #ghst-footer { clear:both; border-top:1px solid rgba(68, 68, 68, .3);
  245. padding-top:5px; }
  246. `);
  247. div.id = "ghst-settings";
  248. let options = "";
  249. locales.forEach(loc => {
  250. let sel = loc.abbr === locale ? " selected" : "";
  251. options += `<option value="${loc.abbr}"${sel}>${loc.name}</option>`;
  252. });
  253. div.innerHTML = `
  254. <div id="ghst-settings-inner" class="boxed-group">
  255. <h3>GitHub Static Time Settings</h3>
  256. <div class="boxed-group-inner">
  257. <dl class="form-group flattened">
  258. <dt>
  259. <label for="ghst-locale">Select a locale</label>
  260. </dt>
  261. <dd>
  262. <select id="ghst-locale" class="form-select float-right" value="${locale}">
  263. ${options}
  264. </select>
  265. <br>
  266. </dd>
  267. </dl>
  268. <dl class="form-group flattened">
  269. <dt>
  270. <label for="ghst-utc">Show UTC time (use "z" in format below)</label>
  271. </dt>
  272. <dd>
  273. <div class="form-checkbox">
  274. <input id="ghst-utc" type="checkbox" class="float-right">
  275. </div>
  276. <br>
  277. </dd>
  278. </dl>
  279. <dl class="form-group flattened">
  280. <dt>
  281. <label for="ghst-format">
  282. Set <a href="https://momentjs.com/docs/#/displaying/format/" target="_blank">
  283. MomentJS
  284. </a> format (e.g. "MMMM Do YYYY, h:mm A"):
  285. </label>
  286. </dt>
  287. <dd>
  288. <input id="ghst-format" type="text" class="form-control" value="${timeFormat}"/>
  289. </dd>
  290. </dl>
  291. <div id="ghst-footer">
  292. <button type="button" id="ghst-cancel" class="btn btn-sm float-right">Cancel</button>
  293. <button type="button" id="ghst-save" class="btn btn-sm float-right">Save</button>
  294. </div>
  295. </div>
  296. </div>`;
  297. $("body").appendChild(div);
  298. $("#ghst-utc").checked = useUTC === "true";
  299. on($("#ghst-settings"), "click", closePanel);
  300. on($("body"), "keyup", event => {
  301. if (
  302. event.key === "Escape" &&
  303. $("#ghst-settings").classList.contains("ghst-open")
  304. ) {
  305. closePanel(event);
  306. return false;
  307. } else if (event.key === "Enter" && event.shiftKey) {
  308. closePanel();
  309. update("save");
  310. }
  311. });
  312. on($("#ghst-settings-inner"), "click", event => {
  313. event.stopPropagation();
  314. // event.preventDefault();
  315. });
  316. on($("#ghst-save"), "click", () => {
  317. closePanel();
  318. update("save");
  319. });
  320. on($("#ghst-locale"), "change", update);
  321. on($("#ghst-utc"), "change", update);
  322. on($("#ghst-format"), "change", update);
  323. on($("#ghst-cancel"), "click", closePanel);
  324. }
  325.  
  326. function closePanel(event) {
  327. $("#ghst-settings").classList.remove("ghst-open");
  328. if (event) {
  329. return update("revert");
  330. }
  331. }
  332.  
  333. function update(mode) {
  334. if (mode === "revert") {
  335. $("#ghst-locale").value = locale;
  336. $("#ghst-format").value = timeFormat;
  337. $("#ghst-utc").checked = useUTC === "true";
  338. }
  339. let loc = $("#ghst-locale").value || "en";
  340. let time = $("#ghst-format").value || "LLL";
  341. let utc = $("#ghst-utc").checked ? "true" : "false";
  342. if (mode === "save") {
  343. timeFormat = time;
  344. locale = loc;
  345. useUTC = utc;
  346. GM_setValue("ghst-format", timeFormat);
  347. GM_setValue("ghst-locale", locale);
  348. GM_setValue("ghst-utc", useUTC);
  349. }
  350. moment.locale(loc);
  351. staticTime(time);
  352. return false;
  353. }
  354.  
  355. function init() {
  356. addPanel();
  357. moment.locale(locale);
  358. staticTime();
  359. }
  360.  
  361. // Add GM options
  362. GM_registerMenuCommand("Set GitHub static time format", () => {
  363. $("#ghst-settings").classList.add("ghst-open");
  364. });
  365.  
  366. // repo file list needs additional time to render
  367. document.addEventListener("ghmo:container", () => {
  368. setTimeout(() => {
  369. staticTime();
  370. }, 100);
  371. });
  372. document.addEventListener("ghmo:preview", staticTime);
  373. init();
  374.  
  375. })();