Feature: Custom poster for standby
This commit is contained in:
parent
e7931174de
commit
03474298a9
@ -1,5 +1,5 @@
|
||||
const DB_NAME = 'PartyMediaDB';
|
||||
const DB_VERSION = 1;
|
||||
const DB_VERSION = 2;
|
||||
|
||||
export const MediaStorage = {
|
||||
open: () => {
|
||||
@ -9,6 +9,7 @@ export const MediaStorage = {
|
||||
const db = e.target.result;
|
||||
if (!db.objectStoreNames.contains('music')) db.createObjectStore('music');
|
||||
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
||||
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
|
||||
};
|
||||
request.onsuccess = (e) => resolve(e.target.result);
|
||||
request.onerror = (e) => reject(e);
|
||||
@ -42,10 +43,24 @@ export const MediaStorage = {
|
||||
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 () => {
|
||||
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('tapes').clear();
|
||||
tx.objectStore('poster').clear();
|
||||
}
|
||||
};
|
||||
@ -136,6 +136,40 @@ export class ConfigUI extends SceneFeature {
|
||||
});
|
||||
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
|
||||
const loadTapesBtn = document.createElement('button');
|
||||
loadTapesBtn.id = 'loadTapeButton';
|
||||
@ -235,6 +269,11 @@ export class ConfigUI extends SceneFeature {
|
||||
}
|
||||
}
|
||||
|
||||
if (state.posterImage) {
|
||||
URL.revokeObjectURL(state.posterImage);
|
||||
state.posterImage = null;
|
||||
}
|
||||
|
||||
showStandbyScreen();
|
||||
|
||||
const defaults = {
|
||||
@ -259,6 +298,14 @@ export class ConfigUI extends SceneFeature {
|
||||
document.body.appendChild(container);
|
||||
this.container = container;
|
||||
this.updateStatus();
|
||||
|
||||
// Restore poster
|
||||
MediaStorage.getPoster().then(file => {
|
||||
if (file) {
|
||||
state.posterImage = URL.createObjectURL(file);
|
||||
showStandbyScreen();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
updateStatus() {
|
||||
|
||||
@ -298,47 +298,53 @@ export class ProjectionScreen extends SceneFeature {
|
||||
export function showStandbyScreen() {
|
||||
if (projectionScreenInstance) projectionScreenInstance.isVisualizerActive = false;
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 576;
|
||||
const ctx = canvas.getContext('2d');
|
||||
let texture;
|
||||
|
||||
// Draw Color Bars
|
||||
const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff'];
|
||||
const barWidth = canvas.width / colors.length;
|
||||
colors.forEach((color, i) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * barWidth, 0, barWidth, canvas.height);
|
||||
});
|
||||
if (state.posterImage) {
|
||||
texture = new THREE.TextureLoader().load(state.posterImage);
|
||||
} else {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1024;
|
||||
canvas.height = 576;
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
// Semi-transparent overlay
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
// Draw Color Bars
|
||||
const colors = ['#ffffff', '#ffff00', '#00ffff', '#00ff00', '#ff00ff', '#ff0000', '#0000ff'];
|
||||
const barWidth = canvas.width / colors.length;
|
||||
colors.forEach((color, i) => {
|
||||
ctx.fillStyle = color;
|
||||
ctx.fillRect(i * barWidth, 0, barWidth, canvas.height);
|
||||
});
|
||||
|
||||
// Text settings
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
// Semi-transparent overlay
|
||||
ctx.fillStyle = 'rgba(0, 0, 0, 0.7)';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// Song Title
|
||||
let text = "PLEASE STAND BY";
|
||||
if (state.music && state.music.songTitle) {
|
||||
text = state.music.songTitle.toUpperCase();
|
||||
// Text settings
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.textAlign = 'center';
|
||||
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({
|
||||
map: texture,
|
||||
side: THREE.DoubleSide
|
||||
|
||||
@ -44,6 +44,7 @@ export function initState() {
|
||||
videoUrls: [],
|
||||
videoFilenames: [],
|
||||
currentVideoIndex: -1,
|
||||
posterImage: null,
|
||||
|
||||
// Scene constants
|
||||
originalLampIntensity: 0.3,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user