Feature: success and failure toast and greener buttons
This commit is contained in:
parent
89dc5db53c
commit
cf6dda2d35
@ -1,8 +1,10 @@
|
|||||||
const DB_NAME = 'PartyMediaDB';
|
const DB_NAME = 'PartyMediaDB';
|
||||||
const DB_VERSION = 2;
|
const DB_VERSION = 2;
|
||||||
|
let dbInstance = null;
|
||||||
|
|
||||||
export const MediaStorage = {
|
export const MediaStorage = {
|
||||||
open: () => {
|
open: () => {
|
||||||
|
if (dbInstance) return Promise.resolve(dbInstance);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
||||||
request.onupgradeneeded = (e) => {
|
request.onupgradeneeded = (e) => {
|
||||||
@ -11,14 +13,23 @@ export const MediaStorage = {
|
|||||||
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
if (!db.objectStoreNames.contains('tapes')) db.createObjectStore('tapes');
|
||||||
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
|
if (!db.objectStoreNames.contains('poster')) db.createObjectStore('poster');
|
||||||
};
|
};
|
||||||
request.onsuccess = (e) => resolve(e.target.result);
|
request.onsuccess = (e) => {
|
||||||
|
dbInstance = e.target.result;
|
||||||
|
dbInstance.onclose = () => { dbInstance = null; };
|
||||||
|
dbInstance.onversionchange = () => { if (dbInstance) dbInstance.close(); dbInstance = null; };
|
||||||
|
resolve(dbInstance);
|
||||||
|
};
|
||||||
request.onerror = (e) => reject(e);
|
request.onerror = (e) => reject(e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
saveMusic: async (file) => {
|
saveMusic: async (file) => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
const tx = db.transaction('music', 'readwrite');
|
const tx = db.transaction('music', 'readwrite');
|
||||||
|
tx.oncomplete = () => resolve();
|
||||||
|
tx.onerror = (e) => reject(e.target.error);
|
||||||
tx.objectStore('music').put(file, 'currentSong');
|
tx.objectStore('music').put(file, 'currentSong');
|
||||||
|
});
|
||||||
},
|
},
|
||||||
getMusic: async () => {
|
getMusic: async () => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
@ -56,6 +67,11 @@ export const MediaStorage = {
|
|||||||
req.onerror = () => resolve(null);
|
req.onerror = () => resolve(null);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
deletePoster: async () => {
|
||||||
|
const db = await MediaStorage.open();
|
||||||
|
const tx = db.transaction('poster', 'readwrite');
|
||||||
|
tx.objectStore('poster').delete('currentPoster');
|
||||||
|
},
|
||||||
clear: async () => {
|
clear: async () => {
|
||||||
const db = await MediaStorage.open();
|
const db = await MediaStorage.open();
|
||||||
const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite');
|
const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite');
|
||||||
|
|||||||
34
party-stage/src/core/ui-utils.js
Normal file
34
party-stage/src/core/ui-utils.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
export function showToast(message, type = 'info') {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.innerText = message;
|
||||||
|
let bg = '#333';
|
||||||
|
if (type === 'error') bg = '#dc3545';
|
||||||
|
else if (type === 'success') bg = '#28a745';
|
||||||
|
else if (type === 'warning') bg = '#ff9800';
|
||||||
|
|
||||||
|
Object.assign(toast.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
bottom: '30px',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
backgroundColor: bg,
|
||||||
|
color: 'white',
|
||||||
|
padding: '10px 20px',
|
||||||
|
borderRadius: '5px',
|
||||||
|
zIndex: '10000',
|
||||||
|
fontFamily: 'sans-serif',
|
||||||
|
boxShadow: '0 2px 10px rgba(0,0,0,0.5)',
|
||||||
|
opacity: '0',
|
||||||
|
transition: 'opacity 0.5s'
|
||||||
|
});
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => { toast.style.opacity = '1'; });
|
||||||
|
|
||||||
|
const duration = type === 'success' ? 1000 : 3000;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = '0';
|
||||||
|
setTimeout(() => { if (toast.parentNode) toast.parentNode.removeChild(toast); }, 500);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { state } from '../state.js';
|
|||||||
import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js';
|
import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js';
|
||||||
import sceneFeatureManager from '../scene/SceneFeatureManager.js';
|
import sceneFeatureManager from '../scene/SceneFeatureManager.js';
|
||||||
import { MediaStorage } from './media-storage.js';
|
import { MediaStorage } from './media-storage.js';
|
||||||
|
import { showToast } from './ui-utils.js';
|
||||||
|
|
||||||
// Register a feature to handle party start
|
// Register a feature to handle party start
|
||||||
sceneFeatureManager.register({
|
sceneFeatureManager.register({
|
||||||
@ -178,6 +179,9 @@ export function loadVideoFile(event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
processVideoFiles(files);
|
processVideoFiles(files);
|
||||||
|
if (state.videoUrls.length > 0) {
|
||||||
|
showToast(`Loaded ${state.videoUrls.length} tapes`, 'success');
|
||||||
|
}
|
||||||
MediaStorage.saveTapes(files);
|
MediaStorage.saveTapes(files);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +194,7 @@ function processVideoFiles(files) {
|
|||||||
// 2. Populate the new videoUrls array
|
// 2. Populate the new videoUrls array
|
||||||
for (let i = 0; i < files.length; i++) {
|
for (let i = 0; i < files.length; i++) {
|
||||||
const file = files[i];
|
const file = files[i];
|
||||||
if (file.type.startsWith('video/')) {
|
if (file.type.startsWith('video/') && file.size > 0) {
|
||||||
state.videoUrls.push(URL.createObjectURL(file));
|
state.videoUrls.push(URL.createObjectURL(file));
|
||||||
state.videoFilenames.push(file.name);
|
state.videoFilenames.push(file.name);
|
||||||
}
|
}
|
||||||
@ -198,6 +202,7 @@ function processVideoFiles(files) {
|
|||||||
|
|
||||||
if (state.videoUrls.length === 0) {
|
if (state.videoUrls.length === 0) {
|
||||||
console.info('No valid video files selected.');
|
console.info('No valid video files selected.');
|
||||||
|
showToast('Error: No valid video files loaded.', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,7 +217,6 @@ function processVideoFiles(files) {
|
|||||||
startVideoPlayback();
|
startVideoPlayback();
|
||||||
} else {
|
} else {
|
||||||
console.info("Tapes loaded. Waiting for party start...");
|
console.info("Tapes loaded. Waiting for party start...");
|
||||||
if (state.loadTapeButton) state.loadTapeButton.innerText = "Tapes Ready";
|
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,6 +4,7 @@ import { SceneFeature } from './SceneFeature.js';
|
|||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
import { MediaStorage } from '../core/media-storage.js';
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
import { showStandbyScreen } from './projection-screen.js';
|
import { showStandbyScreen } from './projection-screen.js';
|
||||||
|
import { showToast } from '../core/ui-utils.js';
|
||||||
|
|
||||||
export class ConfigUI extends SceneFeature {
|
export class ConfigUI extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -357,12 +358,23 @@ export class ConfigUI extends SceneFeature {
|
|||||||
state.posterImage = URL.createObjectURL(file);
|
state.posterImage = URL.createObjectURL(file);
|
||||||
MediaStorage.savePoster(file);
|
MediaStorage.savePoster(file);
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
|
showToast('Poster loaded', 'success');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.body.appendChild(posterInput);
|
document.body.appendChild(posterInput);
|
||||||
|
|
||||||
loadPosterBtn.onclick = () => {
|
loadPosterBtn.onclick = () => {
|
||||||
|
if (state.posterImage) {
|
||||||
|
URL.revokeObjectURL(state.posterImage);
|
||||||
|
state.posterImage = null;
|
||||||
|
MediaStorage.deletePoster();
|
||||||
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
|
posterInput.value = '';
|
||||||
|
} else {
|
||||||
posterInput.click();
|
posterInput.click();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
this.loadPosterBtn = loadPosterBtn;
|
this.loadPosterBtn = loadPosterBtn;
|
||||||
statusContainer.appendChild(loadPosterBtn);
|
statusContainer.appendChild(loadPosterBtn);
|
||||||
@ -512,6 +524,7 @@ export class ConfigUI extends SceneFeature {
|
|||||||
if (file) {
|
if (file) {
|
||||||
state.posterImage = URL.createObjectURL(file);
|
state.posterImage = URL.createObjectURL(file);
|
||||||
showStandbyScreen();
|
showStandbyScreen();
|
||||||
|
this.updateStatus();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -566,9 +579,12 @@ export class ConfigUI extends SceneFeature {
|
|||||||
|
|
||||||
if (this.loadPosterBtn) {
|
if (this.loadPosterBtn) {
|
||||||
this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange;
|
this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange;
|
||||||
|
this.loadPosterBtn.innerText = state.posterImage ? 'Clear Poster' : 'Load Poster';
|
||||||
}
|
}
|
||||||
if (state.loadTapeButton) {
|
if (state.loadTapeButton) {
|
||||||
state.loadTapeButton.style.backgroundColor = (state.videoUrls && state.videoUrls.length > 0) ? green : orange;
|
const hasTapes = state.videoUrls && state.videoUrls.length > 0;
|
||||||
|
state.loadTapeButton.style.backgroundColor = hasTapes ? green : orange;
|
||||||
|
state.loadTapeButton.innerText = hasTapes ? 'Change Tapes' : 'Load Tapes';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update Tape List
|
// Update Tape List
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { SceneFeature } from './SceneFeature.js';
|
|||||||
import sceneFeatureManager from './SceneFeatureManager.js';
|
import sceneFeatureManager from './SceneFeatureManager.js';
|
||||||
import { showStandbyScreen } from './projection-screen.js';
|
import { showStandbyScreen } from './projection-screen.js';
|
||||||
import { MediaStorage } from '../core/media-storage.js';
|
import { MediaStorage } from '../core/media-storage.js';
|
||||||
|
import { showToast } from '../core/ui-utils.js';
|
||||||
|
|
||||||
export class MusicPlayer extends SceneFeature {
|
export class MusicPlayer extends SceneFeature {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -24,18 +25,40 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
state.music.loudness = 0;
|
state.music.loudness = 0;
|
||||||
state.music.loudnessAverage = 0;
|
state.music.loudnessAverage = 0;
|
||||||
const loadButton = document.getElementById('loadMusicButton');
|
const loadButton = document.getElementById('loadMusicButton');
|
||||||
const fileInput = document.getElementById('musicFileInput');
|
|
||||||
|
let fileInput = document.getElementById('musicFileInput');
|
||||||
|
if (!fileInput) {
|
||||||
|
fileInput = document.createElement('input');
|
||||||
|
fileInput.id = 'musicFileInput';
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = 'audio/*';
|
||||||
|
fileInput.style.display = 'none';
|
||||||
|
document.body.appendChild(fileInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset value to allow re-selecting the same file
|
||||||
|
fileInput.onclick = () => { fileInput.value = ''; };
|
||||||
|
|
||||||
// Hide the big start button as we use ConfigUI now
|
// Hide the big start button as we use ConfigUI now
|
||||||
if (loadButton) loadButton.style.display = 'none';
|
if (loadButton) loadButton.style.display = 'none';
|
||||||
|
|
||||||
fileInput.addEventListener('change', (event) => {
|
fileInput.onchange = (event) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
this.loadMusicFile(file);
|
console.info(`[MusicPlayer] File selected from input: ${file.name} (${file.size} bytes)`);
|
||||||
MediaStorage.saveMusic(file);
|
if (file.size === 0) {
|
||||||
|
console.warn('[MusicPlayer] Selected file is empty (0 bytes). Check if file is a cloud placeholder or locked.');
|
||||||
|
showToast(`Error: "${file.name}" is empty (0 bytes). Looks like the browser can't access it.`, 'error');
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.loadMusicFile(file);
|
||||||
|
MediaStorage.saveMusic(file).then(() => showToast(`Loaded "${file.name}"`, 'success'))
|
||||||
|
.catch(e => {
|
||||||
|
console.warn('[MusicPlayer] Failed to save music:', e);
|
||||||
|
showToast('Warning: Failed to save song to storage.', 'warning');
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
state.music.player.addEventListener('ended', () => {
|
state.music.player.addEventListener('ended', () => {
|
||||||
this.stopParty();
|
this.stopParty();
|
||||||
@ -46,12 +69,18 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
// Restore from storage
|
// Restore from storage
|
||||||
MediaStorage.getMusic().then(file => {
|
MediaStorage.getMusic().then(file => {
|
||||||
if (file) {
|
if (file) {
|
||||||
|
console.info(`[MusicPlayer] Restored music from storage: ${file.name} (${file.size} bytes)`);
|
||||||
this.loadMusicFile(file);
|
this.loadMusicFile(file);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
loadMusicFile(file) {
|
loadMusicFile(file) {
|
||||||
|
if (!file || file.size === 0) {
|
||||||
|
console.warn('[MusicPlayer] loadMusicFile called with invalid or empty file.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Setup Web Audio API if not already done
|
// Setup Web Audio API if not already done
|
||||||
if (!this.audioContext) {
|
if (!this.audioContext) {
|
||||||
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
this.audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
@ -68,8 +97,13 @@ export class MusicPlayer extends SceneFeature {
|
|||||||
const songName = file.name.replace(/\.[^/.]+$/, "");
|
const songName = file.name.replace(/\.[^/.]+$/, "");
|
||||||
state.music.songTitle = songName;
|
state.music.songTitle = songName;
|
||||||
|
|
||||||
|
if (state.music.player.src && state.music.player.src.startsWith('blob:')) {
|
||||||
|
URL.revokeObjectURL(state.music.player.src);
|
||||||
|
}
|
||||||
|
|
||||||
const url = URL.createObjectURL(file);
|
const url = URL.createObjectURL(file);
|
||||||
state.music.player.src = url;
|
state.music.player.src = url;
|
||||||
|
console.info(`[MusicPlayer] Audio source set to blob URL: ${url}`);
|
||||||
|
|
||||||
// Update Config UI
|
// Update Config UI
|
||||||
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user