This commit is contained in:
Infinite
2020-01-26 15:08:53 +01:00
parent c2544fee98
commit 3fa3f61d93
18 changed files with 422 additions and 189 deletions

View File

@@ -31,8 +31,6 @@ module.exports = merge(commonConfig, {
compress: true, compress: true,
host: '0.0.0.0', host: '0.0.0.0',
hot: true, // enable HMR on the server 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, port: process.env.PORT,
historyApiFallback: true, historyApiFallback: true,
stats: { stats: {

View File

@@ -1,7 +1,7 @@
import React from "react"; import React from "react";
import { Spinner } from "react-bootstrap";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
import { LoadingIndicator } from 'src/components/Shared';
import { GalleryViewer } from "./GalleryViewer"; import { GalleryViewer } from "./GalleryViewer";
export const Gallery: React.FC = () => { export const Gallery: React.FC = () => {
@@ -11,7 +11,7 @@ export const Gallery: React.FC = () => {
const gallery = data?.findGallery; const gallery = data?.findGallery;
if (loading || !gallery) if (loading || !gallery)
return <Spinner animation="border" variant="light" />; return <LoadingIndicator />;
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
return ( return (

View File

@@ -67,7 +67,7 @@ export const MainNavbar: React.FC = () => {
<Navbar fixed="top" variant="dark" bg="dark"> <Navbar fixed="top" variant="dark" bg="dark">
<Navbar.Brand as="div"> <Navbar.Brand as="div">
<Link to="/"> <Link to="/">
<Button variant="secondary">Stash</Button> <Button className="minimal">Stash</Button>
</Link> </Link>
</Navbar.Brand> </Navbar.Brand>
<Nav className="mr-auto"> <Nav className="mr-auto">
@@ -78,7 +78,7 @@ export const MainNavbar: React.FC = () => {
to={i.href} to={i.href}
key={i.href} key={i.href}
> >
<Button variant="secondary"> <Button className="minimal">
<Icon icon={i.icon} /> <Icon icon={i.icon} />
{i.text} {i.text}
</Button> </Button>
@@ -88,7 +88,7 @@ export const MainNavbar: React.FC = () => {
<Nav> <Nav>
{newButton} {newButton}
<LinkContainer exact to="/settings"> <LinkContainer exact to="/settings">
<Button variant="secondary"> <Button className="minimal">
<Icon icon="cog" /> <Icon icon="cog" />
</Button> </Button>
</LinkContainer> </LinkContainer>

View File

@@ -17,11 +17,13 @@ interface ITypeProps {
type?: "performers" | "studios" | "tags"; type?: "performers" | "studios" | "tags";
} }
interface IFilterProps { interface IFilterProps {
initialIds: string[]; ids?: string[];
initialIds?: string[];
onSelect: (item: ValidTypes[]) => void; onSelect: (item: ValidTypes[]) => void;
noSelectionString?: string; noSelectionString?: string;
className?: string; className?: string;
isMulti?: boolean; isMulti?: boolean;
isClearable?: boolean;
} }
interface ISelectProps { interface ISelectProps {
className?: string; className?: string;
@@ -31,8 +33,9 @@ interface ISelectProps {
onCreateOption?: (value: string) => void; onCreateOption?: (value: string) => void;
isLoading: boolean; isLoading: boolean;
onChange: (item: ValueType<Option>) => void; onChange: (item: ValueType<Option>) => void;
initialIds: string[]; initialIds?: string[];
isMulti?: boolean; isMulti?: boolean;
isClearable?: boolean,
onInputChange?: (input: string) => void; onInputChange?: (input: string) => void;
placeholder?: string; placeholder?: string;
} }
@@ -48,9 +51,10 @@ interface ISceneGallerySelect {
} }
const getSelectedValues = (selectedItems: ValueType<Option>) => const getSelectedValues = (selectedItems: ValueType<Option>) =>
selectedItems ?
(Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map( (Array.isArray(selectedItems) ? selectedItems : [selectedItems]).map(
item => item.value item => item.value
); ) : [];
export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => { export const SceneGallerySelect: React.FC<ISceneGallerySelect> = props => {
const { data, loading } = StashService.useValidGalleriesForScene( const { data, loading } = StashService.useValidGalleriesForScene(
@@ -165,6 +169,8 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
label: item.name ?? "" label: item.name ?? ""
})); }));
const placeholder = props.noSelectionString ?? "Select performer..."; 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 onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
@@ -176,6 +182,7 @@ export const PerformerSelect: React.FC<IFilterProps> = props => {
return ( return (
<SelectComponent <SelectComponent
{...props} {...props}
selectedOptions={selectedOptions}
onChange={onChange} onChange={onChange}
type="performers" type="performers"
isLoading={loading} isLoading={loading}
@@ -194,6 +201,8 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
label: item.name label: item.name
})); }));
const placeholder = props.noSelectionString ?? "Select studio..."; 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 onChange = (selectedItems: ValueType<Option>) => {
const selectedIds = getSelectedValues(selectedItems); const selectedIds = getSelectedValues(selectedItems);
@@ -210,13 +219,14 @@ export const StudioSelect: React.FC<IFilterProps> = props => {
isLoading={loading} isLoading={loading}
items={items} items={items}
placeholder={placeholder} placeholder={placeholder}
selectedOptions={selectedOptions}
/> />
); );
}; };
export const TagSelect: React.FC<IFilterProps> = props => { export const TagSelect: React.FC<IFilterProps> = props => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>(props.ids ?? []);
const { data, loading: dataLoading } = StashService.useAllTagsForFilter(); const { data, loading: dataLoading } = StashService.useAllTagsForFilter();
const [createTag] = StashService.useTagCreate({ name: "" }); const [createTag] = StashService.useTagCreate({ name: "" });
const Toast = useToast(); const Toast = useToast();
@@ -290,6 +300,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
selectedOptions, selectedOptions,
isLoading, isLoading,
onCreateOption, onCreateOption,
isClearable = true,
creatable = false, creatable = false,
isMulti = false, isMulti = false,
onInputChange, onInputChange,
@@ -298,26 +309,58 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
const defaultValue = const defaultValue =
items.filter(item => initialIds?.indexOf(item.value) !== -1) ?? null; 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 = { const props = {
className,
options: items, options: items,
value: selectedOptions, value: selectedOptions,
styles,
className,
onChange, onChange,
isMulti, isMulti,
isClearable,
defaultValue, defaultValue,
noOptionsMessage: () => (type !== "tags" ? "None" : null), noOptionsMessage: () => (type !== "tags" ? "None" : null),
placeholder, placeholder,
onInputChange onInputChange,
isLoading,
components: { IndicatorSeparator: () => null }
}; };
return creatable ? ( return creatable ? (
<CreatableSelect <CreatableSelect
{...props} {...props}
isLoading={isLoading}
isDisabled={isLoading} isDisabled={isLoading}
onCreateOption={onCreateOption} onCreateOption={onCreateOption}
/> />
) : ( ) : (
<Select {...props} isLoading={isLoading} /> <Select {...props} />
); );
}; };

View File

@@ -39,8 +39,6 @@
.scene-wall-item-container { .scene-wall-item-container {
display: flex; display: flex;
justify-content: center; justify-content: center;
// align-items: center;
// overflow: hidden; // Commented out since it shows gaps in the wall
position: relative; position: relative;
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -62,6 +60,7 @@
padding: 5px; padding: 5px;
width: 100%; width: 100%;
bottom: 0; bottom: 0;
left: 0;
background: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.65)); background: linear-gradient(rgba(255, 255, 255, 0.25), rgba(255, 255, 255, 0.65));
overflow: hidden; overflow: hidden;
text-align: center; text-align: center;
@@ -80,18 +79,17 @@
left: -5px; left: -5px;
right: -5px; right: -5px;
bottom: -5px; bottom: -5px;
/*background-color: rgba(255, 255, 255, 0.75);*/
/*backdrop-filter: blur(5px);*/
z-index: -1; z-index: -1;
} }
.wall.grid-item video, .wall.grid-item img { .wall-item video, .wall-item img {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
} }
.wall.grid-item { .wall-item {
width: 20%;
padding: 0 !important; padding: 0 !important;
line-height: 0; line-height: 0;
overflow: visible; overflow: visible;

View File

@@ -1,5 +1,5 @@
import _ from "lodash"; 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 { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService"; import { StashService } from "src/core/StashService";
@@ -16,10 +16,10 @@ interface IWallItemProps {
) => void; ) => void;
} }
export const WallItem: FunctionComponent<IWallItemProps> = ( export const WallItem: React.FC<IWallItemProps> = (
props: IWallItemProps props: IWallItemProps
) => { ) => {
const [videoPath, setVideoPath] = useState<string | undefined>(undefined); const [videoPath, setVideoPath] = useState<string>();
const [previewPath, setPreviewPath] = useState<string>(""); const [previewPath, setPreviewPath] = useState<string>("");
const [screenshotPath, setScreenshotPath] = useState<string>(""); const [screenshotPath, setScreenshotPath] = useState<string>("");
const [title, setTitle] = useState<string>(""); const [title, setTitle] = useState<string>("");
@@ -28,10 +28,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (
const videoHoverHook = VideoHoverHook.useVideoHover({ const videoHoverHook = VideoHoverHook.useVideoHover({
resetOnMouseLeave: true resetOnMouseLeave: true
}); });
const showTextContainer = const showTextContainer = config.data?.configuration.interface.wallShowTitle ?? true;
!!config.data && !!config.data.configuration
? config.data.configuration.interface.wallShowTitle
: true;
function onMouseEnter() { function onMouseEnter() {
VideoHoverHook.onMouseEnter(videoHoverHook); VideoHoverHook.onMouseEnter(videoHoverHook);
@@ -122,7 +119,7 @@ export const WallItem: FunctionComponent<IWallItemProps> = (
style.transformOrigin = props.origin; style.transformOrigin = props.origin;
} }
return ( return (
<div className="wall grid-item"> <div className="wall-item">
<div <div
className={className.join(" ")} className={className.join(" ")}
style={style} style={style}

View File

@@ -1,4 +1,4 @@
import React, { FunctionComponent, useState } from "react"; import React, { useState } from "react";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { WallItem } from "./WallItem"; import { WallItem } from "./WallItem";
import "./Wall.scss"; import "./Wall.scss";
@@ -11,7 +11,7 @@ interface IWallPanelProps {
) => void; ) => void;
} }
export const WallPanel: FunctionComponent<IWallPanelProps> = ( export const WallPanel: React.FC<IWallPanelProps> = (
props: IWallPanelProps props: IWallPanelProps
) => { ) => {
const [showOverlay, setShowOverlay] = useState<boolean>(false); const [showOverlay, setShowOverlay] = useState<boolean>(false);

View File

@@ -229,7 +229,7 @@ export const AddFilter: React.FC<IAddFilterProps> = (
placement="top" placement="top"
overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>} overlay={<Tooltip id="filter-tooltip">Filter</Tooltip>}
> >
<Button onClick={() => onToggle()} active={isOpen}> <Button variant="secondary" onClick={() => onToggle()} active={isOpen}>
<Icon icon="filter" /> <Icon icon="filter" />
</Button> </Button>
</OverlayTrigger> </OverlayTrigger>

View File

@@ -139,6 +139,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
} }
> >
<Button <Button
variant="secondary"
key={option} key={option}
active={props.filter.displayMode === option} active={props.filter.displayMode === option}
onClick={() => onChangeDisplayMode(option)} onClick={() => onChangeDisplayMode(option)}
@@ -157,7 +158,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
onClick={() => onClickCriterionTag(criterion)} onClick={() => onClickCriterionTag(criterion)}
> >
{criterion.getLabel()} {criterion.getLabel()}
<Button onClick={() => onRemoveCriterionTag(criterion)}> <Button variant="secondary" onClick={() => onRemoveCriterionTag(criterion)}>
<Icon icon="times" /> <Icon icon="times" />
</Button> </Button>
</Badge> </Badge>
@@ -226,16 +227,15 @@ export const ListFilter: React.FC<IListFilterProps> = (
function maybeRenderZoom() { function maybeRenderZoom() {
if (props.onChangeZoom) { if (props.onChangeZoom) {
return ( return (
<span className="zoom-slider"> <Form.Control
<Form.Control className="zoom-slider"
type="range" type="range"
min={0} min={0}
max={3} max={3}
onChange={(event: any) => onChange={(event: any) =>
onChangeZoom(Number.parseInt(event.target.value, 10)) onChangeZoom(Number.parseInt(event.target.value, 10))
} }
/> />
</span>
); );
} }
} }
@@ -243,7 +243,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
function render() { function render() {
return ( return (
<> <>
<div className="d-flex justify-content-center m-auto"> <div className="filter-container">
<Form.Control <Form.Control
placeholder="Search..." placeholder="Search..."
value={props.filter.searchTerm} value={props.filter.searchTerm}
@@ -262,32 +262,32 @@ export const ListFilter: React.FC<IListFilterProps> = (
))} ))}
</Form.Control> </Form.Control>
<ButtonGroup className="filter-item"> <ButtonGroup className="filter-item">
<Dropdown> <Dropdown as={ButtonGroup}>
<Dropdown.Toggle variant="secondary" id="more-menu"> <Dropdown.Toggle split variant="secondary" id="more-menu">
{props.filter.sortBy} {props.filter.sortBy}
</Dropdown.Toggle> </Dropdown.Toggle>
<Dropdown.Menu>{renderSortByOptions()}</Dropdown.Menu> <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> </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> </ButtonGroup>
<AddFilter <AddFilter

View File

@@ -39,6 +39,7 @@ export const Pagination: React.FC<IPaginationProps> = ({
const pageButtons = pages.map((page: number) => ( const pageButtons = pages.map((page: number) => (
<Button <Button
variant="secondary"
key={page} key={page}
active={currentPage === page} active={currentPage === page}
onClick={() => onChangePage(page)} onClick={() => onChangePage(page)}
@@ -48,11 +49,12 @@ export const Pagination: React.FC<IPaginationProps> = ({
)); ));
return ( return (
<ButtonGroup className="filter-container"> <ButtonGroup className="filter-container pagination">
<Button disabled={currentPage === 1} onClick={() => onChangePage(1)}> <Button variant="secondary" disabled={currentPage === 1} onClick={() => onChangePage(1)}>
First First
</Button> </Button>
<Button <Button
variant="secondary"
disabled={currentPage === 1} disabled={currentPage === 1}
onClick={() => onChangePage(currentPage - 1)} onClick={() => onChangePage(currentPage - 1)}
> >
@@ -60,12 +62,14 @@ export const Pagination: React.FC<IPaginationProps> = ({
</Button> </Button>
{pageButtons} {pageButtons}
<Button <Button
variant="secondary"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={() => onChangePage(currentPage + 1)} onClick={() => onChangePage(currentPage + 1)}
> >
Next Next
</Button> </Button>
<Button <Button
variant="secondary"
disabled={currentPage === totalPages} disabled={currentPage === totalPages}
onClick={() => onChangePage(totalPages)} onClick={() => onChangePage(totalPages)}
> >

View 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;
}

View File

@@ -91,7 +91,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button> <Button className="minimal">
<Icon icon="tag" /> <Icon icon="tag" />
{props.scene.tags.length} {props.scene.tags.length}
</Button> </Button>
@@ -115,7 +115,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button> <Button className="minimal">
<Icon icon="user" /> <Icon icon="user" />
{props.scene.performers.length} {props.scene.performers.length}
</Button> </Button>
@@ -133,8 +133,8 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<HoverPopover placement="bottom" content={popoverContent}> <HoverPopover placement="bottom" content={popoverContent}>
<Button> <Button className="minimal">
<Icon icon="tag" /> <Icon icon="map-marker-alt" />
{props.scene.scene_markers.length} {props.scene.scene_markers.length}
</Button> </Button>
</HoverPopover> </HoverPopover>
@@ -150,7 +150,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<> <>
<hr /> <hr />
<ButtonGroup className="mr-2"> <ButtonGroup className="scene-popovers">
{maybeRenderTagPopoverButton()} {maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()} {maybeRenderPerformerPopoverButton()}
{maybeRenderSceneMarkerPopoverButton()} {maybeRenderSceneMarkerPopoverButton()}
@@ -182,7 +182,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<Card <Card
className={`col-4 zoom-${props.zoomIndex}`} className={`zoom-${props.zoomIndex}`}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
@@ -216,11 +216,11 @@ export const SceneCard: React.FC<ISceneCardProps> = (
</div> </div>
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h4 className="text-truncate"> <h5 className="text-truncate">
{props.scene.title {props.scene.title
? props.scene.title ? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)} : TextUtils.fileNameFromPath(props.scene.path)}
</h4> </h5>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
<p> <p>
{TextUtils.truncate( {TextUtils.truncate(

View File

@@ -28,7 +28,7 @@ export const SceneMarkerList: React.FC = () => {
filter: ListFilterModel filter: ListFilterModel
) { ) {
// query for a random scene // query for a random scene
if (result.data && result.data.findSceneMarkers) { if (result.data?.findSceneMarkers) {
const { count } = result.data.findSceneMarkers; const { count } = result.data.findSceneMarkers;
const index = Math.floor(Math.random() * count); const index = Math.floor(Math.random() * count);
@@ -37,10 +37,7 @@ export const SceneMarkerList: React.FC = () => {
filterCopy.currentPage = index + 1; filterCopy.currentPage = index + 1;
const singleResult = await StashService.queryFindSceneMarkers(filterCopy); const singleResult = await StashService.queryFindSceneMarkers(filterCopy);
if ( if (
singleResult && singleResult?.data?.findSceneMarkers?.scene_markers?.length === 1
singleResult.data &&
singleResult.data.findSceneMarkers &&
singleResult.data.findSceneMarkers.scene_markers.length === 1
) { ) {
// navigate to the scene player page // navigate to the scene player page
const url = NavUtils.makeSceneMarkerUrl( const url = NavUtils.makeSceneMarkerUrl(

View File

@@ -16,16 +16,14 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
) => { ) => {
const Toast = useToast(); const Toast = useToast();
const [rating, setRating] = useState<string>(""); const [rating, setRating] = useState<string>("");
const [studioId, setStudioId] = useState<string | undefined>(undefined); const [studioId, setStudioId] = useState<string>();
const [performerIds, setPerformerIds] = useState<string[] | undefined>( const [performerIds, setPerformerIds] = useState<string[]>();
undefined const [tagIds, setTagIds] = useState<string[]>();
);
const [tagIds, setTagIds] = useState<string[] | undefined>(undefined);
const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput()); const [updateScenes] = StashService.useBulkSceneUpdate(getSceneInput());
// Network state // Network state
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(true);
function getSceneInput(): GQL.BulkSceneUpdateInput { function getSceneInput(): GQL.BulkSceneUpdateInput {
// need to determine what we are actually setting on each scene // 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[]) { function updateScenesEditState(state: GQL.SlimSceneDataFragment[]) {
let updateRating = ""; let updateRating = "";
let updateStudioId: string | undefined; let updateStudioId: string|undefined;
let updatePerformerIds: string[] = []; let updatePerformerIds: string[] = [];
let updateTagIds: string[] = []; let updateTagIds: string[] = [];
let first = true; let first = true;
state.forEach((scene: GQL.SlimSceneDataFragment) => { state.forEach((scene: GQL.SlimSceneDataFragment) => {
const thisRating = scene.rating ? scene.rating.toString() : ""; const thisRating = scene.rating?.toString() ?? "";
const thisStudio = scene.studio ? scene.studio.id : undefined; const thisStudio = scene?.studio?.id;
if (first) { if (first) {
updateRating = thisRating; updateRating = thisRating;
@@ -231,76 +229,76 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
useEffect(() => { useEffect(() => {
updateScenesEditState(props.selected); updateScenesEditState(props.selected);
setIsLoading(false);
}, [props.selected]); }, [props.selected]);
function renderMultiSelect( function renderMultiSelect(
type: "performers" | "tags", type: "performers" | "tags",
initialIds: string[] | undefined ids: string[] | undefined
) { ) {
return ( return (
<FilterSelect <FilterSelect
type={type} type={type}
isMulti isMulti
isClearable={false}
onSelect={items => { onSelect={items => {
const ids = items.map(i => i.id); const itemIDs = items.map(i => i.id);
switch (type) { switch (type) {
case "performers": case "performers":
setPerformerIds(ids); setPerformerIds(itemIDS);
break; break;
case "tags": case "tags":
setTagIds(ids); setTagIds(itemIDs);
break; break;
} }
}} }}
initialIds={initialIds ?? []} ids={ids ?? []}
/> />
); );
} }
if(isLoading)
return <Spinner animation="border" variant="light" />;
function render() { function render() {
return ( return (
<> <div className="operation-container">
{isLoading ? <Spinner animation="border" variant="light" /> : undefined} <Form.Group controlId="rating" className="operation-item rating-operation">
<div className="operation-container"> <Form.Label>Rating</Form.Label>
<Form.Group controlId="rating" className="operation-item"> <Form.Control
<Form.Label>Rating</Form.Label> as="select"
<Form.Control onChange={(event: any) => setRating(event.target.value)}
as="select" >
onChange={(event: any) => setRating(event.target.value)} {["", '1', '2', '3', '4', '5'].map(opt => (
> <option selected={opt === rating} value={opt}>
{["", 1, 2, 3, 4, 5].map(opt => ( {opt}
<option selected={opt === rating} value={opt}> </option>
{opt} ))}
</option> </Form.Control>
))} </Form.Group>
</Form.Control>
</Form.Group>
<Form.Group controlId="studio" className="operation-item"> <Form.Group controlId="studio" className="operation-item">
<Form.Label>Studio</Form.Label> <Form.Label>Studio</Form.Label>
<StudioSelect <StudioSelect
onSelect={items => setStudioId(items[0]?.id)} onSelect={items => setStudioId(items[0]?.id)}
initialIds={studioId ? [studioId] : []} ids={studioId ? [studioId] : []}
/> />
</Form.Group> </Form.Group>
<Form.Group className="opeation-item" controlId="performers"> <Form.Group className="operation-item" controlId="performers">
<Form.Label>Performers</Form.Label> <Form.Label>Performers</Form.Label>
{renderMultiSelect("performers", performerIds)} {renderMultiSelect("performers", performerIds)}
</Form.Group> </Form.Group>
<Form.Group className="operation-item" controlId="performers"> <Form.Group className="operation-item" controlId="performers">
<Form.Label>Performers</Form.Label> <Form.Label>Tags</Form.Label>
{renderMultiSelect("tags", tagIds)} {renderMultiSelect("tags", tagIds)}
</Form.Group> </Form.Group>
<ButtonGroup className="operation-item"> <Button variant="primary" onClick={onSave} className="apply-operation">
<Button variant="primary" onClick={onSave}> Apply
Apply </Button>
</Button> </div>
</ButtonGroup>
</div>
</>
); );
} }

View 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;
}
}

View File

@@ -3,6 +3,7 @@
@import "styles/shared/details"; @import "styles/shared/details";
@import "styles/range";
@import "styles/scrollbars"; @import "styles/scrollbars";
@import "styles/variables"; @import "styles/variables";
@@ -17,7 +18,6 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
height: 100vh; height: 100vh;
background: $dark-gray2;
} }
code { code {
@@ -37,10 +37,6 @@ code {
margin: 0; margin: 0;
} }
& .bp3-button.favorite .bp3-icon {
color: #ff7373 !important
}
& .performer-list-thumbnail { & .performer-list-thumbnail {
min-width: 50px; min-width: 50px;
height: 100px; height: 100px;
@@ -55,58 +51,59 @@ code {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
}
.grid-item { .card {
// flex: auto; margin: 0 0 10px 10px;
width: 320px; overflow: hidden;
min-width: 185px;
margin: 0px 0 $pt-grid-size $pt-grid-size;
overflow: hidden;
&.wall { &.zoom-0 {
width: calc(20%); width: 15rem;
margin: 0;
}
&.zoom-0 { & .previewable {
width: 240px; max-height: 11.25rem;
}
& .previewable.portrait {
max-height: 11.25rem;
}
}
&.zoom-1 {
width: 20rem;
& .previewable { & .previewable {
max-height: 180px; max-height: 15rem;
}
& .previewable.portrait {
height: 15rem;
}
} }
& .previewable.portrait { &.zoom-2 {
height: 180px; width: 30rem;
}
}
&.zoom-1 {
width: 320px;
& .previewable { & .previewable {
max-height: 240px; max-height: 22.5rem;
}
& .previewable.portrait {
height: 22.5rem;
}
} }
& .previewable.portrait { &.zoom-3 {
height: 240px; width: 40rem;
}
}
&.zoom-2 {
width: 480px;
& .previewable { & .previewable {
max-height: 360px; max-height: 30rem;
}
& .previewable.portrait {
height: 30rem;
}
} }
& .previewable.portrait {
height: 360px;
}
}
&.zoom-3 {
width: 640px;
& .previewable { .card-select {
max-height: 480px; position: absolute;
} padding-left: 15px;
& .previewable.portrait { margin-top: -12px;
height: 480px; z-index: 1;
opacity: 0.5;
width: 1.2rem;
} }
} }
} }
@@ -125,14 +122,6 @@ code {
height: 240px; height: 240px;
} }
.grid-item label.card-select {
position: absolute;
padding-left: 15px;
margin-top: -12px;
z-index: 9;
opacity: 0.5;
}
.video-container { .video-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -336,6 +325,7 @@ span.block {
.performer-tag-container { .performer-tag-container {
margin: 5px; margin: 5px;
display: inline-block;
} }
.performer-tag.image { .performer-tag.image {
@@ -548,7 +538,7 @@ span.block {
background-color: #30404d; background-color: #30404d;
border-radius: 3px; 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); 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 { .toast-container {

View 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;
}

View File

@@ -1,9 +1,80 @@
/* Blueprint dark theme */ /* 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; $text-muted: #bfccd6;
$link-color: #48aff0; $link-color: #48aff0;
$link-hover-color: #48aff0; $link-hover-color: #48aff0;
$text-color: f5f8fa; $text-color: #f5f8fa;
$pre-color: $text-color; $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"; @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;
}
}
}