Compare commits

..

2 Commits

Author SHA1 Message Date
Brandon4466
9dcf3c237a Merge branch 'master' of https://git.bbrunson.com/brandon/spotify-gui-electron 2025-07-25 16:11:51 -07:00
Brandon4466
427d855e59 added touch support, changed appearance, new icons 2025-07-25 16:11:37 -07:00
10 changed files with 833 additions and 338 deletions

View File

@@ -1,17 +0,0 @@
on run argv
set var to item 1 of argv
if var is "playpause" then
using terms from application "Spotify"
tell application "Spotify" to playpause
end using terms from
else if var is "next" then
using terms from application "Spotify"
tell application "Spotify" to next track
end using terms from
else if var is "previous" then
using terms from application "Spotify"
tell application "Spotify" to previous track
end using terms from
end if
end run

View File

@@ -1,22 +0,0 @@
#!/bin/bash
# Get the currently playing Spotify track title
process_info=$(osascript -e 'tell application "Spotify" to if player state is playing then name of current track & " - " & artist of current track')
if [[ -n "$process_info" ]]; then
if [[ "$process_info" =~ ^(.*)\ -\ (.*)$ ]]; then
artist="${BASH_REMATCH[2]}"
songTitle="${BASH_REMATCH[1]}"
else
artist="Unknown"
songTitle="$process_info"
fi
else
artist=""
songTitle=""
fi
# Create JSON output
json_output=$(jq -n --arg title "$songTitle" --arg artist "$artist" --arg amUri "" '{title: $title, artist: $artist, amUri: $amUri}')
echo "$json_output"

228
index.html Normal file
View File

@@ -0,0 +1,228 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Now Playing</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #181c24;
color: #fff;
height: 100vh;
width: 100vw;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
.main-container {
display: flex;
flex-direction: row;
align-items: center;
/* Remove background, border-radius, box-shadow, and padding */
/* min-width: 1600px;
max-width: 1600px; */
min-height: 400px;
gap: 80px;
position: relative;
z-index: 1;
}
.album-art {
width: 320px;
height: 320px;
border-radius: 24px;
object-fit: cover;
background: #333;
box-shadow: 0 8px 32px rgba(0,0,0,0.25);
transition: background 1s;
}
.info-section {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center; /* Center content horizontally */
width: 800px; /* Fixed width for info-section */
min-width: 800px;
max-width: 800px;
word-break: break-word;
/* Remove min-width and max-width to allow full width usage */
}
.song-title {
font-size: 3.2rem;
font-weight: 700;
margin-bottom: 2px;
color: #fff;
letter-spacing: 1px;
text-shadow: 0 4px 16px rgba(0,0,0,0.18);
text-align: center; /* Center text */
}
.artist-name {
font-size: 2.2rem;
color: #b0b8c1;
margin-bottom: 96px;
font-weight: 500;
text-align: center; /* Center text */
}
.controls {
display: flex;
flex-direction: row;
gap: 56px;
margin-top: 16px;
justify-content: space-between; /* Spread controls out horizontally */
width: 100%; /* Take full width of info-section */
max-width: 500px; /* Optional: limit max width for better spacing */
}
.controls button {
background: none;
border: none;
color: #fff;
font-size: 5rem;
border-radius: 0;
width: auto;
height: auto;
cursor: pointer;
box-shadow: none;
padding: 0 16px;
transition: color 0.2s, transform 0.1s;
}
.controls button img {
filter: invert(1) brightness(2);
transition: filter 0.2s;
}
.controls button:hover {
color: #b0b8c1;
background: none;
transform: scale(1.15);
}
.controls button:hover img {
filter: invert(0.8) brightness(2.5) drop-shadow(0 0 8px #fff2);
}
/* Top right window controls */
.window-controls {
position: fixed;
top: 12px;
right: 12px;
z-index: 100;
display: flex;
gap: 8px;
}
.window-controls button {
background: none;
border: none;
color: #fff;
font-size: 1.2rem;
cursor: pointer;
opacity: 0.7;
transition: opacity 0.2s;
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
}
.window-controls button:hover {
opacity: 1;
}
/* Hide window controls by default */
.window-controls {
opacity: 0;
pointer-events: none;
transition: opacity 0.3s;
}
body.hovering .window-controls {
opacity: 1;
pointer-events: auto;
}
.background-blur {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 0;
background-size: cover;
background-position: center;
filter: blur(36px) brightness(0.25) saturate(1.2);
transition: background-image 1s;
}
</style>
</head>
<body>
<div class="background-blur" id="backgroundBlur"></div>
<div class="window-controls">
<button onclick="minimizeApp()" title="Minimize"></button>
<button onclick="closeApp()" title="Close"></button>
</div>
<div class="main-container">
<img class="album-art" id="albumArt" src="https://via.placeholder.com/320" alt="Album Art">
<div class="info-section">
<div class="song-title" id="songTitle">Loading...</div>
<div class="artist-name" id="songArtist">&nbsp;</div>
<div class="controls">
<button onclick="sendControl('previous')" title="Previous">
<img src="previous.svg" alt="Previous" style="width: 56px; height: 56px; vertical-align: middle;">
</button>
<button id="playPauseButton" onclick="sendControl('playpause')" title="Play/Pause">
<img id="playPauseIcon" src="play.svg" alt="Play/Pause" style="width: 64px; height: 64px; vertical-align: middle;">
</button>
<button onclick="sendControl('next')" title="Next">
<img src="skip.svg" alt="Next" style="width: 56px; height: 56px; vertical-align: middle;">
</button>
</div>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
ipcRenderer.on('song-update', (event, data) => {
const songTitleElem = document.getElementById('songTitle');
const songArtistElem = document.getElementById('songArtist');
const albumArtElem = document.getElementById('albumArt');
const playPauseButton = document.getElementById('playPauseButton');
const playPauseIcon = document.getElementById('playPauseIcon');
const backgroundBlur = document.getElementById('backgroundBlur');
if (data.paused) {
playPauseIcon.src = "play.svg";
} else {
playPauseIcon.src = "pause.svg";
songTitleElem.innerText = data.title ?? "Unknown Title";
songArtistElem.innerText = data.artist ?? "Unknown Artist";
albumArtElem.src = data.albumArt ?? "https://via.placeholder.com/320";
if (backgroundBlur && data.albumArt) {
backgroundBlur.style.backgroundImage = `url('${data.albumArt}')`;
}
}
});
function sendControl(command) {
ipcRenderer.send('media-control', command);
}
function closeApp() {
ipcRenderer.send('close-app');
}
function minimizeApp() {
ipcRenderer.send('minimize-app');
}
// Show window controls only when mouse is over the window
document.body.addEventListener('mouseenter', () => {
document.body.classList.add('hovering');
});
document.body.addEventListener('mouseleave', () => {
document.body.classList.remove('hovering');
});
// Add touch event listeners to controls so touch does not move the Windows mouse
function addTouchListeners() {
const controls = document.querySelectorAll('.controls button');
controls.forEach(btn => {
btn.addEventListener('touchstart', (e) => {
e.preventDefault(); // Prevent mouse event emulation
btn.click(); // Trigger the same logic as a mouse click
}, { passive: false });
});
}
window.addEventListener('DOMContentLoaded', addTouchListeners);
</script>
</body>
</html>

542
main-old.js Normal file
View File

@@ -0,0 +1,542 @@
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const { exec } = require('child_process');
const https = require('https');
const http = require('http');
const path = require('path');
let win;
let lastSong = "";
let lastPaused = true;
app.whenReady().then(() => {
createWindow();
checkNowPlaying();
setInterval(checkNowPlaying, 2500);
// Start polling for Plex now-playing details
checkPlexNowPlaying();
setInterval(checkPlexNowPlaying, 3000);
// Start an HTTP server to receive notification requests (POST /notify)
http.createServer((req, res) => {
if(req.method === 'POST' && req.url === '/notify'){
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const notifData = JSON.parse(body);
if(win){
win.webContents.send('notification', notifData);
}
res.writeHead(200, {"Content-Type": "application/json"});
res.end(JSON.stringify({status:"ok"}));
} catch(e){
console.error("Notification parse error:", e);
res.writeHead(400);
res.end();
}
});
} else {
res.writeHead(404);
res.end();
}
}).listen(3000, () => {
console.log("Notification server listening on port 3000");
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
});
function createWindow() {
const displays = screen.getAllDisplays();
const externalDisplay = displays[3] || displays[0];
const x = externalDisplay.bounds.x;
const y = externalDisplay.bounds.y;
win = new BrowserWindow({
x: x,
y: y,
fullscreen: true,
webPreferences: {
nodeIntegration: true,
contextIsolation: false,
}
});
win.removeMenu();
// win.webContents.openDevTools();
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML()));
win.on('closed', () => {
win = null;
});
}
async function checkNowPlaying() {
const result = await checkOwnSong();
if (result.updated && win) {
win.webContents.send('song-update', result.data);
}
}
function checkOwnSong() {
return new Promise((resolve) => {
exec(`powershell -ExecutionPolicy Bypass -File get-media.ps1`, async (error, stdout) => {
if (error) {
console.error("PowerShell error:", error);
resolve({ updated: false });
return;
}
try {
const data = JSON.parse(stdout);
// If there's NO valid data => user is paused/stopped
if (!data.title && !data.artist) {
// If we weren't already paused, now we are => update
if (!lastPaused) {
lastPaused = true;
resolve({
updated: true,
data: { paused: true }
});
} else {
// Still paused, no change
resolve({ updated: false });
}
} else {
// We DO have valid song data => the user is playing something
const currentSong = `${data.title} - ${data.artist}`;
// If we were paused, or the song changed, send an update
if (lastPaused || currentSong !== lastSong) {
lastPaused = false;
lastSong = currentSong;
const albumArt = await fetchAlbumArt(data.title, data.artist);
resolve({
updated: true,
data: {
paused: false,
title: data.title,
artist: data.artist,
albumArt
}
});
} else {
// No change in paused/playing state, no change in track
resolve({ updated: false });
}
}
} catch (err) {
console.error("JSON parse error:", stdout, err);
resolve({ updated: false });
}
});
});
}
function checkPlexNowPlaying() {
const plexToken = "kbGwoiA_QEGzw7MgSZrY"; // <-- replace with your Plex token
const plexHost = "spyro.corp.bbrunson.com"; // <-- adjust if needed
const url = `http://${plexHost}:32400/status/sessions?X-Plex-Token=${plexToken}`;
http.get(url, (res) => {
let data = "";
res.on('data', chunk => data += chunk);
res.on('end', () => {
// For simplicity we check for a known tag in the XML response.
let nowPlaying = "No media playing";
if(data.includes("MediaContainer") && data.includes("Video")) {
nowPlaying = "Plex is playing media";
}
if(win){
win.webContents.send('plex-update', { details: nowPlaying });
}
});
}).on('error', err => {
console.error("Error fetching Plex now playing:", err);
});
}
function fetchAlbumArt(title, artist) {
return new Promise((resolve) => {
const query = encodeURIComponent(`${artist} ${title}`);
const url = `https://itunes.apple.com/search?term=${query}&limit=1`;
https.get(url, (res) => {
let body = '';
res.on('data', chunk => (body += chunk));
res.on('end', () => {
try {
const results = JSON.parse(body).results;
if (results.length > 0) {
// Replace 100x100 with 300x300 for better quality
const art = results[0].artworkUrl100.replace('100x100bb', '300x300bb');
resolve(art);
} else {
resolve("https://via.placeholder.com/100");
}
} catch (e) {
console.error("Album art fetch error:", e);
resolve("https://via.placeholder.com/100");
}
});
}).on('error', () => {
resolve("https://via.placeholder.com/100");
});
});
}
ipcMain.on('media-control', (event, command) => {
if (command) {
exec(`MediaControl.exe ${command}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error running MediaControl.exe:`, error);
} else {
console.log(`Media command executed: ${stdout || command}`);
}
});
}
});
ipcMain.on('minimize-app', (event) => {
const window = BrowserWindow.getFocusedWindow();
if (window) window.minimize();
});
ipcMain.on('close-app', (event) => {
win.close();
// or app.quit();
});
function getMainPageHTML() {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Now Playing</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #121212;
color: #ffffff;
overflow: hidden;
}
/* Close and minimize buttons styles... */
.close-button,
.minimize-button {
position: fixed;
top: 10px;
background: none;
border: none;
color: #ffffff;
font-size: 24px;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
z-index: 9999;
pointer-events: auto;
}
.minimize-button { right: 50px; }
.close-button { right: 10px; }
body:hover .close-button,
body:hover .minimize-button {
opacity: 1;
}
/* Top bar (Plex or Notification) */
#plexSection {
position: fixed;
top: 0;
width: 100%;
background: #333;
color: #fff;
padding: 10px;
text-align: center;
z-index: 1000;
}
/* Container */
.container {
position: relative;
width: 100vw;
height: 100vh;
}
/* Card styles note the constant translate for centering */
.card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -60%);
background-color: #1e1e1e;
border-radius: 16px;
padding: 50px 60px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
min-width: 700px;
max-width: 900px;
transition: top 0.3s ease;
}
/* New fade animations (only opacity changes) */
@keyframes fadeOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.fade-out {
animation: fadeOut 0.3s ease-out forwards;
}
.fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
/* Show lyrics state */
.container.show-lyrics .card {
top: 20%;
transform: translate(-50%, -20%);
}
/* Info and album art */
.info {
display: flex;
align-items: center;
gap: 40px;
transition: transform 0.3s ease;
}
body:hover .info { transform: translateY(-20px); }
.album-art {
width: 150px;
height: 150px;
background: #333;
border-radius: 12px;
object-fit: cover;
}
.text-info {
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
}
.title { font-size: 32px; font-weight: bold; margin-bottom: 8px; }
.artist { font-size: 28px; color: #bbbbbb; }
/* Controls */
.controls {
display: flex;
gap: 20px;
opacity: 0;
pointer-events: none;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
body:hover .controls {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.controls button {
background-color: #333;
border: none;
color: white;
padding: 10px 16px;
font-size: 24px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.controls button:hover { background-color: #555; }
/* Lyrics */
.lyrics {
position: absolute;
bottom: 100px;
width: 100%;
text-align: center;
font-size: 24px;
color: #bbbbbb;
opacity: 0;
transition: opacity 0.3s ease;
}
.container.show-lyrics .lyrics { opacity: 1; }
/* Arrow button */
.arrow-button {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: none;
border: none;
font-size: 36px;
color: #ffffff;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
}
body:hover .arrow-button { opacity: 0; }
/* Notification overlay */
.notification-overlay {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
color: #fff;
text-align: center;
z-index: 10000;
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.notification-overlay.show {
opacity: 1;
pointer-events: auto;
}
</style>
</head>
<body>
<!-- Top bar for Plex Now Playing or Notification -->
<div id="plexSection">
Plex Now Playing: <span id="plexNowPlaying">Loading...</span>
</div>
<!-- Fixed minimize and close buttons -->
<button class="minimize-button" onclick="minimizeApp()"></button>
<button class="close-button" onclick="closeApp()">✕</button>
<div class="container" id="container" style="padding-top: 60px;">
<div class="card" id="card">
<div class="info">
<img class="album-art" id="albumArt" src="https://via.placeholder.com/150" alt="Album Art">
<div class="text-info">
<div class="title" id="songTitle">Loading...</div>
<div class="artist" id="songArtist"></div>
</div>
</div>
<div class="controls">
<button onclick="sendControl('previous')">⏮</button>
<button id="playPauseButton" onclick="sendControl('playpause')">⏯</button>
<button onclick="sendControl('next')">⏭</button>
</div>
</div>
<div class="lyrics" id="lyricsLine">
“Hello, is it me youre looking for?”
</div>
<button class="arrow-button" id="arrowButton" onclick="toggleLyrics()">▲</button>
</div>
<div class="notification-overlay" id="notificationOverlay">
<div class="notification-content">
<h1 id="notificationTitle"></h1>
<p id="notificationMessage"></p>
</div>
</div>
<script>
const { ipcRenderer } = require('electron');
let notificationActive = false;
let notificationTimeoutId = null;
ipcRenderer.on('song-update', (event, data) => {
const songTitleElem = document.getElementById('songTitle');
const songArtistElem = document.getElementById('songArtist');
const albumArtElem = document.getElementById('albumArt');
const playPauseButton = document.getElementById('playPauseButton');
const cardElem = document.getElementById('card');
if (data.paused) {
playPauseButton.textContent = "▶️";
} else {
playPauseButton.textContent = "⏸️";
// Start fade-out animation
cardElem.classList.remove('fade-in', 'fade-out');
cardElem.classList.add('fade-out');
cardElem.addEventListener('animationend', function handler(e) {
if (e.animationName === 'fadeOut') {
// Update the content once fade-out completes
songTitleElem.innerText = data.title ?? "Unknown Title";
songArtistElem.innerText = data.artist ?? "Unknown Artist";
albumArtElem.src = data.albumArt ?? "https://via.placeholder.com/150";
// Fade the card in with the new data
cardElem.classList.remove('fade-out');
cardElem.classList.add('fade-in');
cardElem.removeEventListener('animationend', handler);
}
});
}
});
ipcRenderer.on('notification', (event, data) => {
const overlay = document.getElementById('notificationOverlay');
const titleElem = document.getElementById('notificationTitle');
const messageElem = document.getElementById('notificationMessage');
titleElem.innerText = data.title || "Notification";
messageElem.innerText = data.message || "";
overlay.classList.add('show');
// Hide the overlay after 5 seconds (unchanged)
setTimeout(() => {
overlay.classList.remove('show');
}, 5000);
// Show notification on top bar for 60 seconds and reposition card
const plexSection = document.getElementById('plexSection');
notificationActive = true;
plexSection.innerHTML = (data.title || "Notification") + ' - ' + (data.message || "");
plexSection.style.display = "block";
// Clear any pre-existing timer for top bar notification
if (notificationTimeoutId) {
clearTimeout(notificationTimeoutId);
}
notificationTimeoutId = setTimeout(() => {
plexSection.style.display = "none";
notificationActive = false;
// Re-position the card in the middle of the screen
const cardElem = document.getElementById('card');
cardElem.style.top = "50%";
cardElem.style.transform = "translate(-50%, -60%)";
}, 60000);
});
// Prevent plex updates while a notification is actively displayed on the top bar
ipcRenderer.on('plex-update', (event, data) => {
if(!notificationActive){
const plexElem = document.getElementById('plexNowPlaying');
plexElem.textContent = data.details;
}
});
function sendControl(command) {
ipcRenderer.send('media-control', command);
}
function closeApp() {
ipcRenderer.send('close-app');
}
function minimizeApp() {
ipcRenderer.send('minimize-app');
}
function toggleLyrics() {
const container = document.getElementById('container');
const arrowBtn = document.getElementById('arrowButton');
container.classList.toggle('show-lyrics');
arrowBtn.textContent = container.classList.contains('show-lyrics') ? '▼' : '▲';
}
</script>
</body>
</html>
`;
}

329
main.js
View File

@@ -1,29 +1,45 @@
const { app, BrowserWindow, ipcMain, screen } = require('electron');
const { exec } = require('child_process');
const https = require('https');
const http = require('http');
const path = require('path');
let win;
let lastSong = "";
let lastPaused = true;
if (process.platform === 'win32') {
getMediaCommand = 'powershell -ExecutionPolicy Bypass -File get-media.ps1';
mediaControlCommand = 'MediaControl.exe';
} else if (process.platform === 'darwin') {
getMediaCommand = './get-media.sh'; // Adjust this path/command to your macOS script
mediaControlCommand = 'osascript MediaControl.scpt';
} else {
console.error("Unsupported OS");
resolve({ updated: false });
return;
}
app.whenReady().then(() => {
createWindow();
checkNowPlaying();
setInterval(checkNowPlaying, 2500);
// Start an HTTP server to receive notification requests (POST /notify)
http.createServer((req, res) => {
if(req.method === 'POST' && req.url === '/notify'){
let body = '';
req.on('data', chunk => body += chunk);
req.on('end', () => {
try {
const notifData = JSON.parse(body);
if(win){
win.webContents.send('notification', notifData);
}
res.writeHead(200, {"Content-Type": "application/json"});
res.end(JSON.stringify({status:"ok"}));
} catch(e){
console.error("Notification parse error:", e);
res.writeHead(400);
res.end();
}
});
} else {
res.writeHead(404);
res.end();
}
}).listen(3000, () => {
console.log("Notification server listening on port 3000");
});
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
@@ -49,7 +65,7 @@ function createWindow() {
win.removeMenu();
// win.webContents.openDevTools();
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML()));
win.loadFile(path.join(__dirname, 'index.html'));
win.on('closed', () => {
win = null;
});
@@ -64,7 +80,7 @@ async function checkNowPlaying() {
function checkOwnSong() {
return new Promise((resolve) => {
exec(getMediaCommand, async (error, stdout) => {
exec(`powershell -ExecutionPolicy Bypass -File get-media.ps1`, async (error, stdout) => {
if (error) {
console.error("PowerShell error:", error);
resolve({ updated: false });
@@ -119,7 +135,6 @@ function checkOwnSong() {
});
}
function fetchAlbumArt(title, artist) {
return new Promise((resolve) => {
const query = encodeURIComponent(`${artist} ${title}`);
@@ -151,7 +166,7 @@ function fetchAlbumArt(title, artist) {
ipcMain.on('media-control', (event, command) => {
if (command) {
exec(`${mediaControlCommand} ${command}`, (error, stdout, stderr) => {
exec(`MediaControl.exe ${command}`, (error, stdout, stderr) => {
if (error) {
console.error(`Error running MediaControl.exe:`, error);
} else {
@@ -167,288 +182,6 @@ ipcMain.on('minimize-app', (event) => {
});
ipcMain.on('close-app', (event) => {
// If you have a reference to the BrowserWindow (e.g. mainWindow),
// you can do something like:
win.close();
// or app.quit();
});
function getMainPageHTML() {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Now Playing</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #121212;
color: #ffffff;
overflow: hidden; /* hide scrollbars if the card moves */
}
/* Close and minimize buttons (top-right of the window) */
.close-button,
.minimize-button {
position: fixed;
top: 10px;
background: none;
border: none;
color: #ffffff;
font-size: 24px;
cursor: pointer;
opacity: 0; /* hidden by default */
transition: opacity 0.3s ease;
z-index: 9999; /* ensure it's on top */
pointer-events: auto; /* ensure clickability */
}
.minimize-button {
right: 50px; /* place it left of the close button */
}
.close-button {
right: 10px;
}
/* Show the buttons when hovering anywhere on the body */
body:hover .close-button,
body:hover .minimize-button {
opacity: 1;
}
/* Container that spans the full window */
.container {
position: relative;
width: 100vw;
height: 100vh;
}
/* The card is centered and will have transitions */
.card {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #1e1e1e;
border-radius: 16px;
padding: 50px 60px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
/* Center content vertically and horizontally */
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0;
min-width: 700px;
max-width: 900px;
/* We'll allow a smooth transition for the content inside it */
transition: top 0.3s ease, transform 0.3s ease;
}
/* When .container has class .show-lyrics, move the card closer to the top. */
.container.show-lyrics .card {
top: 20%;
transform: translate(-50%, -20%);
}
/* The info section (album art + text) */
.info {
display: flex;
align-items: center;
gap: 40px;
transition: transform 0.3s ease;
}
/* Slide the .info section up when the body is hovered */
body:hover .info {
transform: translateY(-20px);
}
.album-art {
width: 150px;
height: 150px;
background: #333;
border-radius: 12px;
object-fit: cover;
}
.text-info {
display: flex;
flex-direction: column;
justify-content: center;
text-align: left;
}
.title {
font-size: 32px;
font-weight: bold;
margin-bottom: 8px;
}
.artist {
font-size: 28px;
color: #bbbbbb;
}
/* Hide controls by default; show them on hover of the body */
.controls {
display: flex;
gap: 20px;
/* Hidden initially */
opacity: 0;
pointer-events: none;
transform: translateY(20px);
transition: opacity 0.3s ease, transform 0.3s ease;
}
body:hover .controls {
opacity: 1;
pointer-events: auto;
transform: translateY(0);
}
.controls button {
background-color: #333;
border: none;
color: white;
padding: 10px 16px;
font-size: 24px;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.2s ease;
}
.controls button:hover {
background-color: #555;
}
/* Single-line lyrics container at the bottom (hidden by default). */
.lyrics {
position: absolute;
bottom: 100px; /* push it above the arrow */
width: 100%;
text-align: center;
font-size: 24px;
color: #bbbbbb;
opacity: 0;
transition: opacity 0.3s ease;
}
/* When .container is .show-lyrics, reveal the lyrics. */
.container.show-lyrics .lyrics {
opacity: 1;
}
/* The arrow button at the bottom center of the window. */
.arrow-button {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: none;
border: none;
font-size: 36px;
color: #ffffff;
cursor: pointer;
opacity: 0;
transition: opacity 0.3s ease;
}
/* Show the arrow button when hovering anywhere on body */
body:hover .arrow-button {
// opacity: 1;
opacity: 0;
}
</style>
</head>
<body>
<!-- Fixed minimize and close buttons in top-right -->
<button class="minimize-button" onclick="minimizeApp()"></button>
<button class="close-button" onclick="closeApp()">✕</button>
<div class="container" id="container">
<div class="card">
<div class="info">
<img class="album-art" id="albumArt" src="https://via.placeholder.com/150" alt="Album Art">
<div class="text-info">
<div class="title" id="songTitle">Loading...</div>
<div class="artist" id="songArtist"></div>
</div>
</div>
<div class="controls">
<button onclick="sendControl('previous')">⏮</button>
<button id="playPauseButton" onclick="sendControl('playpause')">⏯</button>
<button onclick="sendControl('next')">⏭</button>
</div>
</div>
<!-- Single line of lyrics that appears in the bottom half -->
<div class="lyrics" id="lyricsLine">
“Hello, is it me youre looking for?”
</div>
<!-- Arrow at the bottom -->
<button class="arrow-button" id="arrowButton" onclick="toggleLyrics()">▲</button>
</div>
<script>
const { ipcRenderer } = require('electron');
ipcRenderer.on('song-update', (event, data) => {
// Elements on the UI
const songTitleElem = document.getElementById('songTitle');
const songArtistElem = document.getElementById('songArtist');
const albumArtElem = document.getElementById('albumArt');
const playPauseButton = document.getElementById('playPauseButton');
if (data.paused) {
// Player is paused => show "Play" button
playPauseButton.textContent = "⏵︎";
} else {
// Player is playing => show "Pause" button and update track info
playPauseButton.textContent = "⏸︎";
songTitleElem.innerText = data.title ?? "Unknown Title";
songArtistElem.innerText = data.artist ?? "Unknown Artist";
albumArtElem.src = data.albumArt ?? "https://via.placeholder.com/150";
}
});
// Sends commands (previous, playpause, next) to main process
function sendControl(command) {
ipcRenderer.send('media-control', command);
}
function closeApp() {
ipcRenderer.send('close-app');
}
function minimizeApp() {
ipcRenderer.send('minimize-app');
}
// Toggle lyrics arrow logic
function toggleLyrics() {
const container = document.getElementById('container');
const arrowBtn = document.getElementById('arrowButton');
container.classList.toggle('show-lyrics');
if (container.classList.contains('show-lyrics')) {
arrowBtn.textContent = '▼';
} else {
arrowBtn.textContent = '▲';
}
}
</script>
</body>
</html>
`;
}

1
pause.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M48 64C21.5 64 0 85.5 0 112L0 400c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48L48 64zm192 0c-26.5 0-48 21.5-48 48l0 288c0 26.5 21.5 48 48 48l32 0c26.5 0 48-21.5 48-48l0-288c0-26.5-21.5-48-48-48l-32 0z"/></svg>

After

Width:  |  Height:  |  Size: 460 B

1
play.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 384 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80L0 432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z"/></svg>

After

Width:  |  Height:  |  Size: 379 B

27
plex.py Normal file
View File

@@ -0,0 +1,27 @@
// Start polling for Plex now-playing details
checkPlexNowPlaying();
setInterval(checkPlexNowPlaying, 3000);
function checkPlexNowPlaying() {
const plexToken = "kbGwoiA_QEGzw7MgSZrY"; // <-- replace with your Plex token
const plexHost = "spyro.corp.bbrunson.com"; // <-- adjust if needed
const url = `http://${plexHost}:32400/status/sessions?X-Plex-Token=${plexToken}`;
http.get(url, (res) => {
let data = "";
res.on('data', chunk => data += chunk);
res.on('end', () => {
// For simplicity we check for a known tag in the XML response.
let nowPlaying = "No media playing";
if(data.includes("MediaContainer") && data.includes("Video")) {
nowPlaying = "Plex is playing media";
}
if(win){
win.webContents.send('plex-update', { details: nowPlaying });
}
});
}).on('error', err => {
console.error("Error fetching Plex now playing:", err);
});
}

1
previous.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M459.5 440.6c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29l0-320c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4L288 214.3l0 41.7 0 41.7L459.5 440.6zM256 352l0-96 0-128 0-32c0-12.4-7.2-23.7-18.4-29s-24.5-3.6-34.1 4.4l-192 160C4.2 237.5 0 246.5 0 256s4.2 18.5 11.5 24.6l192 160c9.5 7.9 22.8 9.7 34.1 4.4s18.4-16.6 18.4-29l0-64z"/></svg>

After

Width:  |  Height:  |  Size: 550 B

1
skip.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc.--><path d="M52.5 440.6c-9.5 7.9-22.8 9.7-34.1 4.4S0 428.4 0 416L0 96C0 83.6 7.2 72.3 18.4 67s24.5-3.6 34.1 4.4L224 214.3l0 41.7 0 41.7L52.5 440.6zM256 352l0-96 0-128 0-32c0-12.4 7.2-23.7 18.4-29s24.5-3.6 34.1 4.4l192 160c7.3 6.1 11.5 15.1 11.5 24.6s-4.2 18.5-11.5 24.6l-192 160c-9.5 7.9-22.8 9.7-34.1 4.4s-18.4-16.6-18.4-29l0-64z"/></svg>

After

Width:  |  Height:  |  Size: 549 B