diff --git a/modules/apple-music/apple-music.js b/modules/apple-music/apple-music.js
new file mode 100644
index 0000000..1c936c2
--- /dev/null
+++ b/modules/apple-music/apple-music.js
@@ -0,0 +1,261 @@
+// Apple Music Overlay Module
+// This module connects to a local Cider instance to display Apple Music playback information.
+// Initial sources were taken from https://github.com/nuttylmao/nutty.gg
+
+;(function() {
+ let visibilityDefault = 5;
+ let visibilityDuration = visibilityDefault;
+ let animationSpeed = 0.5;
+
+ const moduleUrl = window.getModuleUrl();
+ loadCSSModule('overlay-applemusic-css', moduleUrl + '/css/apple-music.css');
+ loadModuleResources([
+ { type:'css', url:'https://cdn.jsdelivr.net/npm/@xz/fonts@1/serve/metropolis.min.css', id:'xz-fonts' },
+ { type:'js', url:'https://cdn.socket.io/4.7.5/socket.io.min.js', id:'socket-io', crossorigin:'anonymous' },
+ { type:'js', url:'https://cdnjs.cloudflare.com/ajax/libs/gsap/1.18.0/TweenMax.min.js', id:'tweenmax' }
+ ]).then(() => {
+ initModule();
+ let outer = document.getElementById('musicWidgetContainer');
+ let maxWidth = outer.clientWidth+50;
+ window.addEventListener("resize", resize);
+ resize();
+ connectws();
+
+ function initModule() {
+ const container = document.getElementById('mainContainer') || document.body;
+
+ // Crée le conteneur principal
+ if (!document.getElementById('musicWidgetContainer')) {
+ const widget = document.createElement('div');
+ widget.id = 'musicWidgetContainer';
+
+ // Album art box
+ const artBox = document.createElement('div');
+ artBox.id = 'albumArtBox';
+ const albumArt = document.createElement('img');
+ albumArt.id = 'albumArt';
+ albumArt.src = ''; // placeholder
+ const albumArtBack = document.createElement('img');
+ albumArtBack.id = 'albumArtBack';
+ albumArtBack.src = ''; // placeholder
+ artBox.append(albumArt, albumArtBack);
+
+ // Song info box
+ const infoBox = document.createElement('div');
+ infoBox.id = 'songInfoBox';
+
+ const songInfo = document.createElement('div');
+ songInfo.id = 'songInfo';
+ const innerBox = document.createElement('div');
+ innerBox.id = 'IAmRunningOutOfNamesForTheseBoxes';
+
+ const songLabel = document.createElement('div');
+ songLabel.id = 'songLabel';
+ songLabel.innerText = '-/-';
+ const artistLabel = document.createElement('div');
+ artistLabel.id = 'artistLabel';
+ artistLabel.innerText = '-/-';
+ const albumLabel = document.createElement('div');
+ albumLabel.id = 'albumLabel';
+ albumLabel.innerText = '-/-';
+
+ const times = document.createElement('div');
+ times.id = 'times';
+ const progressTime = document.createElement('div');
+ progressTime.id = 'progressTime';
+ progressTime.innerText = '0:00';
+ const duration = document.createElement('div');
+ duration.id = 'duration';
+ duration.innerText = '0:00';
+ times.append(progressTime, duration);
+
+ const progressBg = document.createElement('div');
+ progressBg.id = 'progressBg';
+ const progressBar = document.createElement('div');
+ progressBar.id = 'progressBar';
+ progressBg.appendChild(progressBar);
+
+ innerBox.append(songLabel, artistLabel, albumLabel, times, progressBg);
+ songInfo.appendChild(innerBox);
+
+ const backgroundArt = document.createElement('div');
+ backgroundArt.id = 'backgroundArt';
+ const backgroundImage = document.createElement('img');
+ backgroundImage.id = 'backgroundImage';
+ backgroundImage.src = '';
+ const backgroundImageBack = document.createElement('img');
+ backgroundImageBack.id = 'backgroundImageBack';
+ backgroundImageBack.src = '';
+ backgroundArt.append(backgroundImage, backgroundImageBack);
+
+ infoBox.append(songInfo, backgroundArt);
+
+ widget.append(artBox, infoBox);
+ container.appendChild(widget);
+ }
+ }
+
+ if (window.SBdispatcher) {
+ SBdispatcher.on('musicPlaying', data => {
+ visibilityDuration = data.param1 || visibilityDefault;
+ showSongInfo();
+ });
+ }
+
+ function connectws() {
+ if ("WebSocket" in window) {
+ const CiderApp = io("http://localhost:10767/", {
+ transports: ['websocket']
+ });
+
+ CiderApp.on("disconnect", (event) => {
+ SetConnectionStatus(false);
+ setTimeout(connectws, 5000);
+ });
+
+ CiderApp.on("connect", (event) => {
+ SetConnectionStatus(true);
+ });
+
+ // Set up websocket artwork/information handling
+ CiderApp.on("API:Playback", ({ data, type }) => {
+ switch (type) {
+ // Song changes
+ case ("playbackStatus.nowPlayingItemDidChange"):
+ UpdateSongInfo(data);
+ break;
+
+ // Progress bar moves
+ case ("playbackStatus.playbackTimeDidChange"):
+ UpdateProgressBar(data);
+ break;
+
+ // Pause/unpause
+ case ("playbackStatus.playbackStateDidChange"):
+ UpdatePlaybackState(data);
+ break;
+ }
+ });
+ }
+ }
+
+ function showSongInfo() {
+ setTimeout(() => {
+ SetVisibility(true);
+ }, animationSpeed * 500);
+
+ if (visibilityDuration > 0) {
+ setTimeout(() => {
+ SetVisibility(false);
+ }, visibilityDuration * 1000);
+ }
+ }
+
+ function UpdateSongInfo(data) {
+ // Set the user's info
+ let albumArtUrl = data.artwork.url;
+ albumArtUrl = albumArtUrl.replace("{w}", data.artwork.width);
+ albumArtUrl = albumArtUrl.replace("{h}", data.artwork.height);
+
+ UpdateAlbumArt(document.getElementById("albumArt"), albumArtUrl);
+ UpdateAlbumArt(document.getElementById("backgroundImage"), albumArtUrl);
+
+ setTimeout(() => {
+ UpdateTextLabel(document.getElementById("songLabel"), data.name);
+ UpdateTextLabel(document.getElementById("artistLabel"), data.artistName);
+ }, animationSpeed * 500);
+
+ setTimeout(() => {
+ document.getElementById("albumArtBack").src = albumArtUrl;
+ document.getElementById("backgroundImageBack").src = albumArtUrl;
+ }, 2 * animationSpeed * 500);
+
+ showSongInfo()
+
+ }
+
+ function UpdateTextLabel(div, text) {
+ if (div.innerHTML != text) {
+ div.setAttribute("class", "text-fade");
+ setTimeout(() => {
+ div.innerHTML = text;
+ div.setAttribute("class", ".text-show");
+ }, animationSpeed * 250);
+ }
+ }
+
+ function UpdateAlbumArt(div, imgsrc) {
+ if (div.src != imgsrc) {
+ div.setAttribute("class", "text-fade");
+ setTimeout(() => {
+ div.src = imgsrc;
+ div.setAttribute("class", "text-show");
+ }, animationSpeed * 500);
+ }
+ }
+
+ function UpdateProgressBar(data) {
+ const progress = ((data.currentPlaybackTime / data.currentPlaybackDuration) * 100);
+ const progressTime = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTime);
+ const duration = ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(data.currentPlaybackTimeRemaining);
+ document.getElementById("progressBar").style.width = `${progress}%`;
+ document.getElementById("progressTime").innerHTML = progressTime;
+ document.getElementById("duration").innerHTML = `-${duration}`;
+ }
+
+ function UpdatePlaybackState(data) {
+ console.log(data);
+ switch (data.state) {
+ case ("paused"):
+ case ("stopped"):
+ SetVisibility(false);
+ break;
+ case ("playing"):
+ UpdateSongInfo(data.attributes);
+ break;
+ }
+ }
+
+
+ function ConvertSecondsToMinutesSoThatItLooksBetterOnTheOverlay(time) {
+ const minutes = Math.floor(time / 60);
+ const seconds = Math.trunc(time - minutes * 60);
+
+ return `${minutes}:${('0' + seconds).slice(-2)}`;
+ }
+
+ function SetVisibility(isVisible) {
+ widgetVisibility = isVisible;
+
+ const musicWidgetContainer = document.getElementById("musicWidgetContainer");
+
+ if (isVisible) {
+ var tl = new TimelineMax();
+ tl
+ .to(musicWidgetContainer, animationSpeed, { bottom: "50%", ease: Power1.easeInOut }, 'label')
+ .to(musicWidgetContainer, animationSpeed, { opacity: 1, ease: Power1.easeInOut }, 'label')
+ }
+ else {
+ var tl = new TimelineMax();
+ tl
+ .to(musicWidgetContainer, animationSpeed, { bottom: "45%", ease: Power1.easeInOut }, 'label')
+ .to(musicWidgetContainer, animationSpeed, { opacity: 0, ease: Power1.easeInOut }, 'label')
+ }
+ }
+
+ function SetConnectionStatus(connected) {
+ if (connected) {
+ console.log("Connected to Cider!");
+ }
+ else {
+ console.log("Not connected to Cider...");
+ }
+ }
+
+ function resize() {
+ const scale = window.innerWidth / maxWidth;
+ outer.style.transform = 'translate(0%, 0%) scale(' + scale + ')';
+ }
+ }).catch(err => console.error(err));
+
+})();
\ No newline at end of file
diff --git a/modules/apple-music/css/apple-music.css b/modules/apple-music/css/apple-music.css
new file mode 100644
index 0000000..35d9780
--- /dev/null
+++ b/modules/apple-music/css/apple-music.css
@@ -0,0 +1,139 @@
+#musicWidgetContainer {
+ --corner-radius: 10px;
+ --album-art-size: 100px;
+ position: absolute;
+ width: 600px;
+ height: 140px;
+ top: 20px;
+ right: 0px;
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
+ display: flex;
+ height: var(--album-art-size);
+ margin: 20px;
+ filter: drop-shadow(0px 0px 4px #000000);
+ width: 100%;
+ max-width: 500px;
+ bottom: 45%;
+ opacity: 0;
+}
+#musicWidgetContainer #albumArtBox {
+ background: rgba(0, 0, 0, 0.5);
+ position: relative;
+ border-radius: var(--corner-radius);
+ overflow: hidden;
+ margin: 0px 8px 0px 0px;
+}
+#musicWidgetContainer #albumArt {
+ position: absolute;
+ width: var(--album-art-size);
+}
+#musicWidgetContainer #albumArtBack {
+ width: var(--album-art-size);
+}
+#musicWidgetContainer #songInfoBox {
+ position: relative;
+ color: white;
+ width: calc(100% - 125px);
+ display: flex;
+ flex-direction: column;
+ flex: 0 1 auto;
+ justify-content: center;
+ z-index: 1;
+ text-shadow: 2px 2px 2px black;
+ overflow: hidden;
+ z-index: 4;
+}
+#musicWidgetContainer #songInfo {
+ background: rgba(0, 0, 0, 0.5);
+ position: relative;
+ border-radius: var(--corner-radius);
+ padding: 0px 20px;
+ height: 100%;
+ overflow: hidden;
+}
+#musicWidgetContainer #IAmRunningOutOfNamesForTheseBoxes {
+ width: calc(100% - 40px);
+ position: absolute;
+ top: 50%;
+ -ms-transform: translateY(-50%);
+ transform: translateY(-50%);
+}
+#musicWidgetContainer #backgroundArt {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ border-radius: var(--corner-radius);
+ overflow: hidden;
+ z-index: -1;
+ opacity: 0.9;
+}
+#musicWidgetContainer #backgroundImage {
+ filter: blur(20px);
+ position: absolute;
+ width: 140%;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%);
+}
+#musicWidgetContainer #backgroundImageBack {
+ filter: blur(20px);
+ position: absolute;
+ width: 140%;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ z-index: -1;
+}
+#musicWidgetContainer #songLabel {
+ font-weight: bold;
+ font-size: 20px;
+ white-space: nowrap;
+}
+#musicWidgetContainer #artistLabel {
+ font-size: 16px;
+ font-weight: 100;
+ font-style: italic;
+ white-space: nowrap;
+}
+#musicWidgetContainer #albumLabel {
+ font-size: 12px;
+ font-weight: 100;
+ white-space: nowrap;
+}
+#musicWidgetContainer #progressBg {
+ margin-top: 5px;
+ width: 100%;
+ height: auto;
+ border-radius: 5px;
+ background-color: #1F1F1F;
+}
+#musicWidgetContainer #progressBar {
+ border-radius: 5px;
+ height: 5px;
+ width: 20%;
+ background-color: #ffffff;
+ margin: 10px 0px;
+}
+#musicWidgetContainer #times {
+ position: relative;
+ height: 10px;
+ font-size: 8px;
+ font-weight: 700;
+ line-height: 3.5;
+}
+#musicWidgetContainer #progressTime {
+ position: absolute;
+}
+#musicWidgetContainer #duration {
+ position: absolute;
+ width: 100%;
+ text-align: right;
+}
+#musicWidgetContainer .text-show {
+ opacity: 1;
+ transition: all 0.25s ease;
+}
+#musicWidgetContainer .text-fade {
+ opacity: 0;
+ transition: all 0.25s ease;
+}
diff --git a/modules/apple-music/css/apple-music.less b/modules/apple-music/css/apple-music.less
new file mode 100644
index 0000000..abf3945
--- /dev/null
+++ b/modules/apple-music/css/apple-music.less
@@ -0,0 +1,163 @@
+// out: apple-music.css, sourcemap: false, compress: false
+
+#musicWidgetContainer {
+ --corner-radius: 10px;
+ --album-art-size: 100px;
+
+ position: absolute;
+ width: 600px;
+ height: 140px;
+ top: 20px;
+ right: 0px;
+
+ font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
+ display: flex;
+ height: var(--album-art-size);
+ margin: 20px;
+ filter: drop-shadow(0px 0px 4px rgba(0, 0, 0, 1));
+ width: 100%;
+ max-width: 500px;
+ bottom: 45%;
+ opacity: 0;
+
+ #albumArtBox {
+ background: rgba(0, 0, 0, 0.5);
+ position: relative;
+ border-radius: var(--corner-radius);
+ overflow: hidden;
+ margin: 0px 8px 0px 0px;
+ }
+
+ #albumArt {
+ position: absolute;
+ width: var(--album-art-size);
+ }
+
+ #albumArtBack {
+ width: var(--album-art-size);
+ }
+
+ #songInfoBox {
+ position: relative;
+ color: white;
+ width: calc(100% - 125px);
+ display: flex;
+ flex-direction: column;
+ flex: 0 1 auto;
+ justify-content: center;
+ z-index: 1;
+ text-shadow: 2px 2px 2px black;
+ overflow: hidden;
+ z-index: 4;
+ }
+
+ #songInfo {
+ background: rgba(0, 0, 0, 0.5);
+ position: relative;
+ border-radius: var(--corner-radius);
+ padding: 0px 20px;
+ height: 100%;
+ overflow: hidden;
+ }
+
+ #IAmRunningOutOfNamesForTheseBoxes {
+ position: absolute;
+ width: calc(100% - 40px);
+ position: absolute;
+ top: 50%;
+ -ms-transform: translateY(-50%);
+ transform: translateY(-50%);
+ }
+
+ #backgroundArt {
+ position: absolute;
+ height: 100%;
+ width: 100%;
+ border-radius: var(--corner-radius);
+ overflow: hidden;
+ z-index: -1;
+ opacity: 0.9;
+ }
+
+ #backgroundImage {
+ filter: blur(20px);
+ position: absolute;
+ width: 140%;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ }
+
+ #backgroundImageBack {
+ filter: blur(20px);
+ position: absolute;
+ width: 140%;
+ top: 50%;
+ left: 50%;
+ transform: translateX(-50%) translateY(-50%);
+ z-index: -1;
+ }
+
+ #songLabel {
+ font-weight: bold;
+ font-size: 20px;
+ white-space: nowrap;
+ }
+
+ #artistLabel {
+ font-size: 16px;
+ font-weight: 100;
+ font-style: italic;
+ white-space: nowrap;
+ }
+
+ #albumLabel {
+ font-size: 12px;
+ font-weight: 100;
+ white-space: nowrap;
+ }
+
+ #progressBg {
+ margin-top: 5px;
+ width: 100%;
+ height: auto;
+ border-radius: 5px;
+ background-color: #1F1F1F;
+ }
+
+ #progressBar {
+ border-radius: 5px;
+ height: 5px;
+ width: 20%;
+ background-color: #ffffff;
+ margin: 10px 0px;
+ }
+
+ #times {
+ position: relative;
+ height: 10px;
+ font-size: 8px;
+ font-weight: 700;
+ line-height: 3.5;
+ }
+
+ #progressTime {
+ position: absolute;
+ }
+
+ #duration {
+ position: absolute;
+ width: 100%;
+ text-align: right;
+ }
+
+ .text-show {
+ opacity: 1;
+ transition: all 0.25s ease;
+ }
+
+ .text-fade {
+ opacity: 0;
+ transition: all 0.25s ease;
+ }
+}
\ No newline at end of file
diff --git a/overlay-core.js b/overlay-core.js
index 18a17e5..49cd994 100644
--- a/overlay-core.js
+++ b/overlay-core.js
@@ -111,6 +111,34 @@ function getModuleUrl() {
return url.substring(0, url.lastIndexOf('/'));
}
+function loadModuleResources(resources) {
+ const promises = resources.map(res => {
+ const key = res.id || res.url;
+ if (document.getElementById(key)) {
+ return Promise.resolve();
+ }
+ if (res.type === 'css') {
+ loadCSSModule(key, res.url);
+ return Promise.resolve();
+ }
+ if (res.type === 'js') {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.id = key;
+ script.src = res.url;
+ if (res.integrity) script.integrity = res.integrity;
+ if (res.crossorigin) script.crossOrigin = res.crossorigin;
+ script.onload = () => resolve();
+ script.onerror = () => reject(new Error(`Échec chargement script ${res.url}`));
+ document.head.appendChild(script);
+ });
+ }
+ return Promise.resolve();
+ });
+ return Promise.all(promises).then(() => {});
+}
+
+
function getBooleanParam(paramName, defaultValue) {
const urlParams = new URLSearchParams(window.location.search);
const paramValue = urlParams.get(paramName);
@@ -245,4 +273,4 @@ function positionDiv(container) {
window.loadCSSModule = loadCSSModule;
window.getModuleUrl = getModuleUrl;
-window.getBooleanParam = getBooleanParam;
+window.loadModuleResources = loadModuleResources;
diff --git a/overlay.html b/overlay.html
index 61ec306..8e925dc 100644
--- a/overlay.html
+++ b/overlay.html
@@ -17,4 +17,5 @@
+