Remove hotkeys and fix tag selection (#505)

* Remove broken scene player hotkeys
* Disable closing tag select menu after a select
This commit is contained in:
InfiniteTF 2020-04-29 01:55:34 +02:00 committed by GitHub
parent 52a1059380
commit a0306bfbd2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 116 additions and 142 deletions

View File

@ -46,7 +46,6 @@
"react-apollo": "^3.1.3",
"react-bootstrap": "^1.0.0-beta.16",
"react-dom": "16.12.0",
"react-hotkeys": "^2.0.0",
"react-images": "0.5.19",
"react-intl": "^3.12.0",
"react-jw-player": "1.19.0",

View File

@ -45,7 +45,10 @@ export const GalleryList: React.FC = () => {
</Link>
</td>
<td className="d-none d-sm-block">
<Link to={`/galleries/${gallery.id}`}>{gallery.path} ({gallery.files.length} {gallery.files.length === 1 ? 'image' : 'images'})</Link>
<Link to={`/galleries/${gallery.id}`}>
{gallery.path} ({gallery.files.length}{" "}
{gallery.files.length === 1 ? "image" : "images"})
</Link>
</td>
</tr>
))}

View File

@ -253,7 +253,7 @@ export const Movie: React.FC = () => {
})}
{TableUtils.renderHtmlSelect({
title: "Rating",
value: rating ? rating : "",
value: rating ?? "",
isEditing,
onChange: (value: string) =>
setRating(Number.parseInt(value, 10)),

View File

@ -189,7 +189,10 @@ export const Performer: React.FC = () => {
{performer.twitter && (
<Button className="minimal">
<a
href={TextUtils.sanitiseURL(performer.twitter, TextUtils.twitterURL)}
href={TextUtils.sanitiseURL(
performer.twitter,
TextUtils.twitterURL
)}
className="twitter"
target="_blank"
rel="noopener noreferrer"
@ -201,7 +204,10 @@ export const Performer: React.FC = () => {
{performer.instagram && (
<Button className="minimal">
<a
href={TextUtils.sanitiseURL(performer.instagram, TextUtils.instagramURL)}
href={TextUtils.sanitiseURL(
performer.instagram,
TextUtils.instagramURL
)}
className="instagram"
target="_blank"
rel="noopener noreferrer"
@ -215,18 +221,14 @@ export const Performer: React.FC = () => {
function renderPerformerImage() {
if (imagePreview) {
return (
<img className="photo" src={imagePreview} alt="Performer" />
);
return <img className="photo" src={imagePreview} alt="Performer" />;
}
}
function renderNewView() {
return (
<div className="row new-view">
<div className="col-4">
{renderPerformerImage()}
</div>
<div className="col-4">{renderPerformerImage()}</div>
<div className="col-6">
<h2>Create Performer</h2>
{renderTabs()}

View File

@ -11,7 +11,12 @@ import {
ScrapePerformerSuggest,
LoadingIndicator,
} from "src/components/Shared";
import { ImageUtils, TableUtils, TextUtils, EditableTextUtils } from "src/utils";
import {
ImageUtils,
TableUtils,
TextUtils,
EditableTextUtils,
} from "src/utils";
import { useToast } from "src/hooks";
interface IPerformerDetails {
@ -100,22 +105,22 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
);
}
function translateScrapedGender(gender?: string) {
if (!gender) {
function translateScrapedGender(scrapedGender?: string) {
if (!scrapedGender) {
return;
}
let retEnum : GQL.GenderEnum | undefined;
let retEnum: GQL.GenderEnum | undefined;
// try to translate from enum values first
const upperGender = gender?.toUpperCase();
const upperGender = scrapedGender?.toUpperCase();
const asEnum = StashService.genderToString(upperGender as GQL.GenderEnum);
if (asEnum) {
retEnum = StashService.stringToGender(asEnum);
} else {
// try to match against gender strings
const caseInsensitive = true;
retEnum = StashService.stringToGender(gender, caseInsensitive);
retEnum = StashService.stringToGender(scrapedGender, caseInsensitive);
}
return StashService.genderToString(retEnum);
@ -131,7 +136,10 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
// image is a base64 string
// #404: don't overwrite image if it has been modified by the user
if (image === undefined && (state as GQL.ScrapedPerformerDataFragment).image !== undefined) {
if (
image === undefined &&
(state as GQL.ScrapedPerformerDataFragment).image !== undefined
) {
const imageStr = (state as GQL.ScrapedPerformerDataFragment).image;
setImage(imageStr ?? undefined);
if (onImageChange) {

View File

@ -145,7 +145,7 @@ export const ParserInput: React.FC<IParserInputProps> = (
key={item.field}
onSelect={() => addParserField(item)}
>
<span>{item.field || "{}"}</span>
<span className="mr-2">{item.field || "{}"}</span>
<span className="ml-auto text-muted">{item.helperText}</span>
</Dropdown.Item>
))}

View File

@ -1,6 +1,5 @@
import React from "react";
import ReactJWPlayer from "react-jw-player";
import { HotKeys } from "react-hotkeys";
import * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import { JWUtils } from "src/utils";
@ -21,13 +20,6 @@ interface IScenePlayerState {
config: Record<string, any>;
}
const KeyMap = {
NUM0: "0",
NUM1: "1",
NUM2: "2",
SPACE: " ",
};
export class ScenePlayerImpl extends React.Component<
IScenePlayerProps,
IScenePlayerState
@ -37,21 +29,6 @@ export class ScenePlayerImpl extends React.Component<
private player: any;
private lastTime = 0;
private KeyHandlers = {
NUM0: () => {
this.onReset();
},
NUM1: () => {
this.onDecrease();
},
NUM2: () => {
this.onIncrease();
},
SPACE: () => {
this.onPause();
},
};
constructor(props: IScenePlayerProps) {
super(props);
this.onReady = this.onReady.bind(this);
@ -207,31 +184,25 @@ export class ScenePlayerImpl extends React.Component<
public render() {
return (
<HotKeys
keyMap={KeyMap}
handlers={this.KeyHandlers}
className="row scene-player"
<div
id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter"
>
<div
id="jwplayer-container"
className="w-100 col-sm-9 m-sm-auto no-gutter"
>
<ReactJWPlayer
playerId={JWUtils.playerID}
playerScript="/jwplayer/jwplayer.js"
customProps={this.state.config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
<ScenePlayerScrubber
scene={this.props.scene}
position={this.state.scrubberPosition}
onSeek={this.onScrubberSeek}
onScrolled={this.onScrubberScrolled}
/>
</div>
</HotKeys>
<ReactJWPlayer
playerId={JWUtils.playerID}
playerScript="/jwplayer/jwplayer.js"
customProps={this.state.config}
onReady={this.onReady}
onSeeked={this.onSeeked}
onTime={this.onTime}
/>
<ScenePlayerScrubber
scene={this.props.scene}
position={this.state.scrubberPosition}
onSeek={this.onScrubberSeek}
onScrolled={this.onScrubberScrolled}
/>
</div>
);
}
}

View File

@ -182,7 +182,9 @@ export const SceneCard: React.FC<ISceneCardProps> = (
return (
<div>
<Button className="minimal">
<span className="fa-icon"><SweatDrops /></span>
<span className="fa-icon">
<SweatDrops />
</span>
<span>{props.scene.o_counter}</span>
</Button>
</div>

View File

@ -151,9 +151,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
<div className="row">
<span className="col-4">Downloaded From</span>
<span className="col-8 text-truncate">
<a href={TextUtils.sanitiseURL(props.scene.url)}>
{props.scene.url}
</a>
<a href={TextUtils.sanitiseURL(props.scene.url)}>{props.scene.url}</a>
</span>
</div>
);

View File

@ -84,7 +84,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
)
}
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
mandatory={true}
mandatory
/>
</div>
);

View File

@ -307,7 +307,8 @@ export const SettingsConfigurationPanel: React.FC = () => {
onChange={() => setForceMkv(!forceMkv)}
/>
<Form.Text className="text-muted">
Treat Matroska (MKV) as a supported container. Recommended for Chromium based browsers
Treat Matroska (MKV) as a supported container. Recommended for
Chromium based browsers
</Form.Text>
</Form.Group>
<Form.Group id="force-options-hevc">
@ -318,7 +319,8 @@ export const SettingsConfigurationPanel: React.FC = () => {
onChange={() => setForceHevc(!forceHevc)}
/>
<Form.Text className="text-muted">
Treat HEVC as a supported codec. Recommended for Safari or some Android based browsers
Treat HEVC as a supported codec. Recommended for Safari or some
Android based browsers
</Form.Text>
</Form.Group>
</Form.Group>

View File

@ -68,9 +68,10 @@ class LogEntry {
const MAX_LOG_ENTRIES = 200;
const logLevels = ["Debug", "Info", "Warning", "Error"];
const logReducer = (existingEntries:LogEntry[], newEntries:LogEntry[]) => (
[...newEntries.reverse(), ...existingEntries]
);
const logReducer = (existingEntries: LogEntry[], newEntries: LogEntry[]) => [
...newEntries.reverse(),
...existingEntries,
];
export const SettingsLogsPanel: React.FC = () => {
const { data, error } = StashService.useLoggingSubscribe();
@ -78,10 +79,9 @@ export const SettingsLogsPanel: React.FC = () => {
const [currentData, dispatchLogUpdate] = useReducer(logReducer, []);
const [logLevel, setLogLevel] = useState<string>("Info");
useEffect(() => {
const newData = (data?.loggingSubscribe ?? []).map((e) => new LogEntry(e));
dispatchLogUpdate(newData)
dispatchLogUpdate(newData);
}, [data]);
const oldData = (existingData?.logs ?? []).map((e) => new LogEntry(e));

View File

@ -105,9 +105,9 @@ export const SettingsTasksPanel: React.FC = () => {
cancel={{ onClick: () => setIsCleanAlertOpen(false) }}
>
<p>
Are you sure you want to Clean? This will delete database information and
generated content for all scenes and galleries that are no longer found in the
filesystem.
Are you sure you want to Clean? This will delete database information
and generated content for all scenes and galleries that are no longer
found in the filesystem.
</p>
</Modal>
);

View File

@ -7,14 +7,19 @@ interface IProps {
disabled?: boolean;
numericValue: number | undefined;
mandatory?: boolean;
onValueChange(valueAsNumber: number | undefined, valueAsString?: string): void;
onValueChange(
valueAsNumber: number | undefined,
valueAsString?: string
): void;
onReset?(): void;
className?: string;
}
export const DurationInput: React.FC<IProps> = (props: IProps) => {
const [value, setValue] = useState<string | undefined>(
props.numericValue !== undefined ? DurationUtils.secondsToString(props.numericValue) : undefined
props.numericValue !== undefined
? DurationUtils.secondsToString(props.numericValue)
: undefined
);
useEffect(() => {

View File

@ -22,7 +22,7 @@ export const ImageInput: React.FC<IImageInput> = ({
<Form.Control
type="file"
onChange={onImageChange}
accept={`.jpg,.jpeg,.png${acceptSVG ? ',.svg' : ''}`}
accept={`.jpg,.jpeg,.png${acceptSVG ? ",.svg" : ""}`}
/>
</Form.Label>
);

View File

@ -42,6 +42,7 @@ interface ISelectProps {
placeholder?: string;
showDropdown?: boolean;
groupHeader?: string;
closeMenuOnSelect?: boolean;
}
interface ISceneGallerySelect {
@ -355,6 +356,7 @@ export const TagSelect: React.FC<IFilterProps> = (props) => {
items={items}
onCreateOption={onCreate}
selectedOptions={selected}
closeMenuOnSelect={false}
/>
);
};
@ -376,6 +378,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
placeholder,
showDropdown = true,
groupHeader,
closeMenuOnSelect = true,
}) => {
const defaultValue =
items.filter((item) => initialIds?.indexOf(item.value) !== -1) ?? null;
@ -421,6 +424,7 @@ const SelectComponent: React.FC<ISelectProps & ITypeProps> = ({
isDisabled,
isLoading,
styles,
closeMenuOnSelect,
components: {
IndicatorSeparator: () => null,
...((!showDropdown || isDisabled) && { DropdownIndicator: () => null }),

View File

@ -10,7 +10,7 @@ export const Stats: React.FC = () => {
if (error) return <span>error.message</span>;
var size = data.stats.scene_size_count.split(" ")
const size = data.stats.scene_size_count.split(" ");
return (
<div className="mt-5">
@ -18,7 +18,7 @@ export const Stats: React.FC = () => {
<div className="stats-element">
<p className="title">
<FormattedNumber value={parseFloat(size[0])} />
{" " + size[1]}
{` ${size[1]}`}
</p>
<p className="heading">
<FormattedMessage id="library-size" defaultMessage="Library size" />

View File

@ -196,14 +196,6 @@ div.dropdown-menu {
.dropdown-item {
display: flex;
& > * {
margin-right: 7px;
}
& > :last-child {
margin-right: 0;
}
}
}
@ -396,8 +388,8 @@ div.dropdown-menu {
overflow: hidden;
position: relative;
input[type=file], /* FF, IE7+, chrome (except button) */
input[type=file]::-webkit-file-upload-button {
input[type="file"], /* FF, IE7+, chrome (except button) */
input[type="file"]::-webkit-file-upload-button {
/* chromes and blink button */
cursor: pointer;
}

View File

@ -43,7 +43,7 @@ export class PerformerIsMissingCriterion extends IsMissingCriterion {
"piercings",
"aliases",
"gender",
"scenes"
"scenes",
];
}

View File

@ -9,7 +9,10 @@ import {
} from "./criterion";
import { FavoriteCriterion } from "./favorite";
import { HasMarkersCriterion } from "./has-markers";
import {PerformerIsMissingCriterion, SceneIsMissingCriterion} from "./is-missing";
import {
PerformerIsMissingCriterion,
SceneIsMissingCriterion,
} from "./is-missing";
import { NoneCriterion } from "./none";
import { PerformersCriterion } from "./performers";
import { RatingCriterion } from "./rating";

View File

@ -29,7 +29,7 @@ import {
import {
IsMissingCriterion,
PerformerIsMissingCriterionOption,
SceneIsMissingCriterionOption
SceneIsMissingCriterionOption,
} from "./criteria/is-missing";
import { NoneCriterionOption } from "./criteria/none";
import {
@ -148,7 +148,9 @@ export class ListFilterModel {
new FavoriteCriterionOption(),
new GenderCriterionOption(),
new PerformerIsMissingCriterionOption(),
...numberCriteria.concat(stringCriteria).map(c => ListFilterModel.createCriterionOption(c))
...numberCriteria
.concat(stringCriteria)
.map((c) => ListFilterModel.createCriterionOption(c)),
];
break;

View File

@ -20,7 +20,7 @@ const renderTextArea = (options: {
value={options.value}
/>
);
}
};
const renderEditableText = (options: {
title?: string;
@ -42,8 +42,8 @@ const renderEditableText = (options: {
}
placeholder={options.title}
/>
)
}
);
};
const renderInputGroup = (options: {
title?: string;
@ -55,16 +55,12 @@ const renderInputGroup = (options: {
}) => {
if (options.url && !options.isEditing) {
return (
<a
href={options.url}
target="_blank"
rel="noopener noreferrer"
>
<a href={options.url} target="_blank" rel="noopener noreferrer">
{options.value}
</a>
);
}
return (
<Form.Control
className="text-input"
@ -77,13 +73,13 @@ const renderInputGroup = (options: {
}
/>
);
}
};
const renderDurationInput = (options: {
value: string | undefined;
isEditing: boolean;
url?: string;
asString?: boolean
asString?: boolean;
onChange: (value: string | undefined) => void;
}) => {
let numericValue: number | undefined;
@ -98,7 +94,7 @@ const renderDurationInput = (options: {
numericValue = DurationUtils.stringToSeconds(options.value);
}
}
if (!options.isEditing) {
let durationString;
if (numericValue !== undefined) {
@ -118,17 +114,18 @@ const renderDurationInput = (options: {
<DurationInput
disabled={!options.isEditing}
numericValue={numericValue}
onValueChange={(valueAsNumber: number, valueAsString? : string) => {
onValueChange={(valueAsNumber: number, valueAsString?: string) => {
let value = valueAsString;
if (!options.asString) {
value = valueAsNumber !== undefined ? valueAsNumber.toString() : undefined;
value =
valueAsNumber !== undefined ? valueAsNumber.toString() : undefined;
}
options.onChange(value);
}}
/>
);
}
};
const renderHtmlSelect = (options: {
value?: string | number;
@ -164,7 +161,7 @@ const renderHtmlSelect = (options: {
))}
</Form.Control>
);
}
};
// TODO: isediting
const renderFilterSelect = (options: {
@ -202,4 +199,4 @@ const EditableTextUtils = {
renderFilterSelect,
renderMultiSelect,
};
export default EditableTextUtils;
export default EditableTextUtils;

View File

@ -9,9 +9,7 @@ const renderEditableTextTableRow = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderEditableText(options)}
</td>
<td>{EditableTextUtils.renderEditableText(options)}</td>
</tr>
);
@ -23,9 +21,7 @@ const renderTextArea = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderTextArea(options)}
</td>
<td>{EditableTextUtils.renderTextArea(options)}</td>
</tr>
);
@ -39,9 +35,7 @@ const renderInputGroup = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderInputGroup(options)}
</td>
<td>{EditableTextUtils.renderInputGroup(options)}</td>
</tr>
);
@ -56,9 +50,7 @@ const renderDurationInput = (options: {
return (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderDurationInput(options)}
</td>
<td>{EditableTextUtils.renderDurationInput(options)}</td>
</tr>
);
};
@ -72,9 +64,7 @@ const renderHtmlSelect = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderHtmlSelect(options)}
</td>
<td>{EditableTextUtils.renderHtmlSelect(options)}</td>
</tr>
);
@ -87,9 +77,7 @@ const renderFilterSelect = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderFilterSelect(options)}
</td>
<td>{EditableTextUtils.renderFilterSelect(options)}</td>
</tr>
);
@ -102,9 +90,7 @@ const renderMultiSelect = (options: {
}) => (
<tr>
<td>{options.title}</td>
<td>
{EditableTextUtils.renderMultiSelect(options)}
</td>
<td>{EditableTextUtils.renderMultiSelect(options)}</td>
</tr>
);

View File

@ -90,7 +90,7 @@ const sanitiseURL = (url?: string, siteURL?: URL) => {
if (!url) {
return url;
}
if (url.startsWith("http://") || url.startsWith("https://")) {
// just return the entire URL
return url;
@ -103,12 +103,12 @@ const sanitiseURL = (url?: string, siteURL?: URL) => {
}
// otherwise, construct the url from the protocol, host and passed url
return siteURL.protocol + siteURL.host + "/" + url;
return `${siteURL.protocol}${siteURL.host}/${url}`;
}
// just prepend the protocol - assume https
return "https://" + url;
}
return `https://${url}`;
};
const TextUtils = {
truncate,