diff options
| -rw-r--r-- | web/css/player.css | 1 | ||||
| -rw-r--r-- | web/index.html | 2 | ||||
| -rw-r--r-- | web/js/app.js | 4 | ||||
| -rw-r--r-- | web/js/keyboard.js | 22 | ||||
| -rw-r--r-- | web/js/player.js | 72 |
5 files changed, 96 insertions, 5 deletions
diff --git a/web/css/player.css b/web/css/player.css index 7a8b561..c35cb07 100644 --- a/web/css/player.css +++ b/web/css/player.css @@ -130,6 +130,7 @@ .player.crop-mode video { object-fit: fill; + object-position: var(--crop-x, 50%) var(--crop-y, 50%); } .crop-indicator { diff --git a/web/index.html b/web/index.html index 6e85ec6..7b6d221 100644 --- a/web/index.html +++ b/web/index.html @@ -147,6 +147,8 @@ <div class="help-row"><span><kbd>+</kbd> <kbd>−</kbd></span><span>Zoom in / out (image viewer)</span></div> <div class="help-row"><span><kbd>Shift+S</kbd></span><span>Toggle slideshow (images)</span></div> <div class="help-row"><span><kbd>c</kbd></span><span>Toggle crop mode (fullscreen)</span></div> + <div class="help-row"><span><kbd>Shift+↑</kbd> <kbd>Shift+↓</kbd> <kbd>Shift+←</kbd> <kbd>Shift+→</kbd></span><span>Nudge crop position</span></div> + <div class="help-row"><span><kbd>x</kbd></span><span>Cycle crop anchor preset</span></div> <div class="help-row"><span><kbd>L</kbd></span><span>My Shares</span></div> <div class="help-row"><span><kbd>?</kbd></span><span>Show / hide help</span></div> </div> diff --git a/web/js/app.js b/web/js/app.js index 99ae0f3..189040a 100644 --- a/web/js/app.js +++ b/web/js/app.js @@ -1,7 +1,7 @@ import { API } from './api.js'; import { initKeyboard } from './keyboard.js'; import { initSelection, clearSelection, select, selectByElement, next, prev, currentIndex, currentElement, navUp, navDown, navLeft, navRight } from './selection.js'; -import { initPlayer, togglePlay, toggleFullscreen, toggleMinimize, toggleDetach, exitFullscreenIfNeeded, toggleCrop, currentMediaId, currentMediaInfo, hasLoadedMedia, isPlaybackActive, seekRelative, seekPercent, selectAndPlay, zoomIn as playerZoomIn, zoomOut as playerZoomOut, toggleSlideshow as playerToggleSlideshow, isImageMode as playerIsImageMode } from './player.js'; +import { initPlayer, togglePlay, toggleFullscreen, toggleMinimize, toggleDetach, exitFullscreenIfNeeded, toggleCrop, shiftCropPosition, cycleCropPosition, currentMediaId, currentMediaInfo, hasLoadedMedia, isPlaybackActive, seekRelative, seekPercent, selectAndPlay, zoomIn as playerZoomIn, zoomOut as playerZoomOut, toggleSlideshow as playerToggleSlideshow, isImageMode as playerIsImageMode } from './player.js'; import { initSearch, focusSearch, trigger as triggerSearch, parseQuery, showSearchHelp } from './search.js'; import { initShuffle, toggle as toggleShuffle, isOn as isShuffle } from './shuffle.js'; import { initThemes } from './themes.js'; @@ -141,6 +141,8 @@ async function initApp() { fullscreen: () => toggleFullscreen(), toggleMinimize: () => toggleMinimize(), toggleCrop: () => toggleCrop(), + shiftCropPosition: (dx, dy) => shiftCropPosition(dx, dy), + cycleCropPosition: () => cycleCropPosition(), escape: () => { exitFullscreenIfNeeded(); closeLightbox(); const el = currentElement(); if (el) el.classList.remove('selected'); closeAllModals(); }, shuffle: () => { toggleShuffle(); loadMedia(); }, share: () => shareSelected(), diff --git a/web/js/keyboard.js b/web/js/keyboard.js index 76c3a9d..b0a033e 100644 --- a/web/js/keyboard.js +++ b/web/js/keyboard.js @@ -135,6 +135,24 @@ export function initKeyboard(handlers) { if (e.ctrlKey || e.metaKey || e.altKey) return; // don't intercept browser shortcuts + if (e.shiftKey) { + switch (e.key) { + case 'ArrowUp': + case 'ArrowDown': + case 'ArrowLeft': + case 'ArrowRight': { + const dx = e.key === 'ArrowLeft' ? -10 : e.key === 'ArrowRight' ? 10 : 0; + const dy = e.key === 'ArrowUp' ? -10 : e.key === 'ArrowDown' ? 10 : 0; + const acted = handlers.shiftCropPosition?.(dx, dy); + if (acted) { + e.preventDefault(); + return; + } + break; + } + } + } + if (e.shiftKey && e.code === 'KeyN') { e.preventDefault(); handlers.nextTrack?.(e); @@ -235,6 +253,10 @@ export function initKeyboard(handlers) { case 'C': handlers.toggleMinimize?.(e); break; case 'f': handlers.fullscreen?.(e); break; case 'c': handlers.toggleCrop?.(e); break; + case 'x': + e.preventDefault(); + handlers.cycleCropPosition?.(e); + break; case 'Escape': handlers.escape?.(e); break; case 'Backspace': handlers.backspace?.(e); break; case 'r': handlers.shuffle?.(e); break; diff --git a/web/js/player.js b/web/js/player.js index f9a96da..343bc1b 100644 --- a/web/js/player.js +++ b/web/js/player.js @@ -22,6 +22,16 @@ let panStart = { x: 0, y: 0 }; let slideshowTimer = null; let slideshowPausedUntil = 0; let cropMode = false; +let cropPositionX = 50; +let cropPositionY = 50; +const CROP_PRESETS = [ + { x: 50, y: 50, label: 'C' }, + { x: 50, y: 0, label: 'T' }, + { x: 50, y: 100, label: 'B' }, + { x: 0, y: 50, label: 'L' }, + { x: 100, y: 50, label: 'R' }, +]; +let cropPresetIndex = 0; function currentMediaElement() { const e = els(); @@ -158,7 +168,7 @@ export function initPlayer(options = {}) { if (!document.fullscreenElement) { p.classList.remove('crop-mode'); cropMode = false; - if (e.cropIndicator) e.cropIndicator.classList.add('hidden'); + resetCropPosition(); } }); @@ -563,8 +573,7 @@ export function exitFullscreenIfNeeded() { const p = els().player; if (p) p.classList.remove('is-fullscreen', 'crop-mode'); cropMode = false; - const ind = els().cropIndicator; - if (ind) ind.classList.add('hidden'); + resetCropPosition(); } } @@ -574,7 +583,62 @@ export function toggleCrop() { if (!document.fullscreenElement) return; cropMode = !cropMode; e.player.classList.toggle('crop-mode', cropMode); - if (e.cropIndicator) e.cropIndicator.classList.toggle('hidden', !cropMode); + if (!cropMode) { + resetCropPosition(); + } else { + applyCropPosition(); + } +} + +function applyCropPosition() { + const e = els(); + if (!e.player) return; + e.player.style.setProperty('--crop-x', cropPositionX + '%'); + e.player.style.setProperty('--crop-y', cropPositionY + '%'); + updateCropIndicator(); +} + +function updateCropIndicator() { + const e = els(); + if (!e.cropIndicator) return; + if (!cropMode) { + e.cropIndicator.classList.add('hidden'); + return; + } + const snap = 5; + let label = ''; + if (Math.abs(cropPositionX - 50) <= snap && Math.abs(cropPositionY - 50) <= snap) label = 'C'; + else if (Math.abs(cropPositionX - 50) <= snap && cropPositionY <= snap) label = 'T'; + else if (Math.abs(cropPositionX - 50) <= snap && cropPositionY >= 95) label = 'B'; + else if (cropPositionX <= snap && Math.abs(cropPositionY - 50) <= snap) label = 'L'; + else if (cropPositionX >= 95 && Math.abs(cropPositionY - 50) <= snap) label = 'R'; + else label = Math.round(cropPositionX) + ',' + Math.round(cropPositionY); + e.cropIndicator.textContent = label; + e.cropIndicator.classList.remove('hidden'); +} + +function resetCropPosition() { + cropPositionX = 50; + cropPositionY = 50; + cropPresetIndex = 0; + applyCropPosition(); +} + +export function shiftCropPosition(dx, dy) { + if (!cropMode) return false; + cropPositionX = Math.max(0, Math.min(100, cropPositionX + dx)); + cropPositionY = Math.max(0, Math.min(100, cropPositionY + dy)); + applyCropPosition(); + return true; +} + +export function cycleCropPosition() { + if (!cropMode) return; + cropPresetIndex = (cropPresetIndex + 1) % CROP_PRESETS.length; + const preset = CROP_PRESETS[cropPresetIndex]; + cropPositionX = preset.x; + cropPositionY = preset.y; + applyCropPosition(); } function startProgressTimer() { |
