summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--web/css/player.css1
-rw-r--r--web/index.html2
-rw-r--r--web/js/app.js4
-rw-r--r--web/js/keyboard.js22
-rw-r--r--web/js/player.js72
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() {