From cf6dda2d35c6023cdc0d3b34929d808f9065f986 Mon Sep 17 00:00:00 2001 From: Dejvino Date: Mon, 5 Jan 2026 21:17:12 +0000 Subject: [PATCH] Feature: success and failure toast and greener buttons --- party-stage/src/core/media-storage.js | 22 ++++++++++++-- party-stage/src/core/ui-utils.js | 34 ++++++++++++++++++++++ party-stage/src/core/video-player.js | 8 +++-- party-stage/src/scene/config-ui.js | 20 +++++++++++-- party-stage/src/scene/music-player.js | 42 ++++++++++++++++++++++++--- 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 party-stage/src/core/ui-utils.js diff --git a/party-stage/src/core/media-storage.js b/party-stage/src/core/media-storage.js index 32c6685..080c0a5 100644 --- a/party-stage/src/core/media-storage.js +++ b/party-stage/src/core/media-storage.js @@ -1,8 +1,10 @@ const DB_NAME = 'PartyMediaDB'; const DB_VERSION = 2; +let dbInstance = null; export const MediaStorage = { open: () => { + if (dbInstance) return Promise.resolve(dbInstance); return new Promise((resolve, reject) => { const request = indexedDB.open(DB_NAME, DB_VERSION); request.onupgradeneeded = (e) => { @@ -11,14 +13,23 @@ export const MediaStorage = { 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) => { + dbInstance = e.target.result; + dbInstance.onclose = () => { dbInstance = null; }; + dbInstance.onversionchange = () => { if (dbInstance) dbInstance.close(); dbInstance = null; }; + resolve(dbInstance); + }; request.onerror = (e) => reject(e); }); }, saveMusic: async (file) => { const db = await MediaStorage.open(); - const tx = db.transaction('music', 'readwrite'); - tx.objectStore('music').put(file, 'currentSong'); + return new Promise((resolve, reject) => { + const tx = db.transaction('music', 'readwrite'); + tx.oncomplete = () => resolve(); + tx.onerror = (e) => reject(e.target.error); + tx.objectStore('music').put(file, 'currentSong'); + }); }, getMusic: async () => { const db = await MediaStorage.open(); @@ -56,6 +67,11 @@ export const MediaStorage = { req.onerror = () => resolve(null); }); }, + deletePoster: async () => { + const db = await MediaStorage.open(); + const tx = db.transaction('poster', 'readwrite'); + tx.objectStore('poster').delete('currentPoster'); + }, clear: async () => { const db = await MediaStorage.open(); const tx = db.transaction(['music', 'tapes', 'poster'], 'readwrite'); diff --git a/party-stage/src/core/ui-utils.js b/party-stage/src/core/ui-utils.js new file mode 100644 index 0000000..cc0b4ff --- /dev/null +++ b/party-stage/src/core/ui-utils.js @@ -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); +} \ No newline at end of file diff --git a/party-stage/src/core/video-player.js b/party-stage/src/core/video-player.js index 5fc9f5c..12ed2dc 100644 --- a/party-stage/src/core/video-player.js +++ b/party-stage/src/core/video-player.js @@ -3,6 +3,7 @@ import { state } from '../state.js'; import { turnTvScreenOff, turnTvScreenOn, showStandbyScreen } from '../scene/projection-screen.js'; import sceneFeatureManager from '../scene/SceneFeatureManager.js'; import { MediaStorage } from './media-storage.js'; +import { showToast } from './ui-utils.js'; // Register a feature to handle party start sceneFeatureManager.register({ @@ -178,6 +179,9 @@ export function loadVideoFile(event) { } processVideoFiles(files); + if (state.videoUrls.length > 0) { + showToast(`Loaded ${state.videoUrls.length} tapes`, 'success'); + } MediaStorage.saveTapes(files); } @@ -190,7 +194,7 @@ function processVideoFiles(files) { // 2. Populate the new videoUrls array for (let i = 0; i < files.length; 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.videoFilenames.push(file.name); } @@ -198,6 +202,7 @@ function processVideoFiles(files) { if (state.videoUrls.length === 0) { console.info('No valid video files selected.'); + showToast('Error: No valid video files loaded.', 'error'); return; } @@ -212,7 +217,6 @@ function processVideoFiles(files) { startVideoPlayback(); } else { console.info("Tapes loaded. Waiting for party start..."); - if (state.loadTapeButton) state.loadTapeButton.innerText = "Tapes Ready"; showStandbyScreen(); } } \ No newline at end of file diff --git a/party-stage/src/scene/config-ui.js b/party-stage/src/scene/config-ui.js index cf3fdda..62dc434 100644 --- a/party-stage/src/scene/config-ui.js +++ b/party-stage/src/scene/config-ui.js @@ -4,6 +4,7 @@ import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; import { MediaStorage } from '../core/media-storage.js'; import { showStandbyScreen } from './projection-screen.js'; +import { showToast } from '../core/ui-utils.js'; export class ConfigUI extends SceneFeature { constructor() { @@ -357,12 +358,23 @@ export class ConfigUI extends SceneFeature { state.posterImage = URL.createObjectURL(file); MediaStorage.savePoster(file); showStandbyScreen(); + this.updateStatus(); + showToast('Poster loaded', 'success'); } }; document.body.appendChild(posterInput); loadPosterBtn.onclick = () => { - posterInput.click(); + if (state.posterImage) { + URL.revokeObjectURL(state.posterImage); + state.posterImage = null; + MediaStorage.deletePoster(); + showStandbyScreen(); + this.updateStatus(); + posterInput.value = ''; + } else { + posterInput.click(); + } }; this.loadPosterBtn = loadPosterBtn; statusContainer.appendChild(loadPosterBtn); @@ -512,6 +524,7 @@ export class ConfigUI extends SceneFeature { if (file) { state.posterImage = URL.createObjectURL(file); showStandbyScreen(); + this.updateStatus(); } }); } @@ -566,9 +579,12 @@ export class ConfigUI extends SceneFeature { if (this.loadPosterBtn) { this.loadPosterBtn.style.backgroundColor = state.posterImage ? green : orange; + this.loadPosterBtn.innerText = state.posterImage ? 'Clear Poster' : 'Load Poster'; } 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 diff --git a/party-stage/src/scene/music-player.js b/party-stage/src/scene/music-player.js index cf528bd..5c601e5 100644 --- a/party-stage/src/scene/music-player.js +++ b/party-stage/src/scene/music-player.js @@ -3,6 +3,7 @@ import { SceneFeature } from './SceneFeature.js'; import sceneFeatureManager from './SceneFeatureManager.js'; import { showStandbyScreen } from './projection-screen.js'; import { MediaStorage } from '../core/media-storage.js'; +import { showToast } from '../core/ui-utils.js'; export class MusicPlayer extends SceneFeature { constructor() { @@ -24,18 +25,40 @@ export class MusicPlayer extends SceneFeature { state.music.loudness = 0; state.music.loudnessAverage = 0; 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 if (loadButton) loadButton.style.display = 'none'; - fileInput.addEventListener('change', (event) => { + fileInput.onchange = (event) => { const file = event.target.files[0]; if (file) { + console.info(`[MusicPlayer] File selected from input: ${file.name} (${file.size} bytes)`); + 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); + 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', () => { this.stopParty(); @@ -46,12 +69,18 @@ export class MusicPlayer extends SceneFeature { // Restore from storage MediaStorage.getMusic().then(file => { if (file) { + console.info(`[MusicPlayer] Restored music from storage: ${file.name} (${file.size} bytes)`); this.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 if (!this.audioContext) { this.audioContext = new (window.AudioContext || window.webkitAudioContext)(); @@ -68,8 +97,13 @@ export class MusicPlayer extends SceneFeature { const songName = file.name.replace(/\.[^/.]+$/, ""); 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); state.music.player.src = url; + console.info(`[MusicPlayer] Audio source set to blob URL: ${url}`); // Update Config UI const configUI = sceneFeatureManager.features.find(f => f.constructor.name === 'ConfigUI');