This commit is contained in:
Infinite
2020-01-30 23:34:20 +01:00
parent c1ce6d539d
commit 4a32f90382
30 changed files with 321 additions and 397 deletions

View File

@@ -19,7 +19,7 @@ export class ErrorBoundary extends React.Component<any, any> {
return ( return (
<div> <div>
<h2>Something went wrong.</h2> <h2>Something went wrong.</h2>
<details style={{ whiteSpace: "pre-wrap" }}> <details className="error-message">
{this.state.error && this.state.error.toString()} {this.state.error && this.state.error.toString()}
<br /> <br />
{this.state.errorInfo.componentStack} {this.state.errorInfo.componentStack}

View File

@@ -14,7 +14,7 @@ export const Gallery: React.FC = () => {
if (error) return <div>{error.message}</div>; if (error) return <div>{error.message}</div>;
return ( return (
<div style={{ width: "75vw", margin: "0 auto" }}> <div className="col-9 m-auto">
<GalleryViewer gallery={gallery as any} /> <GalleryViewer gallery={gallery as any} />
</div> </div>
); );

View File

@@ -23,7 +23,7 @@ export const GalleryList: React.FC = () => {
} }
if (filter.displayMode === DisplayMode.List) { if (filter.displayMode === DisplayMode.List) {
return ( return (
<Table style={{ margin: "0 auto" }}> <Table className="m-auto">
<thead> <thead>
<tr> <tr>
<th>Preview</th> <th>Preview</th>

View File

@@ -156,11 +156,12 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Group className="row" controlId="ignored-words"> <Form.Group className="row" controlId="ignored-words">
<Form.Label className="col-2">Ignored words</Form.Label> <Form.Label className="col-2">Ignored words</Form.Label>
<InputGroup className="col-8">
<Form.Control <Form.Control
className="col-8"
onChange={(newValue: any) => setIgnoreWords(newValue.target.value)} onChange={(newValue: any) => setIgnoreWords(newValue.target.value)}
value={ignoreWords} value={ignoreWords}
/> />
</InputGroup>
<Form.Text className="text-muted col-10 offset-2"> <Form.Text className="text-muted col-10 offset-2">
Matches with {"{i}"} Matches with {"{i}"}
</Form.Text> </Form.Text>
@@ -171,27 +172,29 @@ export const ParserInput: React.FC<IParserInputProps> = (
<Form.Label htmlFor="whitespace-characters" className="col-2"> <Form.Label htmlFor="whitespace-characters" className="col-2">
Whitespace characters: Whitespace characters:
</Form.Label> </Form.Label>
<InputGroup className="col-8">
<Form.Control <Form.Control
className="col-8"
onChange={(newValue: any) => onChange={(newValue: any) =>
setWhitespaceCharacters(newValue.target.value) setWhitespaceCharacters(newValue.target.value)
} }
value={whitespaceCharacters} value={whitespaceCharacters}
/> />
</InputGroup>
<Form.Text className="text-muted col-10 offset-2"> <Form.Text className="text-muted col-10 offset-2">
These characters will be replaced with whitespace in the title These characters will be replaced with whitespace in the title
</Form.Text> </Form.Text>
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group>
<Form.Label htmlFor="capitalize-title" className="col-2"> <Form.Check
Capitalize title inline
</Form.Label> className="m-0"
<Form.Control id="capitalize-title"
className="col-8"
type="checkbox"
checked={capitalizeTitle} checked={capitalizeTitle}
onChange={() => setCapitalizeTitle(!capitalizeTitle)} onChange={() => setCapitalizeTitle(!capitalizeTitle)}
/> />
<Form.Label htmlFor="capitalize-title">
Capitalize title
</Form.Label>
</Form.Group> </Form.Group>
{/* TODO - mapping stuff will go here */} {/* TODO - mapping stuff will go here */}
@@ -222,12 +225,11 @@ export const ParserInput: React.FC<IParserInputProps> = (
</Form.Group> </Form.Group>
<Form.Group className="row"> <Form.Group className="row">
<Button variant="secondary" className="col-1" onClick={onFind}> <Button variant="secondary" className="ml-3 col-1" onClick={onFind}>
Find Find
</Button> </Button>
<Form.Control <Form.Control
as="select" as="select"
style={{ flexBasis: "min-content" }}
options={PAGE_SIZE_OPTIONS} options={PAGE_SIZE_OPTIONS}
onChange={(event: any) => onChange={(event: any) =>
props.onPageSizeChanged(parseInt(event.target.value, 10)) props.onPageSizeChanged(parseInt(event.target.value, 10))
@@ -236,7 +238,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
className="col-1 filter-item" className="col-1 filter-item"
> >
{PAGE_SIZE_OPTIONS.map(val => ( {PAGE_SIZE_OPTIONS.map(val => (
<option value="val">{val}</option> <option key={val} value={val}>{val}</option>
))} ))}
</Form.Control> </Form.Control>
</Form.Group> </Form.Group>

View File

@@ -414,8 +414,7 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<> <>
<td> <td>
<Form.Control <Form.Check
type="checkbox"
checked={props.parserResult.set} checked={props.parserResult.set}
onChange={() => { onChange={() => {
props.onSetChanged(!props.parserResult.set); props.onSetChanged(!props.parserResult.set);
@@ -458,7 +457,7 @@ export const SceneFilenameParser: React.FC = () => {
disabled={!props.parserResult.set} disabled={!props.parserResult.set}
className={props.className} className={props.className}
value={props.parserResult.value || ""} value={props.parserResult.value || ""}
onBlur={(event: any) => props.onChange(event.target.value)} onChange={(event: any) => props.onChange(event.target.value)}
/> />
); );
} }
@@ -494,7 +493,7 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<div> <div>
{elements.map((name: string) => ( {elements.map((name: string) => (
<Badge variant="secondary">{name}</Badge> <Badge key={name} variant="secondary">{name}</Badge>
))} ))}
</div> </div>
); );
@@ -592,7 +591,7 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<tr className="scene-parser-row"> <tr className="scene-parser-row">
<td style={{ textAlign: "left" }}>{props.scene.filename}</td> <td className="text-left">{props.scene.filename}</td>
<SceneParserField <SceneParserField
key="title" key="title"
fieldName="Title" fieldName="Title"
@@ -689,9 +688,8 @@ export const SceneFilenameParser: React.FC = () => {
return ( return (
<> <>
<td> <td className="w-15">
<Form.Control <Form.Check
type="checkbox"
checked={allSet} checked={allSet}
onChange={() => { onChange={() => {
onAllSet(!allSet); onAllSet(!allSet);
@@ -715,7 +713,7 @@ export const SceneFilenameParser: React.FC = () => {
<Table> <Table>
<thead> <thead>
<tr className="scene-parser-row"> <tr className="scene-parser-row">
<th>Filename</th> <th className="w-25">Filename</th>
{renderHeader("Title", allTitleSet, onSelectAllTitleSet)} {renderHeader("Title", allTitleSet, onSelectAllTitleSet)}
{renderHeader("Date", allDateSet, onSelectAllDateSet)} {renderHeader("Date", allDateSet, onSelectAllDateSet)}
{renderHeader( {renderHeader(

View File

@@ -7,13 +7,13 @@ import { useToast } from "src/hooks";
export const SettingsInterfacePanel: React.FC = () => { export const SettingsInterfacePanel: React.FC = () => {
const Toast = useToast(); const Toast = useToast();
const config = StashService.useConfiguration(); const config = StashService.useConfiguration();
const [soundOnPreview, setSoundOnPreview] = useState<boolean>(); const [soundOnPreview, setSoundOnPreview] = useState<boolean>(true);
const [wallShowTitle, setWallShowTitle] = useState<boolean>(); const [wallShowTitle, setWallShowTitle] = useState<boolean>(true);
const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0); const [maximumLoopDuration, setMaximumLoopDuration] = useState<number>(0);
const [autostartVideo, setAutostartVideo] = useState<boolean>(); const [autostartVideo, setAutostartVideo] = useState<boolean>(false);
const [showStudioAsText, setShowStudioAsText] = useState<boolean>(); const [showStudioAsText, setShowStudioAsText] = useState<boolean>(false);
const [css, setCSS] = useState<string>(); const [css, setCSS] = useState<string>();
const [cssEnabled, setCSSEnabled] = useState<boolean>(); const [cssEnabled, setCSSEnabled] = useState<boolean>(false);
const [updateInterfaceConfig] = StashService.useConfigureInterface({ const [updateInterfaceConfig] = StashService.useConfigureInterface({
soundOnPreview, soundOnPreview,

View File

@@ -340,6 +340,10 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
container: (base: CSSProperties, state: any) => ({ container: (base: CSSProperties, state: any) => ({
...base, ...base,
zIndex: state.isFocused ? 10 : base.zIndex zIndex: state.isFocused ? 10 : base.zIndex
}),
multiValueRemove: (base: CSSProperties, state: any) => ({
...base,
color: state.isFocused ? base.color: '#333333'
}) })
}; };

View File

@@ -12,6 +12,7 @@ interface IProps {
tag?: Partial<TagDataFragment>; tag?: Partial<TagDataFragment>;
performer?: Partial<PerformerDataFragment>; performer?: Partial<PerformerDataFragment>;
marker?: Partial<SceneMarkerDataFragment>; marker?: Partial<SceneMarkerDataFragment>;
className?: string;
} }
export const TagLink: React.FC<IProps> = (props: IProps) => { export const TagLink: React.FC<IProps> = (props: IProps) => {
@@ -30,7 +31,7 @@ export const TagLink: React.FC<IProps> = (props: IProps) => {
)}`; )}`;
} }
return ( return (
<Badge className="tag-item" variant="secondary"> <Badge className={`tag-item ${props.className}`} variant="secondary">
<Link to={link}>{title}</Link> <Link to={link}>{title}</Link>
</Badge> </Badge>
); );

View File

@@ -10,8 +10,8 @@ interface IProps {
export const StudioCard: React.FC<IProps> = ({ studio }) => { export const StudioCard: React.FC<IProps> = ({ studio }) => {
return ( return (
<Card className="studio-card"> <Card className="studio-card">
<Link to={`/studios/${studio.id}`} className="studio-image"> <Link to={`/studios/${studio.id}`} className="studio-card-header">
<img alt={studio.name} src={studio.image_path ?? ""} /> <img className="studio-card-image" alt={studio.name} src={studio.image_path ?? ""} />
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate">{studio.name}</h5> <h5 className="text-truncate">{studio.name}</h5>

View File

@@ -160,7 +160,7 @@ export const Studio: React.FC = () => {
> >
{isNew && <h2>Add Studio</h2>} {isNew && <h2>Add Studio</h2>}
<img className="logo" alt={name} src={imagePreview} /> <img className="logo" alt={name} src={imagePreview} />
<Table id="performer-details" style={{ width: "100%" }}> <Table>
<tbody> <tbody>
{TableUtils.renderInputGroup({ {TableUtils.renderInputGroup({
title: "Name", title: "Name",

View File

@@ -18,7 +18,7 @@ export const StudioList: React.FC = () => {
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="grid"> <div className="row px-xl-5 justify-content-center">
{result.data.findStudios.studios.map(studio => ( {result.data.findStudios.studios.map(studio => (
<StudioCard key={studio.id} studio={studio} /> <StudioCard key={studio.id} studio={studio} />
))} ))}

View File

@@ -98,11 +98,11 @@ export const TagList: React.FC = () => {
const tagElements = data.allTags.map(tag => { const tagElements = data.allTags.map(tag => {
return ( return (
<div key={tag.id} className="tag-list-row"> <div key={tag.id} className="tag-list-row row">
<Button variant="link" onClick={() => setEditingTag(tag)}> <Button variant="link" onClick={() => setEditingTag(tag)}>
{tag.name} {tag.name}
</Button> </Button>
<div style={{ float: "right" }}> <div className="ml-auto">
<Button variant="secondary" onClick={() => onAutoTag(tag)}> <Button variant="secondary" onClick={() => onAutoTag(tag)}>
Auto Tag Auto Tag
</Button> </Button>
@@ -131,7 +131,7 @@ export const TagList: React.FC = () => {
<div id="tag-list-container"> <div id="tag-list-container">
<Button <Button
variant="primary" variant="primary"
style={{ marginTop: "20px" }} className="mt-2"
onClick={() => setEditingTag({})} onClick={() => setEditingTag({})}
> >
New Tag New Tag

View File

@@ -62,6 +62,7 @@
color: #444; color: #444;
font-weight: 700; font-weight: 700;
left: 0; left: 0;
line-height: 1;
overflow: hidden; overflow: hidden;
padding: 5px; padding: 5px;
position: absolute; position: absolute;

View File

@@ -142,7 +142,7 @@ export const WallItem: React.FC<IWallItemProps> = (props: IWallItemProps) => {
/> />
{showTextContainer ? ( {showTextContainer ? (
<div className="scene-wall-item-text-container"> <div className="scene-wall-item-text-container">
<div style={{ lineHeight: 1 }}>{title}</div> <div>{title}</div>
{tags} {tags}
</div> </div>
) : ( ) : (

View File

@@ -257,7 +257,6 @@ export const ListFilter: React.FC<IListFilterProps> = (
defaultValue={props.filter.searchTerm} defaultValue={props.filter.searchTerm}
onChange={onChangeQuery} onChange={onChangeQuery}
className="filter-item col-5 col-sm-2" className="filter-item col-5 col-sm-2"
style={{ width: "inherit" }}
/> />
<Form.Control <Form.Control
as="select" as="select"
@@ -316,13 +315,7 @@ export const ListFilter: React.FC<IListFilterProps> = (
{renderMore()} {renderMore()}
</ButtonGroup> </ButtonGroup>
</div> </div>
<div <div className="d-flex justify-content-center">
style={{
display: "flex",
justifyContent: "center",
margin: "10px auto"
}}
>
{renderFilterTags()} {renderFilterTags()}
</div> </div>
</> </>

View File

@@ -10,35 +10,36 @@ interface IPerformerCardProps {
} }
export const PerformerCard: React.FC<IPerformerCardProps> = ( export const PerformerCard: React.FC<IPerformerCardProps> = (
props: IPerformerCardProps { performer, ageFromDate }
) => { ) => {
const age = TextUtils.age(props.performer.birthdate, props.ageFromDate); const age = TextUtils.age(performer.birthdate, ageFromDate);
const ageString = `${age} years old${ const ageString = `${age} years old${
props.ageFromDate ? " in this scene." : "." ageFromDate ? " in this scene." : "."
}`; }`;
function maybeRenderFavoriteBanner() { function maybeRenderFavoriteBanner() {
if (props.performer.favorite === false) { if (performer.favorite === false) {
return; return;
} }
return <div className="rating-banner rating-5">FAVORITE</div>; return <div className="rating-banner rating-5">FAVORITE</div>;
} }
return ( return (
<Card className="performer-card"> <Card>
<Link <Link
to={`/performers/${props.performer.id}`} to={`/performers/${performer.id}`}
className="performer previewable image"
style={{ backgroundImage: `url(${props.performer.image_path})` }}
> >
<img
className="image-thumbnail card-image"
alt={performer.name ?? ''} src={performer.image_path ?? ''} />
{maybeRenderFavoriteBanner()} {maybeRenderFavoriteBanner()}
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate">{props.performer.name}</h5> <h5 className="text-truncate">{performer.name}</h5>
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""} {age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<div className="text-muted"> <div className="text-muted">
Stars in {props.performer.scene_count}{" "} Stars in {performer.scene_count}{" "}
<Link to={NavUtils.makePerformerScenesUrl(props.performer)}> <Link to={NavUtils.makePerformerScenesUrl(performer)}>
scenes scenes
</Link> </Link>
. .

View File

@@ -222,6 +222,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
{queryableScrapers {queryableScrapers
? queryableScrapers.map(s => ( ? queryableScrapers.map(s => (
<Button <Button
key={s.name}
variant="link" variant="link"
onClick={() => onDisplayFreeOnesDialog(s)} onClick={() => onDisplayFreeOnesDialog(s)}
> >
@@ -294,7 +295,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
</td> </td>
<td> <td>
<Form.Control <Form.Control
value={url} value={url ?? ''}
readOnly={!isEditing} readOnly={!isEditing}
plaintext={!isEditing} plaintext={!isEditing}
placeholder="URL" placeholder="URL"
@@ -310,7 +311,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
function maybeRenderButtons() { function maybeRenderButtons() {
if (isEditing) { if (isEditing) {
return ( return (
<> <div className="row">
<Button <Button
className="edit-button" className="edit-button"
variant="primary" variant="primary"
@@ -330,7 +331,11 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
"" ""
)} )}
{renderScraperMenu()} {renderScraperMenu()}
</> <ImageInput
isEditing={!!isEditing}
onImageChange={onImageChangeHandler}
/>
</div>
); );
} }
} }
@@ -377,7 +382,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
{renderDeleteAlert()} {renderDeleteAlert()}
{renderScraperDialog()} {renderScraperDialog()}
<Table id="performer-details" style={{ width: "100%" }}> <Table id="performer-details" className="w-100">
<tbody> <tbody>
{maybeRenderName()} {maybeRenderName()}
{maybeRenderAliases()} {maybeRenderAliases()}
@@ -449,10 +454,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
isEditing: !!isEditing, isEditing: !!isEditing,
onChange: setInstagram onChange: setInstagram
})} })}
<ImageInput
isEditing={!!isEditing}
onImageChange={onImageChangeHandler}
/>
</tbody> </tbody>
</Table> </Table>

View File

@@ -14,48 +14,29 @@ interface IPerformerListTableProps {
export const PerformerListTable: React.FC<IPerformerListTableProps> = ( export const PerformerListTable: React.FC<IPerformerListTableProps> = (
props: IPerformerListTableProps props: IPerformerListTableProps
) => { ) => {
function maybeRenderFavoriteHeart(performer: GQL.PerformerDataFragment) { const renderPerformerRow = (performer: GQL.PerformerDataFragment) => (
if (!performer.favorite) { <tr key={performer.id}>
return; <td>
}
return (
<Button disabled className="favorite">
<Icon icon="heart" />
</Button>
);
}
function renderPerformerImage(performer: GQL.PerformerDataFragment) {
const style: React.CSSProperties = {
backgroundImage: `url('${performer.image_path}')`,
lineHeight: 5,
backgroundSize: "contain",
display: "inline-block",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
};
return (
<Link <Link
className="performer-list-thumbnail"
to={`/performers/${performer.id}`} to={`/performers/${performer.id}`}
style={style} >
/> <img className="image-thumbnail" alt={performer.name ?? ""} src={performer.image_path ?? ''} />
); </Link>
} </td>
<td className="text-left">
function renderPerformerRow(performer: GQL.PerformerDataFragment) {
return (
<>
<tr>
<td>{renderPerformerImage(performer)}</td>
<td style={{ textAlign: "left" }}>
<Link to={`/performers/${performer.id}`}> <Link to={`/performers/${performer.id}`}>
<h5 className="text-truncate">{performer.name}</h5> <h5 className="text-truncate">{performer.name}</h5>
</Link> </Link>
</td> </td>
<td>{performer.aliases ? performer.aliases : ""}</td> <td>{performer.aliases ? performer.aliases : ""}</td>
<td>{maybeRenderFavoriteHeart(performer)}</td> <td>{
performer.favorite && (
<Button disabled className="favorite">
<Icon icon="heart" />
</Button>
)
}
</td>
<td> <td>
<Link to={NavUtils.makePerformerScenesUrl(performer)}> <Link to={NavUtils.makePerformerScenesUrl(performer)}>
<h6>{performer.scene_count}</h6> <h6>{performer.scene_count}</h6>
@@ -64,12 +45,9 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<td>{performer.birthdate}</td> <td>{performer.birthdate}</td>
<td>{performer.height}</td> <td>{performer.height}</td>
</tr> </tr>
</>
); );
}
return ( return (
<>
<div className="grid"> <div className="grid">
<Table bordered striped> <Table bordered striped>
<thead> <thead>
@@ -86,6 +64,5 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
<tbody>{props.performers.map(renderPerformerRow)}</tbody> <tbody>{props.performers.map(renderPerformerRow)}</tbody>
</Table> </Table>
</div> </div>
</>
); );
}; };

View File

@@ -1,11 +1,3 @@
.performer.image {
background-position: center;
background-repeat: no-repeat;
background-size: cover;
height: 50vh;
min-height: 400px;
}
#performer-details { #performer-details {
td { td {
padding: 2px 0; padding: 2px 0;

View File

@@ -63,20 +63,13 @@ export const SceneCard: React.FC<ISceneCardProps> = (
function maybeRenderSceneStudioOverlay() { function maybeRenderSceneStudioOverlay() {
if (!props.scene.studio) return; if (!props.scene.studio) return;
let style: React.CSSProperties = {
backgroundImage: `url('${props.scene.studio.image_path}')`
};
let text = "";
if (showStudioAsText) {
style = {};
text = props.scene.studio.name;
}
return ( return (
<div className="scene-studio-overlay"> <div className="scene-studio-overlay">
<Link to={`/studios/${props.scene.studio.id}`} style={style}> <Link to={`/studios/${props.scene.studio.id}`}>
{text} { showStudioAsText
? props.scene.studio.name
: <img className="image-thumbnail" alt={props.scene.studio.name} src={props.scene.studio.image_path ?? ''} />
}
</Link> </Link>
</div> </div>
); );
@@ -103,13 +96,14 @@ export const SceneCard: React.FC<ISceneCardProps> = (
if (props.scene.performers.length <= 0) return; if (props.scene.performers.length <= 0) return;
const popoverContent = props.scene.performers.map(performer => ( const popoverContent = props.scene.performers.map(performer => (
<div className="performer-tag-container" key="performer"> <div className="performer-tag-container row" key="performer">
<Link <Link
to={`/performers/${performer.id}`} to={`/performers/${performer.id}`}
className="performer-tag previewable image" className="performer-tag col m-auto zoom-2"
style={{ backgroundImage: `url(${performer.image_path})` }} >
/> <img className="image-thumbnail" alt={performer.name ?? ''} src={performer.image_path ?? ''} />
<TagLink key={performer.id} performer={performer} /> </Link>
<TagLink key={performer.id} performer={performer} className="d-block" />
</div> </div>
)); ));
@@ -182,7 +176,7 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return ( return (
<Card <Card
className={`zoom-${props.zoomIndex} scene-card`} className={`scene-card zoom-${props.zoomIndex}`}
onMouseEnter={onMouseEnter} onMouseEnter={onMouseEnter}
onMouseLeave={onMouseLeave} onMouseLeave={onMouseLeave}
> >
@@ -200,20 +194,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (
{maybeRenderSceneStudioOverlay()} {maybeRenderSceneStudioOverlay()}
<Link <Link
to={`/scenes/${props.scene.id}`} to={`/scenes/${props.scene.id}`}
className={cx("image", "previewable", { portrait: isPortrait() })} className="scene-card-link"
> >
<div className="video-container">
{maybeRenderRatingBanner()} {maybeRenderRatingBanner()}
{maybeRenderSceneSpecsOverlay()} {maybeRenderSceneSpecsOverlay()}
<video <video
loop loop
className={cx("preview", { portrait: isPortrait() })} className={cx('scene-card-video', { portrait: isPortrait() })}
poster={props.scene.paths.screenshot || ""} poster={props.scene.paths.screenshot || ""}
ref={videoHoverHook.videoEl} ref={videoHoverHook.videoEl}
> >
{previewPath ? <source src={previewPath} /> : ""} {previewPath ? <source src={previewPath} /> : ""}
</video> </video>
</div>
</Link> </Link>
<div className="card-section"> <div className="card-section">
<h5 className="text-truncate"> <h5 className="text-truncate">
@@ -222,13 +214,15 @@ export const SceneCard: React.FC<ISceneCardProps> = (
: TextUtils.fileNameFromPath(props.scene.path)} : TextUtils.fileNameFromPath(props.scene.path)}
</h5> </h5>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
{ props.scene.details && (
<p> <p>
{TextUtils.truncate( {TextUtils.truncate(
props.scene.details ?? "", props.scene.details,
100, 100,
"... (continued)" "... (continued)"
)} )}
</p> </p>
)}
</div> </div>
{maybeRenderPopoverButtonGroup()} {maybeRenderPopoverButtonGroup()}

View File

@@ -37,13 +37,13 @@ export const PrimaryTags: React.FC<IPrimaryTags> = ({
return ( return (
<div key={marker.id}> <div key={marker.id}>
<hr /> <hr />
<div> <div className="row">
<Button variant="link" onClick={() => onClickMarker(marker)}> <Button variant="link" onClick={() => onClickMarker(marker)}>
{marker.title} {marker.title}
</Button> </Button>
<Button <Button
variant="link" variant="link"
style={{ float: "right" }} className="ml-auto"
onClick={() => onEdit(marker)} onClick={() => onEdit(marker)}
> >
Edit Edit

View File

@@ -153,17 +153,17 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
</div> </div>
</Form.Group> </Form.Group>
</div> </div>
<div className="buttons-container"> <div className="buttons-container row">
<Button variant="primary" type="submit"> <Button variant="primary" type="submit">
Submit Submit
</Button> </Button>
<Button type="button" onClick={onClose}> <Button variant="secondary" type="button" onClick={onClose} className="ml-2">
Cancel Cancel
</Button> </Button>
{editingMarker && ( {editingMarker && (
<Button <Button
variant="danger" variant="danger"
style={{ float: "right", marginRight: "10px" }} className="ml-auto"
onClick={() => onDelete()} onClick={() => onDelete()}
> >
Delete Delete

View File

@@ -125,7 +125,7 @@ export const SceneList: React.FC<ISceneList> = ({
} }
if (filter.displayMode === DisplayMode.Grid) { if (filter.displayMode === DisplayMode.Grid) {
return ( return (
<div className="grid"> <div className="row justify-content-center">
{result.data.findScenes.scenes.map(scene => {result.data.findScenes.scenes.map(scene =>
renderSceneCard(scene, selectedIds, zoomIndex) renderSceneCard(scene, selectedIds, zoomIndex)
)} )}

View File

@@ -11,63 +11,30 @@ interface ISceneListTableProps {
export const SceneListTable: React.FC<ISceneListTableProps> = ( export const SceneListTable: React.FC<ISceneListTableProps> = (
props: ISceneListTableProps props: ISceneListTableProps
) => { ) => {
function renderSceneImage(scene: GQL.SlimSceneDataFragment) { const renderTags = (tags: GQL.Tag[]) => (
const style: React.CSSProperties = { tags.map(tag => (
backgroundImage: `url('${scene.paths.screenshot}')`,
lineHeight: 5,
backgroundSize: "contain",
display: "inline-block",
backgroundPosition: "center",
backgroundRepeat: "no-repeat"
};
return (
<Link
className="scene-list-thumbnail"
to={`/scenes/${scene.id}`}
style={style}
/>
);
}
function renderDuration(scene: GQL.SlimSceneDataFragment) {
if (scene.file.duration === undefined) {
return;
}
return TextUtils.secondsToTimestamp(scene.file.duration ?? 0);
}
function renderTags(tags: GQL.Tag[]) {
return tags.map(tag => (
<Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}> <Link key={tag.id} to={NavUtils.makeTagScenesUrl(tag)}>
<h6>{tag.name}</h6> <h6>{tag.name}</h6>
</Link> </Link>
)); ))
}
function renderPerformers(performers: Partial<GQL.Performer>[]) {
return performers.map(performer => (
<Link key={performer.id} to={NavUtils.makePerformerScenesUrl(performer)}>
<h6>{performer.name}</h6>
</Link>
));
}
function renderStudio(studio: Partial<GQL.Studio> | undefined) {
if (studio) {
return (
<Link to={NavUtils.makeStudioScenesUrl(studio)}>
<h6>{studio.name}</h6>
</Link>
); );
}
}
function renderSceneRow(scene: GQL.SlimSceneDataFragment) { const renderPerformers = (performers: Partial<GQL.Performer>[]) => (
return ( performers.map(performer => (
<Link key={performer.id} to={NavUtils.makePerformerScenesUrl(performer)} />
))
);
const renderSceneRow = (scene: GQL.SlimSceneDataFragment) => (
<tr key={scene.id}> <tr key={scene.id}>
<td>{renderSceneImage(scene)}</td> <td>
<td style={{ textAlign: "left" }}> <Link
to={`/scenes/${scene.id}`}
>
<img className="image-thumbnail" alt={scene.title ?? ''} src={scene.paths.screenshot ?? ''} />
</Link>
</td>
<td className="text-left">
<Link to={`/scenes/${scene.id}`}> <Link to={`/scenes/${scene.id}`}>
<h5 className="text-truncate"> <h5 className="text-truncate">
{scene.title ?? TextUtils.fileNameFromPath(scene.path)} {scene.title ?? TextUtils.fileNameFromPath(scene.path)}
@@ -75,13 +42,17 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
</Link> </Link>
</td> </td>
<td>{scene.rating ? scene.rating : ""}</td> <td>{scene.rating ? scene.rating : ""}</td>
<td>{renderDuration(scene)}</td> <td>{scene.file.duration && TextUtils.secondsToTimestamp(scene.file.duration) }</td>
<td>{renderTags(scene.tags)}</td> <td>{renderTags(scene.tags)}</td>
<td>{renderPerformers(scene.performers)}</td> <td>{renderPerformers(scene.performers)}</td>
<td>{renderStudio(scene.studio ?? undefined)}</td> <td>{ scene.studio && (
<Link to={NavUtils.makeStudioScenesUrl(scene.studio)}>
<h6>{scene.studio.name}</h6>
</Link>
)}
</td>
</tr> </tr>
); )
}
return ( return (
<div className="grid"> <div className="grid">

View File

@@ -194,7 +194,7 @@ export class ScenePlayerImpl extends React.Component<
return ( return (
<ReactJWPlayer <ReactJWPlayer
playerId={JWUtils.playerID} playerId={JWUtils.playerID}
playerScript="http://192.168.1.65:9999/jwplayer/jwplayer.js" playerScript="http://localhost:9999/jwplayer/jwplayer.js"
customProps={config} customProps={config}
onReady={this.onReady} onReady={this.onReady}
onSeeked={this.onSeeked} onSeeked={this.onSeeked}

View File

@@ -264,10 +264,11 @@ export const SceneSelectedOptions: React.FC<IListOperationProps> = (
<Form.Label>Rating</Form.Label> <Form.Label>Rating</Form.Label>
<Form.Control <Form.Control
as="select" as="select"
value={rating}
onChange={(event: any) => setRating(event.target.value)} onChange={(event: any) => setRating(event.target.value)}
> >
{["", "1", "2", "3", "4", "5"].map(opt => ( {["", "1", "2", "3", "4", "5"].map(opt => (
<option selected={opt === rating} value={opt}> <option key={opt} value={opt}>
{opt} {opt}
</option> </option>
))} ))}

View File

@@ -13,12 +13,33 @@
} }
} }
.grid { .row {
.scene-card { .scene-card {
padding-bottom: 0; overflow: hidden;
padding: 0;
} }
} }
.scene-card-link {
position: relative;
}
.card-section {
margin-bottom: 0;
padding: .5rem 1rem 0 1rem;
}
.card-select {
left: .5rem;
margin-top: -12px;
opacity: .5;
padding-left: 15px;
position: absolute;
top: .7rem;
width: 1.2rem;
z-index: 1;
}
.performer-tag-container { .performer-tag-container {
display: inline-block; display: inline-block;
margin: 5px; margin: 5px;
@@ -105,3 +126,24 @@
overflow-y: auto; overflow-y: auto;
} }
} }
.studio-card {
padding: .5rem;
&-header {
height: 150px;
line-height: 150px;
text-align: center;
}
&-image {
max-height: 150px;
object-fit: contain;
vertical-align: middle;
width: 320px;
@media (max-width: 576px) {
width: 100%;
}
}
}

View File

@@ -1,15 +0,0 @@
.studio-card {
padding: .5rem;
}
.studio-image {
height: 150px;
line-height: 150px;
text-align: center;
img {
max-height: 100%;
max-width: 100%;
vertical-align: middle;
}
}

View File

@@ -25,126 +25,71 @@ code {
margin: $pt-grid-size $pt-grid-size 0 0; margin: $pt-grid-size $pt-grid-size 0 0;
padding: 0; padding: 0;
.performer-card,
.studio-card {
min-width: 185px;
width: 320px;
}
&.wall { &.wall {
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
& .performer-list-thumbnail {
height: 100px;
min-width: 50px;
}
& .scene-list-thumbnail {
min-height: 50px;
width: 150px;
}
table td { table td {
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
} }
}
.card { .card {
margin: 0 0 10px 10px; margin: 5px;
overflow: hidden; }
@media (min-width: 576px) { @media (min-width: 576px) {
&.zoom-0 { .zoom-0 {
width: 15rem; width: 15rem;
.previewable {
max-height: 11.25rem;
} }
.previewable.portrait { .zoom-1 {
max-height: 11.25rem;
}
}
&.zoom-1 {
width: 20rem; width: 20rem;
.previewable {
max-height: 15rem;
} }
.previewable.portrait { .zoom-2 {
width: 30rem;
}
.zoom-3 {
width: 40rem;
}
}
.scene-card-video {
height: auto;
width: 100%;
.zoom-0 {
height: 11.25rem;
}
.zoom-1 {
height: 15rem; height: 15rem;
} }
}
&.zoom-2 { .zoom-2 {
width: 30rem;
.previewable {
max-height: 22.5rem;
}
.previewable.portrait {
height: 22.5rem; height: 22.5rem;
} }
}
&.zoom-3 { .zoom-3 {
width: 40rem;
.previewable {
max-height: 30rem;
}
.previewable.portrait {
height: 30rem; height: 30rem;
} }
}
}
.card-select {
margin-top: -12px;
opacity: .5;
padding-left: 15px;
position: absolute;
width: 1.2rem;
z-index: 1;
}
}
} }
.previewable { .image-thumbnail {
display: block; height: 100px;
line-height: 0; min-width: 50px;
margin: -20px 0 0 -20px;
max-height: 240px;
overflow: hidden;
position: relative;
width: calc(100% + 40px);
}
.previewable.portrait {
height: 240px;
}
.video-container {
height: 100%;
width: 100%;
}
video.preview {
display: block;
margin: 0 auto;
object-fit: cover; object-fit: cover;
width: 100%; object-position: center 20%;
} }
video.preview.portrait { .card-image {
height: 100%; height: 30rem;
width: auto; min-width: 11.25rem;
width: 20rem;
} }
.filter-item, .filter-item,
@@ -204,16 +149,6 @@ video.preview.portrait {
margin: 10px auto; margin: 10px auto;
} }
.card-section {
padding: 10px 0 0 0;
&.centered {
display: flex;
flex-flow: wrap;
justify-content: center;
}
}
.rating-5 { .rating-5 {
background: #ff2f39; background: #ff2f39;
} }
@@ -264,26 +199,27 @@ video.preview.portrait {
.scene-studio-overlay { .scene-studio-overlay {
display: block; display: block;
font-weight: 900; font-weight: 900;
height: 20%; height: 10%;
max-width: 40%;
opacity: .75; opacity: .75;
position: absolute; position: absolute;
right: .7rem; right: .7rem;
top: .7rem; top: .7rem;
width: 40%;
z-index: 9; z-index: 9;
.image-thumbnail {
height: auto;
max-height: 50px;
max-width: 100%;
}
a { a {
background-position: right top;
background-repeat: no-repeat;
background-size: contain;
color: $text-color; color: $text-color;
display: inline-block; display: inline-block;
height: 100%;
letter-spacing: -.03rem; letter-spacing: -.03rem;
text-align: right; text-align: right;
text-decoration: none; text-decoration: none;
text-shadow: 0 0 3px #000; text-shadow: 0 0 3px #000;
width: 100%;
} }
} }
@@ -468,6 +404,12 @@ video.preview.portrait {
.form-control { .form-control {
&-plaintext { &-plaintext {
color: $text-color; color: $text-color;
margin: 0;
padding: 0;
option {
color: black;
}
&::placeholder { &::placeholder {
color: transparent; color: transparent;
@@ -496,6 +438,7 @@ video.preview.portrait {
.image-input { .image-input {
margin-bottom: 0; margin-bottom: 0;
margin-left: .5rem;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@@ -504,7 +447,6 @@ video.preview.portrait {
} }
[type=file] { [type=file] {
cursor: inherit;
display: block; display: block;
filter: alpha(opacity=0); filter: alpha(opacity=0);
font-size: 999px; font-size: 999px;
@@ -515,6 +457,11 @@ video.preview.portrait {
right: 0; right: 0;
text-align: right; text-align: right;
top: 0; top: 0;
&:hover {
cursor: pointer;
}
} }
} }
@@ -565,3 +512,7 @@ video.preview.portrait {
} }
} }
} }
.error-message {
white-space: "pre-wrap";
}

View File

@@ -99,6 +99,16 @@ hr {
} }
} }
.table {
td {
padding: .25rem .75rem;
}
}
.popover { .popover {
max-width: inherit; max-width: inherit;
} }
.card {
border: none;
}