tilting + background play fixes
This commit is contained in:
parent
0ac73db811
commit
c8f64a52e8
5 changed files with 158 additions and 2 deletions
26
index.html
26
index.html
|
|
@ -3748,6 +3748,32 @@
|
||||||
<span class="slider"></span>
|
<span class="slider"></span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Fullscreen Cover Tilt</span>
|
||||||
|
<span class="description"
|
||||||
|
>3D tilt effect on album cover in fullscreen view</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<label class="toggle-switch">
|
||||||
|
<input type="checkbox" id="fullscreen-tilt-toggle" checked />
|
||||||
|
<span class="slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="setting-item">
|
||||||
|
<div class="info">
|
||||||
|
<span class="label">Preload Next Track</span>
|
||||||
|
<span class="description">Seconds before track ends to start loading next</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
id="preload-time-input"
|
||||||
|
min="5"
|
||||||
|
max="60"
|
||||||
|
value="15"
|
||||||
|
style="width: 60px"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="settings-group">
|
<div class="settings-group">
|
||||||
|
|
|
||||||
30
js/player.js
30
js/player.js
|
|
@ -16,6 +16,7 @@ import {
|
||||||
exponentialVolumeSettings,
|
exponentialVolumeSettings,
|
||||||
audioEffectsSettings,
|
audioEffectsSettings,
|
||||||
radioSettings,
|
radioSettings,
|
||||||
|
playbackSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { audioContextManager } from './audio-context.js';
|
import { audioContextManager } from './audio-context.js';
|
||||||
import { isIos, isSafari } from './platform-detection.js';
|
import { isIos, isSafari } from './platform-detection.js';
|
||||||
|
|
@ -48,6 +49,7 @@ export class Player {
|
||||||
this.repeatMode = REPEAT_MODE.OFF;
|
this.repeatMode = REPEAT_MODE.OFF;
|
||||||
this.preloadCache = new Map();
|
this.preloadCache = new Map();
|
||||||
this.preloadAbortController = null;
|
this.preloadAbortController = null;
|
||||||
|
this._lastPreloadTime = null;
|
||||||
this.currentTrack = null;
|
this.currentTrack = null;
|
||||||
this.currentRgValues = null;
|
this.currentRgValues = null;
|
||||||
this.userVolume = parseFloat(localStorage.getItem('volume') || '0.7');
|
this.userVolume = parseFloat(localStorage.getItem('volume') || '0.7');
|
||||||
|
|
@ -106,7 +108,6 @@ export class Player {
|
||||||
bufferingGoal: 30,
|
bufferingGoal: 30,
|
||||||
rebufferingGoal: 2,
|
rebufferingGoal: 2,
|
||||||
bufferBehind: 30,
|
bufferBehind: 30,
|
||||||
jumpLargeGaps: true,
|
|
||||||
},
|
},
|
||||||
abr: {
|
abr: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
@ -150,7 +151,6 @@ export class Player {
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
const el = this.activeElement;
|
const el = this.activeElement;
|
||||||
if (document.visibilityState === 'visible' && !el.paused) {
|
if (document.visibilityState === 'visible' && !el.paused) {
|
||||||
// Ensure audio context is resumed when user returns to the app
|
|
||||||
if (!audioContextManager.isReady()) {
|
if (!audioContextManager.isReady()) {
|
||||||
audioContextManager.init(el);
|
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();
|
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) {
|
async setupHlsVideo(video, result, fallbackImg) {
|
||||||
const url = result.videoUrl || result.hlsUrl || result;
|
const url = result.videoUrl || result.hlsUrl || result;
|
||||||
const Hls = (await import('hls.js')).default;
|
const Hls = (await import('hls.js')).default;
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import {
|
||||||
visualizerSettings,
|
visualizerSettings,
|
||||||
playlistSettings,
|
playlistSettings,
|
||||||
equalizerSettings,
|
equalizerSettings,
|
||||||
|
playbackSettings,
|
||||||
listenBrainzSettings,
|
listenBrainzSettings,
|
||||||
malojaSettings,
|
malojaSettings,
|
||||||
libreFmSettings,
|
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
|
// ReplayGain Settings
|
||||||
const replayGainMode = document.getElementById('replay-gain-mode');
|
const replayGainMode = document.getElementById('replay-gain-mode');
|
||||||
if (replayGainMode) {
|
if (replayGainMode) {
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
export const equalizerSettings = {
|
||||||
ENABLED_KEY: 'equalizer-enabled',
|
ENABLED_KEY: 'equalizer-enabled',
|
||||||
GAINS_KEY: 'equalizer-gains',
|
GAINS_KEY: 'equalizer-gains',
|
||||||
|
|
|
||||||
52
js/ui.js
52
js/ui.js
|
|
@ -26,6 +26,7 @@ import {
|
||||||
fontSettings,
|
fontSettings,
|
||||||
contentBlockingSettings,
|
contentBlockingSettings,
|
||||||
settingsUiState,
|
settingsUiState,
|
||||||
|
playbackSettings,
|
||||||
} from './storage.js';
|
} from './storage.js';
|
||||||
import { db } from './db.js';
|
import { db } from './db.js';
|
||||||
import { getVibrantColorFromImage } from './vibrant-color.js';
|
import { getVibrantColorFromImage } from './vibrant-color.js';
|
||||||
|
|
@ -148,6 +149,9 @@ export class UIRenderer {
|
||||||
this.lastRecommendedTracks = [];
|
this.lastRecommendedTracks = [];
|
||||||
this.currentArtistId = null;
|
this.currentArtistId = null;
|
||||||
|
|
||||||
|
this._handleTiltMove = this._handleTiltMove.bind(this);
|
||||||
|
this._handleTiltLeave = this._handleTiltLeave.bind(this);
|
||||||
|
|
||||||
// Listen for dynamic color reset events
|
// Listen for dynamic color reset events
|
||||||
window.addEventListener('reset-dynamic-color', () => {
|
window.addEventListener('reset-dynamic-color', () => {
|
||||||
this.resetVibrantColor();
|
this.resetVibrantColor();
|
||||||
|
|
@ -1225,6 +1229,14 @@ export class UIRenderer {
|
||||||
|
|
||||||
overlay.style.display = 'flex';
|
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 () => {
|
const startVisualizer = async () => {
|
||||||
if (!visualizerSettings.isEnabled()) {
|
if (!visualizerSettings.isEnabled()) {
|
||||||
if (this.visualizer) this.visualizer.stop();
|
if (this.visualizer) this.visualizer.stop();
|
||||||
|
|
@ -1318,6 +1330,46 @@ export class UIRenderer {
|
||||||
clearTimeout(this.uiToggleMouseTimer);
|
clearTimeout(this.uiToggleMouseTimer);
|
||||||
this.uiToggleMouseTimer = null;
|
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) {
|
setupUIToggleButton(overlay) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue