diff --git a/package-lock.json b/package-lock.json
index fff28cd..264999e 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11846,6 +11846,11 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true
},
+ "spacetime": {
+ "version": "6.12.2",
+ "resolved": "https://registry.npmjs.org/spacetime/-/spacetime-6.12.2.tgz",
+ "integrity": "sha512-w0St4Q9X8KtuZ/JY8+FM8a4hMrAoNNUWQCt9UQQAUzwk8eDW5wrGh4SaNvEg+9cjLF++vixm6SgJyC6F7ALF/A=="
+ },
"sparkles": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/sparkles/-/sparkles-1.0.1.tgz",
diff --git a/package.json b/package.json
index d32c98f..13d85aa 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"homepage": "https://github.com/Serraniel/AniwatchPlus#readme",
"dependencies": {
"color": "^3.1.3",
+ "spacetime": "^6.12.2",
"uuid": "^8.3.2"
},
"devDependencies": {
diff --git a/src/html/settings.html b/src/html/settings.html
index 82d48c8..ff1ea70 100644
--- a/src/html/settings.html
+++ b/src/html/settings.html
@@ -16,6 +16,9 @@
+
+
+
diff --git a/src/javascript/app.ts b/src/javascript/app.ts
index a5966eb..0e714aa 100644
--- a/src/javascript/app.ts
+++ b/src/javascript/app.ts
@@ -9,6 +9,7 @@ import { init as fontColor } from './enhancements/fontColor';
import { init as languageDisplay } from './enhancements/languageDisplay';
import { init as notifications } from './enhancements/notifications';
import { init as quickSearch } from './enhancements/quickSearch';
+import { init as timeConversion } from './enhancements/timeConversion';
import { init as watch2getherChat } from './enhancements/watch2getherChat';
// css
import { init as cssEnhancements } from './enhancements/cssEnhancements';
@@ -26,6 +27,7 @@ fontColor();
languageDisplay();
notifications();
quickSearch();
+timeConversion();
watch2getherChat();
// css
diff --git a/src/javascript/configuration/configuration.ts b/src/javascript/configuration/configuration.ts
index 841301b..2e78d91 100644
--- a/src/javascript/configuration/configuration.ts
+++ b/src/javascript/configuration/configuration.ts
@@ -3,6 +3,7 @@ import { assigned } from "../utils/helpers";
// website
export const SETTINGS_websiteDisplayQuickSearch = 'websiteDisplayQuickSearch';
+export const SETTINGS_websiteAutoTimeConversion = 'websiteAutoTimeConversion';
export const SETTINGS_websiteShowNotificationsCountInTab = 'websiteShowNotificationsCountInTab';
export const SETTINGS_websiteHideUnusedTabs = 'websiteHideUnusedTabs';
export const SETTINGS_websiteOptimizeListAppearance = 'websiteOptimizeListAppearance';
diff --git a/src/javascript/enhancements/timeConversion.js b/src/javascript/enhancements/timeConversion.js
new file mode 100644
index 0000000..a479759
--- /dev/null
+++ b/src/javascript/enhancements/timeConversion.js
@@ -0,0 +1,224 @@
+import spacetime from 'spacetime';
+import { getGlobalConfiguration, SETTINGS_websiteAutoTimeConversion } from '../configuration/configuration';
+import * as core from '../utils/aniwatchCore';
+import * as helper from '../utils/helpers';
+
+const __alteredNodes = [];
+const DAYS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
+
+export function init() {
+ getGlobalConfiguration().getProperty(SETTINGS_websiteAutoTimeConversion, value => {
+ if (value) {
+ // The regexp pattern matches anything except the airing page.
+ // This is because we would have to restructure the complete site to update time data.
+ // Additionally, there is a big hint that all data would be UTC+1
+ core.runAfterLoad(() => {
+ updateTimestamps(document.documentElement);
+ }, "^/(?!airing).*$");
+
+ core.runAfterLocationChange(() => {
+ updateTimestamps(document.documentElement);
+ }, "^/(?!airing).*$");
+
+ core.registerScript(node => {
+ updateTimestamps(node);
+ }, "^/(?!airing).*$");
+ }
+ });
+}
+
+function getSpaceDateTimeFormat(use24Format) {
+ return `${getSpaceDateFormat()} ${getSpaceTimeFormat(use24Format)}`;
+}
+
+function getSpaceTimeFormat(use24Format) {
+ if (use24Format) {
+ return '{time-24}';
+ }
+
+ return '{time}';
+}
+
+function getSpaceDateFormat() {
+ return '{date}. {month-short} {year}';
+}
+
+function tryUpdateDateTime(node) {
+ const REG_DATETIME = /(\d{2}(\/|\.)){2}\d{4} *\d?\d:\d{2}( (AM|PM))?/g;
+ const REG_TIME = /\d?\d:\d{2}/;
+ const REG_AMPM = /\s(am|pm)/i;
+
+ let hits = Array.from(node.textContent.matchAll(REG_DATETIME), match => match[0]);
+
+ if (hits.length === 0) {
+ return false;
+ }
+
+ hits.forEach(hit => {
+ let use24Format = false;
+ let processedStr = hit
+
+ // string must be converted into 12h format
+ if (processedStr.search(REG_AMPM) < 0) {
+ let timeStr = processedStr.match(REG_TIME)[0];
+ let hm = timeStr.split(':');
+ let hour = parseInt(hm[0]);
+
+ if (hour >= 12) {
+ timeStr = timeStr.replace(`${hour}:`, `${hour - 12}:`);
+ timeStr += 'pm';
+ }
+ else {
+ timeStr += 'am';
+ }
+
+ processedStr = processedStr.replace(REG_TIME, timeStr);
+ use24Format = true;
+ }
+
+ // if time has a space before am/pm, this has to be removed for spacetime
+ processedStr = processedStr.replace(REG_AMPM, '$1');
+
+ let datetime = spacetime(processedStr, 'UTC+1', { dmy: true });
+ datetime = datetime.goto(spacetime().tz);
+ let replaceText = datetime.format(getSpaceDateTimeFormat(use24Format));
+
+ node.textContent = node.textContent.replace(hit, replaceText);
+ });
+
+ return true;
+}
+
+function tryUpdateDate(node) {
+ const REG_DATE = /(\d{2}(\/|\.)){2}\d{4}/g;
+
+ let hits = Array.from(node.textContent.matchAll(REG_DATE), match => match[0]);
+
+ if (hits.length === 0) {
+ return false;
+ }
+
+ hits.forEach(hit => {
+ let datetime = spacetime(hit, 'UTC+1', { dmy: true });
+ datetime = datetime.goto(spacetime().tz);
+ let replaceText = datetime.format(getSpaceDateFormat());
+
+ node.textContent = node.textContent.replace(hit, replaceText);
+ });
+
+ return true;
+}
+
+function tryUpdateTime(node) {
+ const REG_TIME = /\d?\d:\d{2}( (AM|PM))?/g;
+ const REG_AMPM = /\s(am|pm)/i;
+
+ let hits = Array.from(node.textContent.matchAll(REG_TIME), match => match[0]);
+
+ if (hits.length === 0) {
+ return false;
+ }
+
+ hits.forEach(hit => {
+ let use24Format = false;
+ let processedStr = hit
+
+ // string must be converted into 12h format
+ if (processedStr.search(REG_AMPM) < 0) {
+ let timeStr = processedStr.match(REG_TIME)[0];
+ let hm = timeStr.split(':');
+ let hour = parseInt(hm[0]);
+
+ if (hour >= 12) {
+ timeStr = timeStr.replace(`${hour}:`, `${hour - 12}:`);
+ timeStr += 'pm';
+ }
+ else {
+ timeStr += 'am';
+ }
+
+ processedStr = processedStr.replace(REG_TIME, timeStr);
+ use24Format = true;
+ }
+
+ // if time has a space before am/pm, this has to be removed for spacetime
+ processedStr = processedStr.replace(REG_AMPM, '$1');
+
+ let datetime = spacetime();
+ datetime = datetime.goto('UTC+1');
+ datetime = datetime.time(processedStr);
+ datetime = datetime.goto(spacetime().tz);
+ let replaceText = datetime.format(getSpaceTimeFormat(use24Format));
+
+ node.textContent = node.textContent.replace(hit, replaceText);
+
+ // SPECIAL CASE: Anime has the day written in broadcast bade. This may be different in another timezone
+ let tzMeta = spacetime().timezone();
+ let originalH = datetime.hour() - tzMeta.current.offset + 1;
+
+ let dOffset = 0;
+ // we moved to next day
+ if (originalH < 0) {
+ dOffset = 1;
+ }
+ // we moved to previous day
+ else if (originalH > 24) {
+ dOffset = -1;
+ }
+
+ // if day changed
+ if (dOffset != 0) {
+ let dayNode = node.parentNode.previousElementSibling;
+ if (helper.assigned(dayNode)) {
+ for (let i = 0; i < DAYS.length; i++) {
+ if (dayNode.textContent.indexOf(DAYS[i]) >= 0) {
+ dayNode.textContent = dayNode.textContent.replace(DAYS[i], DAYS[(i + DAYS.length + dOffset) % DAYS.length]); // add DAYS.length to avoid negative numbers in the modulo operation
+ break;
+ }
+ }
+ }
+ }
+ });
+
+ return true;
+}
+
+function tryUpdateTimeZone(node) {
+ const HINT_UTC = 'UTC+1';
+ if (node.textContent === HINT_UTC) {
+ let tzMeta = spacetime().timezone();
+
+ node.textContent = `${tzMeta.name} (UTC${tzMeta.current.offset >= 0 ? '+' : ''}${tzMeta.current.offset})`;
+ }
+}
+
+function updateTimestamps(node) {
+ let nodes = helper.findTextNodes(node);
+
+ nodes.forEach(node => {
+ // avoid double updates
+ if (__alteredNodes.indexOf(node) >= 0) {
+ return;
+ }
+
+ if (tryUpdateDateTime(node)) {
+ __alteredNodes.push(node);
+ return;
+ }
+
+ if (tryUpdateDate(node)) {
+ __alteredNodes.push(node);
+ return;
+ }
+
+ if (tryUpdateTime(node)) {
+ __alteredNodes.push(node);
+ return;
+ }
+
+ if (tryUpdateTimeZone(node)) {
+ __alteredNodes.push(node);
+ return;
+ }
+ });
+}
\ No newline at end of file
diff --git a/src/javascript/utils/helpers.ts b/src/javascript/utils/helpers.ts
index 12dd315..82a65c5 100644
--- a/src/javascript/utils/helpers.ts
+++ b/src/javascript/utils/helpers.ts
@@ -36,4 +36,19 @@ function handleKeyToggle(event: KeyboardEvent, isPressed: boolean) {
} else if (event.key === 'Control') {
isCtrlPressed = isPressed;
}
+}
+
+export function findTextNodes(baseNode) {
+ if (!assigned(baseNode)) {
+ baseNode = document.documentElement;
+ }
+
+ let walker = document.createTreeWalker(baseNode, NodeFilter.SHOW_TEXT, null, false);
+ let node;
+ let results = [];
+ while (node = walker.nextNode()) {
+ results.push(node);
+ }
+
+ return results;
}
\ No newline at end of file