mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Mobile UI improvements (#1104)
* Use dropdown for o-counter instead of hover * Always show previews on non-hoverable device * Add IntersectionObserver polyfill * Prevent video previews playing fullscreen
This commit is contained in:
@@ -46,6 +46,7 @@
|
||||
"graphql": "^15.4.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"i18n-iso-countries": "^6.4.0",
|
||||
"intersection-observer": "^0.12.0",
|
||||
"jimp": "^0.16.1",
|
||||
"localforage": "1.9.0",
|
||||
"lodash": "^4.17.20",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
### 🎨 Improvements
|
||||
* Auto-play video previews on mobile devices.
|
||||
* Replace hover menu with dropdown menu for O-Counter.
|
||||
* Support random strings for scraper cookie values.
|
||||
* Added Rescan button to scene, image, gallery details overflow button.
|
||||
|
||||
### 🐛 Bug fixes
|
||||
* Prevent scene card previews playing in full-screen on iOS devices.
|
||||
|
||||
@@ -29,17 +29,14 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
|
||||
const videoEl = useRef<HTMLVideoElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.intersectionRatio > 0)
|
||||
// Catch is necessary due to DOMException if user hovers before clicking on page
|
||||
videoEl.current?.play().catch(() => {});
|
||||
else videoEl.current?.pause();
|
||||
});
|
||||
},
|
||||
{ root: document.documentElement }
|
||||
);
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.intersectionRatio > 0)
|
||||
// Catch is necessary due to DOMException if user hovers before clicking on page
|
||||
videoEl.current?.play().catch(() => {});
|
||||
else videoEl.current?.pause();
|
||||
});
|
||||
});
|
||||
|
||||
if (videoEl.current) observer.observe(videoEl.current);
|
||||
});
|
||||
@@ -53,6 +50,8 @@ export const ScenePreview: React.FC<IScenePreviewProps> = ({
|
||||
<div className={cx("scene-card-preview", { portrait: isPortrait })}>
|
||||
<img className="scene-card-preview-image" src={image} alt="" />
|
||||
<video
|
||||
disableRemotePlayback
|
||||
playsInline
|
||||
className="scene-card-preview-video"
|
||||
loop
|
||||
preload="none"
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import React from "react";
|
||||
import { Button, Spinner } from "react-bootstrap";
|
||||
import { Icon, HoverPopover, SweatDrops } from "src/components/Shared";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Dropdown,
|
||||
DropdownButton,
|
||||
Spinner,
|
||||
} from "react-bootstrap";
|
||||
import { Icon, SweatDrops } from "src/components/Shared";
|
||||
|
||||
export interface IOCounterButtonProps {
|
||||
loading: boolean;
|
||||
@@ -19,7 +25,7 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||
|
||||
const renderButton = () => (
|
||||
<Button
|
||||
className="minimal"
|
||||
className="minimal pr-1"
|
||||
onClick={props.onIncrement}
|
||||
variant="secondary"
|
||||
title="O-Counter"
|
||||
@@ -29,41 +35,32 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (props.value) {
|
||||
return (
|
||||
<HoverPopover
|
||||
content={
|
||||
<div>
|
||||
<div>
|
||||
<Button
|
||||
className="minimal"
|
||||
onClick={props.onDecrement}
|
||||
variant="secondary"
|
||||
>
|
||||
<Icon icon="minus" />
|
||||
<span>Decrement</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className="minimal"
|
||||
onClick={props.onReset}
|
||||
variant="secondary"
|
||||
>
|
||||
<Icon icon="ban" />
|
||||
<span>Reset</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
enterDelay={1000}
|
||||
placement="bottom"
|
||||
onOpen={props.onMenuOpened}
|
||||
onClose={props.onMenuClosed}
|
||||
>
|
||||
{renderButton()}
|
||||
</HoverPopover>
|
||||
);
|
||||
}
|
||||
return renderButton();
|
||||
const maybeRenderDropdown = () => {
|
||||
if (props.value) {
|
||||
return (
|
||||
<DropdownButton
|
||||
as={ButtonGroup}
|
||||
title=" "
|
||||
variant="secondary"
|
||||
className="pl-0 show-carat"
|
||||
>
|
||||
<Dropdown.Item onClick={props.onDecrement}>
|
||||
<Icon icon="minus" />
|
||||
<span>Decrement</span>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item onClick={props.onReset}>
|
||||
<Icon icon="ban" />
|
||||
<span>Reset</span>
|
||||
</Dropdown.Item>
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ButtonGroup className="o-counter">
|
||||
{renderButton()}
|
||||
{maybeRenderDropdown()}
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -209,15 +209,36 @@ textarea.scene-description {
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.scene-specs-overlay,
|
||||
.rating-banner,
|
||||
.scene-studio-overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
@media (pointer: fine) {
|
||||
&:hover {
|
||||
.scene-specs-overlay,
|
||||
.rating-banner,
|
||||
.scene-studio-overlay {
|
||||
opacity: 0;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.scene-studio-overlay:hover {
|
||||
.scene-studio-overlay:hover {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.scene-card-check {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
|
||||
.scene-card-preview-video {
|
||||
top: 0;
|
||||
transition-delay: 0.2s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* replicate hover for non-hoverable interfaces */
|
||||
@media (hover: none), (pointer: coarse), (pointer: none) {
|
||||
/* don't hide overlays */
|
||||
.scene-studio-overlay {
|
||||
opacity: 0.75;
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
@@ -545,3 +566,10 @@ input[type="range"].blue-slider {
|
||||
color: #664c3f;
|
||||
}
|
||||
}
|
||||
|
||||
.o-counter .dropdown-toggle {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
border: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0.25rem;
|
||||
}
|
||||
|
||||
@@ -57,6 +57,8 @@ const Preview: React.FC<{
|
||||
);
|
||||
const video = (
|
||||
<video
|
||||
disableRemotePlayback
|
||||
playsInline
|
||||
src={previews.video}
|
||||
poster={previews.image}
|
||||
autoPlay={previewType === "video"}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { shouldPolyfill as shouldPolyfillCanonicalLocales } from "@formatjs/intl
|
||||
import { shouldPolyfill as shouldPolyfillLocale } from "@formatjs/intl-locale/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillNumberformat } from "@formatjs/intl-numberformat/should-polyfill";
|
||||
import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/should-polyfill";
|
||||
import "intersection-observer/intersection-observer";
|
||||
|
||||
// Required for browsers older than August 2020ish. Can be removed at some point.
|
||||
replaceAll.shim();
|
||||
|
||||
@@ -62,7 +62,7 @@ hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
:not(.show-carat) > .dropdown-toggle::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -8058,6 +8058,11 @@ internal-slot@^1.0.2:
|
||||
has "^1.0.3"
|
||||
side-channel "^1.0.2"
|
||||
|
||||
intersection-observer@^0.12.0:
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/intersection-observer/-/intersection-observer-0.12.0.tgz#6c84628f67ce8698e5f9ccf857d97718745837aa"
|
||||
integrity sha512-2Vkz8z46Dv401zTWudDGwO7KiGHNDkMv417T5ItcNYfmvHR/1qCTVBO9vwH8zZmQ0WkA/1ARwpysR9bsnop4NQ==
|
||||
|
||||
intl-messageformat-parser@6.1.3:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz#c333850f66d686eca5c9d87eff1ad46f8721b64d"
|
||||
|
||||
Reference in New Issue
Block a user