mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 16:34:02 +01:00
Display mode options dropdown (#5923)
* Separate ZoomSlider into own component * Turn ListViewOptions into dropdown Also puts zoom slider in the dropdown * Move ZoomSlider into separate file * Add title * Restyle slider
This commit is contained in:
parent
574fd680c9
commit
a145576f39
5 changed files with 223 additions and 110 deletions
|
|
@ -1,21 +1,17 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import {
|
import { Button, Dropdown, Overlay, Popover } from "react-bootstrap";
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Form,
|
|
||||||
OverlayTrigger,
|
|
||||||
Tooltip,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
import { DisplayMode } from "src/models/list-filter/types";
|
import { DisplayMode } from "src/models/list-filter/types";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { Icon } from "../Shared/Icon";
|
import { Icon } from "../Shared/Icon";
|
||||||
import {
|
import {
|
||||||
|
faChevronDown,
|
||||||
faList,
|
faList,
|
||||||
faSquare,
|
faSquare,
|
||||||
faTags,
|
faTags,
|
||||||
faThLarge,
|
faThLarge,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ZoomSelect } from "./ZoomSlider";
|
||||||
|
|
||||||
interface IListViewOptionsProps {
|
interface IListViewOptionsProps {
|
||||||
zoomIndex?: number;
|
zoomIndex?: number;
|
||||||
|
|
@ -25,6 +21,38 @@ interface IListViewOptionsProps {
|
||||||
displayModeOptions: DisplayMode[];
|
displayModeOptions: DisplayMode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getIcon(option: DisplayMode) {
|
||||||
|
switch (option) {
|
||||||
|
case DisplayMode.Grid:
|
||||||
|
return faThLarge;
|
||||||
|
case DisplayMode.List:
|
||||||
|
return faList;
|
||||||
|
case DisplayMode.Wall:
|
||||||
|
return faSquare;
|
||||||
|
case DisplayMode.Tagger:
|
||||||
|
return faTags;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLabelId(option: DisplayMode) {
|
||||||
|
let displayModeId = "unknown";
|
||||||
|
switch (option) {
|
||||||
|
case DisplayMode.Grid:
|
||||||
|
displayModeId = "grid";
|
||||||
|
break;
|
||||||
|
case DisplayMode.List:
|
||||||
|
displayModeId = "list";
|
||||||
|
break;
|
||||||
|
case DisplayMode.Wall:
|
||||||
|
displayModeId = "wall";
|
||||||
|
break;
|
||||||
|
case DisplayMode.Tagger:
|
||||||
|
displayModeId = "tagger";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return `display_mode.${displayModeId}`;
|
||||||
|
}
|
||||||
|
|
||||||
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
zoomIndex,
|
zoomIndex,
|
||||||
onSetZoom,
|
onSetZoom,
|
||||||
|
|
@ -37,6 +65,9 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const overlayTarget = useRef(null);
|
||||||
|
const [showOptions, setShowOptions] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("v g", () => {
|
Mousetrap.bind("v g", () => {
|
||||||
if (displayModeOptions.includes(DisplayMode.Grid)) {
|
if (displayModeOptions.includes(DisplayMode.Grid)) {
|
||||||
|
|
@ -53,82 +84,16 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
onSetDisplayMode(DisplayMode.Wall);
|
onSetDisplayMode(DisplayMode.Wall);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Mousetrap.bind("+", () => {
|
|
||||||
if (onSetZoom && zoomIndex !== undefined && zoomIndex < maxZoom) {
|
|
||||||
onSetZoom(zoomIndex + 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
Mousetrap.bind("-", () => {
|
|
||||||
if (onSetZoom && zoomIndex !== undefined && zoomIndex > minZoom) {
|
|
||||||
onSetZoom(zoomIndex - 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("v g");
|
Mousetrap.unbind("v g");
|
||||||
Mousetrap.unbind("v l");
|
Mousetrap.unbind("v l");
|
||||||
Mousetrap.unbind("v w");
|
Mousetrap.unbind("v w");
|
||||||
Mousetrap.unbind("+");
|
|
||||||
Mousetrap.unbind("-");
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function maybeRenderDisplayModeOptions() {
|
function getLabel(option: DisplayMode) {
|
||||||
function getIcon(option: DisplayMode) {
|
return intl.formatMessage({ id: getLabelId(option) });
|
||||||
switch (option) {
|
|
||||||
case DisplayMode.Grid:
|
|
||||||
return faThLarge;
|
|
||||||
case DisplayMode.List:
|
|
||||||
return faList;
|
|
||||||
case DisplayMode.Wall:
|
|
||||||
return faSquare;
|
|
||||||
case DisplayMode.Tagger:
|
|
||||||
return faTags;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function getLabel(option: DisplayMode) {
|
|
||||||
let displayModeId = "unknown";
|
|
||||||
switch (option) {
|
|
||||||
case DisplayMode.Grid:
|
|
||||||
displayModeId = "grid";
|
|
||||||
break;
|
|
||||||
case DisplayMode.List:
|
|
||||||
displayModeId = "list";
|
|
||||||
break;
|
|
||||||
case DisplayMode.Wall:
|
|
||||||
displayModeId = "wall";
|
|
||||||
break;
|
|
||||||
case DisplayMode.Tagger:
|
|
||||||
displayModeId = "tagger";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return intl.formatMessage({ id: `display_mode.${displayModeId}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (displayModeOptions.length < 2) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ButtonGroup className="ml-2">
|
|
||||||
{displayModeOptions.map((option) => (
|
|
||||||
<OverlayTrigger
|
|
||||||
key={option}
|
|
||||||
overlay={
|
|
||||||
<Tooltip id="display-mode-tooltip">{getLabel(option)}</Tooltip>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
active={displayMode === option}
|
|
||||||
onClick={() => onSetDisplayMode(option)}
|
|
||||||
>
|
|
||||||
<Icon icon={getIcon(option)} />
|
|
||||||
</Button>
|
|
||||||
</OverlayTrigger>
|
|
||||||
))}
|
|
||||||
</ButtonGroup>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onChangeZoom(v: number) {
|
function onChangeZoom(v: number) {
|
||||||
|
|
@ -137,29 +102,60 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderZoom() {
|
|
||||||
if (onSetZoom && displayMode === DisplayMode.Grid) {
|
|
||||||
return (
|
|
||||||
<div className="ml-2 d-none d-sm-inline-flex">
|
|
||||||
<Form.Control
|
|
||||||
className="zoom-slider ml-1"
|
|
||||||
type="range"
|
|
||||||
min={minZoom}
|
|
||||||
max={maxZoom}
|
|
||||||
value={zoomIndex}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
|
||||||
onChangeZoom(Number.parseInt(e.currentTarget.value, 10))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{maybeRenderDisplayModeOptions()}
|
<Button
|
||||||
{maybeRenderZoom()}
|
className="display-mode-select"
|
||||||
|
ref={overlayTarget}
|
||||||
|
variant="secondary"
|
||||||
|
title={intl.formatMessage(
|
||||||
|
{ id: "display_mode.label_current" },
|
||||||
|
{ current: getLabel(displayMode) }
|
||||||
|
)}
|
||||||
|
onClick={() => setShowOptions(!showOptions)}
|
||||||
|
>
|
||||||
|
<Icon icon={getIcon(displayMode)} />
|
||||||
|
<Icon size="xs" icon={faChevronDown} />
|
||||||
|
</Button>
|
||||||
|
<Overlay
|
||||||
|
target={overlayTarget.current}
|
||||||
|
show={showOptions}
|
||||||
|
placement="bottom"
|
||||||
|
rootClose
|
||||||
|
onHide={() => setShowOptions(false)}
|
||||||
|
>
|
||||||
|
{({ placement, arrowProps, show: _show, ...props }) => (
|
||||||
|
<div className="popover" {...props} style={{ ...props.style }}>
|
||||||
|
<Popover.Content className="display-mode-popover">
|
||||||
|
<div className="display-mode-menu">
|
||||||
|
{onSetZoom &&
|
||||||
|
zoomIndex !== undefined &&
|
||||||
|
displayMode === DisplayMode.Grid ? (
|
||||||
|
<div className="zoom-slider-container">
|
||||||
|
<ZoomSelect
|
||||||
|
minZoom={minZoom}
|
||||||
|
maxZoom={maxZoom}
|
||||||
|
zoomIndex={zoomIndex}
|
||||||
|
onChangeZoom={onChangeZoom}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{displayModeOptions.map((option) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={option}
|
||||||
|
active={displayMode === option}
|
||||||
|
onClick={() => {
|
||||||
|
onSetDisplayMode(option);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon={getIcon(option)} /> {getLabel(option)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Overlay>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
50
ui/v2.5/src/components/List/ZoomSlider.tsx
Normal file
50
ui/v2.5/src/components/List/ZoomSlider.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import Mousetrap from "mousetrap";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
|
||||||
|
export interface IZoomSelectProps {
|
||||||
|
minZoom: number;
|
||||||
|
maxZoom: number;
|
||||||
|
zoomIndex: number;
|
||||||
|
onChangeZoom: (v: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ZoomSelect: React.FC<IZoomSelectProps> = ({
|
||||||
|
minZoom,
|
||||||
|
maxZoom,
|
||||||
|
zoomIndex,
|
||||||
|
onChangeZoom,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
Mousetrap.bind("+", () => {
|
||||||
|
if (zoomIndex !== undefined && zoomIndex < maxZoom) {
|
||||||
|
onChangeZoom(zoomIndex + 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Mousetrap.bind("-", () => {
|
||||||
|
if (zoomIndex !== undefined && zoomIndex > minZoom) {
|
||||||
|
onChangeZoom(zoomIndex - 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
Mousetrap.unbind("+");
|
||||||
|
Mousetrap.unbind("-");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
className="zoom-slider"
|
||||||
|
type="range"
|
||||||
|
min={minZoom}
|
||||||
|
max={maxZoom}
|
||||||
|
value={zoomIndex}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChangeZoom(Number.parseInt(e.currentTarget.value, 10));
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -34,6 +34,68 @@
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.display-mode-select {
|
||||||
|
padding-left: 0.375rem;
|
||||||
|
padding-right: 0.375rem;
|
||||||
|
text-wrap: nowrap;
|
||||||
|
|
||||||
|
> svg:first-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-mode-menu {
|
||||||
|
.dropdown-item {
|
||||||
|
color: #f5f8fa;
|
||||||
|
font-size: 1rem;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(138, 155, 168, 0.15);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-slider-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
min-height: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zoom-slider {
|
||||||
|
&::-webkit-slider-thumb {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
background-color: $body-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::-webkit-slider-runnable-track {
|
||||||
|
background-color: lighten($body-bg, 5%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-thumb {
|
||||||
|
background-color: $primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
background-color: $body-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus::-moz-range-track {
|
||||||
|
background-color: lighten($body-bg, 5%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-mode-popover {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
input[type="range"].zoom-slider {
|
input[type="range"].zoom-slider {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
@ -810,6 +872,10 @@ input[type="range"].zoom-slider {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
row-gap: 0.5rem;
|
row-gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.display-mode-select {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-pane .filtered-list-toolbar {
|
.sidebar-pane .filtered-list-toolbar {
|
||||||
|
|
|
||||||
|
|
@ -995,6 +995,7 @@
|
||||||
"disambiguation": "Disambiguation",
|
"disambiguation": "Disambiguation",
|
||||||
"display_mode": {
|
"display_mode": {
|
||||||
"grid": "Grid",
|
"grid": "Grid",
|
||||||
|
"label_current": "Display Mode: {current}",
|
||||||
"list": "List",
|
"list": "List",
|
||||||
"tagger": "Tagger",
|
"tagger": "Tagger",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ input[type="range"] {
|
||||||
|
|
||||||
&::-webkit-slider-runnable-track {
|
&::-webkit-slider-runnable-track {
|
||||||
animate: 0.2s;
|
animate: 0.2s;
|
||||||
background: #137cbd;
|
background: $primary;
|
||||||
border: 0 solid #000101;
|
border: 0 solid #000101;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
@ -28,12 +28,12 @@ input[type="range"] {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
background: #394b59;
|
background: #394b59;
|
||||||
border: 0 solid #000;
|
border: 0 solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 16px;
|
height: 15px;
|
||||||
margin-top: -5px;
|
margin-top: -4px;
|
||||||
width: 16px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus::-webkit-slider-runnable-track {
|
&:focus::-webkit-slider-runnable-track {
|
||||||
|
|
@ -42,7 +42,7 @@ input[type="range"] {
|
||||||
|
|
||||||
&::-moz-range-track {
|
&::-moz-range-track {
|
||||||
animate: 0.2s;
|
animate: 0.2s;
|
||||||
background: #137cbd;
|
background: $primary;
|
||||||
border: 0 solid #000101;
|
border: 0 solid #000101;
|
||||||
border-radius: 25px;
|
border-radius: 25px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
@ -54,11 +54,11 @@ input[type="range"] {
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
background: #394b59;
|
background: #394b59;
|
||||||
border: 0 solid #000;
|
border: 0 solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 16px;
|
height: 15px;
|
||||||
width: 16px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-ms-track {
|
&::-ms-track {
|
||||||
|
|
@ -72,14 +72,14 @@ input[type="range"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-ms-fill-lower {
|
&::-ms-fill-lower {
|
||||||
background: #137cbd;
|
background: $primary;
|
||||||
border: 0 solid #000101;
|
border: 0 solid #000101;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-ms-fill-upper {
|
&::-ms-fill-upper {
|
||||||
background: #137cbd;
|
background: $primary;
|
||||||
border: 0 solid #000101;
|
border: 0 solid #000101;
|
||||||
border-radius: 50px;
|
border-radius: 50px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
|
|
@ -88,11 +88,11 @@ input[type="range"] {
|
||||||
&::-ms-thumb {
|
&::-ms-thumb {
|
||||||
background: #394b59;
|
background: #394b59;
|
||||||
border: 0 solid #000;
|
border: 0 solid #000;
|
||||||
border-radius: 5px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 0 0 #000;
|
box-shadow: 0 0 0 #000;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
margin-top: 1px;
|
margin-top: 2px;
|
||||||
width: 16px;
|
width: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue