From 66ae6e10a7e6add6193648290f5dcb9dfec1a4ba Mon Sep 17 00:00:00 2001 From: nutty Date: Mon, 24 Aug 2026 06:45:49 +1000 Subject: [PATCH 01/11] Payload changes YUCK YUCK YUCK! --- twitch-ad-countdown-timer/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/twitch-ad-countdown-timer/script.js b/twitch-ad-countdown-timer/script.js index 7d3b993..bae7b05 100644 --- a/twitch-ad-countdown-timer/script.js +++ b/twitch-ad-countdown-timer/script.js @@ -146,7 +146,7 @@ client.on('Twitch.UpcomingAd', (response) => { ///////////////////// function TwitchAdRun(data) { - const duration = data.length_seconds; + const duration = data.lengthSeconds ?? data.length_seconds; // Unset the upcoming ad countdown warning upcomingAdWarningStartDelay = null; From c1f0b0dbaec7ffc4775cd3d8bc6183b6fb7607ef Mon Sep 17 00:00:00 2001 From: nutty Date: Mon, 24 Aug 2026 07:20:26 +1000 Subject: [PATCH 02/11] YouTube will now use random user colors by default --- multichat-overlay/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 374e95a..e44eba2 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -90,7 +90,7 @@ const furryMode = GetBooleanParam("furryMode", false); //////////////////// const animationSpeed = GetIntParam("animationSpeed", 0.1); -const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", false); +const randomYouTubeColors = GetBooleanParam("randomYouTubeColors", true); const youtubeColor = urlParams.get("youtubeColor") || "#f70000"; const youtubeCustomSubIcon = urlParams.get("youtubeCustomSubIcon") || ""; let twitchUsername = urlParams.get("twitchUsername") || ""; From 60764eea75a5edc47ef613ef1d47372c62a6fd0a Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 10:34:30 +1000 Subject: [PATCH 03/11] Fixed 'TwitchRewardRedemption' for SB v1.1.0a --- horizontal-chat/script.js | 8 ++++---- multichat-overlay/script.js | 10 +++++----- multistream-alerts/script.js | 6 +++--- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index d2170ef..c2730a5 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -782,12 +782,12 @@ async function TwitchRewardRedemption(data) { if (!showTwitchChannelPointRedemptions) return; - let username = data.user_name; - if (data.user_name.toLowerCase() != data.user_login.toLowerCase()) - username = `${data.user_name} (${data.user_login})`; + let username = data.user_name ?? data.user.name; + if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase()) + username = `${username} (${data.user_login ?? data.user.login})`; const rewardName = data.reward.title; const cost = data.reward.cost; - const userInput = data.user_input; + const userInput = data.user_input ?? data.userInput; const channelPointIcon = ``; let message = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index e44eba2..37f267d 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -1154,7 +1154,7 @@ async function TwitchRewardRedemption(data) { if (showAvatar) { // Render avatars - const username = data.user_login; + const username = data.user_login ?? data.user.login; const avatarURL = await GetAvatar(username, 'twitch'); const avatar = new Image(); avatar.src = avatarURL; @@ -1163,12 +1163,12 @@ async function TwitchRewardRedemption(data) { } // Set the text - let username = data.user_name; - if (data.user_name.toLowerCase() != data.user_login.toLowerCase()) - username = `${data.user_name} (${data.user_login})`; + let username = data.user_name ?? data.user.name; + if (username.toLowerCase() != (data.user_login ?? data.user.login).toLowerCase()) + username = `${username} (${data.user_login ?? data.user.login})`; const rewardName = data.reward.title; const cost = data.reward.cost; - const userInput = data.user_input; + const userInput = data.user_input ?? data.userInput; const channelPointIcon = ``; titleDiv.innerHTML = `${username} redeemed ${rewardName} ${channelPointIcon} ${cost}`; diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 6721483..4d92bae 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -687,14 +687,14 @@ async function TwitchRewardRedemption(data) { if (!showTwitchChannelPointRedemptions) return; - const username = data.user_name; + const username = data.user_name ?? data.user.name; const rewardName = data.reward.title; const cost = data.reward.cost; - const userInput = data.user_input; + const userInput = data.user_input ?? data.userInput; const channelPointIcon = ``; // Render avatars - const avatarURL = await GetAvatar(data.user_login, 'twitch'); + const avatarURL = await GetAvatar(data.user_login ?? data.user.login, 'twitch'); UpdateAlertBox( 'twitch', From 26bc7bc2c3eb74b27ee488cc9f803c451dcef8b2 Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 11:30:34 +1000 Subject: [PATCH 04/11] Updated SMTC Bridge version v0.0.4 -> v0.0.6 --- now-playing/script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/now-playing/script.js b/now-playing/script.js index f910ced..ece5b44 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -9,7 +9,7 @@ const urlParams = new URLSearchParams(queryString); // CONSTANTS // //////////////// -const REQUIRED_VERSION = '0.0.4'; +const REQUIRED_VERSION = '0.0.6'; const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases'; let VersionChecked = false; From e355275cc27bb952b336af0b424be38548df72de Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 11:52:57 +1000 Subject: [PATCH 05/11] Added a "Show While Paused" option --- now-playing/script.js | 11 ++++++----- now-playing/settings/settings.json | 24 ++++++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/now-playing/script.js b/now-playing/script.js index ece5b44..f52886c 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -36,6 +36,8 @@ const useCustomColors = GetBooleanParam("useCustomColors", false); const color1 = urlParams.get('color1') || '#ffffff'; const color2 = urlParams.get('color2') || '#1d1d1d'; +const autoHide = GetBooleanParam("autoHide", false); +const showWhilePaused = GetBooleanParam("showWhilePaused", false); const includedApplications = urlParams.get('includedApplications') || ''; const excludedApplications = urlParams.get('excludedApplications') || ''; const showAlbumArt = GetBooleanParam("showAlbumArt", true); @@ -43,7 +45,6 @@ const showProgressBar = GetBooleanParam("showProgressBar", true); const swapArtistTrack = GetBooleanParam("swapArtistTrack", false); const showPrimary = GetBooleanParam("showPrimary", true); const showSecondary = GetBooleanParam("showSecondary", true); -const autoHide = GetBooleanParam("autoHide", false); const displayDuration = GetIntParam("displayDuration", 5); const showAnimation = urlParams.get('showAnimation') || 'slide-in-from-bottom'; const hideAnimation = urlParams.get('hideAnimation') || 'slide-out-bottom'; @@ -363,7 +364,7 @@ async function UpdatePlayerState(data) { for (const targetApp of includedList) { targetSession = validSessions.find(s => { const matchesApp = (s.source_app_id || "").toLowerCase().includes(targetApp); - const isPlaying = s.playback_info && s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING; + const isPlaying = s.playback_info && (s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && s.playback_info.PlaybackStatus === PlaybackStatus.PAUSED)); return matchesApp && isPlaying; }); if (targetSession) break; @@ -387,7 +388,7 @@ async function UpdatePlayerState(data) { // Priority 2: If the current session isn't available/valid, find any session that is currently playing if (!targetSession || targetSession.playback_info.PlaybackStatus !== PlaybackStatus.PLAYING) { - const playingSession = validSessions.find(s => s.playback_info && s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING); + const playingSession = validSessions.find(s => s.playback_info && (s.playback_info.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && s.playback_info.PlaybackStatus === PlaybackStatus.PAUSED))); if (playingSession) { targetSession = playingSession; } @@ -411,7 +412,7 @@ async function UpdatePlayerState(data) { // 1. Check if playback status has changed and update visibility accordingly if (playbackInfo.PlaybackStatus !== CurrentPlaybackStatus) { - if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING) + if (playbackInfo.PlaybackStatus === PlaybackStatus.PLAYING || (showWhilePaused && playbackInfo.PlaybackStatus === PlaybackStatus.PAUSED)) SetVisibility(true); else SetVisibility(false); @@ -421,7 +422,7 @@ async function UpdatePlayerState(data) { // 2. Check if the track name/artist have changed - this is our indicator that the next track has loaded // Only proceed if the player state is actively playing audio - if (CurrentPlaybackStatus == PlaybackStatus.PLAYING) { + if (CurrentPlaybackStatus == PlaybackStatus.PLAYING || (showWhilePaused && CurrentPlaybackStatus == PlaybackStatus.PAUSED)) { const newTrackKey = `${mediaProps.Title}-${mediaProps.Artist}-${mediaProps.Thumbnail}`; if (newTrackKey !== CurrentSongKey) { ChangeTrack(mediaProps, accentColorPalette); // Now trigger your cross-fade logic here! diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json index ee532ca..fa70ba5 100644 --- a/now-playing/settings/settings.json +++ b/now-playing/settings/settings.json @@ -150,6 +150,22 @@ "showIf": "useCustomColors", "group": "Appearance" }, + { + "id": "autoHide", + "label": "Auto-Hide on Track Change", + "description": "If enabled, the widget will show for a set duration whenever a new track starts.", + "type": "checkbox", + "defaultValue": false, + "group": "General" + }, + { + "id": "showWhilePaused", + "label": "Show While Paused", + "description": "If enabled, the widget will be visible even when the track is paused.", + "type": "checkbox", + "defaultValue": false, + "group": "General" + }, { "id": "includedApplications", "label": "Included Apps", @@ -208,14 +224,6 @@ "defaultValue": true, "group": "General" }, - { - "id": "autoHide", - "label": "Auto-Hide on Track Change", - "description": "If enabled, the widget will show for a set duration whenever a new track starts.", - "type": "checkbox", - "defaultValue": false, - "group": "General" - }, { "id": "displayDuration", "label": "Display Duration (seconds)", From 41a66d8c63217a33b64fff7599c32b4479778f8b Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 14:23:21 +1000 Subject: [PATCH 06/11] Added 'Show While Paused' option to all themes --- now-playing/script.js | 2 +- now-playing/settings/settings.json | 32 +++++------ now-playing/style.css | 8 +-- now-playing/themes/album-art/index.html | 5 ++ now-playing/themes/album-art/script.js | 11 +++- now-playing/themes/album-art/style.css | 30 ++++++++++ now-playing/themes/card/index.html | 5 ++ now-playing/themes/card/script.js | 8 ++- now-playing/themes/card/style.css | 30 ++++++++++ now-playing/themes/classic/index.html | 5 ++ now-playing/themes/classic/script.js | 8 ++- now-playing/themes/classic/style.css | 31 +++++++++++ now-playing/themes/color-palette/script.js | 2 +- now-playing/themes/compact/script.js | 8 ++- now-playing/themes/compact/style.css | 4 ++ now-playing/themes/simple/index.html | 5 ++ now-playing/themes/simple/script.js | 12 +++- now-playing/themes/simple/style.css | 31 +++++++++++ now-playing/themes/standard/index.html | 5 ++ now-playing/themes/standard/script.js | 16 ++++-- now-playing/themes/standard/style.css | 30 ++++++++++ now-playing/themes/vinyl/script.js | 51 ++++++++++++++++- now-playing/themes/vinyl/style.css | 65 ++++++++++++++++++++-- 23 files changed, 364 insertions(+), 40 deletions(-) diff --git a/now-playing/script.js b/now-playing/script.js index f52886c..f680e7a 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -443,7 +443,7 @@ async function UpdatePlayerState(data) { const isPlaying = (targetSession.playback_info.PlaybackStatus === PlaybackStatus.PLAYING); const currentPositionMs = isPlaying && timelineProps.EndTime > 0 ? timelineProps.Position + driftMs : timelineProps.Position; - SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette); + SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, targetSession.playback_info.PlaybackStatus); } } else { diff --git a/now-playing/settings/settings.json b/now-playing/settings/settings.json index fa70ba5..39a677f 100644 --- a/now-playing/settings/settings.json +++ b/now-playing/settings/settings.json @@ -150,6 +150,14 @@ "showIf": "useCustomColors", "group": "Appearance" }, + { + "id": "showWhilePaused", + "label": "Show While Paused", + "description": "If enabled, the widget will be visible even when the track is paused.", + "type": "checkbox", + "defaultValue": false, + "group": "General" + }, { "id": "autoHide", "label": "Auto-Hide on Track Change", @@ -159,11 +167,14 @@ "group": "General" }, { - "id": "showWhilePaused", - "label": "Show While Paused", - "description": "If enabled, the widget will be visible even when the track is paused.", - "type": "checkbox", - "defaultValue": false, + "id": "displayDuration", + "label": "Display Duration (seconds)", + "description": "How long the widget should remain visible.", + "type": "number", + "min": 1, + "max": 60, + "defaultValue": 5, + "showIf": "autoHide", "group": "General" }, { @@ -224,17 +235,6 @@ "defaultValue": true, "group": "General" }, - { - "id": "displayDuration", - "label": "Display Duration (seconds)", - "description": "How long the widget should remain visible.", - "type": "number", - "min": 1, - "max": 60, - "defaultValue": 5, - "showIf": "autoHide", - "group": "General" - }, { "id": "showAnimation", "label": "Show Animation", diff --git a/now-playing/style.css b/now-playing/style.css index de16278..f33f7eb 100644 --- a/now-playing/style.css +++ b/now-playing/style.css @@ -97,7 +97,7 @@ body { @keyframes slide-in-from-left { 0% { - transform: translateX(-1em); + transform: translateX(-100%); opacity: 0; } @@ -114,14 +114,14 @@ body { } 100% { - transform: translateX(-1em); + transform: translateX(-100%); opacity: 0; } } @keyframes slide-in-from-right { 0% { - transform: translateX(1em); + transform: translateX(100%); opacity: 0; } @@ -138,7 +138,7 @@ body { } 100% { - transform: translateX(1em); + transform: translateX(100%); opacity: 0; } } diff --git a/now-playing/themes/album-art/index.html b/now-playing/themes/album-art/index.html index bfac360..a75a2ec 100644 --- a/now-playing/themes/album-art/index.html +++ b/now-playing/themes/album-art/index.html @@ -3,6 +3,11 @@
+
+ + + +
diff --git a/now-playing/themes/album-art/script.js b/now-playing/themes/album-art/script.js index e5a82e4..7536fc2 100644 --- a/now-playing/themes/album-art/script.js +++ b/now-playing/themes/album-art/script.js @@ -96,7 +96,12 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + albumArtContainer.classList.add('is-paused'); + else + albumArtContainer.classList.remove('is-paused'); // Set progressbar // Ensure we don't divide by zero or exceed 100% @@ -105,7 +110,7 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { progressPercent = Math.min(100, Math.max(0, progressPercent)); progressBar.style.width = `${progressPercent}%`; if (!useCustomColors) - progressBar.style.setProperty('--accent-color', accentColorPalette.LightVibrant); + document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant); else - progressBar.style.setProperty('--accent-color', color1); + document.body.style.setProperty('--accent-color', color1); } \ No newline at end of file diff --git a/now-playing/themes/album-art/style.css b/now-playing/themes/album-art/style.css index bd86dd2..4c156d2 100644 --- a/now-playing/themes/album-art/style.css +++ b/now-playing/themes/album-art/style.css @@ -21,6 +21,36 @@ display: var(--show-album-art); } +#album-art-container.is-paused #album-art-layer, +#album-art-container.is-paused #album-art-transition-layer { + opacity: 0.7; +} + +#pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10; + border-radius: inherit; +} + +#pause-overlay svg { + height: 30%; + width: auto; +} + +#album-art-container.is-paused #pause-overlay { + opacity: 1; +} + #album-art-layer, #album-art-transition-layer { position: absolute; diff --git a/now-playing/themes/card/index.html b/now-playing/themes/card/index.html index 7bcf8c2..99e8297 100644 --- a/now-playing/themes/card/index.html +++ b/now-playing/themes/card/index.html @@ -9,6 +9,11 @@
+
+ + + +
 
diff --git a/now-playing/themes/card/script.js b/now-playing/themes/card/script.js index baf4f4c..280bc64 100644 --- a/now-playing/themes/card/script.js +++ b/now-playing/themes/card/script.js @@ -84,13 +84,19 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Update the label using your naming convention currentTimeLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); durationLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); + + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + albumArtContainer.classList.add('is-paused'); + else + albumArtContainer.classList.remove('is-paused'); // Set progressbar // Ensure we don't divide by zero or exceed 100% diff --git a/now-playing/themes/card/style.css b/now-playing/themes/card/style.css index c578d13..f2a8af1 100644 --- a/now-playing/themes/card/style.css +++ b/now-playing/themes/card/style.css @@ -23,6 +23,36 @@ display: var(--show-album-art); } +#album-art-container.is-paused #album-art-layer, +#album-art-container.is-paused #album-art-transition-layer { + opacity: 0.2; +} + +#pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10; + border-radius: inherit; +} + +#pause-overlay svg { + height: 30%; + width: auto; +} + +#album-art-container.is-paused #pause-overlay { + opacity: 0.8; +} + #album-art-layer, #album-art-transition-layer { position: absolute; diff --git a/now-playing/themes/classic/index.html b/now-playing/themes/classic/index.html index 5aab0e6..8d366f3 100644 --- a/now-playing/themes/classic/index.html +++ b/now-playing/themes/classic/index.html @@ -3,6 +3,11 @@
+
+ + + +
diff --git a/now-playing/themes/classic/script.js b/now-playing/themes/classic/script.js index 64b7fd6..2a5c385 100644 --- a/now-playing/themes/classic/script.js +++ b/now-playing/themes/classic/script.js @@ -84,13 +84,19 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Update the label using your naming convention currentTimeLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); durationLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); + + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + albumArtContainer.classList.add('is-paused'); + else + albumArtContainer.classList.remove('is-paused'); // Set progressbar // Ensure we don't divide by zero or exceed 100% diff --git a/now-playing/themes/classic/style.css b/now-playing/themes/classic/style.css index 1223703..8880684 100644 --- a/now-playing/themes/classic/style.css +++ b/now-playing/themes/classic/style.css @@ -24,6 +24,37 @@ flex-shrink: 0; } +#album-art-container.is-paused #album-art-layer, +#album-art-container.is-paused #album-art-transition-layer { + opacity: 0.7; + filter: grayscale(50%); +} + +#pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10; + border-radius: inherit; +} + +#pause-overlay svg { + height: 40%; + width: auto; +} + +#album-art-container.is-paused #pause-overlay { + opacity: 1; +} + #album-art-layer, #album-art-transition-layer { position: absolute; diff --git a/now-playing/themes/color-palette/script.js b/now-playing/themes/color-palette/script.js index bffb9b2..bb194e4 100644 --- a/now-playing/themes/color-palette/script.js +++ b/now-playing/themes/color-palette/script.js @@ -94,7 +94,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { const durationMs = timelineProps.EndTime; let progressPercent = durationMs > 0 ? (currentPositionMs / durationMs) * 100 : 0; diff --git a/now-playing/themes/compact/script.js b/now-playing/themes/compact/script.js index 7945afb..337e363 100644 --- a/now-playing/themes/compact/script.js +++ b/now-playing/themes/compact/script.js @@ -97,7 +97,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Set progressbar // Ensure we don't divide by zero or exceed 100% const durationMs = timelineProps.EndTime; @@ -122,6 +122,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { // const transitionWidth = '1em'; // progressBar.style.maskImage = `linear-gradient(to right, black calc(${clipRight}% - ${transitionWidth}), transparent ${clipRight}%)` // progressBarTrack.style.maskImage = `linear-gradient(to right, transparent calc(${clipRight}% - ${transitionWidth}), black ${clipRight}%)`; + + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + songInfoContainer.classList.add('is-paused'); + else + songInfoContainer.classList.remove('is-paused'); } // MARQUEE LOGIC diff --git a/now-playing/themes/compact/style.css b/now-playing/themes/compact/style.css index 31b651d..f5747ab 100644 --- a/now-playing/themes/compact/style.css +++ b/now-playing/themes/compact/style.css @@ -24,6 +24,10 @@ grid-template-areas: "stack"; } +#song-info-container.is-paused { + opacity: 0.8; +} + /* The content sits on top */ #progress-bar { position: relative; diff --git a/now-playing/themes/simple/index.html b/now-playing/themes/simple/index.html index 5c200da..c810810 100644 --- a/now-playing/themes/simple/index.html +++ b/now-playing/themes/simple/index.html @@ -2,6 +2,11 @@
+
+ + + +
diff --git a/now-playing/themes/simple/script.js b/now-playing/themes/simple/script.js index 5b85975..c775c49 100644 --- a/now-playing/themes/simple/script.js +++ b/now-playing/themes/simple/script.js @@ -78,13 +78,19 @@ async function ChangeTrack(mediaProps) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Update the label using your naming convention currentTimeLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); durationLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); + + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + albumArtContainer.classList.add('is-paused'); + else + albumArtContainer.classList.remove('is-paused'); // Set progressbar // Ensure we don't divide by zero or exceed 100% @@ -95,12 +101,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { if (!useCustomColors) { - progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant); + document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant); document.body.style.color = accentColorPalette.LightVibrant; } else { - progressBarFill.style.setProperty('--accent-color', color1); + document.body.style.setProperty('--accent-color', color1); document.body.style.color = color1; } } diff --git a/now-playing/themes/simple/style.css b/now-playing/themes/simple/style.css index 283f1d7..b1c000e 100644 --- a/now-playing/themes/simple/style.css +++ b/now-playing/themes/simple/style.css @@ -23,6 +23,37 @@ flex-shrink: 0; } +#album-art-container.is-paused #album-art-layer, +#album-art-container.is-paused #album-art-transition-layer { + opacity: 0.7; + filter: grayscale(50%); +} + +#pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10; + border-radius: inherit; +} + +#pause-overlay svg { + height: 40%; + width: auto; +} + +#album-art-container.is-paused #pause-overlay { + opacity: 1; +} + #album-art-layer, #album-art-transition-layer { position: absolute; diff --git a/now-playing/themes/standard/index.html b/now-playing/themes/standard/index.html index bd9b89c..c1ef9b2 100644 --- a/now-playing/themes/standard/index.html +++ b/now-playing/themes/standard/index.html @@ -6,6 +6,11 @@
+
+ + + +
diff --git a/now-playing/themes/standard/script.js b/now-playing/themes/standard/script.js index 0ca049a..2be6e45 100644 --- a/now-playing/themes/standard/script.js +++ b/now-playing/themes/standard/script.js @@ -147,7 +147,7 @@ async function ChangeTrack(mediaProps, accentColorPalette) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Update the label using your naming convention currentTimeLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(currentPositionMs); @@ -155,6 +155,12 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { durationLabel.innerText = ConvertMillisecondsToHoursMinutesSecondsSoItLooksBetterAndNotCringe(timelineProps.EndTime); + // Set the pause overlay visibility based on playback status + if (playbackStatus === PlaybackStatus.PAUSED) + albumArtContainer.classList.add('is-paused'); + else + albumArtContainer.classList.remove('is-paused'); + // Set progressbar // Ensure we don't divide by zero or exceed 100% const durationMs = timelineProps.EndTime; @@ -165,9 +171,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { if (!useCustomColors) { switch (themeVariant) { case "matte": - progressBarFill.style.setProperty('--accent-color', accentColorPalette.DarkVibrant); + document.body.style.setProperty('--accent-color', accentColorPalette.DarkVibrant); break; case "matte-dark": + document.body.style.setProperty('--accent-color', accentColorPalette.LightVibrant); + break; default: progressBarFill.style.setProperty('--accent-color', accentColorPalette.LightVibrant); break; @@ -177,11 +185,11 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { { switch (themeVariant) { case "matte": - progressBarFill.style.setProperty('--accent-color', color2); + document.body.style.setProperty('--accent-color', color2); break; // case "matte-dark": default: - progressBarFill.style.setProperty('--accent-color', color1); + document.body.style.setProperty('--accent-color', color1); break; } } diff --git a/now-playing/themes/standard/style.css b/now-playing/themes/standard/style.css index 90959e8..cfb32eb 100644 --- a/now-playing/themes/standard/style.css +++ b/now-playing/themes/standard/style.css @@ -25,6 +25,36 @@ flex-shrink: 0; } +#album-art-container.is-paused #album-art-layer, +#album-art-container.is-paused #album-art-transition-layer { + opacity: 0.2; +} + +#pause-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + z-index: 10; + border-radius: inherit; +} + +#pause-overlay svg { + height: 50%; + width: auto; +} + +#album-art-container.is-paused #pause-overlay { + opacity: 1; +} + #album-art-layer, #album-art-transition-layer { position: absolute; diff --git a/now-playing/themes/vinyl/script.js b/now-playing/themes/vinyl/script.js index c4d2d48..8171fcb 100644 --- a/now-playing/themes/vinyl/script.js +++ b/now-playing/themes/vinyl/script.js @@ -8,6 +8,7 @@ const albumArtContainer = document.getElementById('album-art-container'); const albumArtLayer = document.getElementById('album-art-layer'); const albumArtTransition = document.getElementById('album-art-transition-layer'); const progressCircle = document.getElementById('progress-pie-circle'); +const vinylContainer = document.getElementById('vinyl-container'); /////////////// // CONSTANTS // @@ -60,7 +61,7 @@ async function ChangeTrack(mediaProps) { }, 250); } -function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { +function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette, playbackStatus) { // Set progressbar // Ensure we don't divide by zero or exceed 100% const durationMs = timelineProps.EndTime; @@ -75,4 +76,52 @@ function SetProgressInfo(timelineProps, currentPositionMs, accentColorPalette) { // 0% progress = 157.1 offset. 100% progress = 0 offset. const offset = circumference - (progressPercent / 100) * circumference; progressCircle.style.strokeDashoffset = offset; + + // Adjust 'Paused' check to match whatever string/value your backend sends + if (playbackStatus === PlaybackStatus.PAUSED) + setVinylPaused(true); + else + setVinylPaused(false); +} + +const vinylAnimation = vinylContainer.animate( + [ + { transform: "rotate(0deg)" }, + { transform: "rotate(360deg)" } + ], + { + duration: 10000, + iterations: Infinity, + easing: "linear" + } +); + +let animationFrame; + +function setVinylPaused(shouldPause) { + cancelAnimationFrame(animationFrame); + + const from = vinylAnimation.playbackRate; + const to = shouldPause ? 0 : 1; + const duration = 700; + const start = performance.now(); + + vinylAnimation.play(); + + function update(now) { + const progress = Math.min((now - start) / duration, 1); + + // Smooth acceleration/deceleration + const eased = progress * progress * (3 - 2 * progress); + + vinylAnimation.playbackRate = from + (to - from) * eased; + + if (progress < 1) { + animationFrame = requestAnimationFrame(update); + } else if (shouldPause) { + vinylAnimation.pause(); + } + } + + animationFrame = requestAnimationFrame(update); } \ No newline at end of file diff --git a/now-playing/themes/vinyl/style.css b/now-playing/themes/vinyl/style.css index cce72c6..991e3c1 100644 --- a/now-playing/themes/vinyl/style.css +++ b/now-playing/themes/vinyl/style.css @@ -16,7 +16,7 @@ justify-content: center; width: 100%; filter: drop-shadow(0px 10px 15px rgba(0, 0, 0, 0.5)); - animation: spin 10s linear infinite; + transition: transform 1.2s cubic-bezier(0.25, 1, 0.5, 1); } #vinyl-background { @@ -91,7 +91,64 @@ transition: all 1s linear; } -@keyframes spin { - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } + + +@keyframes slide-in-from-top { + 0% { + transform: translateY(-100%); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slide-in-from-top { + 0% { + transform: translateY(-100%); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slide-out-top { + 0% { + transform: translateY(0); + opacity: 1; + } + + 100% { + transform: translateY(-100%); + opacity: 0; + } +} + +@keyframes slide-in-from-bottom { + 0% { + transform: translateY(100%); + opacity: 0; + } + + 100% { + transform: translateY(0); + opacity: 1; + } +} + +@keyframes slide-out-bottom { + 0% { + transform: translateY(0); + opacity: 1; + } + + 100% { + transform: translateY(100%); + opacity: 0; + } } \ No newline at end of file From d44e6d31f5eddf08a647c89ae60b2ee1ec06bc9e Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 14:51:57 +1000 Subject: [PATCH 07/11] Added "Skip Fourthwall Free Orders" setting --- horizontal-chat/script.js | 5 +++++ horizontal-chat/settings/settings.json | 9 +++++++++ multichat-overlay/script.js | 5 +++++ multichat-overlay/settings/settings.json | 9 +++++++++ multistream-alerts/script.js | 5 +++++ multistream-alerts/settings/settings.json | 9 +++++++++ 6 files changed, 42 insertions(+) diff --git a/horizontal-chat/script.js b/horizontal-chat/script.js index c2730a5..23ef007 100644 --- a/horizontal-chat/script.js +++ b/horizontal-chat/script.js @@ -75,6 +75,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true); const showKofiDonations = GetBooleanParam("showKofiDonations", true); const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true); +const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true); const furryMode = GetBooleanParam("furryMode", false); @@ -1198,6 +1199,10 @@ function FourthwallOrderPlaced(data) { const item = data.variants[0].name; const itemsOrdered = data.variants.length; + // Skip free orders if the user has chosen to do so + if (skipFourthwallFreeOrders && orderTotal == 0) + return; + let message = ""; // If there user did not provide a username, just say "Someone" diff --git a/horizontal-chat/settings/settings.json b/horizontal-chat/settings/settings.json index 7ecd4b6..a99b145 100644 --- a/horizontal-chat/settings/settings.json +++ b/horizontal-chat/settings/settings.json @@ -379,6 +379,15 @@ "defaultValue": true, "group": "Which donation messages do you want to see?" }, + { + "id": "skipFourthwallFreeOrders", + "label": "Skip Free Orders", + "description": "If enabled, free orders from Fourthwall will be skipped.", + "type": "checkbox", + "defaultValue": true, + "showIf": "showFourthwallAlerts", + "group": "Which donation messages do you want to see?" + }, { "id": "furryMode", "label": "Furry Mode", diff --git a/multichat-overlay/script.js b/multichat-overlay/script.js index 37f267d..b731db9 100644 --- a/multichat-overlay/script.js +++ b/multichat-overlay/script.js @@ -82,6 +82,7 @@ const showPatreonMemberships = GetBooleanParam("showPatreonMemberships", true); const showKofiDonations = GetBooleanParam("showKofiDonations", true); const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", true); const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", true); +const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true); const furryMode = GetBooleanParam("furryMode", false); @@ -2045,6 +2046,10 @@ function FourthwallOrderPlaced(data) { const itemImageUrl = data.variants[0].image; const fourthwallProductImage = ``; + // Skip free orders if the user has chosen to do so + if (skipFourthwallFreeOrders && orderTotal == 0) + return; + avatarDiv.innerHTML = fourthwallProductImage; let contents = ""; diff --git a/multichat-overlay/settings/settings.json b/multichat-overlay/settings/settings.json index 3c73b97..fee2d12 100644 --- a/multichat-overlay/settings/settings.json +++ b/multichat-overlay/settings/settings.json @@ -517,6 +517,15 @@ "defaultValue": true, "group": "Which donation messages do you want to see?" }, + { + "id": "skipFourthwallFreeOrders", + "label": "Skip Free Orders", + "description": "If enabled, free orders from Fourthwall will be skipped.", + "type": "checkbox", + "defaultValue": true, + "showIf": "showFourthwallAlerts", + "group": "Which donation messages do you want to see?" + }, { "id": "furryMode", "label": "Furry Mode", diff --git a/multistream-alerts/script.js b/multistream-alerts/script.js index 4d92bae..7638a4a 100644 --- a/multistream-alerts/script.js +++ b/multistream-alerts/script.js @@ -112,6 +112,7 @@ const showTipeeeStreamDonations = GetBooleanParam("showTipeeeStreamDonations", f const tipeeestreamDonationAction = urlParams.get("tipeeestreamDonationAction") || ""; const showFourthwallAlerts = GetBooleanParam("showFourthwallAlerts", false); const fourthwallAlertAction = urlParams.get("fourthwallAlertAction") || ""; +const skipFourthwallFreeOrders = GetBooleanParam("skipFourthwallFreeOrders", true); //////////////////// // HIDDEN OPTIONS // @@ -1124,6 +1125,10 @@ function FourthwallOrderPlaced(data) { const message = DecodeHTMLString(data.statmessageus); const itemImageUrl = data.variants[0].image; + // Skip free orders if the user has chosen to do so + if (skipFourthwallFreeOrders && orderTotal == 0) + return; + // If there user did not provide a username, just say "Someone" if (user == undefined) user = "Someone"; diff --git a/multistream-alerts/settings/settings.json b/multistream-alerts/settings/settings.json index 49bd7f9..ec19143 100644 --- a/multistream-alerts/settings/settings.json +++ b/multistream-alerts/settings/settings.json @@ -628,6 +628,15 @@ "showIf": "showFourthwallAlerts", "group": "Which donation alerts do you want to see?" }, + { + "id": "skipFourthwallFreeOrders", + "label": "Skip Free Orders", + "description": "If enabled, free orders from Fourthwall will be skipped.", + "type": "checkbox", + "defaultValue": true, + "showIf": "showFourthwallAlerts", + "group": "Which donation alerts do you want to see?" + }, { "id": "address", "label": "IP Address", From 121c351416f2b67bb8c3c6b38105180b10a648b1 Mon Sep 17 00:00:00 2001 From: nutty Date: Thu, 27 Aug 2026 15:09:34 +1000 Subject: [PATCH 08/11] Sanitized custom message fields --- .common/utils/helpers.js | 53 ++++++++++++++++++++++++++++++++++ printer-bot/contents/script.js | 16 +++++----- 2 files changed, 61 insertions(+), 8 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index d7c4b99..77d6ca5 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -709,4 +709,57 @@ function VersionCheck(requiredVersion, installedVersion) { // 4. PERFECT: Installed version meets or exceeds the required baseline. console.log(`VersionCheck: Installed version meets or exceeds the required baseline. Required: ${requiredVersion}, Installed: ${installedVersion}`); return 'compatible'; +} + +function SanitizeHTML(htmlString) { + // 1. Parse the string into a temporary DOM document + const parser = new DOMParser(); + const doc = parser.parseFromString(htmlString, 'text/html'); + + // 2. Define allowed tags and attributes + const allowedTags = ['IMG', 'SPAN', 'B', 'I', 'BR', 'EM', 'STRONG']; + const allowedAttrs = ['src', 'class']; + + // 3. Walk through all elements in the parsed document + const allElements = doc.body.querySelectorAll('*'); + allElements.forEach(el => { + // Check if it's an tag and enforce class restrictions + if (el.tagName === 'IMG') { + const className = el.getAttribute('class') || ''; + // Split classes by whitespace in case there are multiple (e.g., class="emote custom") + const classes = className.split(/\s+/); + + // Must contain either 'emote' or 'bits' to be allowed + const hasValidClass = classes.includes('emote') || classes.includes('bits'); + + if (!hasValidClass) { + // Unwrap or remove the unauthorized tag + const parent = el.parentNode; + while (el.firstChild) { + parent.insertBefore(el.firstChild, el); + } + parent.removeChild(el); + return; + } + } + + // If any other tag isn't in our allowed list, unwrap it + if (!allowedTags.includes(el.tagName)) { + const parent = el.parentNode; + while (el.firstChild) { + parent.insertBefore(el.firstChild, el); + } + parent.removeChild(el); + return; + } + + // Strip out any attributes not explicitly allowed (like onerror, onload, etc.) + Array.from(el.attributes).forEach(attr => { + if (!allowedAttrs.includes(attr.name.toLowerCase())) { + el.removeAttribute(attr.name); + } + }); + }); + + return doc.body.innerHTML; } \ No newline at end of file diff --git a/printer-bot/contents/script.js b/printer-bot/contents/script.js index 5fa6563..b705cfb 100644 --- a/printer-bot/contents/script.js +++ b/printer-bot/contents/script.js @@ -51,7 +51,7 @@ async function CustomEvent(data) { subtitleEl.innerText = `${data.user}`; const messageEl = document.createElement('div'); - messageEl.innerHTML = data.message; + messageEl.innerHTML = SanitizeHTML(data.message); // Render emotes for (i in data.emotes) { @@ -116,7 +116,7 @@ async function CustomEvent(data) { const messageEl = document.createElement('div'); messageEl.innerHTML = `${data.cumulative} months${data.monthStreak > 1 ? ' (' + data.monthStreak + ' in a row!)' : ''}`; if (data.messageStripped) - messageEl.innerHTML += `

${data.messageStripped}`; + messageEl.innerHTML += `

${SanitizeHTML(data.messageStripped)}`; contentEl.appendChild(messageEl); @@ -196,7 +196,7 @@ async function CustomEvent(data) { subtitleEl.innerText = `${data.user}`; const messageEl = document.createElement('div'); - messageEl.innerHTML = data.rawInput; + messageEl.innerHTML = SanitizeHTML(data.rawInput); // // Render emotes // for (i in data.emotes) { @@ -284,7 +284,7 @@ async function CustomEvent(data) { const messageEl = document.createElement('div'); messageEl.innerHTML = `${data.user}
sent a Super Chat!`; if (data.message) - messageEl.innerHTML += `

${data.message}`; + messageEl.innerHTML += `

${SanitizeHTML(data.message)}`; contentEl.appendChild(messageEl); @@ -424,7 +424,7 @@ async function CustomEvent(data) { if (data.tipMessage) { const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.tipMessage}`; + messageEl.innerHTML = `${SanitizeHTML(data.tipMessage)}`; contentEl.appendChild(messageEl); } @@ -448,7 +448,7 @@ async function CustomEvent(data) { if (data.donationMessage) { const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data.donationMessage}`; + messageEl.innerHTML = `${SanitizeHTML(data.donationMessage)}`; contentEl.appendChild(messageEl); } @@ -475,7 +475,7 @@ async function CustomEvent(data) { if (data["fw.message"]) { const messageEl = document.createElement('div'); - messageEl.innerHTML = `${data["fw.message"]}`; + messageEl.innerHTML = `${SanitizeHTML(data["fw.message"])}`; contentEl.appendChild(messageEl); } @@ -537,7 +537,7 @@ async function CustomEvent(data) { if (customMessage) { const txt = document.createElement("textarea"); txt.innerHTML = customMessage; - customMessageEl.innerHTML += `
${txt.value}`; + customMessageEl.innerHTML += `
${SanitizeHTML(txt.value)}`; } // Add a cute thank you message because you're uwu like that From acc187d0726bb403a64924cb25a5657138e15d7f Mon Sep 17 00:00:00 2001 From: nutty Date: Sat, 29 Aug 2026 12:11:04 +1000 Subject: [PATCH 09/11] =?UTF-8?q?=E2=80=A2=20Added=20language=20support=20?= =?UTF-8?q?=E2=80=A2=20Added=20timezone=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .common/core/settings-core/script.js | 44 +++++++++++++++++++++- clocko/script.js | 55 ++++++++++++++++++---------- clocko/settings/settings.json | 49 +++++++++++++++++++++++++ 3 files changed, 128 insertions(+), 20 deletions(-) diff --git a/.common/core/settings-core/script.js b/.common/core/settings-core/script.js index f0f0e8c..ab4af18 100644 --- a/.common/core/settings-core/script.js +++ b/.common/core/settings-core/script.js @@ -188,6 +188,21 @@ function LoadJSON(settingsJson) { await PopulateFontDatalist(); }, { once: true }); break; + case 'timezone': + inputElement = document.createElement('input'); + inputElement.type = 'text'; + inputElement.placeholder = 'Type to search timezone...'; + inputElement.id = setting.id; + inputElement.value = settingsMap.has(setting.id) ? settingsMap.get(setting.id) : setting.defaultValue; + inputElement.setAttribute('list', 'timezones'); + inputElement.autocomplete = 'off'; + + // Populate datalist on focus (runs once) + inputElement.addEventListener('focus', function loadOnce() { + inputElement.removeEventListener('focus', loadOnce); + PopulateTimezoneDatalist(); + }, { once: true }); + break; case 'button': inputElement = document.createElement('button'); inputElement.id = setting.id; //Added setting ID @@ -431,6 +446,30 @@ async function PopulateFontDatalist() { } } +function PopulateTimezoneDatalist() { + try { + // Fetch all official IANA timezones natively from the browser + const timezones = Intl.supportedValuesOf('timeZone'); + + // Create the datalist element + const datalistElement = document.createElement('datalist'); + datalistElement.id = 'timezones'; + + // Append each timezone as an option + timezones.forEach(tz => { + const option = document.createElement('option'); + option.value = tz; + datalistElement.appendChild(option); + }); + + document.body.appendChild(datalistElement); + console.debug(`Loaded ${timezones.length} timezones into auto-suggest.`); + + } catch (err) { + console.error("Error generating timezone suggestions:", err); + } +} + ///////////////////////// @@ -599,4 +638,7 @@ LoadSettingsFromStorage(); LoadJSON(settingsJson); // Populate local fonts for auto-suggest -PopulateFontDatalist(); \ No newline at end of file +PopulateFontDatalist(); + +// Populate timezones for auto-suggest +PopulateTimezoneDatalist(); diff --git a/clocko/script.js b/clocko/script.js index 81bbd89..ae77c2a 100644 --- a/clocko/script.js +++ b/clocko/script.js @@ -27,6 +27,8 @@ const line3 = document.getElementById('line3'); ///////////// const font = urlParams.get("font") || ""; +const language = urlParams.get("language") || "en"; +const timezone = urlParams.get("timezone") || ""; const enableLine1 = GetBooleanParam("enableLine1", true); const line1Format = urlParams.get("line1Format") || "hh:mm:ss A"; @@ -63,7 +65,7 @@ const line3TextAlignment = urlParams.get("line3TextAlignment") || "center"; // Set the font for the entire page if specified if (font) - document.body.style.fontFamily = `'${font}'`; + document.body.style.fontFamily = `'${font}'`; // Hide lines that are not enabled if (!enableLine1) @@ -75,38 +77,53 @@ if (!enableLine3) -//////////// +//////////////// // CLOCKO // -//////////// +//////////////// -// Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS') -const usesMilliseconds = - (enableLine1 && line1Format.includes('S')) || - (enableLine2 && line2Format.includes('S')) || - (enableLine3 && line3Format.includes('S')); +// Helper function to start the clock loop +function StartClock() { + // Check if any active format string includes milliseconds (e.g., 'S', 'SS', 'SSS') + const usesMilliseconds = + (enableLine1 && line1Format.includes('S')) || + (enableLine2 && line2Format.includes('S')) || + (enableLine3 && line3Format.includes('S')); -UpdateTime(); + UpdateTime(); -if (usesMilliseconds) { - // High-frequency updates for millisecond precision - function updateFrame() { - UpdateTime(); + if (usesMilliseconds) { + function updateFrame() { + UpdateTime(); + requestAnimationFrame(updateFrame); + } requestAnimationFrame(updateFrame); + } else { + setInterval(UpdateTime, 1000); } - requestAnimationFrame(updateFrame); -} else { - // Efficient 1-second interval for standard clocks - setInterval(UpdateTime, 1000); } function UpdateTime() { - const now = dayjs().tz(dayjs.tz.guess()); - + const activeTimezone = timezone ? timezone : dayjs.tz.guess(); + const now = dayjs().tz(activeTimezone); + if (enableLine1) line1.textContent = now.format(line1Format); if (enableLine2) line2.textContent = now.format(line2Format); if (enableLine3) line3.textContent = now.format(line3Format); } +// Set the language and start the clock only after it's loaded +const script = document.createElement('script'); +script.src = `https://cdn.jsdelivr.net/npm/dayjs@1/locale/${language}.js`; +script.onload = () => { + dayjs.locale(language); + StartClock(); +}; +script.onerror = () => { + console.warn(`Locale '${language}' failed to load. Falling back to English.`); + dayjs.locale('en'); + StartClock(); +}; +document.head.appendChild(script); ///////////// diff --git a/clocko/settings/settings.json b/clocko/settings/settings.json index de49bb5..d47db6e 100644 --- a/clocko/settings/settings.json +++ b/clocko/settings/settings.json @@ -1,5 +1,54 @@ { "settings": [ + { + "id": "language", + "label": "Language", + "description": "", + "type": "select", + "options": [ + { "value": "en", "label": "English" }, + { "value": "ar", "label": "العربية (Arabic)" }, + { "value": "bg", "label": "Български (Bulgarian)" }, + { "value": "zh-cn", "label": "中文 (Chinese Simplified)" }, + { "value": "zh-tw", "label": "中文 (Chinese Traditional)" }, + { "value": "cs", "label": "Čeština (Czech)" }, + { "value": "da", "label": "Dansk (Danish)" }, + { "value": "nl", "label": "Nederlands (Dutch)" }, + { "value": "fi", "label": "Suomi (Finnish)" }, + { "value": "fr", "label": "Français (French)" }, + { "value": "de", "label": "Deutsch (German)" }, + { "value": "el", "label": "Ελληνικά (Greek)" }, + { "value": "he", "label": "עברית (Hebrew)" }, + { "value": "hi", "label": "हिन्दी (Hindi)" }, + { "value": "hu", "label": "Magyar (Hungarian)" }, + { "value": "id", "label": "Bahasa Indonesia (Indonesian)" }, + { "value": "it", "label": "Italiano (Italian)" }, + { "value": "ja", "label": "日本語 (Japanese)" }, + { "value": "ko", "label": "한국어 (Korean)" }, + { "value": "nb", "label": "Norsk bokmål (Norwegian Bokmål)" }, + { "value": "pl", "label": "Polski (Polish)" }, + { "value": "pt", "label": "Português (Portuguese)" }, + { "value": "ro", "label": "Română (Romanian)" }, + { "value": "ru", "label": "Русский (Russian)" }, + { "value": "sk", "label": "Slovenčina (Slovak)" }, + { "value": "es", "label": "Español (Spanish)" }, + { "value": "sv", "label": "Svenska (Swedish)" }, + { "value": "th", "label": "ไทย (Thai)" }, + { "value": "tr", "label": "Türkçe (Turkish)" }, + { "value": "uk", "label": "Українська (Ukrainian)" }, + { "value": "vi", "label": "Tiếng Việt (Vietnamese)" } + ], + "defaultValue": "en", + "group": "General" + }, + { + "id": "timezone", + "label": "Timezone", + "description": "Enter an IANA timezone (e.g., America/New_York, Europe/London). Leave blank to use local time.", + "type": "timezone", + "defaultValue": "", + "group": "General" + }, { "id": "font", "label": "Font", From f7b703007fb19c649fd48fc609fdce098cb9b34d Mon Sep 17 00:00:00 2001 From: nutty Date: Sat, 29 Aug 2026 12:19:08 +1000 Subject: [PATCH 10/11] Made "Text Alignment" a global setting instead of a per line setting --- clocko/script.js | 15 +++++----- clocko/settings/settings.json | 55 +++++++++-------------------------- 2 files changed, 20 insertions(+), 50 deletions(-) diff --git a/clocko/script.js b/clocko/script.js index ae77c2a..9f23f87 100644 --- a/clocko/script.js +++ b/clocko/script.js @@ -26,10 +26,12 @@ const line3 = document.getElementById('line3'); // OPTIONS // ///////////// -const font = urlParams.get("font") || ""; const language = urlParams.get("language") || "en"; const timezone = urlParams.get("timezone") || ""; +const font = urlParams.get("font") || ""; +const textAlignment = urlParams.get("textAlignment") || "center"; + const enableLine1 = GetBooleanParam("enableLine1", true); const line1Format = urlParams.get("line1Format") || "hh:mm:ss A"; const line1FontSize = GetIntParam("line1FontSize", 50); @@ -37,7 +39,6 @@ const line1FontWeight = urlParams.get("line1FontWeight") || "700"; const line1FontColor = urlParams.get("line1FontColor") || "#ffffff"; const line1FontOpacity = urlParams.get("line1FontOpacity") || "1"; const line1TextTransform = urlParams.get("line1TextTransform") || "none"; -const line1TextAlignment = urlParams.get("line1TextAlignment") || "center"; const enableLine2 = GetBooleanParam("enableLine2", true); const line2Format = urlParams.get("line2Format") || "ddd, MMM D"; @@ -46,7 +47,6 @@ const line2FontWeight = urlParams.get("line2FontWeight") || "400"; const line2FontColor = urlParams.get("line2FontColor") || "#ffffff"; const line2FontOpacity = urlParams.get("line2FontOpacity") || "0.7"; const line2TextTransform = urlParams.get("line2TextTransform") || "none"; -const line2TextAlignment = urlParams.get("line2TextAlignment") || "center"; const enableLine3 = GetBooleanParam("enableLine3", false); const line3Format = urlParams.get("line3Format") || "ddd DD MMM YYYY hh:mm:ss A z"; @@ -55,7 +55,6 @@ const line3FontWeight = urlParams.get("line3FontWeight") || "600"; const line3FontColor = urlParams.get("line3FontColor") || "#ffffff"; const line3FontOpacity = urlParams.get("line3FontOpacity") || "1"; const line3TextTransform = urlParams.get("line3TextTransform") || "none"; -const line3TextAlignment = urlParams.get("line3TextAlignment") || "center"; @@ -130,7 +129,7 @@ document.head.appendChild(script); // STYLING // ///////////// -function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform, textAlignment) { +function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTransform) { el.style.fontSize = fontSize + "px"; el.style.fontWeight = fontWeight; el.style.color = fontColor; @@ -139,6 +138,6 @@ function ApplyStyling(el, fontSize, fontWeight, fontColor, fontOpacity, textTran el.style.textAlign = textAlignment; } -ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform, line1TextAlignment); -ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform, line2TextAlignment); -ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform, line3TextAlignment); \ No newline at end of file +ApplyStyling(line1, line1FontSize, line1FontWeight, line1FontColor, line1FontOpacity, line1TextTransform); +ApplyStyling(line2, line2FontSize, line2FontWeight, line2FontColor, line2FontOpacity, line2TextTransform); +ApplyStyling(line3, line3FontSize, line3FontWeight, line3FontColor, line3FontOpacity, line3TextTransform); \ No newline at end of file diff --git a/clocko/settings/settings.json b/clocko/settings/settings.json index d47db6e..7b81529 100644 --- a/clocko/settings/settings.json +++ b/clocko/settings/settings.json @@ -57,6 +57,19 @@ "defaultValue": "", "group": "Appearance" }, + { + "id": "textAlignment", + "label": "Text Alignment", + "description": "", + "type": "select", + "options": [ + { "value": "left", "label": "Left" }, + { "value": "center", "label": "Center" }, + { "value": "right", "label": "Right" } + ], + "defaultValue": "center", + "group": "Appearance" + }, { "id": "enableLine1", "label": "Enable Line 1", @@ -141,20 +154,6 @@ "showIf": "enableLine1", "group": "Appearance" }, - { - "id": "line1TextAlignment", - "label": "Text Alignment", - "description": "", - "type": "select", - "options": [ - { "value": "left", "label": "Left" }, - { "value": "center", "label": "Center" }, - { "value": "right", "label": "Right" } - ], - "defaultValue": "center", - "showIf": "enableLine1", - "group": "Appearance" - }, { "id": "enableLine2", "label": "Enable Line 2", @@ -239,20 +238,6 @@ "showIf": "enableLine2", "group": "Appearance" }, - { - "id": "line2TextAlignment", - "label": "Text Alignment", - "description": "", - "type": "select", - "options": [ - { "value": "left", "label": "Left" }, - { "value": "center", "label": "Center" }, - { "value": "right", "label": "Right" } - ], - "defaultValue": "center", - "showIf": "enableLine2", - "group": "Appearance" - }, { "id": "enableLine3", "label": "Enable Line 3", @@ -336,20 +321,6 @@ "defaultValue": "none", "showIf": "enableLine3", "group": "Appearance" - }, - { - "id": "line3TextAlignment", - "label": "Text Alignment", - "description": "", - "type": "select", - "options": [ - { "value": "left", "label": "Left" }, - { "value": "center", "label": "Center" }, - { "value": "right", "label": "Right" } - ], - "defaultValue": "center", - "showIf": "enableLine3", - "group": "Appearance" } ] } \ No newline at end of file From 8cbc4bd19b4fd7b61c8aed430cd1a7f6118cf3f8 Mon Sep 17 00:00:00 2001 From: nutty Date: Tue, 1 Sep 2026 16:06:05 +1000 Subject: [PATCH 11/11] Updated SMTC Bridge version number v0.0.6 -> v1.0.0 --- .common/utils/helpers.js | 27 +++++++++------------------ now-playing/script.js | 2 +- 2 files changed, 10 insertions(+), 19 deletions(-) diff --git a/.common/utils/helpers.js b/.common/utils/helpers.js index 77d6ca5..2014a42 100644 --- a/.common/utils/helpers.js +++ b/.common/utils/helpers.js @@ -688,27 +688,18 @@ function VersionCheck(requiredVersion, installedVersion) { const [rMajor, rMinor, rPatch] = requiredVersion.split('.').map(Number); const [iMajor, iMinor, iPatch] = installedVersion.split('.').map(Number); - // 1. CRITICAL: Major versions must match exactly. - if (iMajor !== rMajor) { - console.log(`VersionCheck: Major version mismatch. Required: ${rMajor}, Installed: ${iMajor}`); - return 'incompatible'; - } + // Compare Major + if (iMajor > rMajor) return 'compatible'; + if (iMajor < rMajor) return 'incompatible'; - // 2. SOFT WARNING: The installed minor version is lower than what is required. - if (iMinor < rMinor) { - console.log(`VersionCheck: Minor version mismatch. Required: ${rMinor}, Installed: ${iMinor}`); - return 'soft-warning'; - } + // Major matches, compare Minor + if (iMinor > rMinor) return 'compatible'; + if (iMinor < rMinor) return 'incompatible'; // or 'incompatible' depending on your policy - // 3. SILENT WARNING: Minors match, but the installed patch version is lagging. - if (iMinor === rMinor && iPatch < rPatch) { - console.log(`VersionCheck: Patch version mismatch. Required: ${rPatch}, Installed: ${iPatch}`); - return 'compatible'; - } + // Major and Minor match, compare Patch + if (iPatch >= rPatch) return 'compatible'; - // 4. PERFECT: Installed version meets or exceeds the required baseline. - console.log(`VersionCheck: Installed version meets or exceeds the required baseline. Required: ${requiredVersion}, Installed: ${installedVersion}`); - return 'compatible'; + return 'soft-warning'; // Installed patch is lower than required } function SanitizeHTML(htmlString) { diff --git a/now-playing/script.js b/now-playing/script.js index f680e7a..697bbe8 100644 --- a/now-playing/script.js +++ b/now-playing/script.js @@ -9,7 +9,7 @@ const urlParams = new URLSearchParams(queryString); // CONSTANTS // //////////////// -const REQUIRED_VERSION = '0.0.6'; +const REQUIRED_VERSION = '1.0.0'; const SMTC_BRIDGE_DOWNLOAD_URL = 'https://github.com/nuttylmao/smtc-bridge/releases'; let VersionChecked = false;