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": "^15.4.0",
|
||||||
"graphql-tag": "^2.11.0",
|
"graphql-tag": "^2.11.0",
|
||||||
"i18n-iso-countries": "^6.4.0",
|
"i18n-iso-countries": "^6.4.0",
|
||||||
|
"intersection-observer": "^0.12.0",
|
||||||
"jimp": "^0.16.1",
|
"jimp": "^0.16.1",
|
||||||
"localforage": "1.9.0",
|
"localforage": "1.9.0",
|
||||||
"lodash": "^4.17.20",
|
"lodash": "^4.17.20",
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Auto-play video previews on mobile devices.
|
||||||
|
* Replace hover menu with dropdown menu for O-Counter.
|
||||||
* Support random strings for scraper cookie values.
|
* Support random strings for scraper cookie values.
|
||||||
* Added Rescan button to scene, image, gallery details overflow button.
|
* 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);
|
const videoEl = useRef<HTMLVideoElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const observer = new IntersectionObserver(
|
const observer = new IntersectionObserver((entries) => {
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.intersectionRatio > 0)
|
if (entry.intersectionRatio > 0)
|
||||||
// Catch is necessary due to DOMException if user hovers before clicking on page
|
// Catch is necessary due to DOMException if user hovers before clicking on page
|
||||||
videoEl.current?.play().catch(() => {});
|
videoEl.current?.play().catch(() => {});
|
||||||
else videoEl.current?.pause();
|
else videoEl.current?.pause();
|
||||||
});
|
});
|
||||||
},
|
});
|
||||||
{ root: document.documentElement }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (videoEl.current) observer.observe(videoEl.current);
|
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 })}>
|
<div className={cx("scene-card-preview", { portrait: isPortrait })}>
|
||||||
<img className="scene-card-preview-image" src={image} alt="" />
|
<img className="scene-card-preview-image" src={image} alt="" />
|
||||||
<video
|
<video
|
||||||
|
disableRemotePlayback
|
||||||
|
playsInline
|
||||||
className="scene-card-preview-video"
|
className="scene-card-preview-video"
|
||||||
loop
|
loop
|
||||||
preload="none"
|
preload="none"
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button, Spinner } from "react-bootstrap";
|
import {
|
||||||
import { Icon, HoverPopover, SweatDrops } from "src/components/Shared";
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Dropdown,
|
||||||
|
DropdownButton,
|
||||||
|
Spinner,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import { Icon, SweatDrops } from "src/components/Shared";
|
||||||
|
|
||||||
export interface IOCounterButtonProps {
|
export interface IOCounterButtonProps {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
@@ -19,7 +25,7 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||||||
|
|
||||||
const renderButton = () => (
|
const renderButton = () => (
|
||||||
<Button
|
<Button
|
||||||
className="minimal"
|
className="minimal pr-1"
|
||||||
onClick={props.onIncrement}
|
onClick={props.onIncrement}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
title="O-Counter"
|
title="O-Counter"
|
||||||
@@ -29,41 +35,32 @@ export const OCounterButton: React.FC<IOCounterButtonProps> = (
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const maybeRenderDropdown = () => {
|
||||||
if (props.value) {
|
if (props.value) {
|
||||||
return (
|
return (
|
||||||
<HoverPopover
|
<DropdownButton
|
||||||
content={
|
as={ButtonGroup}
|
||||||
<div>
|
title=" "
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="minimal"
|
|
||||||
onClick={props.onDecrement}
|
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
className="pl-0 show-carat"
|
||||||
>
|
>
|
||||||
|
<Dropdown.Item onClick={props.onDecrement}>
|
||||||
<Icon icon="minus" />
|
<Icon icon="minus" />
|
||||||
<span>Decrement</span>
|
<span>Decrement</span>
|
||||||
</Button>
|
</Dropdown.Item>
|
||||||
</div>
|
<Dropdown.Item onClick={props.onReset}>
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="minimal"
|
|
||||||
onClick={props.onReset}
|
|
||||||
variant="secondary"
|
|
||||||
>
|
|
||||||
<Icon icon="ban" />
|
<Icon icon="ban" />
|
||||||
<span>Reset</span>
|
<span>Reset</span>
|
||||||
</Button>
|
</Dropdown.Item>
|
||||||
</div>
|
</DropdownButton>
|
||||||
</div>
|
|
||||||
}
|
|
||||||
enterDelay={1000}
|
|
||||||
placement="bottom"
|
|
||||||
onOpen={props.onMenuOpened}
|
|
||||||
onClose={props.onMenuClosed}
|
|
||||||
>
|
|
||||||
{renderButton()}
|
|
||||||
</HoverPopover>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return renderButton();
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup className="o-counter">
|
||||||
|
{renderButton()}
|
||||||
|
{maybeRenderDropdown()}
|
||||||
|
</ButtonGroup>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ textarea.scene-description {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (pointer: fine) {
|
||||||
&:hover {
|
&:hover {
|
||||||
.scene-specs-overlay,
|
.scene-specs-overlay,
|
||||||
.rating-banner,
|
.rating-banner,
|
||||||
@@ -234,6 +235,26 @@ textarea.scene-description {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card-check {
|
||||||
|
opacity: 0.75;
|
||||||
|
transition: opacity 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scene-card-preview-video {
|
||||||
|
top: 0;
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.scene-card.card {
|
.scene-card.card {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -545,3 +566,10 @@ input[type="range"].blue-slider {
|
|||||||
color: #664c3f;
|
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 = (
|
const video = (
|
||||||
<video
|
<video
|
||||||
|
disableRemotePlayback
|
||||||
|
playsInline
|
||||||
src={previews.video}
|
src={previews.video}
|
||||||
poster={previews.image}
|
poster={previews.image}
|
||||||
autoPlay={previewType === "video"}
|
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 shouldPolyfillLocale } from "@formatjs/intl-locale/should-polyfill";
|
||||||
import { shouldPolyfill as shouldPolyfillNumberformat } from "@formatjs/intl-numberformat/should-polyfill";
|
import { shouldPolyfill as shouldPolyfillNumberformat } from "@formatjs/intl-numberformat/should-polyfill";
|
||||||
import { shouldPolyfill as shouldPolyfillPluralRules } from "@formatjs/intl-pluralrules/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.
|
// Required for browsers older than August 2020ish. Can be removed at some point.
|
||||||
replaceAll.shim();
|
replaceAll.shim();
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ hr {
|
|||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-toggle::after {
|
:not(.show-carat) > .dropdown-toggle::after {
|
||||||
content: none;
|
content: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8058,6 +8058,11 @@ internal-slot@^1.0.2:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.2"
|
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:
|
intl-messageformat-parser@6.1.3:
|
||||||
version "6.1.3"
|
version "6.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz#c333850f66d686eca5c9d87eff1ad46f8721b64d"
|
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-6.1.3.tgz#c333850f66d686eca5c9d87eff1ad46f8721b64d"
|
||||||
|
|||||||
Reference in New Issue
Block a user