Feature: Custom poster for standby

This commit is contained in:
Dejvino 2026-01-03 22:56:30 +00:00
parent e7931174de
commit 03474298a9
4 changed files with 106 additions and 37 deletions

View File

@ -1,5 +1,5 @@
const DB_NAME = 'PartyMediaDB'; const DB_NAME = 'PartyMediaDB';
const DB_VERSION = 1; const DB_VERSION = 2;
export const MediaStorage = { export const MediaStorage = {
open: () => { open: () => {
@ -9,6 +9,7 @@ export const MediaStorage = {
const db = e.target.result; const db = e.target.result;
if (!db.objectStoreNames.contains('music')) db.createObjectStore('music'); if (!db.objectStoreNames.contains('music')) db.createObjectStore('music');
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes'); if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
}; };
request.onsuccess = (e) => resolve(e.target.result); request.onsuccess = (e) => resolve(e.target.result);
request.onerror = (e) => reject(e); request.onerror = (e) => reject(e);
@ -42,10 +43,24 @@ export const MediaStorage = {
req.onerror = () => resolve([]); req.onerror = () => resolve([]);
}); });
}, },
savePoster: async (file) => {
const db = await MediaStorage.open();
const tx = db.transaction('poster', 'readwrite');
tx.objectStore('poster').put(file, 'currentPoster');
},
getPoster: async () => {
const db = await MediaStorage.open();
return new Promise((resolve) => {
const req = db.transaction('poster', 'readonly').objectStore('poster').get('currentPoster');
req.onsuccess = () => resolve(req.result);
req.onerror = () => resolve(null);
});
},
clear: async () => { clear: async () => {
const db = await MediaStorage.open(); const db = await MediaStorage.open();
const tx = db.transaction(['music', 'tapes'], 'readwrite'); const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite');
tx.objectStore('music').clear(); tx.objectStore('music').clear();
tx.objectStore('tapes').clear(); tx.objectStore('tapes').clear();
tx.objectStore('poster').clear();
} }
}; };

View File

@ -136,6 +136,40 @@ export class ConfigUI extends SceneFeature {
}); });
statusContainer.appendChild(this.tapeList); statusContainer.appendChild(this.tapeList);
// Load Poster Button
const loadPosterBtn = document.createElement('button');
loadPosterBtn.innerText = 'Load Poster';
Object.assign(loadPosterBtn.style, {
marginTop: '10px',
padding: '8px',
cursor: 'pointer',
backgroundColor: '#555',
color: 'white',
border: 'none',
borderRadius: '4px',
fontSize: '14px'
});
const posterInput = document.createElement('input');
posterInput.type = 'file';
posterInput.accept = 'image/*';
posterInput.style.display = 'none';
posterInput.onchange = (e) => {
const file = e.target.files[0];
if (file) {
if (state.posterImage) URL.revokeObjectURL(state.posterImage);
state.posterImage = URL.createObjectURL(file);
MediaStorage.savePoster(file);
showStandbyScreen();
}
};
document.body.appendChild(posterInput);
loadPosterBtn.onclick = () => {
posterInput.click();
};
statusContainer.appendChild(loadPosterBtn);
// Load Tapes Button // Load Tapes Button
const loadTapesBtn = document.createElement('button'); const loadTapesBtn = document.createElement('button');
loadTapesBtn.id = 'loadTapeButton'; loadTapesBtn.id = 'loadTapeButton';
@ -235,6 +269,11 @@ export class ConfigUI extends SceneFeature {
} }
} }
if (state.posterImage) {
URL.revokeObjectURL(state.posterImage);
state.posterImage = null;
}
showStandbyScreen(); showStandbyScreen();
const defaults = { const defaults = {
@ -259,6 +298,14 @@ export class ConfigUI extends SceneFeature {
document.body.appendChild(container); document.body.appendChild(container);
this.container = container; this.container = container;
this.updateStatus(); this.updateStatus();
// Restore poster
MediaStorage.getPoster().then(file => {
if (file) {
state.posterImage = URL.createObjectURL(file);
showStandbyScreen();
}
});
} }
updateStatus() { updateStatus() {

View File

@ -298,47 +298,53 @@ export class ProjectionScreen extends SceneFeature {
export function showStandbyScreen() { export function showStandbyScreen() {
if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false; if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false;
const canvas = document.createElement('canvas'); let texture;
canvas.width = 1024;
canvas.height = 576;
const ctx = canvas.getContext('2d');
// Draw Color Bars if (state.posterImage) {
const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff']; texture = new THREE.TextureLoader().load(state.posterImage);
const barWidth = canvas.width / colors.length; } else {
colors.forEach((color, i) => { const canvas = document.createElement('canvas');
ctx.fillStyle = color; canvas.width = 1024;
ctx.fillRect(i * barWidth, 0, barWidth, canvas.height); canvas.height = 576;
}); const ctx = canvas.getContext('2d');
// Semi-transparent overlay // Draw Color Bars
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)'; const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff'];
ctx.fillRect(0, 0, canvas.width, canvas.height); const barWidth = canvas.width / colors.length;
colors.forEach((color, i) => {
ctx.fillStyle = color;
ctx.fillRect(i * barWidth, 0, barWidth, canvas.height);
});
// Text settings // Semi-transparent overlay
ctx.fillStyle = '#ffffff'; ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
ctx.textAlign = 'center'; ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.textBaseline = 'middle';
// Song Title // Text settings
let text = "PLEASE STAND BY"; ctx.fillStyle = '#ffffff';
if (state.music && state.music.songTitle) { ctx.textAlign = 'center';
text = state.music.songTitle.toUpperCase(); ctx.textBaseline = 'middle';
// Song Title
let text = "PLEASE STAND BY";
if (state.music && state.music.songTitle) {
text = state.music.songTitle.toUpperCase();
}
ctx.font = 'bold 60px monospace';
if (ctx.measureText(text).width > canvas.width * 0.9) {
ctx.font = 'bold 40px monospace';
}
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - 20);
// Subtext
ctx.font = '30px monospace';
ctx.fillStyle = '#cccccc';
ctx.fillText("WAITING FOR PARTY START", canvas.width / 2, canvas.height / 2 + 50);
texture = new THREE.CanvasTexture(canvas);
} }
ctx.font = 'bold 60px monospace';
if (ctx.measureText(text).width > canvas.width * 0.9) {
ctx.font = 'bold 40px monospace';
}
ctx.fillText(text, canvas.width / 2, canvas.height / 2 - 20);
// Subtext
ctx.font = '30px monospace';
ctx.fillStyle = '#cccccc';
ctx.fillText("WAITING FOR PARTY START", canvas.width / 2, canvas.height / 2 + 50);
const texture = new THREE.CanvasTexture(canvas);
const material = new THREE.MeshBasicMaterial({ const material = new THREE.MeshBasicMaterial({
map: texture, map: texture,
side: THREE.DoubleSide side: THREE.DoubleSide

View File

@ -44,6 +44,7 @@ export function initState() {
videoUrls: [], videoUrls: [],
videoFilenames: [], videoFilenames: [],
currentVideoIndex: -1, currentVideoIndex: -1,
posterImage: null,
// Scene constants // Scene constants
originalLampIntensity: 0.3, originalLampIntensity: 0.3,