Feature: Custom poster for standby
This commit is contained in:
parent
e7931174de
commit
03474298a9
@ -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();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -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() {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user