Created
February 6, 2026 23:07
-
-
Save arasovic/27e5b46ca549fe27972a581030bf8cff to your computer and use it in GitHub Desktop.
BTT Apple Music Floating Controller — dark minimal widget with playback, shuffle, repeat, scrub, volume controls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "BTTPresetName": "Apple Music Controller", | |
| "BTTPresetUUID": "B3F7A2C1-8D4E-4F6A-9B2D-1E5C8A3F7D9B", | |
| "BTTPresetContent": [ | |
| { | |
| "BTTAppBundleIdentifier": "BT.G", | |
| "BTTAppName": "Global", | |
| "BTTAppAutoInvertIcon": 1, | |
| "BTTTriggers": [ | |
| { | |
| "BTTTriggerType": 767, | |
| "BTTTriggerClass": "BTTTriggerTypeFloatingMenu", | |
| "BTTTriggerTypeDescription": "Floating Menu", | |
| "BTTTriggerName": "Floating Menu: Apple Music Controller", | |
| "BTTUUID": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890", | |
| "BTTEnabled": 1, | |
| "BTTEnabled2": 1, | |
| "BTTOrder": 0, | |
| "BTTMenuConfig": { | |
| "BTTMenuFrameWidth": 280, | |
| "BTTMenuFrameHeight": 120, | |
| "BTTMenuWindowLevel": 3, | |
| "BTTMenuStealKeyboardFocusOnShow": 0, | |
| "BTTMenuPositioningType": 1, | |
| "BTTMenuAnchorRelation": 3, | |
| "BTTMenuPositionRelativeTo": 7 | |
| }, | |
| "BTTMenuItems": [ | |
| { | |
| "BTTTriggerType": 873, | |
| "BTTTriggerClass": "BTTTriggerTypeFloatingMenu", | |
| "BTTTriggerTypeDescription": "Floating Menu: Webview", | |
| "BTTUUID": "C1D2E3F4-A5B6-C7D8-E9F0-112233445566", | |
| "BTTEnabled": 1, | |
| "BTTEnabled2": 1, | |
| "BTTOrder": 0, | |
| "BTTMenuItemWebViewURL": "localfile:///path/to/AppleMusicWidget.html", | |
| "BTTMenuItemMaxWidth": 280, | |
| "BTTMenuItemMaxHeight": 120, | |
| "BTTMenuItemMinWidth": 260, | |
| "BTTMenuItemMinHeight": 110, | |
| "BTTMenuItemBackgroundColor": "0,0,0,0", | |
| "BTTMenuItemCornerRadius": 14, | |
| "BTTMenuItemBorderWidth": 0, | |
| "BTTMenuItemVisibleWhileInactive": 1 | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| ], | |
| "BTTPresetSnapAreas": [] | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', system-ui, sans-serif; | |
| background: transparent; | |
| color: #ffffff; | |
| overflow: visible; | |
| user-select: none; | |
| -webkit-user-select: none; | |
| cursor: default; | |
| } | |
| .widget { | |
| background: rgba(28, 28, 30, 0.97); | |
| border: 0.5px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 14px; | |
| padding: 12px 14px 12px; | |
| backdrop-filter: blur(30px); | |
| -webkit-backdrop-filter: blur(30px); | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| width: 280px; | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4), 0 1px 3px rgba(0, 0, 0, 0.2); | |
| } | |
| .track-info { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 2px; | |
| min-height: 32px; | |
| justify-content: center; | |
| padding: 0 2px; | |
| } | |
| .track-name { | |
| font-size: 13px; | |
| font-weight: 600; | |
| letter-spacing: -0.2px; | |
| line-height: 1.3; | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| color: #ffffff; | |
| transition: color 0.3s ease; | |
| } | |
| .track-artist { | |
| font-size: 11px; | |
| font-weight: 400; | |
| line-height: 1.3; | |
| color: rgba(255, 255, 255, 0.45); | |
| white-space: nowrap; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| } | |
| .idle .track-name { | |
| color: rgba(255, 255, 255, 0.3); | |
| font-weight: 500; | |
| } | |
| .controls { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 5px; | |
| } | |
| .btn { | |
| appearance: none; | |
| border: none; | |
| outline: none; | |
| background: rgba(255, 255, 255, 0.07); | |
| border-radius: 10px; | |
| color: rgba(255, 255, 255, 0.8); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| transition: all 0.15s ease; | |
| height: 34px; | |
| width: 44px; | |
| } | |
| .btn:hover { | |
| background: rgba(255, 255, 255, 0.14); | |
| color: #ffffff; | |
| } | |
| .btn:active { | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: scale(0.93); | |
| transition: all 0.06s ease; | |
| } | |
| .btn.play-btn { | |
| width: 56px; | |
| height: 36px; | |
| background: rgba(255, 255, 255, 0.12); | |
| color: rgba(255, 255, 255, 0.9); | |
| } | |
| .btn.play-btn:hover { | |
| background: rgba(255, 255, 255, 0.2); | |
| color: #ffffff; | |
| } | |
| .btn.play-btn:active { | |
| background: rgba(255, 255, 255, 0.26); | |
| } | |
| .btn svg { | |
| width: 13px; | |
| height: 13px; | |
| fill: currentColor; | |
| } | |
| .btn.play-btn svg { | |
| width: 15px; | |
| height: 15px; | |
| } | |
| .btn.scrub-btn { | |
| width: 30px; | |
| height: 28px; | |
| background: rgba(255, 255, 255, 0.04); | |
| color: rgba(255, 255, 255, 0.45); | |
| } | |
| .btn.scrub-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: rgba(255, 255, 255, 0.75); | |
| } | |
| .btn.scrub-btn svg { | |
| width: 11px; | |
| height: 11px; | |
| } | |
| .btn.mode-btn { | |
| width: 36px; | |
| height: 30px; | |
| background: rgba(255, 255, 255, 0.04); | |
| color: rgba(255, 255, 255, 0.4); | |
| } | |
| .btn.mode-btn:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: rgba(255, 255, 255, 0.7); | |
| } | |
| .btn.mode-btn.active { | |
| color: #fc3c44; | |
| background: rgba(252, 60, 68, 0.12); | |
| } | |
| .btn.mode-btn.active:hover { | |
| background: rgba(252, 60, 68, 0.2); | |
| } | |
| .btn.mode-btn svg { | |
| width: 12px; | |
| height: 12px; | |
| } | |
| .mode-dot { | |
| position: absolute; | |
| bottom: 2px; | |
| width: 3px; | |
| height: 3px; | |
| border-radius: 50%; | |
| background: #fc3c44; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .btn.mode-btn.active .mode-dot { | |
| opacity: 1; | |
| } | |
| .btn.mode-btn { | |
| position: relative; | |
| } | |
| .volume-row { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 6px; | |
| } | |
| .btn-vol { | |
| appearance: none; | |
| border: none; | |
| outline: none; | |
| background: rgba(255, 255, 255, 0.05); | |
| border-radius: 8px; | |
| color: rgba(255, 255, 255, 0.55); | |
| cursor: pointer; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| width: 32px; | |
| height: 26px; | |
| font-size: 14px; | |
| font-weight: 300; | |
| transition: all 0.15s ease; | |
| flex-shrink: 0; | |
| } | |
| .btn-vol:hover { | |
| background: rgba(255, 255, 255, 0.12); | |
| color: #ffffff; | |
| } | |
| .btn-vol:active { | |
| background: rgba(255, 255, 255, 0.18); | |
| transform: scale(0.93); | |
| } | |
| .btn-vol.mute-btn { | |
| color: rgba(255, 255, 255, 0.45); | |
| } | |
| .btn-vol.mute-btn.muted { | |
| color: #fc3c44; | |
| background: rgba(252, 60, 68, 0.1); | |
| } | |
| .btn-vol svg { | |
| width: 13px; | |
| height: 13px; | |
| fill: currentColor; | |
| } | |
| .volume-label { | |
| font-size: 11px; | |
| color: rgba(255, 255, 255, 0.4); | |
| min-width: 28px; | |
| text-align: center; | |
| font-variant-numeric: tabular-nums; | |
| font-weight: 500; | |
| } | |
| .playing-indicator { | |
| display: inline-flex; | |
| align-items: flex-end; | |
| gap: 1.5px; | |
| height: 12px; | |
| margin-right: 6px; | |
| vertical-align: middle; | |
| opacity: 0; | |
| transition: opacity 0.3s ease; | |
| } | |
| .playing-indicator.active { | |
| opacity: 1; | |
| } | |
| .playing-indicator .bar { | |
| width: 2.5px; | |
| background: #fc3c44; | |
| border-radius: 1px; | |
| animation: equalizer 0.8s ease-in-out infinite alternate; | |
| } | |
| .playing-indicator .bar:nth-child(1) { height: 4px; animation-delay: 0s; } | |
| .playing-indicator .bar:nth-child(2) { height: 8px; animation-delay: 0.15s; } | |
| .playing-indicator .bar:nth-child(3) { height: 5px; animation-delay: 0.3s; } | |
| @keyframes equalizer { | |
| 0% { height: 3px; } | |
| 100% { height: 12px; } | |
| } | |
| .repeat-one-label { | |
| font-size: 7px; | |
| font-weight: 700; | |
| position: absolute; | |
| top: 4px; | |
| right: 5px; | |
| color: #fc3c44; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| } | |
| .repeat-one .repeat-one-label { | |
| opacity: 1; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="widget"> | |
| <div class="track-info idle" id="trackInfo"> | |
| <div class="track-name" id="trackName"> | |
| <span class="playing-indicator" id="playingIndicator"> | |
| <span class="bar"></span> | |
| <span class="bar"></span> | |
| <span class="bar"></span> | |
| </span> | |
| <span id="trackNameText">Not Playing</span> | |
| </div> | |
| <div class="track-artist" id="trackArtist"></div> | |
| </div> | |
| <div class="controls"> | |
| <button class="btn mode-btn" id="shuffleBtn" onclick="toggleShuffle()" title="Shuffle"> | |
| <svg viewBox="0 0 16 16"><path d="M11 2l3 3-3 3m3-3H9.5a4 4 0 00-4 4v0a4 4 0 01-4 4H1m13-1l-3 3m3-3H9.5a4 4 0 01-4-4v0a4 4 0 00-4-4H1" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| <span class="mode-dot"></span> | |
| </button> | |
| <button class="btn" onclick="prevTrack()" title="Previous"> | |
| <svg viewBox="0 0 16 16"><path d="M2.5 2.5h1.8v11H2.5zM13 2.5L5.8 8 13 13.5z"/></svg> | |
| </button> | |
| <button class="btn scrub-btn" onclick="scrub(-5)" title="-5s"> | |
| <svg viewBox="0 0 16 16"><path d="M8 3a5.5 5.5 0 100 11 5.5 5.5 0 000-11zm0-1.5a7 7 0 110 14 7 7 0 010-14z" fill-rule="evenodd"/><path d="M8.5 5.5v3l-2.5 1.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/><path d="M4 1.5L1.5 3.5 4 5.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| </button> | |
| <button class="btn play-btn" id="playBtn" onclick="playPause()" title="Play/Pause"> | |
| <svg viewBox="0 0 16 16" id="playIcon"><path d="M4.5 2v12l9-6z"/></svg> | |
| </button> | |
| <button class="btn scrub-btn" onclick="scrub(5)" title="+5s"> | |
| <svg viewBox="0 0 16 16"><path d="M8 3a5.5 5.5 0 110 11 5.5 5.5 0 010-11zm0-1.5a7 7 0 100 14 7 7 0 000-14z" fill-rule="evenodd"/><path d="M7.5 5.5v3l2.5 1.5" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/><path d="M12 1.5l2.5 2-2.5 2" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| </button> | |
| <button class="btn" onclick="nextTrack()" title="Next"> | |
| <svg viewBox="0 0 16 16"><path d="M11.7 2.5h1.8v11h-1.8zM3 2.5L10.2 8 3 13.5z"/></svg> | |
| </button> | |
| <button class="btn mode-btn" id="repeatBtn" onclick="toggleRepeat()" title="Repeat"> | |
| <svg viewBox="0 0 16 16"><path d="M3 6h10l-2.5-2.5M13 10H3l2.5 2.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg> | |
| <span class="mode-dot"></span> | |
| <span class="repeat-one-label">1</span> | |
| </button> | |
| </div> | |
| <div class="volume-row"> | |
| <button class="btn-vol mute-btn" id="muteBtn" onclick="toggleMute()" title="Mute"> | |
| <svg viewBox="0 0 16 16" id="volIcon"><path d="M8 2.5L4.5 5.5H1.5v5h3L8 13.5V2.5z"/><path d="M10 5.5a3.5 3.5 0 010 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><path d="M11.5 3.5a6 6 0 010 9" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/></svg> | |
| </button> | |
| <button class="btn-vol" onclick="volDown()" title="Volume Down">−</button> | |
| <span class="volume-label" id="volLabel">50</span> | |
| <button class="btn-vol" onclick="volUp()" title="Volume Up">+</button> | |
| </div> | |
| </div> | |
| <script> | |
| let initialized = false; | |
| let isPlaying = false; | |
| let isMuted = false; | |
| let shuffleOn = false; | |
| let repeatMode = 'off'; | |
| let currentVol = 50; | |
| const PLAY_PATH = 'M4.5 2v12l9-6z'; | |
| const PAUSE_PATH = 'M4 2.5h3v11H4zm5 0h3v11H9z'; | |
| const VOL_ON = '<path d="M8 2.5L4.5 5.5H1.5v5h3L8 13.5V2.5z"/><path d="M10 5.5a3.5 3.5 0 010 5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/><path d="M11.5 3.5a6 6 0 010 9" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round"/>'; | |
| const VOL_MUTED = '<path d="M8 2.5L4.5 5.5H1.5v5h3L8 13.5V2.5z"/><path d="M11 5.5l3.5 5M14.5 5.5L11 10.5" fill="none" stroke="#fc3c44" stroke-width="1.2" stroke-linecap="round"/>'; | |
| function BTTInitialized() { | |
| initialized = true; | |
| refresh(); | |
| refreshVolume(); | |
| setInterval(refresh, 2000); | |
| setInterval(refreshVolume, 3000); | |
| } | |
| async function refresh() { | |
| if (!initialized) return; | |
| const info = document.getElementById('trackInfo'); | |
| const nameText = document.getElementById('trackNameText'); | |
| const artistEl = document.getElementById('trackArtist'); | |
| const icon = document.getElementById('playIcon'); | |
| const indicator = document.getElementById('playingIndicator'); | |
| try { | |
| let result = await runShellScript({ | |
| script: 'osascript -e \'tell application "Music"\' -e \'set p to "0"\' -e \'set t to ""\' -e \'set a to ""\' -e \'set sh to shuffle enabled\' -e \'set r to song repeat as string\' -e \'if player state is playing then\' -e \'set p to "1"\' -e \'end if\' -e \'try\' -e \'set t to name of current track\' -e \'set a to artist of current track\' -e \'end try\' -e \'return p & "|||" & t & "|||" & a & "|||" & sh & "|||" & r\' -e \'end tell\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| let parts = result.trim().split('|||'); | |
| isPlaying = parts[0] === '1'; | |
| let title = parts[1] || ''; | |
| let artist = parts[2] || ''; | |
| shuffleOn = parts[3] === 'true'; | |
| repeatMode = (parts[4] || 'off').trim(); | |
| if (title.length > 0) { | |
| nameText.textContent = title; | |
| artistEl.textContent = artist; | |
| info.classList.remove('idle'); | |
| } else { | |
| nameText.textContent = 'Not Playing'; | |
| artistEl.textContent = ''; | |
| info.classList.add('idle'); | |
| } | |
| icon.innerHTML = '<path d="' + (isPlaying ? PAUSE_PATH : PLAY_PATH) + '"/>'; | |
| indicator.classList.toggle('active', isPlaying); | |
| document.getElementById('shuffleBtn').classList.toggle('active', shuffleOn); | |
| let rb = document.getElementById('repeatBtn'); | |
| rb.classList.toggle('active', repeatMode !== 'off'); | |
| rb.classList.toggle('repeat-one', repeatMode === 'one'); | |
| } catch (e) { | |
| try { | |
| let title = await get_string_variable({variable_name: 'BTTNowPlayingInfoTitle'}); | |
| let artist = await get_string_variable({variable_name: 'BTTNowPlayingInfoArtist'}); | |
| let playing = await get_number_variable({variable_name: 'BTTCurrentlyPlaying'}); | |
| isPlaying = playing === 1; | |
| if (title && title.length > 0) { | |
| nameText.textContent = title; | |
| artistEl.textContent = artist || ''; | |
| info.classList.remove('idle'); | |
| } else { | |
| nameText.textContent = 'Not Playing'; | |
| artistEl.textContent = ''; | |
| info.classList.add('idle'); | |
| } | |
| icon.innerHTML = '<path d="' + (isPlaying ? PAUSE_PATH : PLAY_PATH) + '"/>'; | |
| indicator.classList.toggle('active', isPlaying); | |
| } catch (e2) {} | |
| } | |
| } | |
| async function refreshVolume() { | |
| if (!initialized) return; | |
| try { | |
| let result = await runShellScript({ | |
| script: 'osascript -e \'get volume settings\' | sed -n \'s/.*output volume:\\([0-9]*\\).*output muted:\\(.*\\)/\\1|||\\2/p\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| let parts = result.trim().split('|||'); | |
| currentVol = parseInt(parts[0]) || 0; | |
| isMuted = parts[1] === 'true'; | |
| document.getElementById('volLabel').textContent = isMuted ? 'M' : currentVol; | |
| document.getElementById('volIcon').innerHTML = isMuted ? VOL_MUTED : VOL_ON; | |
| document.getElementById('muteBtn').classList.toggle('muted', isMuted); | |
| } catch (e) {} | |
| } | |
| async function changeVolume(delta) { | |
| if (!initialized) return; | |
| currentVol = Math.max(0, Math.min(100, currentVol + delta)); | |
| document.getElementById('volLabel').textContent = currentVol; | |
| if (isMuted) { | |
| isMuted = false; | |
| document.getElementById('volIcon').innerHTML = VOL_ON; | |
| document.getElementById('muteBtn').classList.remove('muted'); | |
| } | |
| try { | |
| await runShellScript({ | |
| script: 'osascript -e \'set volume output volume ' + currentVol + '\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| } catch (e) {} | |
| } | |
| function volUp() { changeVolume(10); } | |
| function volDown() { changeVolume(-10); } | |
| async function toggleMute() { | |
| if (!initialized) return; | |
| isMuted = !isMuted; | |
| document.getElementById('volIcon').innerHTML = isMuted ? VOL_MUTED : VOL_ON; | |
| document.getElementById('muteBtn').classList.toggle('muted', isMuted); | |
| document.getElementById('volLabel').textContent = isMuted ? 'M' : document.getElementById('volSlider').value; | |
| try { | |
| await runShellScript({ | |
| script: 'osascript -e \'set volume output muted ' + isMuted + '\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| } catch (e) {} | |
| } | |
| async function toggleShuffle() { | |
| if (!initialized) return; | |
| shuffleOn = !shuffleOn; | |
| document.getElementById('shuffleBtn').classList.toggle('active', shuffleOn); | |
| try { | |
| await runShellScript({ | |
| script: 'osascript -e \'tell application "Music" to set shuffle enabled to ' + shuffleOn + '\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| } catch (e) {} | |
| } | |
| async function toggleRepeat() { | |
| if (!initialized) return; | |
| if (repeatMode === 'off') repeatMode = 'all'; | |
| else if (repeatMode === 'all') repeatMode = 'one'; | |
| else repeatMode = 'off'; | |
| let rb = document.getElementById('repeatBtn'); | |
| rb.classList.toggle('active', repeatMode !== 'off'); | |
| rb.classList.toggle('repeat-one', repeatMode === 'one'); | |
| try { | |
| await runShellScript({ | |
| script: 'osascript -e \'tell application "Music" to set song repeat to ' + repeatMode + '\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| } catch (e) {} | |
| } | |
| async function cmd(script) { | |
| if (!initialized) return; | |
| try { | |
| await trigger_action({json: JSON.stringify({ | |
| BTTPredefinedActionType: 172, | |
| BTTInlineAppleScript: script | |
| })}); | |
| setTimeout(refresh, 350); | |
| } catch (e) {} | |
| } | |
| async function scrub(seconds) { | |
| if (!initialized) return; | |
| try { | |
| await runShellScript({ | |
| script: 'osascript -e \'tell application "Music" to set player position to (player position + ' + seconds + ')\'', | |
| launchPath: '/bin/bash', | |
| parameters: '-c' | |
| }); | |
| } catch (e) {} | |
| } | |
| function playPause() { cmd('tell application "Music" to playpause'); } | |
| function prevTrack() { cmd('tell application "Music" to back track'); } | |
| function nextTrack() { cmd('tell application "Music" to next track'); } | |
| setTimeout(function() { | |
| if (!initialized) { | |
| initialized = true; | |
| refresh(); | |
| refreshVolume(); | |
| setInterval(refresh, 3000); | |
| setInterval(refreshVolume, 3000); | |
| } | |
| }, 2000); | |
| </script> | |
| </body> | |
| </html> |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
dark minimal floating widget for apple music on btt. always on top, draggable, works on all spaces.
what it does:
setup:
uses btt's runShellScript bridge + osascript to talk to apple music.
.bttpreset included but you'll need to manually add the webview item and point it to the html file.