From c8f64a52e838c2e56a8fa15904a7db4f9c7b21d3 Mon Sep 17 00:00:00 2001 From: edideaur Date: Wed, 1 Apr 2026 14:47:37 +0000 Subject: [PATCH] tilting + background play fixes --- index.html | 26 +++++++++++++++++++++++++ js/player.js | 30 +++++++++++++++++++++++++++-- js/settings.js | 22 +++++++++++++++++++++ js/storage.js | 30 +++++++++++++++++++++++++++++ js/ui.js | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 158 insertions(+), 2 deletions(-) diff --git a/index.html b/index.html index 9cf8929..e90deb6 100644 --- a/index.html +++ b/index.html @@ -3748,6 +3748,32 @@ +
+
+ Fullscreen Cover Tilt + 3D tilt effect on album cover in fullscreen view +
+ +
+
+
+ Preload Next Track + Seconds before track ends to start loading next +
+ +
diff --git a/js/player.js b/js/player.js index de0b850..4e0ed01 100644 --- a/js/player.js +++ b/js/player.js @@ -16,6 +16,7 @@ import { exponentialVolumeSettings, audioEffectsSettings, radioSettings, + playbackSettings, } from './storage.js'; import { audioContextManager } from './audio-context.js'; import { isIos, isSafari } from './platform-detection.js'; @@ -48,6 +49,7 @@ export class Player { this.repeatMode = REPEAT_MODE.OFF; this.preloadCache = new Map(); this.preloadAbortController = null; + this._lastPreloadTime = null; this.currentTrack = null; this.currentRgValues = null; this.userVolume = parseFloat(localStorage.getItem('volume') || '0.7'); @@ -106,7 +108,6 @@ export class Player { bufferingGoal: 30, rebufferingGoal: 2, bufferBehind: 30, - jumpLargeGaps: true, }, abr: { enabled: true, @@ -150,7 +151,6 @@ export class Player { document.addEventListener('visibilitychange', () => { const el = this.activeElement; if (document.visibilityState === 'visible' && !el.paused) { - // Ensure audio context is resumed when user returns to the app if (!audioContextManager.isReady()) { audioContextManager.init(el); } @@ -162,6 +162,17 @@ export class Player { } }); + // Time-based preload trigger for Safari background playback + this._timeUpdateHandler = this._handleTimeUpdateForPreload.bind(this); + this.audio.addEventListener('timeupdate', this._timeUpdateHandler); + if (this.video) { + this.video.addEventListener('timeupdate', this._timeUpdateHandler); + } + + window.addEventListener('preload-time-change', () => { + this._lastPreloadTime = null; + }); + this._setupVideoSync(); } @@ -516,6 +527,21 @@ export class Player { } } + _handleTimeUpdateForPreload() { + const el = this.activeElement; + if (!el || !el.duration || el.paused) return; + + const preloadTime = playbackSettings.getPreloadTime(); + const timeRemaining = el.duration - el.currentTime; + if (timeRemaining <= preloadTime && timeRemaining > 0) { + const now = Date.now(); + if (!this._lastPreloadTime || now - this._lastPreloadTime > 5000) { + this._lastPreloadTime = now; + this.preloadNextTracks(); + } + } + } + async setupHlsVideo(video, result, fallbackImg) { const url = result.videoUrl || result.hlsUrl || result; const Hls = (await import('hls.js')).default; diff --git a/js/settings.js b/js/settings.js index 9323dad..7cf8e44 100644 --- a/js/settings.js +++ b/js/settings.js @@ -18,6 +18,7 @@ import { visualizerSettings, playlistSettings, equalizerSettings, + playbackSettings, listenBrainzSettings, malojaSettings, libreFmSettings, @@ -1111,6 +1112,27 @@ export async function initializeSettings(scrobbler, player, api, ui) { }); } + // Fullscreen Cover Tilt Toggle + const fullscreenTiltToggle = document.getElementById('fullscreen-tilt-toggle'); + if (fullscreenTiltToggle) { + fullscreenTiltToggle.checked = playbackSettings.isFullscreenTiltEnabled(); + fullscreenTiltToggle.addEventListener('change', (e) => { + playbackSettings.setFullscreenTiltEnabled(e.target.checked); + window.dispatchEvent(new CustomEvent('fullscreen-tilt-toggle', { detail: { enabled: e.target.checked } })); + }); + } + + // Preload Time Input + const preloadTimeInput = document.getElementById('preload-time-input'); + if (preloadTimeInput) { + preloadTimeInput.value = playbackSettings.getPreloadTime(); + preloadTimeInput.addEventListener('change', (e) => { + const val = Math.max(5, Math.min(60, parseInt(e.target.value, 10) || 15)); + playbackSettings.setPreloadTime(val); + window.dispatchEvent(new CustomEvent('preload-time-change', { detail: { seconds: val } })); + }); + } + // ReplayGain Settings const replayGainMode = document.getElementById('replay-gain-mode'); if (replayGainMode) { diff --git a/js/storage.js b/js/storage.js index 8ba7de3..e7b2035 100644 --- a/js/storage.js +++ b/js/storage.js @@ -999,6 +999,36 @@ export const visualizerSettings = { }, }; +export const playbackSettings = { + FULLSCREEN_TILT_KEY: 'playback-fullscreen-tilt', + PRELOAD_TIME_KEY: 'playback-preload-time', + + isFullscreenTiltEnabled() { + try { + return localStorage.getItem(this.FULLSCREEN_TILT_KEY) !== 'false'; + } catch { + return true; + } + }, + + setFullscreenTiltEnabled(enabled) { + localStorage.setItem(this.FULLSCREEN_TILT_KEY, enabled ? 'true' : 'false'); + }, + + getPreloadTime() { + try { + const val = localStorage.getItem(this.PRELOAD_TIME_KEY); + return val ? parseInt(val, 10) : 15; + } catch { + return 15; + } + }, + + setPreloadTime(seconds) { + localStorage.setItem(this.PRELOAD_TIME_KEY, seconds.toString()); + }, +}; + export const equalizerSettings = { ENABLED_KEY: 'equalizer-enabled', GAINS_KEY: 'equalizer-gains', diff --git a/js/ui.js b/js/ui.js index b709182..34375fd 100644 --- a/js/ui.js +++ b/js/ui.js @@ -26,6 +26,7 @@ import { fontSettings, contentBlockingSettings, settingsUiState, + playbackSettings, } from './storage.js'; import { db } from './db.js'; import { getVibrantColorFromImage } from './vibrant-color.js'; @@ -148,6 +149,9 @@ export class UIRenderer { this.lastRecommendedTracks = []; this.currentArtistId = null; + this._handleTiltMove = this._handleTiltMove.bind(this); + this._handleTiltLeave = this._handleTiltLeave.bind(this); + // Listen for dynamic color reset events window.addEventListener('reset-dynamic-color', () => { this.resetVibrantColor(); @@ -1225,6 +1229,14 @@ export class UIRenderer { overlay.style.display = 'flex'; + // Apply vanilla-tilt effect to fullscreen cover if enabled + this._applyFullscreenTilt(overlay); + + // Listen for tilt setting changes + window.addEventListener('fullscreen-tilt-toggle', (e) => { + this._applyFullscreenTilt(overlay, e.detail.enabled); + }); + const startVisualizer = async () => { if (!visualizerSettings.isEnabled()) { if (this.visualizer) this.visualizer.stop(); @@ -1318,6 +1330,46 @@ export class UIRenderer { clearTimeout(this.uiToggleMouseTimer); this.uiToggleMouseTimer = null; } + + // Clean up vanilla-tilt if applied + this._removeFullscreenTilt(); + } + + _applyFullscreenTilt(overlay, enabled = playbackSettings.isFullscreenTiltEnabled()) { + const image = document.getElementById('fullscreen-cover-image'); + if (!image) return; + + this._removeFullscreenTilt(); + + if (!enabled) return; + + image.addEventListener('mousemove', this._handleTiltMove); + image.addEventListener('mouseleave', this._handleTiltLeave); + } + + _handleTiltMove(e) { + const image = e.target; + const rect = image.getBoundingClientRect(); + const x = e.clientX - rect.left; + const y = e.clientY - rect.top; + const centerX = rect.width / 2; + const centerY = rect.height / 2; + const rotateX = ((y - centerY) / centerY) * -10; + const rotateY = ((x - centerX) / centerX) * 10; + + image.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) scale(1.02)`; + } + + _handleTiltLeave(e) { + e.target.style.transform = 'perspective(1000px) rotateX(0) rotateY(0) scale(1)'; + } + + _removeFullscreenTilt() { + const image = document.getElementById('fullscreen-cover-image'); + if (!image) return; + image.removeEventListener('mousemove', this._handleTiltMove); + image.removeEventListener('mouseleave', this._handleTiltLeave); + image.style.transform = ''; } setupUIToggleButton(overlay) {