Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dcf3c237a | ||
|
|
427d855e59 |
228
index.html
Normal file
228
index.html
Normal 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"> </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
542
main-old.js
Normal 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 you’re 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
313
main.js
313
main.js
@@ -1,6 +1,7 @@
|
|||||||
const { app, BrowserWindow, ipcMain, screen } = require('electron');
|
const { app, BrowserWindow, ipcMain, screen } = require('electron');
|
||||||
const { exec } = require('child_process');
|
const { exec } = require('child_process');
|
||||||
const https = require('https');
|
const https = require('https');
|
||||||
|
const http = require('http');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
|
||||||
let win;
|
let win;
|
||||||
@@ -12,6 +13,33 @@ app.whenReady().then(() => {
|
|||||||
checkNowPlaying();
|
checkNowPlaying();
|
||||||
setInterval(checkNowPlaying, 2500);
|
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', () => {
|
app.on('activate', () => {
|
||||||
if (BrowserWindow.getAllWindows().length === 0) {
|
if (BrowserWindow.getAllWindows().length === 0) {
|
||||||
createWindow();
|
createWindow();
|
||||||
@@ -37,7 +65,7 @@ function createWindow() {
|
|||||||
|
|
||||||
win.removeMenu();
|
win.removeMenu();
|
||||||
// win.webContents.openDevTools();
|
// win.webContents.openDevTools();
|
||||||
win.loadURL('data:text/html;charset=UTF-8,' + encodeURIComponent(getMainPageHTML()));
|
win.loadFile(path.join(__dirname, 'index.html'));
|
||||||
win.on('closed', () => {
|
win.on('closed', () => {
|
||||||
win = null;
|
win = null;
|
||||||
});
|
});
|
||||||
@@ -107,7 +135,6 @@ function checkOwnSong() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function fetchAlbumArt(title, artist) {
|
function fetchAlbumArt(title, artist) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const query = encodeURIComponent(`${artist} ${title}`);
|
const query = encodeURIComponent(`${artist} ${title}`);
|
||||||
@@ -155,288 +182,6 @@ ipcMain.on('minimize-app', (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('close-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();
|
win.close();
|
||||||
// or app.quit();
|
// 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 you’re 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
1
pause.svg
Normal 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
1
play.svg
Normal 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
27
plex.py
Normal 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
1
previous.svg
Normal 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
1
skip.svg
Normal 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 |
Reference in New Issue
Block a user