mirror of
https://github.com/stashapp/stash.git
synced 2025-12-14 12:25:23 +01:00
Styling
This commit is contained in:
parent
c2544fee98
commit
3fa3f61d93
18 changed files with 422 additions and 189 deletions
|
|
@ -31,8 +31,6 @@ module.exports = merge(commonConfig, {
|
|||
compress: true,
|
||||
host: '0.0.0.0',
|
||||
hot: true, // enable HMR on the server host: '0.0.0.0',
|
||||
transportMode: 'ws',
|
||||
injectClient: false,
|
||||
port: process.env.PORT,
|
||||
historyApiFallback: true,
|
||||
stats: {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React from "react";
|
||||
import { Spinner } from "react-bootstrap";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { StashService } from "src/core/StashService";
|
||||
import { LoadingIndicator } from 'src/components/Shared';
|
||||
import { GalleryViewer } from "./GalleryViewer";
|
||||
|
||||
export const Gallery: React.FC = () => {
|
||||
|
|
@ -11,7 +11,7 @@ export const Gallery: React.FC = () => {
|
|||
const gallery = data?.findGallery;
|
||||
|
||||
if (loading || !gallery)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
return <LoadingIndicator />;
|
||||
if (error) return <div>{error.message}</div>;
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export const MainNavbar: React.FC = () => {
|
|||
<Navbar fixed="top" variant="dark" bg="dark">
|
||||
<Navbar.Brand as="div">
|
||||
<Link to="/">
|
||||
<Button variant="secondary">Stash</Button>
|
||||
<Button className="minimal">Stash</Button>
|
||||
</Link>
|
||||
</Navbar.Brand>
|
||||
<Nav className="mr-auto">
|
||||
|
|
@ -78,7 +78,7 @@ export const MainNavbar: React.FC = () => {
|
|||
to={i.href}
|
||||
key={i.href}
|
||||
>
|
||||
<Button variant="secondary">
|
||||
<Button className="minimal">
|
||||
<Icon icon={i.icon} />
|
||||
{i.text}
|
||||
</Button>
|
||||
|
|
@ -88,7 +88,7 @@ export const MainNavbar: React.FC = () => {
|
|||
<Nav>
|
||||
{newButton}
|
||||
<LinkContainer exact to="/settings">
|
||||
<Button variant="secondary">
|
||||
<Button className="minimal">
|
||||
<Icon icon="cog" />
|
||||
</Button>
|
||||
</LinkContainer>
|
||||
|
|
|
|||
|
|
@ -17,11 +17,13 @@ interface ITypeProps {
|
|||
type?: "performers" | "studios" | "tags";
|
||||
}
|
||||
interface IFilterProps {
|
||||
initialIds: string[];
|
||||
ids?: string[];
|
||||
initialIds?: string[];
|
||||
onSelect: (item: ValidTypes[]) => void;
|
||||
noSelectionString?: string;
|
||||
className?: string;
|
||||
isMulti?: boolean;
|
||||
isClearable?: boolean;
|
||||
}
|
||||
interface ISelectProps {
|
||||
className?: string;
|
||||
|
|
@ -31,8 +33,9 @@ interface ISelectProps {
|
|||
onCreateOption?: (value: string) => void;
|
||||
isLoading: boolean;
|
||||
onChange: (item: ValueType<Option>) => void;
|
||||
initialIds: string[];
|
||||
initialIds?: string[];
|
||||
isMulti?: boolean;
|
||||
isClearable?: boolean,
|
||||
onInputChange?: (input: string) => void;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
|
@ -48,9 +51,10 @@ interface ISceneGallerySelect {
|
|||
}
|
||||
|
||||
const getSelectedValues = (selectedItems: ValueType<Option>) =>
|
||||
selectedItems ?
|
||||
(Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
|
||||
item => item.value
|
||||
);
|
||||
) : [];
|
||||
|
||||
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
|
||||
const { data, loading } = StashService.useValidGalleriesForScene(
|
||||
|
|
@ -165,6 +169,8 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
|
|||
label: item.name ?? ""
|
||||
}));
|
||||
const placeholder = props.noSelectionString ?? "Select performer...";
|
||||
const selectedOptions:Option[] = props.ids ?
|
||||
items.filter(item => props.ids?.indexOf(item.value) !== -1) : [];
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option>) => {
|
||||
const selectedIds = getSelectedValues(selectedItems);
|
||||
|
|
@ -176,6 +182,7 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
|
|||
return (
|
||||
<SelectComponent
|
||||
{...props}
|
||||
selectedOptions={selectedOptions}
|
||||
onChange={onChange}
|
||||
type="performers"
|
||||
isLoading={loading}
|
||||
|
|
@ -194,6 +201,8 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
|
|||
label: item.name
|
||||
}));
|
||||
const placeholder = props.noSelectionString ?? "Select studio...";
|
||||
const selectedOptions:Option[] = props.ids ?
|
||||
items.filter(item => props.ids?.indexOf(item.value) !== -1) : [];
|
||||
|
||||
const onChange = (selectedItems: ValueType<Option>) => {
|
||||
const selectedIds = getSelectedValues(selectedItems);
|
||||
|
|
@ -210,13 +219,14 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
|
|||
isLoading={loading}
|
||||
items={items}
|
||||
placeholder={placeholder}
|
||||
selectedOptions={selectedOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const TagSelect: React.FC<IFilterProps> = props => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||
const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []);
|
||||
const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
|
||||
const [createTag] = StashService.useTagCreate({ name: "" });
|
||||
const Toast = useToast();
|
||||
|
|
@ -290,6 +300,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||
selectedOptions,
|
||||
isLoading,
|
||||
onCreateOption,
|
||||
isClearable = true,
|
||||
creatable = false,
|
||||
isMulti = false,
|
||||
onInputChange,
|
||||
|
|
@ -298,26 +309,58 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
|
|||
const defaultValue =
|
||||
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null;
|
||||
|
||||
const styles = {
|
||||
control: (provided:any) => ({
|
||||
...provided,
|
||||
background: '#394b59',
|
||||
borderColor: 'rgba(16,22,26,.4)'
|
||||
}),
|
||||
singleValue: (provided:any) => ({
|
||||
...provided,
|
||||
color: 'f5f8fa',
|
||||
}),
|
||||
placeholder: (provided:any) => ({
|
||||
...provided,
|
||||
color: 'f5f8fa',
|
||||
}),
|
||||
menu: (provided:any) => ({
|
||||
...provided,
|
||||
color: 'f5f8fa',
|
||||
background: '#394b59',
|
||||
borderColor: 'rgba(16,22,26,.4)',
|
||||
zIndex: 3
|
||||
}),
|
||||
option: (provided:any, state:any ) => (
|
||||
state.isFocused ? { ...provided, backgroundColor: '#137cbd' } : provided
|
||||
),
|
||||
multiValueRemove: (provided:any, state:any) => (
|
||||
{ ...provided, color: 'black' }
|
||||
)
|
||||
};
|
||||
|
||||
const props = {
|
||||
className,
|
||||
options: items,
|
||||
value: selectedOptions,
|
||||
styles,
|
||||
className,
|
||||
onChange,
|
||||
isMulti,
|
||||
isClearable,
|
||||
defaultValue,
|
||||
noOptionsMessage: () => (type !== "tags" ? "None" : null),
|
||||
placeholder,
|
||||
onInputChange
|
||||
onInputChange,
|
||||
isLoading,
|
||||
components: { IndicatorSeparator: () => null }
|
||||
};
|
||||
|
||||
return creatable ? (
|
||||
<CreatableSelect
|
||||
{...props}
|
||||
isLoading={isLoading}
|
||||
isDisabled={isLoading}
|
||||
onCreateOption={onCreateOption}
|
||||
/>
|
||||
) : (
|
||||
<Select {...props} isLoading={isLoading} />
|
||||
<Select {...props} />
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,8 +39,6 @@
|
|||
.scene-wall-item-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
// align-items: center;
|
||||
// overflow: hidden; // Commented out since it shows gaps in the wall
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -62,6 +60,7 @@
|
|||
padding: 5px;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.65));
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
|
|
@ -80,18 +79,17 @@
|
|||
left: -5px;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
/*background-color: rgba(255, 255, 255, 0.75);*/
|
||||
/*backdrop-filter: blur(5px);*/
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.wall.grid-item video, .wall.grid-item img {
|
||||
.wall-item video, .wall-item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.wall.grid-item {
|
||||
.wall-item {
|
||||
width: 20%;
|
||||
padding: 0 !important;
|
||||
line-height: 0;
|
||||
overflow: visible;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import _ from "lodash";
|
||||
import React, { FunctionComponent, useRef, useState, useEffect } from "react";
|
||||
import React, { useRef, useState, useEffect } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { StashService } from "src/core/StashService";
|
||||
|
|
@ -16,10 +16,10 @@ interface IWallItemProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
export const WallItem: FunctionComponent<IWallItemProps> = (
|
||||
export const WallItem: React.FC<IWallItemProps> = (
|
||||
props: IWallItemProps
|
||||
) => {
|
||||
const [videoPath, setVideoPath] = useState<string | undefined>(undefined);
|
||||
const [videoPath, setVideoPath] = useState<string>();
|
||||
const [previewPath, setPreviewPath] = useState<string>("");
|
||||
const [screenshotPath, setScreenshotPath] = useState<string>("");
|
||||
const [title, setTitle] = useState<string>("");
|
||||
|
|
@ -28,10 +28,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (
|
|||
const videoHoverHook = VideoHoverHook.useVideoHover({
|
||||
resetOnMouseLeave: true
|
||||
});
|
||||
const showTextContainer =
|
||||
!!config.data && !!config.data.configuration
|
||||
? config.data.configuration.interface.wallShowTitle
|
||||
: true;
|
||||
const showTextContainer = config.data?.configuration.interface.wallShowTitle ?? true;
|
||||
|
||||
function onMouseEnter() {
|
||||
VideoHoverHook.onMouseEnter(videoHoverHook);
|
||||
|
|
@ -122,7 +119,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (
|
|||
style.transformOrigin = props.origin;
|
||||
}
|
||||
return (
|
||||
<div className="wall grid-item">
|
||||
<div className="wall-item">
|
||||
<div
|
||||
className={className.join(" ")}
|
||||
style={style}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { FunctionComponent, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { WallItem } from "./WallItem";
|
||||
import "./Wall.scss";
|
||||
|
|
@ -11,7 +11,7 @@ interface IWallPanelProps {
|
|||
) => void;
|
||||
}
|
||||
|
||||
export const WallPanel: FunctionComponent<IWallPanelProps> = (
|
||||
export const WallPanel: React.FC<IWallPanelProps> = (
|
||||
props: IWallPanelProps
|
||||
) => {
|
||||
const [showOverlay, setShowOverlay] = useState<boolean>(false);
|
||||
|
|
|
|||
|
|
@ -229,7 +229,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
|
|||
placement="top"
|
||||
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
|
||||
>
|
||||
<Button onClick={() => onToggle()} active={isOpen}>
|
||||
<Button variant="secondary" onClick={() => onToggle()} active={isOpen}>
|
||||
<Icon icon="filter" />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
|
|
|
|||
|
|
@ -139,6 +139,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={option}
|
||||
active={props.filter.displayMode === option}
|
||||
onClick={() => onChangeDisplayMode(option)}
|
||||
|
|
@ -157,7 +158,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
onClick={() => onClickCriterionTag(criterion)}
|
||||
>
|
||||
{criterion.getLabel()}
|
||||
<Button onClick={() => onRemoveCriterionTag(criterion)}>
|
||||
<Button variant="secondary" onClick={() => onRemoveCriterionTag(criterion)}>
|
||||
<Icon icon="times" />
|
||||
</Button>
|
||||
</Badge>
|
||||
|
|
@ -226,16 +227,15 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
function maybeRenderZoom() {
|
||||
if (props.onChangeZoom) {
|
||||
return (
|
||||
<span className="zoom-slider">
|
||||
<Form.Control
|
||||
type="range"
|
||||
min={0}
|
||||
max={3}
|
||||
onChange={(event: any) =>
|
||||
onChangeZoom(Number.parseInt(event.target.value, 10))
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
<Form.Control
|
||||
className="zoom-slider"
|
||||
type="range"
|
||||
min={0}
|
||||
max={3}
|
||||
onChange={(event: any) =>
|
||||
onChangeZoom(Number.parseInt(event.target.value, 10))
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -243,7 +243,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
function render() {
|
||||
return (
|
||||
<>
|
||||
<div className="d-flex justify-content-center m-auto">
|
||||
<div className="filter-container">
|
||||
<Form.Control
|
||||
placeholder="Search..."
|
||||
value={props.filter.searchTerm}
|
||||
|
|
@ -262,32 +262,32 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
|||
))}
|
||||
</Form.Control>
|
||||
<ButtonGroup className="filter-item">
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle variant="secondary" id="more-menu">
|
||||
<Dropdown as={ButtonGroup}>
|
||||
<Dropdown.Toggle split variant="secondary" id="more-menu">
|
||||
{props.filter.sortBy}
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>{renderSortByOptions()}</Dropdown.Menu>
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-direction-tooltip">
|
||||
{props.filter.sortDirection === "asc"
|
||||
? "Ascending"
|
||||
: "Descending"}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button variant="secondary" onClick={onChangeSortDirection}>
|
||||
<Icon
|
||||
icon={
|
||||
props.filter.sortDirection === "asc"
|
||||
? "caret-up"
|
||||
: "caret-down"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</Dropdown>
|
||||
|
||||
<OverlayTrigger
|
||||
overlay={
|
||||
<Tooltip id="sort-direction-tooltip">
|
||||
{props.filter.sortDirection === "asc"
|
||||
? "Ascending"
|
||||
: "Descending"}
|
||||
</Tooltip>
|
||||
}
|
||||
>
|
||||
<Button onClick={onChangeSortDirection}>
|
||||
<Icon
|
||||
icon={
|
||||
props.filter.sortDirection === "asc"
|
||||
? "caret-up"
|
||||
: "caret-down"
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
</ButtonGroup>
|
||||
|
||||
<AddFilter
|
||||
|
|
|
|||
|
|
@ -39,6 +39,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
|
||||
const pageButtons = pages.map((page: number) => (
|
||||
<Button
|
||||
variant="secondary"
|
||||
key={page}
|
||||
active={currentPage === page}
|
||||
onClick={() => onChangePage(page)}
|
||||
|
|
@ -48,11 +49,12 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
));
|
||||
|
||||
return (
|
||||
<ButtonGroup className="filter-container">
|
||||
<Button disabled={currentPage === 1} onClick={() => onChangePage(1)}>
|
||||
<ButtonGroup className="filter-container pagination">
|
||||
<Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}>
|
||||
First
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={currentPage === 1}
|
||||
onClick={() => onChangePage(currentPage - 1)}
|
||||
>
|
||||
|
|
@ -60,12 +62,14 @@ export const Pagination: React.FC<IPaginationProps> = ({
|
|||
</Button>
|
||||
{pageButtons}
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => onChangePage(currentPage + 1)}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
disabled={currentPage === totalPages}
|
||||
onClick={() => onChangePage(totalPages)}
|
||||
>
|
||||
|
|
|
|||
15
ui/v2.5/src/components/list/styles.scss
Normal file
15
ui/v2.5/src/components/list/styles.scss
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
.pagination {
|
||||
.btn {
|
||||
flex-grow: 0;
|
||||
padding-left: 15px;
|
||||
padding-right: 15px;
|
||||
border-left: 1px solid $body-bg;
|
||||
border-right: 1px solid $body-bg;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.zoom-slider {
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
|
@ -91,7 +91,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Button className="minimal">
|
||||
<Icon icon="tag" />
|
||||
{props.scene.tags.length}
|
||||
</Button>
|
||||
|
|
@ -115,7 +115,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Button className="minimal">
|
||||
<Icon icon="user" />
|
||||
{props.scene.performers.length}
|
||||
</Button>
|
||||
|
|
@ -133,8 +133,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
|
||||
return (
|
||||
<HoverPopover placement="bottom" content={popoverContent}>
|
||||
<Button>
|
||||
<Icon icon="tag" />
|
||||
<Button className="minimal">
|
||||
<Icon icon="map-marker-alt" />
|
||||
{props.scene.scene_markers.length}
|
||||
</Button>
|
||||
</HoverPopover>
|
||||
|
|
@ -150,7 +150,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
return (
|
||||
<>
|
||||
<hr />
|
||||
<ButtonGroup className="mr-2">
|
||||
<ButtonGroup className="scene-popovers">
|
||||
{maybeRenderTagPopoverButton()}
|
||||
{maybeRenderPerformerPopoverButton()}
|
||||
{maybeRenderSceneMarkerPopoverButton()}
|
||||
|
|
@ -182,7 +182,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
|
||||
return (
|
||||
<Card
|
||||
className={`col-4 zoom-${props.zoomIndex}`}
|
||||
className={`zoom-${props.zoomIndex}`}
|
||||
onMouseEnter={onMouseEnter}
|
||||
onMouseLeave={onMouseLeave}
|
||||
>
|
||||
|
|
@ -216,11 +216,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
|
|||
</div>
|
||||
</Link>
|
||||
<div className="card-section">
|
||||
<h4 className="text-truncate">
|
||||
<h5 className="text-truncate">
|
||||
{props.scene.title
|
||||
? props.scene.title
|
||||
: TextUtils.fileNameFromPath(props.scene.path)}
|
||||
</h4>
|
||||
</h5>
|
||||
<span>{props.scene.date}</span>
|
||||
<p>
|
||||
{TextUtils.truncate(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ export const SceneMarkerList: React.FC = () => {
|
|||
filter: ListFilterModel
|
||||
) {
|
||||
// query for a random scene
|
||||
if (result.data && result.data.findSceneMarkers) {
|
||||
if (result.data?.findSceneMarkers) {
|
||||
const { count } = result.data.findSceneMarkers;
|
||||
|
||||
const index = Math.floor(Math.random() * count);
|
||||
|
|
@ -37,10 +37,7 @@ export const SceneMarkerList: React.FC = () => {
|
|||
filterCopy.currentPage = index + 1;
|
||||
const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
|
||||
if (
|
||||
singleResult &&
|
||||
singleResult.data &&
|
||||
singleResult.data.findSceneMarkers &&
|
||||
singleResult.data.findSceneMarkers.scene_markers.length === 1
|
||||
singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1
|
||||
) {
|
||||
// navigate to the scene player page
|
||||
const url = NavUtils.makeSceneMarkerUrl(
|
||||
|
|
|
|||
|
|
@ -16,16 +16,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
|||
) => {
|
||||
const Toast = useToast();
|
||||
const [rating, setRating] = useState<string>("");
|
||||
const [studioId, setStudioId] = useState<string | undefined>(undefined);
|
||||
const [performerIds, setPerformerIds] = useState<string[] | undefined>(
|
||||
undefined
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
|
||||
const [studioId, setStudioId] = useState<string>();
|
||||
const [performerIds, setPerformerIds] = useState<string[]>();
|
||||
const [tagIds, setTagIds] = useState<string[]>();
|
||||
|
||||
const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput());
|
||||
|
||||
// Network state
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
function getSceneInput(): GQL.BulkSceneUpdateInput {
|
||||
// need to determine what we are actually setting on each scene
|
||||
|
|
@ -184,14 +182,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
|||
|
||||
function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
|
||||
let updateRating = "";
|
||||
let updateStudioId: string | undefined;
|
||||
let updateStudioId: string|undefined;
|
||||
let updatePerformerIds: string[] = [];
|
||||
let updateTagIds: string[] = [];
|
||||
let first = true;
|
||||
|
||||
state.forEach((scene: GQL.SlimSceneDataFragment) => {
|
||||
const thisRating = scene.rating ? scene.rating.toString() : "";
|
||||
const thisStudio = scene.studio ? scene.studio.id : undefined;
|
||||
const thisRating = scene.rating?.toString() ?? "";
|
||||
const thisStudio = scene?.studio?.id;
|
||||
|
||||
if (first) {
|
||||
updateRating = thisRating;
|
||||
|
|
@ -231,76 +229,76 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
|
|||
|
||||
useEffect(() => {
|
||||
updateScenesEditState(props.selected);
|
||||
setIsLoading(false);
|
||||
}, [props.selected]);
|
||||
|
||||
function renderMultiSelect(
|
||||
type: "performers" | "tags",
|
||||
initialIds: string[] | undefined
|
||||
ids: string[] | undefined
|
||||
) {
|
||||
return (
|
||||
<FilterSelect
|
||||
type={type}
|
||||
isMulti
|
||||
isClearable={false}
|
||||
onSelect={items => {
|
||||
const ids = items.map(i => i.id);
|
||||
const itemIDs = items.map(i => i.id);
|
||||
switch (type) {
|
||||
case "performers":
|
||||
setPerformerIds(ids);
|
||||
setPerformerIds(itemIDS);
|
||||
break;
|
||||
case "tags":
|
||||
setTagIds(ids);
|
||||
setTagIds(itemIDs);
|
||||
break;
|
||||
}
|
||||
}}
|
||||
initialIds={initialIds ?? []}
|
||||
ids={ids ?? []}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if(isLoading)
|
||||
return <Spinner animation="border" variant="light" />;
|
||||
|
||||
function render() {
|
||||
return (
|
||||
<>
|
||||
{isLoading ? <Spinner animation="border" variant="light" /> : undefined}
|
||||
<div className="operation-container">
|
||||
<Form.Group controlId="rating" className="operation-item">
|
||||
<Form.Label>Rating</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event: any) => setRating(event.target.value)}
|
||||
>
|
||||
{["", 1, 2, 3, 4, 5].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
<div className="operation-container">
|
||||
<Form.Group controlId="rating" className="operation-item rating-operation">
|
||||
<Form.Label>Rating</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
onChange={(event: any) => setRating(event.target.value)}
|
||||
>
|
||||
{["", '1', '2', '3', '4', '5'].map(opt => (
|
||||
<option selected={opt === rating} value={opt}>
|
||||
{opt}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" className="operation-item">
|
||||
<Form.Label>Studio</Form.Label>
|
||||
<StudioSelect
|
||||
onSelect={items => setStudioId(items[0]?.id)}
|
||||
initialIds={studioId ? [studioId] : []}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="studio" className="operation-item">
|
||||
<Form.Label>Studio</Form.Label>
|
||||
<StudioSelect
|
||||
onSelect={items => setStudioId(items[0]?.id)}
|
||||
ids={studioId ? [studioId] : []}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="opeation-item" controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
<Form.Group className="operation-item" controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
{renderMultiSelect("performers", performerIds)}
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group className="operation-item" controlId="performers">
|
||||
<Form.Label>Performers</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
<Form.Group className="operation-item" controlId="performers">
|
||||
<Form.Label>Tags</Form.Label>
|
||||
{renderMultiSelect("tags", tagIds)}
|
||||
</Form.Group>
|
||||
|
||||
<ButtonGroup className="operation-item">
|
||||
<Button variant="primary" onClick={onSave}>
|
||||
Apply
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</>
|
||||
<Button variant="primary" onClick={onSave} className="apply-operation">
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
28
ui/v2.5/src/components/scenes/styles.scss
Normal file
28
ui/v2.5/src/components/scenes/styles.scss
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
.scene-popovers {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 10px;
|
||||
|
||||
button {
|
||||
padding-top: 3px;
|
||||
padding-bottom: 3px;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 7px;
|
||||
}
|
||||
}
|
||||
|
||||
.operation-container {
|
||||
.operation-item {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.rating-operation {
|
||||
min-width: 20px;
|
||||
}
|
||||
|
||||
.apply-operation {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
@import "styles/shared/details";
|
||||
|
||||
@import "styles/range";
|
||||
@import "styles/scrollbars";
|
||||
@import "styles/variables";
|
||||
|
||||
|
|
@ -17,7 +18,6 @@ body {
|
|||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
height: 100vh;
|
||||
background: $dark-gray2;
|
||||
}
|
||||
|
||||
code {
|
||||
|
|
@ -37,10 +37,6 @@ code {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
& .bp3-button.favorite .bp3-icon {
|
||||
color: #ff7373 !important
|
||||
}
|
||||
|
||||
& .performer-list-thumbnail {
|
||||
min-width: 50px;
|
||||
height: 100px;
|
||||
|
|
@ -55,58 +51,59 @@ code {
|
|||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item {
|
||||
// flex: auto;
|
||||
width: 320px;
|
||||
min-width: 185px;
|
||||
margin: 0px 0 $pt-grid-size $pt-grid-size;
|
||||
overflow: hidden;
|
||||
.card {
|
||||
margin: 0 0 10px 10px;
|
||||
overflow: hidden;
|
||||
|
||||
&.wall {
|
||||
width: calc(20%);
|
||||
margin: 0;
|
||||
}
|
||||
&.zoom-0 {
|
||||
width: 15rem;
|
||||
|
||||
&.zoom-0 {
|
||||
width: 240px;
|
||||
& .previewable {
|
||||
max-height: 11.25rem;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
max-height: 11.25rem;
|
||||
}
|
||||
}
|
||||
&.zoom-1 {
|
||||
width: 20rem;
|
||||
|
||||
& .previewable {
|
||||
max-height: 180px;
|
||||
& .previewable {
|
||||
max-height: 15rem;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 15rem;
|
||||
}
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 180px;
|
||||
}
|
||||
}
|
||||
&.zoom-1 {
|
||||
width: 320px;
|
||||
&.zoom-2 {
|
||||
width: 30rem;
|
||||
|
||||
& .previewable {
|
||||
max-height: 240px;
|
||||
& .previewable {
|
||||
max-height: 22.5rem;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 22.5rem;
|
||||
}
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 240px;
|
||||
}
|
||||
}
|
||||
&.zoom-2 {
|
||||
width: 480px;
|
||||
&.zoom-3 {
|
||||
width: 40rem;
|
||||
|
||||
& .previewable {
|
||||
max-height: 360px;
|
||||
& .previewable {
|
||||
max-height: 30rem;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 30rem;
|
||||
}
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 360px;
|
||||
}
|
||||
}
|
||||
&.zoom-3 {
|
||||
width: 640px;
|
||||
|
||||
& .previewable {
|
||||
max-height: 480px;
|
||||
}
|
||||
& .previewable.portrait {
|
||||
height: 480px;
|
||||
.card-select {
|
||||
position: absolute;
|
||||
padding-left: 15px;
|
||||
margin-top: -12px;
|
||||
z-index: 1;
|
||||
opacity: 0.5;
|
||||
width: 1.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -125,14 +122,6 @@ code {
|
|||
height: 240px;
|
||||
}
|
||||
|
||||
.grid-item label.card-select {
|
||||
position: absolute;
|
||||
padding-left: 15px;
|
||||
margin-top: -12px;
|
||||
z-index: 9;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
|
@ -336,6 +325,7 @@ span.block {
|
|||
|
||||
.performer-tag-container {
|
||||
margin: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.performer-tag.image {
|
||||
|
|
@ -548,7 +538,7 @@ span.block {
|
|||
background-color: #30404d;
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 0 1px rgba(16,22,26,.4), 0 0 0 rgba(16,22,26,0), 0 0 0 rgba(16,22,26,0);
|
||||
padding: 20px;
|
||||
padding: 20px 20px 0px 20px;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
|
|
|
|||
94
ui/v2.5/src/styles/_range.scss
Normal file
94
ui/v2.5/src/styles/_range.scss
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
input[type=range] {
|
||||
height: 22px;
|
||||
-webkit-appearance: none;
|
||||
margin: 10px 0;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
input[type=range]:focus {
|
||||
border: inherit;
|
||||
box-shadow: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
background: #137cbd;
|
||||
border-radius: 25px;
|
||||
border: 0px solid #000101;
|
||||
}
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 0px solid #000000;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 5px;
|
||||
background: #394B59;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
margin-top: -5px;
|
||||
}
|
||||
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||
background: #137cbd;
|
||||
}
|
||||
input[type=range]::-moz-range-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
background: #137cbd;
|
||||
border-radius: 25px;
|
||||
border: 0px solid #000101;
|
||||
}
|
||||
input[type=range]::-moz-range-thumb {
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 0px solid #000000;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 5px;
|
||||
background: #394B59;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range]::-ms-track {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
cursor: pointer;
|
||||
animate: 0.2s;
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
color: transparent;
|
||||
}
|
||||
input[type=range]::-ms-fill-lower {
|
||||
background: #137cbd;
|
||||
border: 0px solid #000101;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
}
|
||||
input[type=range]::-ms-fill-upper {
|
||||
background: #137cbd;
|
||||
border: 0px solid #000101;
|
||||
border-radius: 50px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
}
|
||||
input[type=range]::-ms-thumb {
|
||||
margin-top: 1px;
|
||||
box-shadow: 0px 0px 0px #000000;
|
||||
border: 0px solid #000000;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
border-radius: 5px;
|
||||
background: #394B59;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-lower {
|
||||
background: #137cbd;
|
||||
}
|
||||
input[type=range]:focus::-ms-fill-upper {
|
||||
background: #137cbd;
|
||||
}
|
||||
|
|
@ -1,9 +1,80 @@
|
|||
|
||||
/* Blueprint dark theme */
|
||||
|
||||
$secondary: #394b59;
|
||||
|
||||
$theme-colors: (
|
||||
primary: #137cbd,
|
||||
secondary: $secondary,
|
||||
success: #0f9960,
|
||||
warning: #d9822b,
|
||||
danger: #db3737,
|
||||
dark: #394b59
|
||||
);
|
||||
|
||||
$body-bg: #202b33;
|
||||
$text-muted: #bfccd6;
|
||||
$link-color: #48aff0;
|
||||
$link-hover-color: #48aff0;
|
||||
$text-color: f5f8fa;
|
||||
$text-color: #f5f8fa;
|
||||
$pre-color: $text-color;
|
||||
$navbar-dark-color: rgb(245, 248, 250);
|
||||
$input-bg: $secondary;
|
||||
$input-color: #f5f8fa;
|
||||
$popover-bg: $secondary;
|
||||
|
||||
@import "node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.btn.active:not(.disabled),
|
||||
.btn.active.minimal:not(.disabled) {
|
||||
background-color: rgba(138,155,168,.3);
|
||||
color: #f5f8fa;
|
||||
}
|
||||
|
||||
a.minimal,
|
||||
button.minimal {
|
||||
background: none;
|
||||
border: none;
|
||||
color: $text-color;
|
||||
transition: none;
|
||||
|
||||
&:hover {
|
||||
background: rgba(138,155,168,.15);
|
||||
color: $text-color;
|
||||
}
|
||||
&:active {
|
||||
background: rgba(138,155,168,.3);
|
||||
color: $text-color;
|
||||
}
|
||||
}
|
||||
|
||||
input.form-control {
|
||||
background-color: rgba(16, 22, 26, 0.3);
|
||||
}
|
||||
.form-control {
|
||||
border-color: rgba(16,22,26,.4);
|
||||
}
|
||||
|
||||
.dropdown-toggle:after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
nav .svg-inline--fa {
|
||||
margin-right: 7px;
|
||||
}
|
||||
|
||||
hr {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.table {
|
||||
th {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
thead {
|
||||
th {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue