diff --git a/changelog.js b/changelog.js index 2b1f1c8..dfc18db 100644 --- a/changelog.js +++ b/changelog.js @@ -1,7 +1,17 @@ // Version tracking -export const APP_VERSION = '1.0.28'; +export const APP_VERSION = '1.0.29'; export const CHANGELOG = { + '1.0.29': { + date: '2026-05-21', + changes: { + features: [ + 'Show the current time underneath the Day display on the timer screen', + 'Add Time settings tab to show or hide the clock and choose 12-hour or 24-hour format', + 'Add optional session countdown under Wake Up with configurable end time', + ], + }, + }, '1.0.28': { date: '2026-04-02', changes: { diff --git a/index.html b/index.html index 9fa82eb..abb21db 100644 --- a/index.html +++ b/index.html @@ -138,10 +138,13 @@
- - - -
Normal
-
+
+ + - +
Normal
+
+ +
00: Nominations open in 0s - +
+ + +
Timer controls @@ -362,6 +374,7 @@

Settings

+ @@ -472,6 +485,54 @@

Display Settings

+ +
+
+

Clock Display

+ + + +
+
+

Session Countdown

+ + +
+
+
diff --git a/package-lock.json b/package-lock.json index 192ccfe..3920bf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -110,9 +110,9 @@ "license": "BSD-3-Clause" }, "node_modules/@hapi/tlds": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", - "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.6.tgz", + "integrity": "sha512-xdi7A/4NZokvV0ewovme3aUO5kQhW9pQ2YD1hRqZGhhSi5rBv4usHYidVocXSi9eihYsznZxLtAiEYYUL6VBGw==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -137,14 +137,14 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.0.tgz", - "integrity": "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==", + "version": "25.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", + "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sinonjs__fake-timers": { @@ -179,6 +179,19 @@ "dev": true, "license": "MIT" }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aggregate-error": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", @@ -372,23 +385,27 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.4.tgz", - "integrity": "sha512-1wVkUaAO6WyaYtCkcYCOx12ZgpGf9Zif+qXa4n+oYzK558YryKqiL6UWwd5DqiH3VRW0GYhTZQ/vlgJrCoNQlg==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", + "integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "dev": true, "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" } }, "node_modules/axios/node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, "node_modules/balanced-match": { "version": "1.0.2", @@ -530,13 +547,13 @@ } }, "node_modules/boxen/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -577,9 +594,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", - "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", "dev": true, "license": "MIT", "dependencies": { @@ -1120,9 +1137,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.19", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", - "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", "dev": true, "license": "MIT" }, @@ -1400,9 +1417,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "dev": true, "funding": [ { @@ -1443,9 +1460,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "dev": true, "funding": [ { @@ -1674,9 +1691,9 @@ } }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", "dev": true, "license": "MIT", "dependencies": { @@ -1701,6 +1718,20 @@ "node": ">=0.10" } }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/human-signals": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", @@ -1879,9 +1910,9 @@ "license": "MIT" }, "node_modules/joi": { - "version": "18.0.2", - "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", - "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "version": "18.2.1", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.2.1.tgz", + "integrity": "sha512-2/OKlogiESf2Nh3TFCrRjrr9z1DRHeW0I+KReF67+4J0Ns+8hBtHRmoWAZ2OFU6I5+TWLEe6sVlSdXPjHm5UbQ==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1891,7 +1922,7 @@ "@hapi/pinpoint": "^2.0.1", "@hapi/tlds": "^1.1.1", "@hapi/topo": "^6.0.2", - "@standard-schema/spec": "^1.0.0" + "@standard-schema/spec": "^1.1.0" }, "engines": { "node": ">= 20" @@ -1926,9 +1957,9 @@ "license": "ISC" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1993,9 +2024,9 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", "dev": true, "license": "MIT" }, @@ -2364,9 +2395,9 @@ } }, "node_modules/pump": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", - "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", "dev": true, "license": "MIT", "dependencies": { @@ -2527,9 +2558,9 @@ "license": "MIT" }, "node_modules/semver": { - "version": "7.7.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", - "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", "dev": true, "license": "ISC", "bin": { @@ -2671,14 +2702,14 @@ } }, "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" + "object-inspect": "^1.13.4" }, "engines": { "node": ">= 0.4" @@ -2788,9 +2819,9 @@ } }, "node_modules/start-server-and-test": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.3.tgz", - "integrity": "sha512-k4EcbNjeg0odaDkAMlIeDVDByqX9PIgL4tivgP2tES6Zd8o+4pTq/HgbWCyA3VHIoZopB+wGnNPKYGGSByNriQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/start-server-and-test/-/start-server-and-test-2.1.5.tgz", + "integrity": "sha512-A/SbXpgXE25ScSkpLLqvGvVZT0ykN6+AzS8tVqMBCTxbJy2Nwuen59opT+afalK5aS+AuQmZs0EsLwjnuDN+/g==", "dev": true, "license": "MIT", "dependencies": { @@ -2801,7 +2832,7 @@ "execa": "5.1.1", "lazy-ass": "1.6.0", "ps-tree": "1.2.0", - "wait-on": "9.0.3" + "wait-on": "9.0.4" }, "bin": { "server-test": "src/bin/start.js", @@ -3044,9 +3075,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "dev": true, "license": "MIT", "optional": true @@ -3086,6 +3117,7 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", "dev": true, "license": "MIT", "bin": { @@ -3118,15 +3150,15 @@ } }, "node_modules/wait-on": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.3.tgz", - "integrity": "sha512-13zBnyYvFDW1rBvWiJ6Av3ymAaq8EDQuvxZnPIw3g04UqGi4TyoIJABmfJ6zrvKo9yeFQExNkOk7idQbDJcuKA==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", + "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", "dev": true, "license": "MIT", "dependencies": { - "axios": "^1.13.2", - "joi": "^18.0.1", - "lodash": "^4.17.21", + "axios": "^1.13.5", + "joi": "^18.0.2", + "lodash": "^4.17.23", "minimist": "^1.2.8", "rxjs": "^7.8.2" }, @@ -3208,13 +3240,13 @@ } }, "node_modules/widest-line/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" diff --git a/script.js b/script.js index a47e66a..42c7346 100644 --- a/script.js +++ b/script.js @@ -39,6 +39,169 @@ let minutesDisplay, changeHistoryDialog, closeChangeHistoryBtn; +let updateCurrentTimeDisplay = () => {}; +let updateSessionCountdownDisplay = () => {}; + +const ONE_HOUR_MS = 60 * 60 * 1000; +const ONE_MINUTE_MS = 60 * 1000; +const SESSION_COUNTDOWN_HOUR_THRESHOLD_MS = 90 * ONE_MINUTE_MS; + +function getSessionEndDate(now, hour, minute) { + const end = new Date(now); + end.setHours(hour, minute, 0, 0); + + if (end <= now) { + if (hour < 12 && now.getHours() >= 12) { + end.setDate(end.getDate() + 1); + } else { + return null; + } + } + + return end; +} + +function formatSessionCountdownText(remainingMs) { + if (remainingMs <= 0) { + return { text: "TIME'S UP!", phase: 'times-up' }; + } + + if (remainingMs >= SESSION_COUNTDOWN_HOUR_THRESHOLD_MS) { + const hours = Math.floor(remainingMs / ONE_HOUR_MS); + return { + text: `${hours} hour${hours === 1 ? '' : 's'} remaining`, + phase: 'hours', + }; + } + + const minutes = Math.max(1, Math.ceil(remainingMs / ONE_MINUTE_MS)); + return { + text: `${minutes} minute${minutes === 1 ? '' : 's'} remaining`, + phase: 'minutes', + }; +} + +function initSessionEndTimeSelectors() { + const hourSelect = document.getElementById('sessionEndHour'); + const minuteSelect = document.getElementById('sessionEndMinute'); + if (!hourSelect || !minuteSelect || hourSelect.options.length > 0) return; + + for (let hour = 0; hour < 24; hour += 1) { + const option = document.createElement('option'); + option.value = hour; + option.textContent = String(hour).padStart(2, '0'); + hourSelect.appendChild(option); + } + + for (let minute = 0; minute < 60; minute += 1) { + const option = document.createElement('option'); + option.value = minute; + option.textContent = String(minute).padStart(2, '0'); + minuteSelect.appendChild(option); + } +} + +function updateTimeSettingsUi() { + const showCurrentTimeInput = document.getElementById('showCurrentTime'); + if (showCurrentTimeInput) { + showCurrentTimeInput.checked = showCurrentTime; + } + + const currentTimeEl = document.getElementById('currentTime'); + if (currentTimeEl) { + currentTimeEl.hidden = !showCurrentTime; + } + + document + .querySelectorAll('label:has(input[name="clockFormat"])') + .forEach((label) => { + label.classList.toggle('inactive', !showCurrentTime); + const input = label.querySelector('input'); + if (input) { + input.disabled = !showCurrentTime; + } + }); + + const showSessionCountdownInput = document.getElementById( + 'showSessionCountdown' + ); + if (showSessionCountdownInput) { + showSessionCountdownInput.checked = showSessionCountdown; + } + + const sessionEndHourSelect = document.getElementById('sessionEndHour'); + const sessionEndMinuteSelect = document.getElementById('sessionEndMinute'); + if (sessionEndHourSelect) { + sessionEndHourSelect.value = String(sessionEndHour); + sessionEndHourSelect.disabled = !showSessionCountdown; + } + if (sessionEndMinuteSelect) { + sessionEndMinuteSelect.value = String(sessionEndMinute); + sessionEndMinuteSelect.disabled = !showSessionCountdown; + } + + const sessionEndTimeLabel = document.getElementById('sessionEndTimeLabel'); + if (sessionEndTimeLabel) { + sessionEndTimeLabel.classList.toggle('inactive', !showSessionCountdown); + } + + const sessionCountdownEl = document.getElementById('sessionCountdown'); + if (sessionCountdownEl) { + sessionCountdownEl.hidden = !showSessionCountdown; + } + + if (showCurrentTime) { + updateCurrentTimeDisplay(); + } + if (showSessionCountdown) { + updateSessionCountdownDisplay(); + } +} + +function initTimeDisplays() { + initSessionEndTimeSelectors(); + + const currentTimeEl = document.getElementById('currentTime'); + const sessionCountdownEl = document.getElementById('sessionCountdown'); + + updateCurrentTimeDisplay = () => { + if (!currentTimeEl) return; + const now = new Date(); + currentTimeEl.dateTime = now.toISOString(); + currentTimeEl.textContent = now.toLocaleTimeString(undefined, { + hour: '2-digit', + minute: '2-digit', + hour12: clockFormat === '12', + }); + }; + + updateSessionCountdownDisplay = () => { + if (!sessionCountdownEl) return; + + const now = new Date(); + const sessionEnd = getSessionEndDate(now, sessionEndHour, sessionEndMinute); + const remainingMs = sessionEnd ? sessionEnd - now : 0; + const { text, phase } = formatSessionCountdownText(remainingMs); + + sessionCountdownEl.textContent = text; + sessionCountdownEl.setAttribute('aria-label', text); + sessionCountdownEl.classList.toggle('final-hour', phase === 'minutes'); + sessionCountdownEl.classList.toggle('times-up', phase === 'times-up'); + }; + + const tickTimeDisplays = () => { + if (showCurrentTime) { + updateCurrentTimeDisplay(); + } + if (showSessionCountdown) { + updateSessionCountdownDisplay(); + } + }; + + tickTimeDisplays(); + setInterval(tickTimeDisplays, 1000); +} + // Utility functions const connectivityUtils = { isOnline: () => navigator.onLine, @@ -400,6 +563,11 @@ let backgroundTheme = 'medieval-cartoon'; // Default background theme let youtubePlaylistUrl = DEFAULT_YOUTUBE_PLAYLIST; // Default playlist let keepDisplayOn = true; // Default to true for wake lock let showPlayerCountQr = false; // Optional QR linking to count.arcane-scripts.net +let showCurrentTime = true; // Show wall clock under day display +let clockFormat = '24'; // '12' or '24' +let showSessionCountdown = false; +let sessionEndHour = 23; +let sessionEndMinute = 0; let youtubePlayer = null; let endOfDaySound = 'cathedral-bell-v2.mp3'; // Default end of day sound let wakeUpSoundFile = 'chisel-bell-01-loud-v2.mp3'; // Default wake up sound @@ -695,6 +863,7 @@ document.addEventListener('DOMContentLoaded', async () => { startBtn = document.getElementById('startBtn'); updateStartButtonText(BUTTON_LABELS.WAKE_UP); startBtn.disabled = false; // Ensure Wake Up button is enabled on load + initTimeDisplays(); resetBtn = document.getElementById('resetBtn'); resetBtn.textContent = BUTTON_LABELS.RESET; resetBtn.disabled = true; // Reset button should be disabled initially @@ -899,6 +1068,36 @@ document.addEventListener('DOMContentLoaded', async () => { .classList.toggle('visible', showPlayerCountQr); saveSettings(); }); + document.getElementById('showCurrentTime').addEventListener('change', (e) => { + showCurrentTime = e.target.checked; + saveSettings(); + updateTimeSettingsUi(); + }); + document + .getElementById('showSessionCountdown') + .addEventListener('change', (e) => { + showSessionCountdown = e.target.checked; + saveSettings(); + updateTimeSettingsUi(); + }); + document.getElementById('sessionEndHour').addEventListener('change', (e) => { + sessionEndHour = Number.parseInt(e.target.value, 10); + saveSettings(); + updateSessionCountdownDisplay(); + }); + document.getElementById('sessionEndMinute').addEventListener('change', (e) => { + sessionEndMinute = Number.parseInt(e.target.value, 10); + saveSettings(); + updateSessionCountdownDisplay(); + }); + document.querySelectorAll('input[name="clockFormat"]').forEach((input) => { + input.addEventListener('change', (e) => { + if (!e.target.checked) return; + clockFormat = e.target.value; + saveSettings(); + updateCurrentTimeDisplay(); + }); + }); // Add keyboard shortcuts event listeners document.querySelectorAll('.shortcut-input').forEach((input) => { @@ -1176,6 +1375,22 @@ function applyParsedSettings(settings) { keepDisplayOn = settings.keepDisplayOn === undefined ? true : settings.keepDisplayOn; showPlayerCountQr = settings.showPlayerCountQr === true; + showCurrentTime = + settings.showCurrentTime === undefined ? true : settings.showCurrentTime; + clockFormat = settings.clockFormat === '12' ? '12' : '24'; + showSessionCountdown = settings.showSessionCountdown === true; + sessionEndHour = + Number.isInteger(settings.sessionEndHour) && + settings.sessionEndHour >= 0 && + settings.sessionEndHour <= 23 + ? settings.sessionEndHour + : 23; + sessionEndMinute = + Number.isInteger(settings.sessionEndMinute) && + settings.sessionEndMinute >= 0 && + settings.sessionEndMinute <= 59 + ? settings.sessionEndMinute + : 0; youtubeVolume = settings.youtubeVolume || 15; backgroundTheme = settings.backgroundTheme || 'medieval-cartoon'; youtubePlaylistUrl = settings.youtubePlaylistUrl || DEFAULT_YOUTUBE_PLAYLIST; @@ -1247,6 +1462,10 @@ function applySettingsToForm() { document.getElementById('musicVolume').value = youtubeVolume; document.getElementById('soundEffectsVolume').value = soundEffectsVolume; document.getElementById('backgroundTheme').value = backgroundTheme; + document.querySelector( + `input[name="clockFormat"][value="${clockFormat}"]` + ).checked = true; + updateTimeSettingsUi(); document.querySelector( 'label:has(#musicVolume) .volume-value' ).textContent = `${youtubeVolume}%`; @@ -1423,6 +1642,11 @@ function saveSettings() { soundEffectsVolume, keepDisplayOn, showPlayerCountQr, + showCurrentTime, + clockFormat, + showSessionCountdown, + sessionEndHour, + sessionEndMinute, youtubeVolume, youtubePlaylistUrl, backgroundTheme, diff --git a/styles.css b/styles.css index 713d7c6..d476cb5 100644 --- a/styles.css +++ b/styles.css @@ -478,12 +478,71 @@ html.fonts-ready .timer-display .time { background-color: var(--colour-accent); } -#startBtn { +.timer-actions { position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; +} + +.day-info { + position: absolute; + top: 50%; + left: 0.5rem; + transform: translateY(-50%); + z-index: 2; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.4rem; + width: 120px; +} + +.current-time { + font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace; + font-size: 0.95rem; + font-weight: 500; + font-variant-numeric: tabular-nums; + color: rgba(255, 255, 255, 0.85); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); + line-height: 1; + width: 100%; + text-align: center; +} + +.session-countdown { + font-size: 0.85rem; + font-weight: 600; + font-variant-numeric: tabular-nums; + color: rgba(255, 255, 255, 0.85); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.4); + line-height: 1.2; + min-width: 140px; + text-align: center; +} + +.session-countdown[hidden] { + display: none; +} + +.session-countdown.final-hour { + color: var(--colour-gold); +} + +.session-countdown.times-up { + color: #ff6b6b; + font-weight: 700; + letter-spacing: 0.02em; +} + +#startBtn { + position: static; + transform: none; background-color: var(--colour-accent); color: white; padding: 1.5rem 1rem; @@ -547,18 +606,18 @@ html.fonts-ready .timer-display .time { #startBtn:hover { background-color: var(--colour-accent-hover); - transform: translateY(calc(-50% + 1px)); + transform: translateY(1px); } #startBtn:active { background-color: var(--colour-accent-hover); - transform: translateY(calc(-50% + 2px)) scale(0.98); + transform: translateY(2px) scale(0.98); } #startBtn:disabled { background-color: #cccccc; cursor: not-allowed; - transform: translateY(-50%); + transform: none; } #resetBtn { @@ -1132,10 +1191,8 @@ body[data-pace='blitz'] .info-value { } .day-display { - position: absolute; - top: 50%; - left: 0.5rem; - transform: translateY(-50%); + position: static; + transform: none; z-index: 2; font-size: 1.25rem; font-weight: 700; @@ -1689,6 +1746,40 @@ body:has(.clocktower-settings.visible) #regularTimerControls { box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); } +.session-end-time-inputs { + display: flex; + align-items: center; + gap: 0.35rem; +} + +.session-end-select { + min-width: 4.5rem; + padding: 0.5rem; + font-size: 1rem; + font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace; + border: 1px solid #4a4a4a; + border-radius: var(--radius-md); + background-color: var(--colour-button-secondary); + color: white; + cursor: pointer; +} + +.session-end-select:hover { + background-color: var(--colour-button-secondary-hover); +} + +.session-end-select:focus { + outline: none; + border-color: var(--colour-accent); + box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2); +} + +.session-end-separator { + color: rgba(255, 255, 255, 0.8); + font-family: 'Azeret Mono', ui-monospace, 'Cascadia Mono', monospace; + font-weight: 600; +} + .game-pace-display { font-size: 1rem; color: var(--colour-gold); @@ -1913,12 +2004,16 @@ button:disabled { } /* Restore checkbox styling */ -.setting-group input[type='checkbox'] { +.setting-group input[type='checkbox'], +.setting-group input[type='radio'] { width: 24px; height: 24px; accent-color: var(--colour-accent); cursor: pointer; position: relative; +} + +.setting-group input[type='checkbox'] { border-radius: var(--radius-md); }