mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 04:14:39 +03:00
Player mobile improvements (#3120)
* Add videojs-mobile-ui * Prevent marker wrapping and fix alignment * Fix marker update on delete * Change hotkey modifier behaviour * Update KeyboardShortcuts.md
This commit is contained in:
@@ -74,8 +74,8 @@
|
||||
"thehandy": "^1.0.3",
|
||||
"universal-cookie": "^4.0.4",
|
||||
"video.js": "^7.20.3",
|
||||
"videojs-landscape-fullscreen": "^11.33.0",
|
||||
"videojs-seek-buttons": "^2.2.0",
|
||||
"videojs-mobile-ui": "^0.8.0",
|
||||
"videojs-seek-buttons": "^3.0.1",
|
||||
"videojs-vtt.js": "^0.15.4",
|
||||
"vite": "^2.9.13",
|
||||
"vite-plugin-compression": "^0.3.5",
|
||||
@@ -103,7 +103,8 @@
|
||||
"@types/react-router-dom": "5.1.7",
|
||||
"@types/react-router-hash-link": "^1.2.1",
|
||||
"@types/react-slick": "^0.23.8",
|
||||
"@types/video.js": "^7.3.28",
|
||||
"@types/video.js": "^7.3.49",
|
||||
"@types/videojs-mobile-ui": "^0.5.0",
|
||||
"@types/videojs-seek-buttons": "^2.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.33.0",
|
||||
"@typescript-eslint/parser": "^4.33.0",
|
||||
|
||||
47
ui/v2.5/src/@types/landscape-fullscreen.d.ts
vendored
47
ui/v2.5/src/@types/landscape-fullscreen.d.ts
vendored
@@ -1,47 +0,0 @@
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
declare module "videojs-landscape-fullscreen" {
|
||||
import videojs from "video.js";
|
||||
|
||||
function landscapeFullscreen(options?: {
|
||||
fullscreen: landscapeFullscreen.Options;
|
||||
}): void;
|
||||
|
||||
namespace landscapeFullscreen {
|
||||
const VERSION: typeof videojs.VERSION;
|
||||
|
||||
interface Options {
|
||||
/**
|
||||
* Enter fullscreen mode on rotating the device to landscape.
|
||||
* @default true
|
||||
*/
|
||||
enterOnRotate?: boolean;
|
||||
/**
|
||||
* Exit fullscreen mode on rotating the device to portrait.
|
||||
* @default true
|
||||
*/
|
||||
exitOnRotate?: boolean;
|
||||
/**
|
||||
* Always enter fullscreen in landscape mode even when device is in portrait mode (works on Chromium, Firefox, and IE >= 11).
|
||||
* @default true
|
||||
*/
|
||||
alwaysInLandscapeMode?: boolean;
|
||||
/**
|
||||
* Whether to use fake fullscreen on iOS (needed for displaying player controls instead of system controls).
|
||||
* @default true
|
||||
*/
|
||||
iOS?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export = landscapeFullscreen;
|
||||
|
||||
declare module "video.js" {
|
||||
interface VideoJsPlayer {
|
||||
landscapeFullscreen: typeof landscapeFullscreen;
|
||||
}
|
||||
interface VideoJsPlayerPluginOptions {
|
||||
landscapeFullscreen?: { fullscreen: landscapeFullscreen.Options };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,15 +7,14 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from "video.js";
|
||||
import "videojs-mobile-ui";
|
||||
import "videojs-seek-buttons";
|
||||
import "videojs-landscape-fullscreen";
|
||||
import "./live";
|
||||
import "./PlaylistButtons";
|
||||
import "./source-selector";
|
||||
import "./persist-volume";
|
||||
import "./markers";
|
||||
import "./vtt-thumbnails";
|
||||
import "./big-buttons";
|
||||
import "./track-activity";
|
||||
import cx from "classnames";
|
||||
import {
|
||||
@@ -36,6 +35,18 @@ import { VIDEO_PLAYER_ID } from "./util";
|
||||
import { IUIConfig } from "src/core/config";
|
||||
|
||||
function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
|
||||
function seekStep(step: number) {
|
||||
const time = player.currentTime() + step;
|
||||
const duration = player.duration();
|
||||
if (time < 0) {
|
||||
player.currentTime(0);
|
||||
} else if (time < duration) {
|
||||
player.currentTime(time);
|
||||
} else {
|
||||
player.currentTime(duration);
|
||||
}
|
||||
}
|
||||
|
||||
function seekPercent(percent: number) {
|
||||
const duration = player.duration();
|
||||
const time = duration * percent;
|
||||
@@ -50,6 +61,21 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
|
||||
player.currentTime(time);
|
||||
}
|
||||
|
||||
let seekFactor = 10;
|
||||
if (event.shiftKey) {
|
||||
seekFactor = 5;
|
||||
} else if (event.ctrlKey || event.altKey) {
|
||||
seekFactor = 60;
|
||||
}
|
||||
switch (event.which) {
|
||||
case 39: // right arrow
|
||||
seekStep(seekFactor);
|
||||
break;
|
||||
case 37: // left arrow
|
||||
seekStep(-seekFactor);
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) {
|
||||
return;
|
||||
}
|
||||
@@ -67,12 +93,6 @@ function handleHotkeys(player: VideoJsPlayer, event: videojs.KeyboardEvent) {
|
||||
if (player.isFullscreen()) player.exitFullscreen();
|
||||
else player.requestFullscreen();
|
||||
break;
|
||||
case 39: // right arrow
|
||||
player.currentTime(Math.min(player.duration(), player.currentTime() + 5));
|
||||
break;
|
||||
case 37: // left arrow
|
||||
player.currentTime(Math.max(0, player.currentTime() - 5));
|
||||
break;
|
||||
case 38: // up arrow
|
||||
player.volume(player.volume() + 0.1);
|
||||
break;
|
||||
@@ -263,7 +283,6 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
markers: {},
|
||||
sourceSelector: {},
|
||||
persistVolume: {},
|
||||
bigButtons: {},
|
||||
seekButtons: {
|
||||
forward: 10,
|
||||
back: 10,
|
||||
@@ -421,20 +440,21 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
interactiveClient.pause();
|
||||
interactiveReady.current = false;
|
||||
|
||||
const alwaysStartFromBeginning =
|
||||
uiConfig?.alwaysStartFromBeginning ?? false;
|
||||
const isLandscape = file.height && file.width && file.width > file.height;
|
||||
|
||||
if (isLandscape) {
|
||||
player.landscapeFullscreen({
|
||||
const mobileUiOptions = {
|
||||
fullscreen: {
|
||||
enterOnRotate: true,
|
||||
exitOnRotate: true,
|
||||
alwaysInLandscapeMode: true,
|
||||
iOS: false,
|
||||
lockOnRotate: true,
|
||||
lockToLandscapeOnEnter: isLandscape,
|
||||
},
|
||||
});
|
||||
}
|
||||
touchControls: {
|
||||
seekSeconds: 10,
|
||||
tapTimeout: 500,
|
||||
disableOnEnd: false,
|
||||
},
|
||||
};
|
||||
player.mobileUi(mobileUiOptions);
|
||||
|
||||
const { duration } = file;
|
||||
const sourceSelector = player.sourceSelector();
|
||||
@@ -455,15 +475,6 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
})
|
||||
);
|
||||
|
||||
const markers = player.markers();
|
||||
markers.clearMarkers();
|
||||
for (const marker of scene.scene_markers) {
|
||||
markers.addMarker({
|
||||
title: getMarkerTitle(marker),
|
||||
time: marker.seconds,
|
||||
});
|
||||
}
|
||||
|
||||
function getDefaultLanguageCode() {
|
||||
let languageCode = window.navigator.language;
|
||||
|
||||
@@ -507,28 +518,25 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (scene.paths.screenshot) {
|
||||
player.poster(scene.paths.screenshot);
|
||||
} else {
|
||||
player.poster("");
|
||||
}
|
||||
|
||||
auto.current =
|
||||
autoplay ||
|
||||
(interfaceConfig?.autostartVideo ?? false) ||
|
||||
_initialTimestamp > 0;
|
||||
|
||||
let startPositition = _initialTimestamp;
|
||||
const alwaysStartFromBeginning =
|
||||
uiConfig?.alwaysStartFromBeginning ?? false;
|
||||
|
||||
let startPosition = _initialTimestamp;
|
||||
if (
|
||||
!startPositition &&
|
||||
!startPosition &&
|
||||
!(alwaysStartFromBeginning || sessionInitialised) &&
|
||||
file.duration > scene.resume_time!
|
||||
) {
|
||||
startPositition = scene.resume_time!;
|
||||
startPosition = scene.resume_time!;
|
||||
}
|
||||
|
||||
initialTimestamp.current = startPositition;
|
||||
setTime(startPositition);
|
||||
initialTimestamp.current = startPosition;
|
||||
setTime(startPosition);
|
||||
setSessionInitialised(true);
|
||||
|
||||
player.load();
|
||||
@@ -556,6 +564,26 @@ export const ScenePlayer: React.FC<IScenePlayerProps> = ({
|
||||
_initialTimestamp,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (!player || !scene) return;
|
||||
|
||||
const markers = player.markers();
|
||||
markers.clearMarkers();
|
||||
for (const marker of scene.scene_markers) {
|
||||
markers.addMarker({
|
||||
title: getMarkerTitle(marker),
|
||||
time: marker.seconds,
|
||||
});
|
||||
}
|
||||
|
||||
if (scene.paths.screenshot) {
|
||||
player.poster(scene.paths.screenshot);
|
||||
} else {
|
||||
player.poster("");
|
||||
}
|
||||
}, [scene]);
|
||||
|
||||
useEffect(() => {
|
||||
const player = playerRef.current;
|
||||
if (!player) return;
|
||||
|
||||
@@ -211,6 +211,8 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = ({
|
||||
|
||||
mouseDown.current = false;
|
||||
|
||||
contentEl.current!.classList.remove("dragging");
|
||||
|
||||
let newPosition = position.current;
|
||||
const midpointOffset = slider.clientWidth / 2;
|
||||
const delta = Math.abs(event.clientX - startMouseEvent.current!.clientX);
|
||||
@@ -256,6 +258,8 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = ({
|
||||
onScroll();
|
||||
}
|
||||
|
||||
contentEl.current!.classList.add("dragging");
|
||||
|
||||
// negative dragging right (past), positive left (future)
|
||||
const delta = event.clientX - lastMouseEvent.current!.clientX;
|
||||
|
||||
@@ -338,7 +342,6 @@ export const ScenePlayerScrubber: React.FC<IScenePlayerScrubberProps> = ({
|
||||
return (
|
||||
<div className="scrubber-wrapper">
|
||||
<Button
|
||||
variant="link"
|
||||
className="scrubber-button"
|
||||
id="scrubber-back"
|
||||
onClick={() => goBack()}
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import videojs, { VideoJsPlayer } from "video.js";
|
||||
|
||||
// prettier-ignore
|
||||
const BigPlayButton = videojs.getComponent("BigPlayButton") as unknown as typeof videojs.BigPlayButton;
|
||||
|
||||
class BigPlayPauseButton extends BigPlayButton {
|
||||
handleClick(event: videojs.EventTarget.Event) {
|
||||
if (this.player().paused()) {
|
||||
super.handleClick(event);
|
||||
} else {
|
||||
this.player().pause();
|
||||
}
|
||||
}
|
||||
|
||||
buildCSSClass() {
|
||||
return "vjs-control vjs-button vjs-big-play-pause-button";
|
||||
}
|
||||
}
|
||||
|
||||
class BigButtonGroup extends videojs.getComponent("Component") {
|
||||
constructor(player: VideoJsPlayer) {
|
||||
super(player);
|
||||
|
||||
this.addChild("seekButton", {
|
||||
direction: "back",
|
||||
seconds: 10,
|
||||
});
|
||||
|
||||
this.addChild("BigPlayPauseButton");
|
||||
|
||||
this.addChild("seekButton", {
|
||||
direction: "forward",
|
||||
seconds: 10,
|
||||
});
|
||||
}
|
||||
|
||||
createEl() {
|
||||
return super.createEl("div", {
|
||||
className: "vjs-big-button-group",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class BigButtonsPlugin extends videojs.getPlugin("plugin") {
|
||||
constructor(player: VideoJsPlayer) {
|
||||
super(player);
|
||||
|
||||
player.ready(() => {
|
||||
player.addChild("BigButtonGroup");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Register the plugin with video.js.
|
||||
videojs.registerComponent("BigButtonGroup", BigButtonGroup);
|
||||
videojs.registerComponent("BigPlayPauseButton", BigPlayPauseButton);
|
||||
videojs.registerPlugin("bigButtons", BigButtonsPlugin);
|
||||
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
declare module "video.js" {
|
||||
interface VideoJsPlayer {
|
||||
bigButtons: () => BigButtonsPlugin;
|
||||
}
|
||||
interface VideoJsPlayerPluginOptions {
|
||||
bigButtons?: {};
|
||||
}
|
||||
}
|
||||
|
||||
export default BigButtonsPlugin;
|
||||
@@ -48,7 +48,13 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
|
||||
const marker = this.markers[i];
|
||||
const markerDiv = this.markerDivs[i];
|
||||
|
||||
markerDiv.style.left = `${(marker.time / duration) * 100}%`;
|
||||
if (duration) {
|
||||
// marker is 6px wide - adjust by 3px to align to center not left side
|
||||
markerDiv.style.left = `calc(${
|
||||
(marker.time / duration) * 100
|
||||
}% - 3px)`;
|
||||
markerDiv.style.visibility = "visible";
|
||||
}
|
||||
if (seekBar) seekBar.appendChild(markerDiv);
|
||||
}
|
||||
});
|
||||
@@ -58,6 +64,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
|
||||
if (!this.markerTooltip) return;
|
||||
|
||||
this.markerTooltip.innerText = title;
|
||||
this.markerTooltip.style.right = `${-this.markerTooltip.clientWidth / 2}px`;
|
||||
this.markerTooltip.style.visibility = "visible";
|
||||
|
||||
// hide default tooltip
|
||||
@@ -76,7 +83,11 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
|
||||
markerDiv.className = "vjs-marker";
|
||||
|
||||
const duration = this.player.duration();
|
||||
markerDiv.style.position = `${(marker.time / duration) * 100}%`;
|
||||
if (duration) {
|
||||
// marker is 6px wide - adjust by 3px to align to center not left side
|
||||
markerDiv.style.left = `calc(${(marker.time / duration) * 100}% - 3px)`;
|
||||
markerDiv.style.visibility = "visible";
|
||||
}
|
||||
|
||||
// bind click event to seek to marker time
|
||||
markerDiv.addEventListener("click", () =>
|
||||
@@ -122,7 +133,7 @@ class MarkersPlugin extends videojs.getPlugin("plugin") {
|
||||
}
|
||||
|
||||
clearMarkers() {
|
||||
this.removeMarkers(this.markers);
|
||||
this.removeMarkers([...this.markers]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
@import "video.js/dist/video-js.css";
|
||||
@import "videojs-mobile-ui/dist/videojs-mobile-ui.css";
|
||||
@import "videojs-seek-buttons/dist/videojs-seek-buttons.css";
|
||||
|
||||
$scrubberHeight: 120px;
|
||||
$menuHeight: 4rem;
|
||||
$sceneTabWidth: 450px;
|
||||
@@ -52,6 +56,10 @@ $sceneTabWidth: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-touch-overlay .vjs-play-control {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.vjs-control-bar {
|
||||
background: none;
|
||||
|
||||
@@ -67,13 +75,16 @@ $sceneTabWidth: 450px;
|
||||
bottom: 0;
|
||||
content: "";
|
||||
height: 10rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-time-control {
|
||||
display: block;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-width: 0;
|
||||
padding: 0 4px;
|
||||
pointer-events: none;
|
||||
@@ -92,17 +103,22 @@ $sceneTabWidth: 450px;
|
||||
}
|
||||
|
||||
.vjs-progress-control {
|
||||
bottom: 3rem;
|
||||
margin-left: 1%;
|
||||
bottom: 2.5em;
|
||||
height: 3em;
|
||||
position: absolute;
|
||||
width: 98%;
|
||||
width: 100%;
|
||||
|
||||
.vjs-play-progress .vjs-time-tooltip,
|
||||
&:hover .vjs-play-progress .vjs-time-tooltip {
|
||||
visibility: hidden;
|
||||
.vjs-progress-holder {
|
||||
margin: 0 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* stylelint-disable declaration-no-important */
|
||||
.vjs-play-progress .vjs-time-tooltip {
|
||||
display: none !important;
|
||||
}
|
||||
/* stylelint-enable declaration-no-important */
|
||||
|
||||
.vjs-volume-control {
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -117,7 +133,7 @@ $sceneTabWidth: 450px;
|
||||
.vjs-vtt-thumbnail-display {
|
||||
border: 2px solid white;
|
||||
border-radius: 2px;
|
||||
bottom: 90px;
|
||||
bottom: 6em;
|
||||
box-shadow: 0 0 7px rgba(0, 0, 0, 0.6);
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
@@ -155,7 +171,7 @@ $sceneTabWidth: 450px;
|
||||
|
||||
.vjs-source-selector {
|
||||
.vjs-menu li {
|
||||
font-size: 12px;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
.vjs-button > .vjs-icon-placeholder::before {
|
||||
@@ -171,36 +187,31 @@ $sceneTabWidth: 450px;
|
||||
left: 0;
|
||||
opacity: 1;
|
||||
position: absolute;
|
||||
-webkit-transition: opacity 0.2s ease;
|
||||
-moz-transition: opacity 0.2s ease;
|
||||
transition: opacity 0.2s ease;
|
||||
visibility: hidden;
|
||||
width: 6px;
|
||||
z-index: 100;
|
||||
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
-webkit-transform: scale(1.3, 1.3);
|
||||
-moz-transform: scale(1.3, 1.3);
|
||||
-o-transform: scale(1.3, 1.3);
|
||||
-ms-transform: scale(1.3, 1.3);
|
||||
transform: scale(1.3, 1.3);
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-marker-tooltip {
|
||||
background-color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.3em;
|
||||
color: white;
|
||||
display: block;
|
||||
color: #000;
|
||||
float: right;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-size: 10px;
|
||||
height: 50px;
|
||||
font-size: 0.6em;
|
||||
padding: 6px 8px 8px 8px;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: -80px;
|
||||
top: -5.4em;
|
||||
width: 160px;
|
||||
top: -3.4em;
|
||||
visibility: hidden;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -244,9 +255,8 @@ $sceneTabWidth: 450px;
|
||||
content: "\f103";
|
||||
}
|
||||
|
||||
// hide the regular play/pause button on touch screens
|
||||
.vjs-play-control {
|
||||
display: none;
|
||||
.vjs-vtt-thumbnail-display {
|
||||
bottom: 2.8em;
|
||||
}
|
||||
|
||||
// hide the regular seek buttons on touch screens
|
||||
@@ -259,10 +269,19 @@ $sceneTabWidth: 450px;
|
||||
// make controls a little more compact on smaller screens
|
||||
@media (max-width: 576px) {
|
||||
.vjs-control-bar {
|
||||
.vjs-control:not(.vjs-progress-control) {
|
||||
.vjs-control {
|
||||
width: 2.5em;
|
||||
}
|
||||
|
||||
.vjs-progress-control {
|
||||
height: 2em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.vjs-playback-rate {
|
||||
width: 3em;
|
||||
}
|
||||
|
||||
.vjs-button > .vjs-icon-placeholder::before,
|
||||
.vjs-skip-button::before {
|
||||
font-size: 1.5em;
|
||||
@@ -270,9 +289,27 @@ $sceneTabWidth: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-menu-button-popup .vjs-menu {
|
||||
width: 8em;
|
||||
|
||||
.vjs-menu-content {
|
||||
max-height: 10em;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-playback-rate .vjs-playback-rate-value {
|
||||
font-size: 1em;
|
||||
line-height: 2.97;
|
||||
}
|
||||
|
||||
.vjs-source-selector {
|
||||
.vjs-menu li {
|
||||
font-size: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.vjs-time-control {
|
||||
font-size: 12px;
|
||||
line-height: 4em;
|
||||
}
|
||||
|
||||
.vjs-big-button-group .vjs-button {
|
||||
@@ -283,10 +320,6 @@ $sceneTabWidth: 450px;
|
||||
.vjs-current-time {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.vjs-vtt-thumbnail-display {
|
||||
bottom: 40px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -301,73 +334,8 @@ $sceneTabWidth: 450px;
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.scene-tabs {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.scene-player-container {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.scene-tabs {
|
||||
flex: 0 0 $sceneTabWidth;
|
||||
max-width: $sceneTabWidth;
|
||||
overflow: auto;
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 15rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-divider {
|
||||
flex: 0 0 15px;
|
||||
max-width: 15px;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:active:not(:hover),
|
||||
&:focus:not(:hover) {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-player-container {
|
||||
flex: 0 0 calc(100% - #{$sceneTabWidth} - 15px);
|
||||
max-width: calc(100% - #{$sceneTabWidth} - 15px);
|
||||
padding-left: 0;
|
||||
|
||||
&.expanded {
|
||||
flex: 0 0 calc(100% - 15px);
|
||||
max-width: calc(100% - 15px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrubber-wrapper {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
margin: 5px 0;
|
||||
overflow: hidden;
|
||||
@@ -387,26 +355,26 @@ $sceneTabWidth: 450px;
|
||||
border: 1px solid #555;
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
font-size: 1.3rem;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
line-height: $scrubberHeight;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 1.5%;
|
||||
width: 1.3rem;
|
||||
}
|
||||
|
||||
.scrubber-content {
|
||||
cursor: grab;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
flex-grow: 1;
|
||||
height: $scrubberHeight;
|
||||
margin: 0 0.5%;
|
||||
margin: 0 7px;
|
||||
overflow: hidden;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
width: 96%;
|
||||
|
||||
&.dragging {
|
||||
cursor: grabbing;
|
||||
@@ -492,7 +460,6 @@ $sceneTabWidth: 450px;
|
||||
|
||||
.scrubber-item {
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 10px;
|
||||
margin: 0 auto;
|
||||
@@ -506,3 +473,74 @@ $sceneTabWidth: 450px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
.scene-tabs {
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
.scene-player-container {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.scrubber-wrapper {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
.scene-tabs {
|
||||
flex: 0 0 $sceneTabWidth;
|
||||
max-width: $sceneTabWidth;
|
||||
overflow: auto;
|
||||
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1 1 auto;
|
||||
min-height: 15rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.scene-divider {
|
||||
flex: 0 0 15px;
|
||||
max-width: 15px;
|
||||
|
||||
button {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
color: $link-color;
|
||||
cursor: pointer;
|
||||
font-size: 10px;
|
||||
font-weight: 800;
|
||||
height: 100%;
|
||||
line-height: 100%;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
|
||||
&:active:not(:hover),
|
||||
&:focus:not(:hover) {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scene-player-container {
|
||||
flex: 0 0 calc(100% - #{$sceneTabWidth} - 15px);
|
||||
max-width: calc(100% - #{$sceneTabWidth} - 15px);
|
||||
padding-left: 0;
|
||||
|
||||
&.expanded {
|
||||
flex: 0 0 calc(100% - 15px);
|
||||
max-width: calc(100% - 15px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,13 @@ const typePolicies: TypePolicies = {
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Scene: {
|
||||
fields: {
|
||||
scene_markers: {
|
||||
merge: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
Tag: {
|
||||
fields: {
|
||||
parents: {
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
* Added tag description filter criterion. ([#3011](https://github.com/stashapp/stash/pull/3011))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Jump back/forward buttons on mobile have been replaced with double-tap gestures on mobile. ([#3120](https://github.com/stashapp/stash/pull/3120))
|
||||
* Added shift- and ctrl-keybinds for seeking for shorter and longer intervals, respectively. ([#3120](https://github.com/stashapp/stash/pull/3120))
|
||||
* Added Estonian and Russian Language translations. Added in-progress Languages for Persian, Ukrainian, Bengali, Thai, Romainian, Hungarian, and Czech.([#3024] (https://github.com/stashapp/stash/pull/3024))
|
||||
* Also show imperial units for performer height and weight. ([#3097](https://github.com/stashapp/stash/pull/3097))
|
||||
* Limit number of items in selector drop-downs to 200. ([#3062](https://github.com/stashapp/stash/pull/3062))
|
||||
|
||||
@@ -64,6 +64,12 @@
|
||||
| `p n` | Play next scene in queue |
|
||||
| `p p` | Play previous scene in queue |
|
||||
| `p r` | Play random scene in queue |
|
||||
| `←` | Seek backwards by 10 seconds |
|
||||
| `→` | Seek forwards by 10 seconds |
|
||||
| `Shift + ←` | Seek backwards by 5 seconds |
|
||||
| `Shift + →` | Seek forwards by 5 seconds |
|
||||
| `Ctrl/Alt + ←` | Seek backwards by 1 minute |
|
||||
| `Ctrl/Alt + →` | Seek forwards by 1 minute |
|
||||
| `{1-9}` | Seek to 10-90% duration |
|
||||
| `[` | Scrub backwards 10% duration |
|
||||
| `]` | Scrub forwards 10% duration |
|
||||
|
||||
@@ -26,8 +26,6 @@
|
||||
@import "src/components/Dialogs/IdentifyDialog/styles.scss";
|
||||
@import "src/components/Dialogs/styles.scss";
|
||||
@import "../node_modules/flag-icon-css/css/flag-icon.min.css";
|
||||
@import "video.js/dist/video-js.css";
|
||||
@import "videojs-seek-buttons/dist/videojs-seek-buttons.css";
|
||||
|
||||
/* stylelint-disable */
|
||||
#root {
|
||||
|
||||
@@ -1576,10 +1576,17 @@
|
||||
resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.3.tgz"
|
||||
integrity sha512-FvUupuM3rlRsRtCN+fDudtmytGO6iHJuuRKS1Ss0pG5z8oX0diNEw94UEL7hgDbpN94rgaK5R7sWm6RrSkZuAQ==
|
||||
|
||||
"@types/video.js@*", "@types/video.js@^7.3.28":
|
||||
version "7.3.28"
|
||||
resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.28.tgz#9acb8c46db3984d556ee7321483ef834bf0a43a2"
|
||||
integrity sha512-vUxgGAQN+tAOx6OVu8wiSKGfvXJXvX++B5xKJTRpWfPHOV8y1VcJxqhIMb9nLfhIz0Pw3R69pWq2DzlOoxOn8Q==
|
||||
"@types/video.js@*", "@types/video.js@^7.3.49":
|
||||
version "7.3.49"
|
||||
resolved "https://registry.yarnpkg.com/@types/video.js/-/video.js-7.3.49.tgz#33fbc421a02827c90935afbf7dcaac77170b6cda"
|
||||
integrity sha512-GtBMH+rm7yyw5DAK7ycQeEd35x/EYoLK/49op+CqDDoNUm9XJEVOfb+EARKKe4TwP5jkaikjWqf5RFjmw8yHoQ==
|
||||
|
||||
"@types/videojs-mobile-ui@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/videojs-mobile-ui/-/videojs-mobile-ui-0.5.0.tgz#66934b140fd437fda361986f8e7e87b01dc39138"
|
||||
integrity sha512-wqeapTB35qpLfERxvL5mZGoexf5bA2TreDpFgc3zyCdr7Acf86VItvo9oTclFeUc11wOo7W7/4ueZZAEYmlTaA==
|
||||
dependencies:
|
||||
"@types/video.js" "*"
|
||||
|
||||
"@types/videojs-seek-buttons@^2.1.0":
|
||||
version "2.1.0"
|
||||
@@ -6673,16 +6680,11 @@ requires-port@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff"
|
||||
integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=
|
||||
|
||||
resize-observer-polyfill@^1.5.0:
|
||||
resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resize-observer-polyfill@^1.5.1:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464"
|
||||
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
|
||||
|
||||
resolve-from@5.0.0, resolve-from@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz"
|
||||
@@ -7910,20 +7912,19 @@ videojs-font@3.2.0:
|
||||
resolved "https://registry.yarnpkg.com/videojs-font/-/videojs-font-3.2.0.tgz#212c9d3f4e4ec3fa7345167d64316add35e92232"
|
||||
integrity sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==
|
||||
|
||||
videojs-landscape-fullscreen@^11.33.0:
|
||||
version "11.33.0"
|
||||
resolved "https://registry.yarnpkg.com/videojs-landscape-fullscreen/-/videojs-landscape-fullscreen-11.33.0.tgz#4033100b3a97399c994426e825662860dfc232e0"
|
||||
integrity sha512-Eex5ovlvIipHHif9LEhVL63zxxmOEQQi6Bt1P+EaA0QpjJAPF+CocWfhMItfGF1GcFQeP1ZFGCPTGw60n7vyUg==
|
||||
videojs-mobile-ui@^0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/videojs-mobile-ui/-/videojs-mobile-ui-0.8.0.tgz#40a1c6f9302071b9bbe95937c934114600916ac5"
|
||||
integrity sha512-Jd+u/ctjUkbZlT1cAA0umTu0LQwSZSFG+02cJxShuwq27B6rfrRALETK/gsuTc7U27lB9fbwcF7HBMaNxW62nA==
|
||||
dependencies:
|
||||
global "^4.4.0"
|
||||
|
||||
videojs-seek-buttons@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/videojs-seek-buttons/-/videojs-seek-buttons-2.2.0.tgz#50b8da1178a5718ee5a7649fbb90a64a518103f7"
|
||||
integrity sha512-yjCA6ntq+8fRKgZi/H6QJlghQWgA1x9oSRl6wfLODAcujhynDXetwMgRKGgl4NlV5af2bKY6erNtJ0kOBko/nQ==
|
||||
videojs-seek-buttons@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/videojs-seek-buttons/-/videojs-seek-buttons-3.0.1.tgz#cc2adc23a6372e8aa6c2e9fd0fe7e7831a46747f"
|
||||
integrity sha512-scVWOqCMqHajlbwYZIzJ5nBYkDXTAhEpWjfcdCu8ykksA1barrKnEKdQvS84TtDWOx6UXDD/e/x0acYEZCDMEQ==
|
||||
dependencies:
|
||||
global "^4.4.0"
|
||||
video.js "^6 || ^7"
|
||||
|
||||
videojs-vtt.js@^0.15.4:
|
||||
version "0.15.4"
|
||||
|
||||
Reference in New Issue
Block a user