-
-
-
{performer.name}
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
index 1ab045cb5..b9dda1de6 100644
--- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
+++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react/no-this-in-sfc */
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 { StashService } from "src/core/StashService";
import {
@@ -11,7 +11,7 @@ import {
ScrapePerformerSuggest,
LoadingIndicator,
} from "src/components/Shared";
-import { ImageUtils, TableUtils } from "src/utils";
+import { ImageUtils, TableUtils, TextUtils, EditableTextUtils } from "src/utils";
import { useToast } from "src/hooks";
interface IPerformerDetails {
@@ -323,16 +323,13 @@ export const PerformerDetailsPanel: React.FC = ({
{maybeRenderScrapeButton()}
|
- ) =>
- setUrl(event.currentTarget.value)
- }
- />
+ {EditableTextUtils.renderInputGroup({
+ title: "URL",
+ value: url,
+ url: TextUtils.sanitiseURL(url),
+ isEditing: !!isEditing,
+ onChange: setUrl,
+ })}
|
);
@@ -486,12 +483,14 @@ export const PerformerDetailsPanel: React.FC = ({
{TableUtils.renderInputGroup({
title: "Twitter",
value: twitter,
+ url: TextUtils.sanitiseURL(twitter, TextUtils.twitterURL),
isEditing: !!isEditing,
onChange: setTwitter,
})}
{TableUtils.renderInputGroup({
title: "Instagram",
value: instagram,
+ url: TextUtils.sanitiseURL(instagram, TextUtils.instagramURL),
isEditing: !!isEditing,
onChange: setInstagram,
})}
diff --git a/ui/v2.5/src/components/Performers/styles.scss b/ui/v2.5/src/components/Performers/styles.scss
index 04c212cd5..7575b926a 100644
--- a/ui/v2.5/src/components/Performers/styles.scss
+++ b/ui/v2.5/src/components/Performers/styles.scss
@@ -6,10 +6,6 @@
}
}
-#url-field {
- line-height: 30px;
-}
-
#performer-page {
flex-direction: row;
margin: 10px auto;
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
index 881030fac..c10310d2c 100644
--- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
@@ -150,7 +150,11 @@ export const SceneFileInfoPanel: React.FC = (
return (
);
}
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx
index c9a8082dc..7833cc466 100644
--- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMarkerForm.tsx
@@ -84,6 +84,7 @@ export const SceneMarkerForm: React.FC = ({
)
}
numericValue={Number.parseInt(fieldProps.field.value ?? "0", 10)}
+ mandatory={true}
/>
);
diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx
index d6d5d67d0..9def1e190 100644
--- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx
+++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel.tsx
@@ -119,7 +119,7 @@ export const SettingsInterfacePanel: React.FC = () => {
setMaximumLoopDuration(duration)}
+ onValueChange={(duration) => setMaximumLoopDuration(duration ?? 0)}
/>
Maximum scene duration where scene player will loop the video - 0 to
diff --git a/ui/v2.5/src/components/Shared/DurationInput.tsx b/ui/v2.5/src/components/Shared/DurationInput.tsx
index bc3d37214..422e08ce4 100644
--- a/ui/v2.5/src/components/Shared/DurationInput.tsx
+++ b/ui/v2.5/src/components/Shared/DurationInput.tsx
@@ -5,54 +5,69 @@ import { DurationUtils } from "src/utils";
interface IProps {
disabled?: boolean;
- numericValue: number;
- onValueChange(valueAsNumber: number): void;
+ numericValue: number | undefined;
+ mandatory?: boolean;
+ onValueChange(valueAsNumber: number | undefined, valueAsString?: string): void;
onReset?(): void;
className?: string;
}
export const DurationInput: React.FC = (props: IProps) => {
- const [value, setValue] = useState(
- DurationUtils.secondsToString(props.numericValue)
+ const [value, setValue] = useState(
+ props.numericValue !== undefined ? DurationUtils.secondsToString(props.numericValue) : undefined
);
useEffect(() => {
- setValue(DurationUtils.secondsToString(props.numericValue));
- }, [props.numericValue]);
+ if (props.numericValue !== undefined || props.mandatory) {
+ setValue(DurationUtils.secondsToString(props.numericValue ?? 0));
+ } else {
+ setValue(undefined);
+ }
+ }, [props.numericValue, props.mandatory]);
function increment() {
+ if (value === undefined) {
+ return;
+ }
+
let seconds = DurationUtils.stringToSeconds(value);
seconds += 1;
- props.onValueChange(seconds);
+ props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
}
function decrement() {
+ if (value === undefined) {
+ return;
+ }
+
let seconds = DurationUtils.stringToSeconds(value);
seconds -= 1;
- props.onValueChange(seconds);
+ props.onValueChange(seconds, DurationUtils.secondsToString(seconds));
}
function renderButtons() {
- return (
-
-
-
-
- );
+ if (!props.disabled) {
+ return (
+
+
+
+
+ );
+ }
}
function onReset() {
@@ -81,10 +96,14 @@ export const DurationInput: React.FC = (props: IProps) => {
onChange={(e: React.FormEvent) =>
setValue(e.currentTarget.value)
}
- onBlur={() =>
- props.onValueChange(DurationUtils.stringToSeconds(value))
- }
- placeholder="hh:mm:ss"
+ onBlur={() => {
+ if (props.mandatory || (value !== undefined && value !== "")) {
+ props.onValueChange(DurationUtils.stringToSeconds(value), value);
+ } else {
+ props.onValueChange(undefined);
+ }
+ }}
+ placeholder={!props.disabled ? "hh:mm:ss" : undefined}
/>
{maybeRenderReset()}
diff --git a/ui/v2.5/src/utils/duration.ts b/ui/v2.5/src/utils/duration.ts
index a2daf4826..ca81863d6 100644
--- a/ui/v2.5/src/utils/duration.ts
+++ b/ui/v2.5/src/utils/duration.ts
@@ -14,7 +14,7 @@ const secondsToString = (seconds: number) => {
return ret;
};
-const stringToSeconds = (v: string) => {
+const stringToSeconds = (v?: string) => {
if (!v) {
return 0;
}
diff --git a/ui/v2.5/src/utils/editabletext.tsx b/ui/v2.5/src/utils/editabletext.tsx
new file mode 100644
index 000000000..27d5e6aa3
--- /dev/null
+++ b/ui/v2.5/src/utils/editabletext.tsx
@@ -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 (
+ ) =>
+ options.onChange(event.currentTarget.value)
+ }
+ value={options.value}
+ />
+ );
+}
+
+const renderEditableText = (options: {
+ title?: string;
+ value?: string | number;
+ isEditing: boolean;
+ onChange: (value: string) => void;
+}) => {
+ return (
+ ) =>
+ 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 (
+
+ {options.value}
+
+ );
+ } else {
+ return (
+ ) =>
+ 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 (
+
+ );
+ } else {
+ return (
+ {
+ 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;
+}) => {
+ if (!options.isEditing) {
+ return (
+
+ );
+ } else {
+ return (
+ ) =>
+ options.onChange(event.currentTarget.value)
+ }
+ >
+ {options.selectOptions.map((opt) => (
+
+ ))}
+
+ );
+ }
+}
+
+// TODO: isediting
+const renderFilterSelect = (options: {
+ type: "performers" | "studios" | "tags";
+ initialId: string | undefined;
+ onChange: (id: string | undefined) => void;
+}) => (
+ 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;
+}) => (
+ options.onChange(items.map((i) => i.id))}
+ initialIds={options.initialIds ?? []}
+ />
+);
+
+const EditableTextUtils = {
+ renderTextArea,
+ renderEditableText,
+ renderInputGroup,
+ renderDurationInput,
+ renderHtmlSelect,
+ renderFilterSelect,
+ renderMultiSelect,
+};
+export default EditableTextUtils;
\ No newline at end of file
diff --git a/ui/v2.5/src/utils/index.ts b/ui/v2.5/src/utils/index.ts
index 45379953d..5fc853d12 100644
--- a/ui/v2.5/src/utils/index.ts
+++ b/ui/v2.5/src/utils/index.ts
@@ -2,6 +2,7 @@ export { default as ImageUtils } from "./image";
export { default as NavUtils } from "./navigation";
export { default as TableUtils } from "./table";
export { default as TextUtils } from "./text";
+export { default as EditableTextUtils } from "./editabletext";
export { default as DurationUtils } from "./duration";
export { default as JWUtils } from "./jwplayer";
export { default as SessionUtils } from "./session";
diff --git a/ui/v2.5/src/utils/table.tsx b/ui/v2.5/src/utils/table.tsx
index 201c3d178..5e3347647 100644
--- a/ui/v2.5/src/utils/table.tsx
+++ b/ui/v2.5/src/utils/table.tsx
@@ -1,6 +1,5 @@
import React from "react";
-import { Form } from "react-bootstrap";
-import { FilterSelect } from "src/components/Shared";
+import EditableTextUtils from "./editabletext";
const renderEditableTextTableRow = (options: {
title: string;
@@ -11,19 +10,7 @@ const renderEditableTextTableRow = (options: {
| {options.title} |
- ) =>
- options.onChange(event.currentTarget.value)
- }
- value={
- typeof options.value === "number"
- ? options.value.toString()
- : options.value
- }
- placeholder={options.title}
- />
+ {EditableTextUtils.renderEditableText(options)}
|
);
@@ -37,16 +24,7 @@ const renderTextArea = (options: {
| {options.title} |
- ) =>
- options.onChange(event.currentTarget.value)
- }
- value={options.value}
- />
+ {EditableTextUtils.renderTextArea(options)}
|
);
@@ -56,27 +34,35 @@ const renderInputGroup = (options: {
placeholder?: string;
value: string | undefined;
isEditing: boolean;
- asURL?: boolean;
- urlPrefix?: string;
+ url?: string;
onChange: (value: string) => void;
}) => (
| {options.title} |
- ) =>
- options.onChange(event.currentTarget.value)
- }
- />
+ {EditableTextUtils.renderInputGroup(options)}
|
);
+const renderDurationInput = (options: {
+ title: string;
+ placeholder?: string;
+ value: string | undefined;
+ isEditing: boolean;
+ asString?: boolean;
+ onChange: (value: string | undefined) => void;
+}) => {
+ return (
+
+ | {options.title} |
+
+ {EditableTextUtils.renderDurationInput(options)}
+ |
+
+ );
+};
+
const renderHtmlSelect = (options: {
title: string;
value?: string | number;
@@ -87,22 +73,7 @@ const renderHtmlSelect = (options: {
| {options.title} |
- ) =>
- options.onChange(event.currentTarget.value)
- }
- >
- {options.selectOptions.map((opt) => (
-
- ))}
-
+ {EditableTextUtils.renderHtmlSelect(options)}
|
);
@@ -117,11 +88,7 @@ const renderFilterSelect = (options: {
| {options.title} |
- options.onChange(items[0]?.id)}
- initialIds={options.initialId ? [options.initialId] : []}
- />
+ {EditableTextUtils.renderFilterSelect(options)}
|
);
@@ -136,12 +103,7 @@ const renderMultiSelect = (options: {
| {options.title} |
- options.onChange(items.map((i) => i.id))}
- initialIds={options.initialIds ?? []}
- />
+ {EditableTextUtils.renderMultiSelect(options)}
|
);
@@ -150,6 +112,7 @@ const Table = {
renderEditableTextTableRow,
renderTextArea,
renderInputGroup,
+ renderDurationInput,
renderHtmlSelect,
renderFilterSelect,
renderMultiSelect,
diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts
index 3e8bfed7c..b897c25f1 100644
--- a/ui/v2.5/src/utils/text.ts
+++ b/ui/v2.5/src/utils/text.ts
@@ -20,7 +20,7 @@ const fileSize = (bytes: number = 0, precision: number = 2) => {
unit++;
}
- return `${bytes.toFixed(+precision)} ${Units[unit]}`;
+ return `${count.toFixed(+precision)} ${Units[unit]}`;
};
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 = {
truncate,
fileSize,
@@ -91,6 +118,9 @@ const TextUtils = {
age: getAge,
bitRate,
resolution,
+ sanitiseURL,
+ twitterURL,
+ instagramURL,
};
export default TextUtils;