mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Performer and Movie UI fixes and improvements (#447)
* Improve gender display * Sanitise performer URLs * Refactor editable text into separate module * Make movie duration DurationInput * Fix clearing sometimes not firing onChange * Set movie duration as string * Fix TextUtil.fileSize * Improve scene URL
This commit is contained in:
@@ -11,7 +11,7 @@ import {
|
|||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Table, Form } from "react-bootstrap";
|
import { Table, Form } from "react-bootstrap";
|
||||||
import { TableUtils, ImageUtils } from "src/utils";
|
import { TableUtils, ImageUtils, EditableTextUtils, TextUtils } from "src/utils";
|
||||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||||
|
|
||||||
export const Movie: React.FC = () => {
|
export const Movie: React.FC = () => {
|
||||||
@@ -206,11 +206,14 @@ export const Movie: React.FC = () => {
|
|||||||
isEditing,
|
isEditing,
|
||||||
onChange: setAliases,
|
onChange: setAliases,
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderDurationInput({
|
||||||
title: "Duration",
|
title: "Duration",
|
||||||
value: duration,
|
value: duration,
|
||||||
isEditing,
|
isEditing,
|
||||||
onChange: setDuration,
|
onChange: (value: string | undefined) => {
|
||||||
|
setDuration(value ?? "");
|
||||||
|
},
|
||||||
|
asString: true,
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Date (YYYY-MM-DD)",
|
title: "Date (YYYY-MM-DD)",
|
||||||
@@ -236,14 +239,14 @@ export const Movie: React.FC = () => {
|
|||||||
|
|
||||||
<Form.Group controlId="url">
|
<Form.Group controlId="url">
|
||||||
<Form.Label>URL</Form.Label>
|
<Form.Label>URL</Form.Label>
|
||||||
<Form.Control
|
<div>
|
||||||
className="text-input"
|
{EditableTextUtils.renderInputGroup({
|
||||||
readOnly={!isEditing}
|
isEditing,
|
||||||
onChange={(newValue: React.FormEvent<HTMLTextAreaElement>) =>
|
onChange: setUrl,
|
||||||
setUrl(newValue.currentTarget.value)
|
value: url,
|
||||||
}
|
url: TextUtils.sanitiseURL(url),
|
||||||
value={url}
|
})}
|
||||||
/>
|
</div>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group controlId="synopsis">
|
<Form.Group controlId="synopsis">
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ export const Performer: React.FC = () => {
|
|||||||
{performer.url && (
|
{performer.url && (
|
||||||
<Button className="minimal">
|
<Button className="minimal">
|
||||||
<a
|
<a
|
||||||
href={performer.url}
|
href={TextUtils.sanitiseURL(performer.url)}
|
||||||
className="link"
|
className="link"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -189,7 +189,7 @@ export const Performer: React.FC = () => {
|
|||||||
{performer.twitter && (
|
{performer.twitter && (
|
||||||
<Button className="minimal">
|
<Button className="minimal">
|
||||||
<a
|
<a
|
||||||
href={`https://www.twitter.com/${performer.twitter}`}
|
href={TextUtils.sanitiseURL(performer.twitter, TextUtils.twitterURL)}
|
||||||
className="twitter"
|
className="twitter"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -201,7 +201,7 @@ export const Performer: React.FC = () => {
|
|||||||
{performer.instagram && (
|
{performer.instagram && (
|
||||||
<Button className="minimal">
|
<Button className="minimal">
|
||||||
<a
|
<a
|
||||||
href={`https://www.instagram.com/${performer.instagram}`}
|
href={TextUtils.sanitiseURL(performer.instagram, TextUtils.instagramURL)}
|
||||||
className="instagram"
|
className="instagram"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
@@ -213,11 +213,19 @@ export const Performer: React.FC = () => {
|
|||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function renderPerformerImage() {
|
||||||
|
if (imagePreview) {
|
||||||
|
return (
|
||||||
|
<img className="photo" src={imagePreview} alt="Performer" />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function renderNewView() {
|
function renderNewView() {
|
||||||
return (
|
return (
|
||||||
<div className="row new-view">
|
<div className="row new-view">
|
||||||
<div className="col-4">
|
<div className="col-4">
|
||||||
<img className="photo" src={imagePreview} alt="Performer" />
|
{renderPerformerImage()}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-6">
|
<div className="col-6">
|
||||||
<h2>Create Performer</h2>
|
<h2>Create Performer</h2>
|
||||||
@@ -242,11 +250,6 @@ export const Performer: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="col col-sm-6">
|
<div className="col col-sm-6">
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="image-container col-6 d-block d-sm-none">
|
|
||||||
<Button variant="link" onClick={() => setLightboxIsOpen(true)}>
|
|
||||||
<img className="performer" src={imagePreview} alt="Performer" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="performer-head col-6 col-sm-12">
|
<div className="performer-head col-6 col-sm-12">
|
||||||
<h2>
|
<h2>
|
||||||
{performer.name}
|
{performer.name}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable react/no-this-in-sfc */
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form, Popover, OverlayTrigger, Table } from "react-bootstrap";
|
import { Button, Popover, OverlayTrigger, Table } from "react-bootstrap";
|
||||||
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";
|
||||||
import {
|
import {
|
||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
ScrapePerformerSuggest,
|
ScrapePerformerSuggest,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { ImageUtils, TableUtils } from "src/utils";
|
import { ImageUtils, TableUtils, TextUtils, EditableTextUtils } from "src/utils";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
@@ -323,16 +323,13 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{maybeRenderScrapeButton()}
|
{maybeRenderScrapeButton()}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Control
|
{EditableTextUtils.renderInputGroup({
|
||||||
className="text-input"
|
title: "URL",
|
||||||
value={url ?? ""}
|
value: url,
|
||||||
readOnly={!isEditing}
|
url: TextUtils.sanitiseURL(url),
|
||||||
plaintext={!isEditing}
|
isEditing: !!isEditing,
|
||||||
placeholder="URL"
|
onChange: setUrl,
|
||||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
})}
|
||||||
setUrl(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -486,12 +483,14 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Twitter",
|
title: "Twitter",
|
||||||
value: twitter,
|
value: twitter,
|
||||||
|
url: TextUtils.sanitiseURL(twitter, TextUtils.twitterURL),
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setTwitter,
|
onChange: setTwitter,
|
||||||
})}
|
})}
|
||||||
{TableUtils.renderInputGroup({
|
{TableUtils.renderInputGroup({
|
||||||
title: "Instagram",
|
title: "Instagram",
|
||||||
value: instagram,
|
value: instagram,
|
||||||
|
url: TextUtils.sanitiseURL(instagram, TextUtils.instagramURL),
|
||||||
isEditing: !!isEditing,
|
isEditing: !!isEditing,
|
||||||
onChange: setInstagram,
|
onChange: setInstagram,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -6,10 +6,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#url-field {
|
|
||||||
line-height: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#performer-page {
|
#performer-page {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin: 10px auto;
|
margin: 10px auto;
|
||||||
|
|||||||
@@ -150,7 +150,11 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||||||
return (
|
return (
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<span className="col-4">Downloaded From</span>
|
<span className="col-4">Downloaded From</span>
|
||||||
<span className="col-8 text-truncate">{props.scene.url}</span>
|
<span className="col-8 text-truncate">
|
||||||
|
<a href={TextUtils.sanitiseURL(props.scene.url)}>
|
||||||
|
{props.scene.url}
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const SceneMarkerForm: React.FC<ISceneMarkerForm> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
|
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
|
||||||
|
mandatory={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ export const SettingsInterfacePanel: React.FC = () => {
|
|||||||
<DurationInput
|
<DurationInput
|
||||||
className="row col col-4"
|
className="row col col-4"
|
||||||
numericValue={maximumLoopDuration}
|
numericValue={maximumLoopDuration}
|
||||||
onValueChange={(duration) => setMaximumLoopDuration(duration)}
|
onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
|
||||||
/>
|
/>
|
||||||
<Form.Text className="text-muted">
|
<Form.Text className="text-muted">
|
||||||
Maximum scene duration where scene player will loop the video - 0 to
|
Maximum scene duration where scene player will loop the video - 0 to
|
||||||
|
|||||||
@@ -5,34 +5,48 @@ import { DurationUtils } from "src/utils";
|
|||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
numericValue: number;
|
numericValue: number | undefined;
|
||||||
onValueChange(valueAsNumber: number): void;
|
mandatory?: boolean;
|
||||||
|
onValueChange(valueAsNumber: number | undefined, valueAsString?: string): void;
|
||||||
onReset?(): void;
|
onReset?(): void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
||||||
const [value, setValue] = useState<string>(
|
const [value, setValue] = useState<string | undefined>(
|
||||||
DurationUtils.secondsToString(props.numericValue)
|
props.numericValue !== undefined ? DurationUtils.secondsToString(props.numericValue) : undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setValue(DurationUtils.secondsToString(props.numericValue));
|
if (props.numericValue !== undefined || props.mandatory) {
|
||||||
}, [props.numericValue]);
|
setValue(DurationUtils.secondsToString(props.numericValue ?? 0));
|
||||||
|
} else {
|
||||||
|
setValue(undefined);
|
||||||
|
}
|
||||||
|
}, [props.numericValue, props.mandatory]);
|
||||||
|
|
||||||
function increment() {
|
function increment() {
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let seconds = DurationUtils.stringToSeconds(value);
|
let seconds = DurationUtils.stringToSeconds(value);
|
||||||
seconds += 1;
|
seconds += 1;
|
||||||
props.onValueChange(seconds);
|
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
function decrement() {
|
function decrement() {
|
||||||
|
if (value === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let seconds = DurationUtils.stringToSeconds(value);
|
let seconds = DurationUtils.stringToSeconds(value);
|
||||||
seconds -= 1;
|
seconds -= 1;
|
||||||
props.onValueChange(seconds);
|
props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderButtons() {
|
function renderButtons() {
|
||||||
|
if (!props.disabled) {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup vertical>
|
<ButtonGroup vertical>
|
||||||
<Button
|
<Button
|
||||||
@@ -54,6 +68,7 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onReset() {
|
function onReset() {
|
||||||
if (props.onReset) {
|
if (props.onReset) {
|
||||||
@@ -81,10 +96,14 @@ export const DurationInput: React.FC<IProps> = (props: IProps) => {
|
|||||||
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
onChange={(e: React.FormEvent<HTMLInputElement>) =>
|
||||||
setValue(e.currentTarget.value)
|
setValue(e.currentTarget.value)
|
||||||
}
|
}
|
||||||
onBlur={() =>
|
onBlur={() => {
|
||||||
props.onValueChange(DurationUtils.stringToSeconds(value))
|
if (props.mandatory || (value !== undefined && value !== "")) {
|
||||||
|
props.onValueChange(DurationUtils.stringToSeconds(value), value);
|
||||||
|
} else {
|
||||||
|
props.onValueChange(undefined);
|
||||||
}
|
}
|
||||||
placeholder="hh:mm:ss"
|
}}
|
||||||
|
placeholder={!props.disabled ? "hh:mm:ss" : undefined}
|
||||||
/>
|
/>
|
||||||
<InputGroup.Append>
|
<InputGroup.Append>
|
||||||
{maybeRenderReset()}
|
{maybeRenderReset()}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const secondsToString = (seconds: number) => {
|
|||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
const stringToSeconds = (v: string) => {
|
const stringToSeconds = (v?: string) => {
|
||||||
if (!v) {
|
if (!v) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
206
ui/v2.5/src/utils/editabletext.tsx
Normal file
206
ui/v2.5/src/utils/editabletext.tsx
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Form } from "react-bootstrap";
|
||||||
|
import { FilterSelect, DurationInput } from "src/components/Shared";
|
||||||
|
import { DurationUtils } from ".";
|
||||||
|
|
||||||
|
const renderTextArea = (options: {
|
||||||
|
value: string | undefined;
|
||||||
|
isEditing: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
as="textarea"
|
||||||
|
readOnly={!options.isEditing}
|
||||||
|
plaintext={!options.isEditing}
|
||||||
|
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
||||||
|
options.onChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
value={options.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderEditableText = (options: {
|
||||||
|
title?: string;
|
||||||
|
value?: string | number;
|
||||||
|
isEditing: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
readOnly={!options.isEditing}
|
||||||
|
plaintext={!options.isEditing}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
options.onChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
value={
|
||||||
|
typeof options.value === "number"
|
||||||
|
? options.value.toString()
|
||||||
|
: options.value
|
||||||
|
}
|
||||||
|
placeholder={options.title}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderInputGroup = (options: {
|
||||||
|
title?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value: string | undefined;
|
||||||
|
isEditing: boolean;
|
||||||
|
url?: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
}) => {
|
||||||
|
if (options.url && !options.isEditing) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={options.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{options.value}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
readOnly={!options.isEditing}
|
||||||
|
plaintext={!options.isEditing}
|
||||||
|
value={options.value}
|
||||||
|
placeholder={options.placeholder ?? options.title}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
options.onChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderDurationInput = (options: {
|
||||||
|
value: string | undefined;
|
||||||
|
isEditing: boolean;
|
||||||
|
url?: string;
|
||||||
|
asString?: boolean
|
||||||
|
onChange: (value: string | undefined) => void;
|
||||||
|
}) => {
|
||||||
|
let numericValue: number | undefined = undefined;
|
||||||
|
if (options.value) {
|
||||||
|
if (!options.asString) {
|
||||||
|
try {
|
||||||
|
numericValue = Number.parseInt(options.value, 10);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
numericValue = DurationUtils.stringToSeconds(options.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.isEditing) {
|
||||||
|
let durationString = undefined;
|
||||||
|
if (numericValue !== undefined) {
|
||||||
|
durationString = DurationUtils.secondsToString(numericValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
readOnly={true}
|
||||||
|
plaintext={true}
|
||||||
|
defaultValue={durationString}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<DurationInput
|
||||||
|
disabled={!options.isEditing}
|
||||||
|
numericValue={numericValue}
|
||||||
|
onValueChange={(valueAsNumber: number, valueAsString? : string) => {
|
||||||
|
if (!options.asString) {
|
||||||
|
valueAsString = valueAsNumber !== undefined ? valueAsNumber.toString() : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
options.onChange(valueAsString);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderHtmlSelect = (options: {
|
||||||
|
value?: string | number;
|
||||||
|
isEditing: boolean;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
selectOptions: Array<string | number>;
|
||||||
|
}) => {
|
||||||
|
if (!options.isEditing) {
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
readOnly={true}
|
||||||
|
plaintext={true}
|
||||||
|
defaultValue={options.value}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<Form.Control
|
||||||
|
as="select"
|
||||||
|
className="input-control"
|
||||||
|
disabled={!options.isEditing}
|
||||||
|
plaintext={!options.isEditing}
|
||||||
|
value={options.value?.toString()}
|
||||||
|
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
||||||
|
options.onChange(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{options.selectOptions.map((opt) => (
|
||||||
|
<option value={opt} key={opt}>
|
||||||
|
{opt}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Form.Control>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: isediting
|
||||||
|
const renderFilterSelect = (options: {
|
||||||
|
type: "performers" | "studios" | "tags";
|
||||||
|
initialId: string | undefined;
|
||||||
|
onChange: (id: string | undefined) => void;
|
||||||
|
}) => (
|
||||||
|
<FilterSelect
|
||||||
|
type={options.type}
|
||||||
|
onSelect={(items) => options.onChange(items[0]?.id)}
|
||||||
|
initialIds={options.initialId ? [options.initialId] : []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// TODO: isediting
|
||||||
|
const renderMultiSelect = (options: {
|
||||||
|
type: "performers" | "studios" | "tags";
|
||||||
|
initialIds: string[] | undefined;
|
||||||
|
onChange: (ids: string[]) => void;
|
||||||
|
}) => (
|
||||||
|
<FilterSelect
|
||||||
|
type={options.type}
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
||||||
|
initialIds={options.initialIds ?? []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const EditableTextUtils = {
|
||||||
|
renderTextArea,
|
||||||
|
renderEditableText,
|
||||||
|
renderInputGroup,
|
||||||
|
renderDurationInput,
|
||||||
|
renderHtmlSelect,
|
||||||
|
renderFilterSelect,
|
||||||
|
renderMultiSelect,
|
||||||
|
};
|
||||||
|
export default EditableTextUtils;
|
||||||
@@ -2,6 +2,7 @@ export { default as ImageUtils } from "./image";
|
|||||||
export { default as NavUtils } from "./navigation";
|
export { default as NavUtils } from "./navigation";
|
||||||
export { default as TableUtils } from "./table";
|
export { default as TableUtils } from "./table";
|
||||||
export { default as TextUtils } from "./text";
|
export { default as TextUtils } from "./text";
|
||||||
|
export { default as EditableTextUtils } from "./editabletext";
|
||||||
export { default as DurationUtils } from "./duration";
|
export { default as DurationUtils } from "./duration";
|
||||||
export { default as JWUtils } from "./jwplayer";
|
export { default as JWUtils } from "./jwplayer";
|
||||||
export { default as SessionUtils } from "./session";
|
export { default as SessionUtils } from "./session";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Form } from "react-bootstrap";
|
import EditableTextUtils from "./editabletext";
|
||||||
import { FilterSelect } from "src/components/Shared";
|
|
||||||
|
|
||||||
const renderEditableTextTableRow = (options: {
|
const renderEditableTextTableRow = (options: {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -11,19 +10,7 @@ const renderEditableTextTableRow = (options: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Control
|
{EditableTextUtils.renderEditableText(options)}
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={
|
|
||||||
typeof options.value === "number"
|
|
||||||
? options.value.toString()
|
|
||||||
: options.value
|
|
||||||
}
|
|
||||||
placeholder={options.title}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -37,16 +24,7 @@ const renderTextArea = (options: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Control
|
{EditableTextUtils.renderTextArea(options)}
|
||||||
className="text-input"
|
|
||||||
as="textarea"
|
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
onChange={(event: React.FormEvent<HTMLTextAreaElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={options.value}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -56,27 +34,35 @@ const renderInputGroup = (options: {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
asURL?: boolean;
|
url?: string;
|
||||||
urlPrefix?: string;
|
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Control
|
{EditableTextUtils.renderInputGroup(options)}
|
||||||
className="text-input"
|
|
||||||
readOnly={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
defaultValue={options.value}
|
|
||||||
placeholder={options.placeholder ?? options.title}
|
|
||||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const renderDurationInput = (options: {
|
||||||
|
title: string;
|
||||||
|
placeholder?: string;
|
||||||
|
value: string | undefined;
|
||||||
|
isEditing: boolean;
|
||||||
|
asString?: boolean;
|
||||||
|
onChange: (value: string | undefined) => void;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>{options.title}</td>
|
||||||
|
<td>
|
||||||
|
{EditableTextUtils.renderDurationInput(options)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderHtmlSelect = (options: {
|
const renderHtmlSelect = (options: {
|
||||||
title: string;
|
title: string;
|
||||||
value?: string | number;
|
value?: string | number;
|
||||||
@@ -87,22 +73,7 @@ const renderHtmlSelect = (options: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<Form.Control
|
{EditableTextUtils.renderHtmlSelect(options)}
|
||||||
as="select"
|
|
||||||
className="input-control"
|
|
||||||
disabled={!options.isEditing}
|
|
||||||
plaintext={!options.isEditing}
|
|
||||||
value={options.value?.toString()}
|
|
||||||
onChange={(event: React.FormEvent<HTMLSelectElement>) =>
|
|
||||||
options.onChange(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{options.selectOptions.map((opt) => (
|
|
||||||
<option value={opt} key={opt}>
|
|
||||||
{opt}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Form.Control>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -117,11 +88,7 @@ const renderFilterSelect = (options: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<FilterSelect
|
{EditableTextUtils.renderFilterSelect(options)}
|
||||||
type={options.type}
|
|
||||||
onSelect={(items) => options.onChange(items[0]?.id)}
|
|
||||||
initialIds={options.initialId ? [options.initialId] : []}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -136,12 +103,7 @@ const renderMultiSelect = (options: {
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{options.title}</td>
|
<td>{options.title}</td>
|
||||||
<td>
|
<td>
|
||||||
<FilterSelect
|
{EditableTextUtils.renderMultiSelect(options)}
|
||||||
type={options.type}
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) => options.onChange(items.map((i) => i.id))}
|
|
||||||
initialIds={options.initialIds ?? []}
|
|
||||||
/>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
@@ -150,6 +112,7 @@ const Table = {
|
|||||||
renderEditableTextTableRow,
|
renderEditableTextTableRow,
|
||||||
renderTextArea,
|
renderTextArea,
|
||||||
renderInputGroup,
|
renderInputGroup,
|
||||||
|
renderDurationInput,
|
||||||
renderHtmlSelect,
|
renderHtmlSelect,
|
||||||
renderFilterSelect,
|
renderFilterSelect,
|
||||||
renderMultiSelect,
|
renderMultiSelect,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const fileSize = (bytes: number = 0, precision: number = 2) => {
|
|||||||
unit++;
|
unit++;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `${bytes.toFixed(+precision)} ${Units[unit]}`;
|
return `${count.toFixed(+precision)} ${Units[unit]}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const secondsToTimestamp = (seconds: number) => {
|
const secondsToTimestamp = (seconds: number) => {
|
||||||
@@ -83,6 +83,33 @@ const resolution = (height: number) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const twitterURL = new URL("https://www.twitter.com");
|
||||||
|
const instagramURL = new URL("https://www.instagram.com");
|
||||||
|
|
||||||
|
const sanitiseURL = (url?: string, siteURL?: URL) => {
|
||||||
|
if (!url) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||||
|
// just return the entire URL
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (siteURL) {
|
||||||
|
// if url starts with the site host, then prepend the protocol
|
||||||
|
if (url.startsWith(siteURL.host)) {
|
||||||
|
return siteURL.protocol + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, construct the url from the protocol, host and passed url
|
||||||
|
return siteURL.protocol + siteURL.host + "/" + url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// just prepend the protocol - assume https
|
||||||
|
return "https://" + url;
|
||||||
|
}
|
||||||
|
|
||||||
const TextUtils = {
|
const TextUtils = {
|
||||||
truncate,
|
truncate,
|
||||||
fileSize,
|
fileSize,
|
||||||
@@ -91,6 +118,9 @@ const TextUtils = {
|
|||||||
age: getAge,
|
age: getAge,
|
||||||
bitRate,
|
bitRate,
|
||||||
resolution,
|
resolution,
|
||||||
|
sanitiseURL,
|
||||||
|
twitterURL,
|
||||||
|
instagramURL,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default TextUtils;
|
export default TextUtils;
|
||||||
|
|||||||
Reference in New Issue
Block a user