inital commit
This commit is contained in:
442
main.js
Normal file
442
main.js
Normal file
@@ -0,0 +1,442 @@
|
||||
const { app, BrowserWindow, ipcMain, screen } = require('electron');
|
||||
const { exec } = require('child_process');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
|
||||
let win;
|
||||
let lastSong = "";
|
||||
let lastPaused = true;
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
checkNowPlaying();
|
||||
setInterval(checkNowPlaying, 2500);
|
||||
|
||||
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 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) => {
|
||||
// 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 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>
|
||||
`;
|
||||
}
|
||||
Reference in New Issue
Block a user