mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +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 {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Form,
|
||||
OverlayTrigger,
|
||||
Tooltip,
|
||||
} from "react-bootstrap";
|
||||
import { Button, Dropdown, Overlay, Popover } from "react-bootstrap";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
import {
|
||||
faChevronDown,
|
||||
faList,
|
||||
faSquare,
|
||||
faTags,
|
||||
faThLarge,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { ZoomSelect } from "./ZoomSlider";
|
||||
|
||||
interface IListViewOptionsProps {
|
||||
zoomIndex?: number;
|
||||
|
|
@ -25,6 +21,38 @@ interface IListViewOptionsProps {
|
|||
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> = ({
|
||||
zoomIndex,
|
||||
onSetZoom,
|
||||
|
|
@ -37,6 +65,9 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
|||
|
||||
const intl = useIntl();
|
||||
|
||||
const overlayTarget = useRef(null);
|
||||
const [showOptions, setShowOptions] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
Mousetrap.bind("v g", () => {
|
||||
if (displayModeOptions.includes(DisplayMode.Grid)) {
|
||||
|
|
@ -53,82 +84,16 @@ export const ListViewOptions: React.FC<IListViewOptionsProps> = ({
|
|||
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 () => {
|
||||
Mousetrap.unbind("v g");
|
||||
Mousetrap.unbind("v l");
|
||||
Mousetrap.unbind("v w");
|
||||
Mousetrap.unbind("+");
|
||||
Mousetrap.unbind("-");
|
||||
};
|
||||
});
|
||||
|
||||
function maybeRenderDisplayModeOptions() {
|
||||
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 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>
|
||||
);
|
||||
return intl.formatMessage({ id: getLabelId(option) });
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
{maybeRenderDisplayModeOptions()}
|
||||
{maybeRenderZoom()}
|
||||
<Button
|
||||
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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
|
|
@ -810,6 +872,10 @@ input[type="range"].zoom-slider {
|
|||
justify-content: center;
|
||||
row-gap: 0.5rem;
|
||||
}
|
||||
|
||||
.btn.display-mode-select {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-pane .filtered-list-toolbar {
|
||||
|
|
|
|||
|
|
@ -995,6 +995,7 @@
|
|||
"disambiguation": "Disambiguation",
|
||||
"display_mode": {
|
||||
"grid": "Grid",
|
||||
"label_current": "Display Mode: {current}",
|
||||
"list": "List",
|
||||
"tagger": "Tagger",
|
||||
"unknown": "Unknown",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ input[type="range"] {
|
|||
|
||||
&::-webkit-slider-runnable-track {
|
||||
animate: 0.2s;
|
||||
background: #137cbd;
|
||||
background: $primary;
|
||||
border: 0 solid #000101;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
|
@ -28,12 +28,12 @@ input[type="range"] {
|
|||
-webkit-appearance: none;
|
||||
background: #394b59;
|
||||
border: 0 solid #000;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
margin-top: -5px;
|
||||
width: 16px;
|
||||
height: 15px;
|
||||
margin-top: -4px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
&:focus::-webkit-slider-runnable-track {
|
||||
|
|
@ -42,7 +42,7 @@ input[type="range"] {
|
|||
|
||||
&::-moz-range-track {
|
||||
animate: 0.2s;
|
||||
background: #137cbd;
|
||||
background: $primary;
|
||||
border: 0 solid #000101;
|
||||
border-radius: 25px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
|
@ -54,11 +54,11 @@ input[type="range"] {
|
|||
&::-moz-range-thumb {
|
||||
background: #394b59;
|
||||
border: 0 solid #000;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
height: 15px;
|
||||
width: 15px;
|
||||
}
|
||||
|
||||
&::-ms-track {
|
||||
|
|
@ -72,14 +72,14 @@ input[type="range"] {
|
|||
}
|
||||
|
||||
&::-ms-fill-lower {
|
||||
background: #137cbd;
|
||||
background: $primary;
|
||||
border: 0 solid #000101;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
}
|
||||
|
||||
&::-ms-fill-upper {
|
||||
background: #137cbd;
|
||||
background: $primary;
|
||||
border: 0 solid #000101;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
|
|
@ -88,11 +88,11 @@ input[type="range"] {
|
|||
&::-ms-thumb {
|
||||
background: #394b59;
|
||||
border: 0 solid #000;
|
||||
border-radius: 5px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 0 0 #000;
|
||||
cursor: pointer;
|
||||
height: 16px;
|
||||
margin-top: 1px;
|
||||
margin-top: 2px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue