mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Link improvements and fixes (#4501)
* Add ExternalLink * Replace <a> with <Link>
This commit is contained in:
@@ -9,6 +9,7 @@ import { ModalComponent } from "src/components/Shared/Modal";
|
|||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
import { faPaperPlane } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: "scene" | "performer";
|
type: "scene" | "performer";
|
||||||
@@ -108,12 +109,12 @@ export const SubmitStashBoxDraft: React.FC<IProps> = ({
|
|||||||
<FormattedMessage id="stashbox.submission_successful" />
|
<FormattedMessage id="stashbox.submission_successful" />
|
||||||
</h6>
|
</h6>
|
||||||
<div>
|
<div>
|
||||||
<a target="_blank" rel="noreferrer noopener" href={reviewUrl}>
|
<ExternalLink href={reviewUrl}>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="stashbox.go_review_draft"
|
id="stashbox.go_review_draft"
|
||||||
values={{ endpoint_name: selectedBox?.name }}
|
values={{ endpoint_name: selectedBox?.name }}
|
||||||
/>
|
/>
|
||||||
</a>
|
</ExternalLink>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
|
|||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
movie: GQL.MovieDataFragment;
|
movie: GQL.MovieDataFragment;
|
||||||
@@ -274,15 +275,13 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
const renderClickableIcons = () => (
|
const renderClickableIcons = () => (
|
||||||
<span className="name-icons">
|
<span className="name-icons">
|
||||||
{movie.url && (
|
{movie.url && (
|
||||||
<Button className="minimal icon-link" title={movie.url}>
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(movie.url)}
|
href={TextUtils.sanitiseURL(movie.url)}
|
||||||
className="link"
|
className="minimal link"
|
||||||
target="_blank"
|
title={movie.url}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faLink} />
|
<Icon icon={faLink} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useIntl } from "react-intl";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import TextUtils from "src/utils/text";
|
import TextUtils from "src/utils/text";
|
||||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
interface IMovieDetailsPanel {
|
interface IMovieDetailsPanel {
|
||||||
movie: GQL.MovieDataFragment;
|
movie: GQL.MovieDataFragment;
|
||||||
@@ -34,9 +35,9 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({
|
|||||||
id="studio"
|
id="studio"
|
||||||
value={
|
value={
|
||||||
movie.studio?.id ? (
|
movie.studio?.id ? (
|
||||||
<a href={`/studios/${movie.studio?.id}`} target="_self">
|
<Link to={`/studios/${movie.studio?.id}`}>
|
||||||
{movie.studio?.name}
|
{movie.studio?.name}
|
||||||
</a>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
|||||||
import { DetailImage } from "src/components/Shared/DetailImage";
|
import { DetailImage } from "src/components/Shared/DetailImage";
|
||||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
performer: GQL.PerformerDataFragment;
|
performer: GQL.PerformerDataFragment;
|
||||||
@@ -493,57 +494,50 @@ const PerformerPage: React.FC<IProps> = ({ performer, tabKey }) => {
|
|||||||
<Icon icon={faHeart} />
|
<Icon icon={faHeart} />
|
||||||
</Button>
|
</Button>
|
||||||
{performer.url && (
|
{performer.url && (
|
||||||
<Button className="minimal icon-link" title={performer.url}>
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(performer.url)}
|
href={TextUtils.sanitiseURL(performer.url)}
|
||||||
className="link"
|
className="minimal link"
|
||||||
target="_blank"
|
title={performer.url}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faLink} />
|
<Icon icon={faLink} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{(urls ?? []).map((url, index) => (
|
{(urls ?? []).map((url, index) => (
|
||||||
<Button key={index} className="minimal icon-link" title={url}>
|
<Button
|
||||||
<a
|
key={index}
|
||||||
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(url)}
|
href={TextUtils.sanitiseURL(url)}
|
||||||
className={`detail-link ${index}`}
|
className={`minimal link detail-link detail-link-${index}`}
|
||||||
target="_blank"
|
title={url}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faLink} />
|
<Icon icon={faLink} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
))}
|
))}
|
||||||
{performer.twitter && (
|
{performer.twitter && (
|
||||||
<Button className="minimal icon-link" title={performer.twitter}>
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(
|
href={TextUtils.sanitiseURL(
|
||||||
performer.twitter,
|
performer.twitter,
|
||||||
TextUtils.twitterURL
|
TextUtils.twitterURL
|
||||||
)}
|
)}
|
||||||
className="twitter"
|
className="minimal link twitter"
|
||||||
target="_blank"
|
title={performer.twitter}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faTwitter} />
|
<Icon icon={faTwitter} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{performer.instagram && (
|
{performer.instagram && (
|
||||||
<Button className="minimal icon-link" title={performer.instagram}>
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(
|
href={TextUtils.sanitiseURL(
|
||||||
performer.instagram,
|
performer.instagram,
|
||||||
TextUtils.instagramURL
|
TextUtils.instagramURL
|
||||||
)}
|
)}
|
||||||
className="instagram"
|
className="minimal link instagram"
|
||||||
target="_blank"
|
title={performer.instagram}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faInstagram} />
|
<Icon icon={faInstagram} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
Option as SelectOption,
|
Option as SelectOption,
|
||||||
} from "../Shared/FilterSelect";
|
} from "../Shared/FilterSelect";
|
||||||
import { useCompare } from "src/hooks/state";
|
import { useCompare } from "src/hooks/state";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export type SelectObject = {
|
export type SelectObject = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -86,10 +87,9 @@ export const PerformerSelect: React.FC<
|
|||||||
...optionProps,
|
...optionProps,
|
||||||
children: (
|
children: (
|
||||||
<span className="react-select-image-option">
|
<span className="react-select-image-option">
|
||||||
<a
|
<Link
|
||||||
href={`/performers/${object.id}`}
|
to={`/performers/${object.id}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
|
||||||
className="performer-select-image-link"
|
className="performer-select-image-link"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -97,7 +97,7 @@ export const PerformerSelect: React.FC<
|
|||||||
src={object.image_path ?? ""}
|
src={object.image_path ?? ""}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</a>
|
</Link>
|
||||||
<span>{name}</span>
|
<span>{name}</span>
|
||||||
{object.disambiguation && (
|
{object.disambiguation && (
|
||||||
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
|
<span className="performer-disambiguation">{` (${object.disambiguation})`}</span>
|
||||||
|
|||||||
@@ -27,22 +27,9 @@
|
|||||||
color: #ff7373;
|
color: #ff7373;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link {
|
|
||||||
color: rgb(191, 204, 214);
|
|
||||||
}
|
|
||||||
|
|
||||||
.instagram {
|
.instagram {
|
||||||
color: pink;
|
color: pink;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-link {
|
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
a {
|
|
||||||
display: inline-block;
|
|
||||||
padding: $btn-padding-y $btn-padding-x;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.rating-number .form-control {
|
.rating-number .form-control {
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
|
|||||||
url={NavUtils.makeScenesPHashMatchUrl(phash?.value)}
|
url={NavUtils.makeScenesPHashMatchUrl(phash?.value)}
|
||||||
target="_self"
|
target="_self"
|
||||||
truncate
|
truncate
|
||||||
trusted
|
internal
|
||||||
/>
|
/>
|
||||||
<URLField
|
<URLField
|
||||||
id="path"
|
id="path"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import { Button } from "react-bootstrap";
|
import { Button } from "react-bootstrap";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { useLatestVersion } from "src/core/StashService";
|
import { useLatestVersion } from "src/core/StashService";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
import { ConstantSetting, SettingGroup } from "./Inputs";
|
import { ConstantSetting, SettingGroup } from "./Inputs";
|
||||||
import { SettingSection } from "./SettingSection";
|
import { SettingSection } from "./SettingSection";
|
||||||
|
|
||||||
@@ -115,13 +116,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
{ id: "config.about.stash_home" },
|
{ id: "config.about.stash_home" },
|
||||||
{
|
{
|
||||||
url: (
|
url: (
|
||||||
<a
|
<ExternalLink href="https://github.com/stashapp/stash">
|
||||||
href="https://github.com/stashapp/stash"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
GitHub
|
GitHub
|
||||||
</a>
|
</ExternalLink>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -131,13 +128,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
{ id: "config.about.stash_wiki" },
|
{ id: "config.about.stash_wiki" },
|
||||||
{
|
{
|
||||||
url: (
|
url: (
|
||||||
<a
|
<ExternalLink href="https://docs.stashapp.cc">
|
||||||
href="https://docs.stashapp.cc"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Documentation
|
Documentation
|
||||||
</a>
|
</ExternalLink>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -147,13 +140,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
{ id: "config.about.stash_discord" },
|
{ id: "config.about.stash_discord" },
|
||||||
{
|
{
|
||||||
url: (
|
url: (
|
||||||
<a
|
<ExternalLink href="https://discord.gg/2TsNFKt">
|
||||||
href="https://discord.gg/2TsNFKt"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Discord
|
Discord
|
||||||
</a>
|
</ExternalLink>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
@@ -163,13 +152,9 @@ export const SettingsAboutPanel: React.FC = () => {
|
|||||||
{ id: "config.about.stash_open_collective" },
|
{ id: "config.about.stash_open_collective" },
|
||||||
{
|
{
|
||||||
url: (
|
url: (
|
||||||
<a
|
<ExternalLink href="https://opencollective.com/stashapp">
|
||||||
href="https://opencollective.com/stashapp"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
Open Collective
|
Open Collective
|
||||||
</a>
|
</ExternalLink>
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { BooleanSetting, StringListSetting, StringSetting } from "./Inputs";
|
|||||||
import { useSettings } from "./context";
|
import { useSettings } from "./context";
|
||||||
import { useIntl } from "react-intl";
|
import { useIntl } from "react-intl";
|
||||||
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
export const SettingsLibraryPanel: React.FC = () => {
|
export const SettingsLibraryPanel: React.FC = () => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
@@ -76,13 +77,9 @@ export const SettingsLibraryPanel: React.FC = () => {
|
|||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "config.general.excluded_video_patterns_desc",
|
id: "config.general.excluded_video_patterns_desc",
|
||||||
})}
|
})}
|
||||||
<a
|
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
|
||||||
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Icon icon={faQuestionCircle} />
|
<Icon icon={faQuestionCircle} />
|
||||||
</a>
|
</ExternalLink>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
value={general.excludes ?? undefined}
|
value={general.excludes ?? undefined}
|
||||||
@@ -98,13 +95,9 @@ export const SettingsLibraryPanel: React.FC = () => {
|
|||||||
{intl.formatMessage({
|
{intl.formatMessage({
|
||||||
id: "config.general.excluded_image_gallery_patterns_desc",
|
id: "config.general.excluded_image_gallery_patterns_desc",
|
||||||
})}
|
})}
|
||||||
<a
|
<ExternalLink href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration">
|
||||||
href="https://docs.stashapp.cc/beginner-guides/exclude-file-configuration"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
<Icon icon={faQuestionCircle} />
|
<Icon icon={faQuestionCircle} />
|
||||||
</a>
|
</ExternalLink>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
value={general.imageExcludes ?? undefined}
|
value={general.imageExcludes ?? undefined}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
AvailablePluginPackages,
|
AvailablePluginPackages,
|
||||||
InstalledPluginPackages,
|
InstalledPluginPackages,
|
||||||
} from "./PluginPackageManager";
|
} from "./PluginPackageManager";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
interface IPluginSettingProps {
|
interface IPluginSettingProps {
|
||||||
pluginID: string;
|
pluginID: string;
|
||||||
@@ -97,15 +98,12 @@ export const SettingsPluginsPanel: React.FC = () => {
|
|||||||
function renderLink(url?: string) {
|
function renderLink(url?: string) {
|
||||||
if (url) {
|
if (url) {
|
||||||
return (
|
return (
|
||||||
<Button className="minimal">
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(url)}
|
href={TextUtils.sanitiseURL(url)}
|
||||||
className="link"
|
className="minimal link"
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faLink} />
|
<Icon icon={faLink} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
AvailableScraperPackages,
|
AvailableScraperPackages,
|
||||||
InstalledScraperPackages,
|
InstalledScraperPackages,
|
||||||
} from "./ScraperPackageManager";
|
} from "./ScraperPackageManager";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
interface IURLList {
|
interface IURLList {
|
||||||
urls: string[];
|
urls: string[];
|
||||||
@@ -42,16 +43,7 @@ const URLList: React.FC<IURLList> = ({ urls }) => {
|
|||||||
const sanitised = TextUtils.sanitiseURL(url);
|
const sanitised = TextUtils.sanitiseURL(url);
|
||||||
const siteURL = linkSite(sanitised!);
|
const siteURL = linkSite(sanitised!);
|
||||||
|
|
||||||
return (
|
return <ExternalLink href={siteURL}>{sanitised}</ExternalLink>;
|
||||||
<a
|
|
||||||
href={siteURL}
|
|
||||||
className="link"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{sanitised}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@
|
|||||||
min-width: 100px;
|
min-width: 100px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
|
||||||
button {
|
.btn {
|
||||||
margin: 0.25rem;
|
margin: 0.25rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useHistory } from "react-router-dom";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useSystemStatus, mutateMigrate } from "src/core/StashService";
|
import { useSystemStatus, mutateMigrate } from "src/core/StashService";
|
||||||
import { migrationNotes } from "src/docs/en/MigrationNotes";
|
import { migrationNotes } from "src/docs/en/MigrationNotes";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
import { LoadingIndicator } from "../Shared/LoadingIndicator";
|
||||||
import { MarkdownPage } from "../Shared/MarkdownPage";
|
import { MarkdownPage } from "../Shared/MarkdownPage";
|
||||||
|
|
||||||
@@ -35,18 +36,12 @@ export const Migrate: React.FC = () => {
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
const discordLink = (
|
const discordLink = (
|
||||||
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
|
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
const githubLink = (
|
const githubLink = (
|
||||||
<a
|
<ExternalLink href="https://github.com/stashapp/stash/issues">
|
||||||
href="https://github.com/stashapp/stash/issues"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<FormattedMessage id="setup.github_repository" />
|
<FormattedMessage id="setup.github_repository" />
|
||||||
</a>
|
</ExternalLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
faQuestionCircle,
|
faQuestionCircle,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { releaseNotes } from "src/docs/en/ReleaseNotes";
|
import { releaseNotes } from "src/docs/en/ReleaseNotes";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
export const Setup: React.FC = () => {
|
export const Setup: React.FC = () => {
|
||||||
const { configuration, loading: configLoading } =
|
const { configuration, loading: configLoading } =
|
||||||
@@ -103,18 +104,12 @@ export const Setup: React.FC = () => {
|
|||||||
}, [configuration]);
|
}, [configuration]);
|
||||||
|
|
||||||
const discordLink = (
|
const discordLink = (
|
||||||
<a href="https://discord.gg/2TsNFKt" target="_blank" rel="noreferrer">
|
<ExternalLink href="https://discord.gg/2TsNFKt">Discord</ExternalLink>
|
||||||
Discord
|
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
const githubLink = (
|
const githubLink = (
|
||||||
<a
|
<ExternalLink href="https://github.com/stashapp/stash/issues">
|
||||||
href="https://github.com/stashapp/stash/issues"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
<FormattedMessage id="setup.github_repository" />
|
<FormattedMessage id="setup.github_repository" />
|
||||||
</a>
|
</ExternalLink>
|
||||||
);
|
);
|
||||||
|
|
||||||
function onConfigLocationChosen(inWorkDir: boolean) {
|
function onConfigLocationChosen(inWorkDir: boolean) {
|
||||||
@@ -825,14 +820,9 @@ export const Setup: React.FC = () => {
|
|||||||
id="setup.success.open_collective"
|
id="setup.success.open_collective"
|
||||||
values={{
|
values={{
|
||||||
open_collective_link: (
|
open_collective_link: (
|
||||||
<a
|
<ExternalLink href="https://opencollective.com/stashapp">
|
||||||
href="https://opencollective.com/stashapp"
|
Open Collective
|
||||||
target="_blank"
|
</ExternalLink>
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{" "}
|
|
||||||
OpenCollective{" "}
|
|
||||||
</a>
|
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
5
ui/v2.5/src/components/Shared/ExternalLink.tsx
Normal file
5
ui/v2.5/src/components/Shared/ExternalLink.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
type IExternalLinkProps = JSX.IntrinsicElements["a"];
|
||||||
|
|
||||||
|
export const ExternalLink: React.FC<IExternalLinkProps> = (props) => {
|
||||||
|
return <a target="_blank" rel="noopener noreferrer" {...props} />;
|
||||||
|
};
|
||||||
@@ -2,6 +2,7 @@ import React, { useMemo } from "react";
|
|||||||
import { StashId } from "src/core/generated-graphql";
|
import { StashId } from "src/core/generated-graphql";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
|
import { ExternalLink } from "./ExternalLink";
|
||||||
|
|
||||||
export type LinkType = "performers" | "scenes" | "studios";
|
export type LinkType = "performers" | "scenes" | "studios";
|
||||||
|
|
||||||
@@ -26,9 +27,7 @@ export const StashIDPill: React.FC<{
|
|||||||
return (
|
return (
|
||||||
<span className="stash-id-pill" data-endpoint={endpointName}>
|
<span className="stash-id-pill" data-endpoint={endpointName}>
|
||||||
<span>{endpointName}</span>
|
<span>{endpointName}</span>
|
||||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
<ExternalLink href={link}>{stash_id}</ExternalLink>
|
||||||
{stash_id}
|
|
||||||
</a>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import { DetailImage } from "src/components/Shared/DetailImage";
|
|||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
import { useLoadStickyHeader } from "src/hooks/detailsPanel";
|
||||||
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
import { useScrollToTopOnMount } from "src/hooks/scrollToTop";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
@@ -285,15 +286,13 @@ const StudioPage: React.FC<IProps> = ({ studio, tabKey }) => {
|
|||||||
const renderClickableIcons = () => (
|
const renderClickableIcons = () => (
|
||||||
<span className="name-icons">
|
<span className="name-icons">
|
||||||
{studio.url && (
|
{studio.url && (
|
||||||
<Button className="minimal icon-link" title={studio.url}>
|
<Button
|
||||||
<a
|
as={ExternalLink}
|
||||||
href={TextUtils.sanitiseURL(studio.url)}
|
href={TextUtils.sanitiseURL(studio.url)}
|
||||||
className="link"
|
className="minimal link"
|
||||||
target="_blank"
|
title={studio.url}
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
<Icon icon={faLink} />
|
<Icon icon={faLink} />
|
||||||
</a>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from "react";
|
|||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { DetailItem } from "src/components/Shared/DetailItem";
|
import { DetailItem } from "src/components/Shared/DetailItem";
|
||||||
import { StashIDPill } from "src/components/Shared/StashID";
|
import { StashIDPill } from "src/components/Shared/StashID";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
interface IStudioDetailsPanel {
|
interface IStudioDetailsPanel {
|
||||||
studio: GQL.StudioDataFragment;
|
studio: GQL.StudioDataFragment;
|
||||||
@@ -51,9 +52,9 @@ export const StudioDetailsPanel: React.FC<IStudioDetailsPanel> = ({
|
|||||||
id="parent_studios"
|
id="parent_studios"
|
||||||
value={
|
value={
|
||||||
studio.parent_studio?.name ? (
|
studio.parent_studio?.name ? (
|
||||||
<a href={`/studios/${studio.parent_studio?.id}`} target="_self">
|
<Link to={`/studios/${studio.parent_studio?.id}`}>
|
||||||
{studio.parent_studio.name}
|
{studio.parent_studio.name}
|
||||||
</a>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
""
|
""
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
faExternalLinkAlt,
|
faExternalLinkAlt,
|
||||||
faTimes,
|
faTimes,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ExternalLink } from "../Shared/ExternalLink";
|
||||||
|
|
||||||
interface IPerformerModalProps {
|
interface IPerformerModalProps {
|
||||||
performer: GQL.ScrapedScenePerformerDataFragment;
|
performer: GQL.ScrapedScenePerformerDataFragment;
|
||||||
@@ -82,12 +83,14 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
[name]: !excluded[name],
|
[name]: !excluded[name],
|
||||||
});
|
});
|
||||||
|
|
||||||
const renderField = (
|
function maybeRenderField(
|
||||||
name: string,
|
name: string,
|
||||||
text: string | null | undefined,
|
text: string | null | undefined,
|
||||||
truncate: boolean = true
|
truncate: boolean = true
|
||||||
) =>
|
) {
|
||||||
text && (
|
if (!text) return;
|
||||||
|
|
||||||
|
return (
|
||||||
<div className="row no-gutters">
|
<div className="row no-gutters">
|
||||||
<div className="col-5 performer-create-modal-field" key={name}>
|
<div className="col-5 performer-create-modal-field" key={name}>
|
||||||
{!create && (
|
{!create && (
|
||||||
@@ -112,11 +115,72 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderImage() {
|
||||||
|
if (!images.length) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="col-5 image-selection">
|
||||||
|
<div className="performer-image">
|
||||||
|
{!create && (
|
||||||
|
<Button
|
||||||
|
onClick={() => toggleField("image")}
|
||||||
|
variant="secondary"
|
||||||
|
className={cx(
|
||||||
|
"performer-image-exclude",
|
||||||
|
excluded.image ? "text-muted" : "text-success"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon icon={excluded.image ? faTimes : faCheck} />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<img
|
||||||
|
src={images[imageIndex]}
|
||||||
|
className={cx({ "d-none": imageState !== "loaded" })}
|
||||||
|
alt=""
|
||||||
|
onLoad={() => handleLoad(imageIndex)}
|
||||||
|
onError={handleError}
|
||||||
|
/>
|
||||||
|
{imageState === "loading" && (
|
||||||
|
<LoadingIndicator message="Loading image..." />
|
||||||
|
)}
|
||||||
|
{imageState === "error" && (
|
||||||
|
<div className="h-100 d-flex justify-content-center align-items-center">
|
||||||
|
<b>Error loading image.</b>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="d-flex mt-3">
|
||||||
|
<Button onClick={setPrev} disabled={images.length === 1}>
|
||||||
|
<Icon icon={faArrowLeft} />
|
||||||
|
</Button>
|
||||||
|
<h5 className="flex-grow-1">
|
||||||
|
Select performer image
|
||||||
|
<br />
|
||||||
|
{imageIndex + 1} of {images.length}
|
||||||
|
</h5>
|
||||||
|
<Button onClick={setNext} disabled={images.length === 1}>
|
||||||
|
<Icon icon={faArrowRight} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderStashBoxLink() {
|
||||||
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
|
const base = endpoint?.match(/https?:\/\/.*?\//)?.[0];
|
||||||
const link = base
|
if (!base) return;
|
||||||
? `${base}performers/${performer.remote_site_id}`
|
|
||||||
: undefined;
|
return (
|
||||||
|
<h6 className="mt-2">
|
||||||
|
<ExternalLink href={`${base}performers/${performer.remote_site_id}`}>
|
||||||
|
<FormattedMessage id="stashbox.source" />
|
||||||
|
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
||||||
|
</ExternalLink>
|
||||||
|
</h6>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function onSaveClicked() {
|
function onSaveClicked() {
|
||||||
if (!performer.name) {
|
if (!performer.name) {
|
||||||
@@ -201,89 +265,37 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-7">
|
<div className="col-7">
|
||||||
{renderField("name", performer.name)}
|
{maybeRenderField("name", performer.name)}
|
||||||
{renderField("disambiguation", performer.disambiguation)}
|
{maybeRenderField("disambiguation", performer.disambiguation)}
|
||||||
{renderField("aliases", performer.aliases)}
|
{maybeRenderField("aliases", performer.aliases)}
|
||||||
{renderField(
|
{maybeRenderField(
|
||||||
"gender",
|
"gender",
|
||||||
performer.gender
|
performer.gender
|
||||||
? intl.formatMessage({ id: "gender_types." + performer.gender })
|
? intl.formatMessage({ id: "gender_types." + performer.gender })
|
||||||
: ""
|
: ""
|
||||||
)}
|
)}
|
||||||
{renderField("birthdate", performer.birthdate)}
|
{maybeRenderField("birthdate", performer.birthdate)}
|
||||||
{renderField("death_date", performer.death_date)}
|
{maybeRenderField("death_date", performer.death_date)}
|
||||||
{renderField("ethnicity", performer.ethnicity)}
|
{maybeRenderField("ethnicity", performer.ethnicity)}
|
||||||
{renderField("country", getCountryByISO(performer.country))}
|
{maybeRenderField("country", getCountryByISO(performer.country))}
|
||||||
{renderField("hair_color", performer.hair_color)}
|
{maybeRenderField("hair_color", performer.hair_color)}
|
||||||
{renderField("eye_color", performer.eye_color)}
|
{maybeRenderField("eye_color", performer.eye_color)}
|
||||||
{renderField("height", performer.height)}
|
{maybeRenderField("height", performer.height)}
|
||||||
{renderField("weight", performer.weight)}
|
{maybeRenderField("weight", performer.weight)}
|
||||||
{renderField("measurements", performer.measurements)}
|
{maybeRenderField("measurements", performer.measurements)}
|
||||||
{performer?.gender !== GQL.GenderEnum.Male &&
|
{performer?.gender !== GQL.GenderEnum.Male &&
|
||||||
renderField("fake_tits", performer.fake_tits)}
|
maybeRenderField("fake_tits", performer.fake_tits)}
|
||||||
{renderField("career_length", performer.career_length)}
|
{maybeRenderField("career_length", performer.career_length)}
|
||||||
{renderField("tattoos", performer.tattoos, false)}
|
{maybeRenderField("tattoos", performer.tattoos, false)}
|
||||||
{renderField("piercings", performer.piercings, false)}
|
{maybeRenderField("piercings", performer.piercings, false)}
|
||||||
{renderField("weight", performer.weight, false)}
|
{maybeRenderField("weight", performer.weight, false)}
|
||||||
{renderField("details", performer.details)}
|
{maybeRenderField("details", performer.details)}
|
||||||
{renderField("url", performer.url)}
|
{maybeRenderField("url", performer.url)}
|
||||||
{renderField("twitter", performer.twitter)}
|
{maybeRenderField("twitter", performer.twitter)}
|
||||||
{renderField("instagram", performer.instagram)}
|
{maybeRenderField("instagram", performer.instagram)}
|
||||||
{link && (
|
{maybeRenderStashBoxLink()}
|
||||||
<h6 className="mt-2">
|
|
||||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
|
||||||
<FormattedMessage id="stashbox.source" />
|
|
||||||
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
|
||||||
</a>
|
|
||||||
</h6>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{images.length > 0 && (
|
{maybeRenderImage()}
|
||||||
<div className="col-5 image-selection">
|
|
||||||
<div className="performer-image">
|
|
||||||
{!create && (
|
|
||||||
<Button
|
|
||||||
onClick={() => toggleField("image")}
|
|
||||||
variant="secondary"
|
|
||||||
className={cx(
|
|
||||||
"performer-image-exclude",
|
|
||||||
excluded.image ? "text-muted" : "text-success"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<Icon icon={excluded.image ? faTimes : faCheck} />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<img
|
|
||||||
src={images[imageIndex]}
|
|
||||||
className={cx({ "d-none": imageState !== "loaded" })}
|
|
||||||
alt=""
|
|
||||||
onLoad={() => handleLoad(imageIndex)}
|
|
||||||
onError={handleError}
|
|
||||||
/>
|
|
||||||
{imageState === "loading" && (
|
|
||||||
<LoadingIndicator message="Loading image..." />
|
|
||||||
)}
|
|
||||||
{imageState === "error" && (
|
|
||||||
<div className="h-100 d-flex justify-content-center align-items-center">
|
|
||||||
<b>Error loading image.</b>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="d-flex mt-3">
|
|
||||||
<Button onClick={setPrev} disabled={images.length === 1}>
|
|
||||||
<Icon icon={faArrowLeft} />
|
|
||||||
</Button>
|
|
||||||
<h5 className="flex-grow-1">
|
|
||||||
Select performer image
|
|
||||||
<br />
|
|
||||||
{imageIndex + 1} of {images.length}
|
|
||||||
</h5>
|
|
||||||
<Button onClick={setNext} disabled={images.length === 1}>
|
|
||||||
<Icon icon={faArrowRight} />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</ModalComponent>
|
</ModalComponent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import PerformerModal from "../PerformerModal";
|
|||||||
import { useUpdatePerformer } from "../queries";
|
import { useUpdatePerformer } from "../queries";
|
||||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { mergeStashIDs } from "src/utils/stashbox";
|
import { mergeStashIDs } from "src/utils/stashbox";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
type JobFragment = Pick<
|
type JobFragment = Pick<
|
||||||
GQL.Job,
|
GQL.Job,
|
||||||
@@ -466,14 +467,12 @@ const PerformerTaggerList: React.FC<IPerformerTaggerListProps> = ({
|
|||||||
if (stashID !== undefined) {
|
if (stashID !== undefined) {
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
const link = base ? (
|
const link = base ? (
|
||||||
<a
|
<ExternalLink
|
||||||
className="small d-block"
|
className="small d-block"
|
||||||
href={`${base}performers/${stashID.stash_id}`}
|
href={`${base}performers/${stashID.stash_id}`}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
{stashID.stash_id}
|
{stashID.stash_id}
|
||||||
</a>
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<div className="small">{stashID.stash_id}</div>
|
<div className="small">{stashID.stash_id}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
} from "src/components/Performers/PerformerSelect";
|
} from "src/components/Performers/PerformerSelect";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IPerformerName {
|
interface IPerformerName {
|
||||||
performer: GQL.ScrapedPerformer | Performer;
|
performer: GQL.ScrapedPerformer | Performer;
|
||||||
@@ -26,9 +27,7 @@ const PerformerName: React.FC<IPerformerName> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const name =
|
const name =
|
||||||
baseURL && id ? (
|
baseURL && id ? (
|
||||||
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
|
<ExternalLink href={`${baseURL}${id}`}>{performer.name}</ExternalLink>
|
||||||
{performer.name}
|
|
||||||
</a>
|
|
||||||
) : (
|
) : (
|
||||||
performer.name
|
performer.name
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import StudioResult from "./StudioResult";
|
|||||||
import { useInitialState } from "src/hooks/state";
|
import { useInitialState } from "src/hooks/state";
|
||||||
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
import { faPlus } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
const getDurationStatus = (
|
const getDurationStatus = (
|
||||||
scene: IScrapedScene,
|
scene: IScrapedScene,
|
||||||
@@ -488,14 +489,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
const url = scene.urls?.length ? scene.urls[0] : null;
|
const url = scene.urls?.length ? scene.urls[0] : null;
|
||||||
|
|
||||||
const sceneTitleEl = url ? (
|
const sceneTitleEl = url ? (
|
||||||
<a
|
<ExternalLink className="scene-link" href={url}>
|
||||||
href={url}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="scene-link"
|
|
||||||
>
|
|
||||||
<TruncatedText text={scene.title} />
|
<TruncatedText text={scene.title} />
|
||||||
</a>
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<TruncatedText text={scene.title} />
|
<TruncatedText text={scene.title} />
|
||||||
);
|
);
|
||||||
@@ -592,9 +588,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
>
|
>
|
||||||
{scene.urls.map((url) => (
|
{scene.urls.map((url) => (
|
||||||
<div key={url}>
|
<div key={url}>
|
||||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
<ExternalLink href={url}>{url}</ExternalLink>
|
||||||
{url}
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</OptionalField>
|
</OptionalField>
|
||||||
@@ -626,9 +620,9 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
|
|||||||
exclude={excludedFields[fields.stash_ids]}
|
exclude={excludedFields[fields.stash_ids]}
|
||||||
setExclude={(v) => setExcludedField(fields.stash_ids, v)}
|
setExclude={(v) => setExcludedField(fields.stash_ids, v)}
|
||||||
>
|
>
|
||||||
<a href={stashBoxURL} target="_blank" rel="noopener noreferrer">
|
<ExternalLink href={stashBoxURL}>
|
||||||
{scene.remote_site_id}
|
{scene.remote_site_id}
|
||||||
</a>
|
</ExternalLink>
|
||||||
</OptionalField>
|
</OptionalField>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { Button, Form } from "react-bootstrap";
|
import { Button, Form } from "react-bootstrap";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
import { excludeFields } from "src/utils/data";
|
import { excludeFields } from "src/utils/data";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IStudioDetailsProps {
|
interface IStudioDetailsProps {
|
||||||
studio: GQL.ScrapedSceneStudioDataFragment;
|
studio: GQL.ScrapedSceneStudioDataFragment;
|
||||||
@@ -83,15 +84,15 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderLink() {
|
function maybeRenderStashBoxLink() {
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<h6 className="mt-2">
|
<h6 className="mt-2">
|
||||||
<a href={link} target="_blank" rel="noopener noreferrer">
|
<ExternalLink href={link}>
|
||||||
<FormattedMessage id="stashbox.source" />
|
<FormattedMessage id="stashbox.source" />
|
||||||
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
<Icon icon={faExternalLinkAlt} className="ml-2" />
|
||||||
</a>
|
</ExternalLink>
|
||||||
</h6>
|
</h6>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -104,7 +105,7 @@ const StudioDetails: React.FC<IStudioDetailsProps> = ({
|
|||||||
{maybeRenderField("name", studio.name, !isNew)}
|
{maybeRenderField("name", studio.name, !isNew)}
|
||||||
{maybeRenderField("url", studio.url)}
|
{maybeRenderField("url", studio.url)}
|
||||||
{maybeRenderField("parent_studio", studio.parent?.name, false)}
|
{maybeRenderField("parent_studio", studio.parent?.name, false)}
|
||||||
{maybeRenderLink()}
|
{maybeRenderStashBoxLink()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import * as GQL from "src/core/generated-graphql";
|
|||||||
import { OptionalField } from "../IncludeButton";
|
import { OptionalField } from "../IncludeButton";
|
||||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { getStashboxBase } from "src/utils/stashbox";
|
import { getStashboxBase } from "src/utils/stashbox";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface IStudioName {
|
interface IStudioName {
|
||||||
studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment;
|
studio: GQL.ScrapedStudio | GQL.SlimStudioDataFragment;
|
||||||
@@ -21,9 +22,7 @@ interface IStudioName {
|
|||||||
const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => {
|
const StudioName: React.FC<IStudioName> = ({ studio, id, baseURL }) => {
|
||||||
const name =
|
const name =
|
||||||
baseURL && id ? (
|
baseURL && id ? (
|
||||||
<a href={`${baseURL}${id}`} target="_blank" rel="noreferrer">
|
<ExternalLink href={`${baseURL}${id}`}>{studio.name}</ExternalLink>
|
||||||
{studio.name}
|
|
||||||
</a>
|
|
||||||
) : (
|
) : (
|
||||||
studio.name
|
studio.name
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
faImage,
|
faImage,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { objectPath, objectTitle } from "src/core/files";
|
import { objectPath, objectTitle } from "src/core/files";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
interface ITaggerSceneDetails {
|
interface ITaggerSceneDetails {
|
||||||
scene: GQL.SlimSceneDataFragment;
|
scene: GQL.SlimSceneDataFragment;
|
||||||
@@ -174,15 +175,13 @@ export const TaggerScene: React.FC<PropsWithChildren<ITaggerScene>> = ({
|
|||||||
const stashLinks = scene.stash_ids.map((stashID) => {
|
const stashLinks = scene.stash_ids.map((stashID) => {
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
const link = base ? (
|
const link = base ? (
|
||||||
<a
|
<ExternalLink
|
||||||
key={`${stashID.endpoint}${stashID.stash_id}`}
|
key={`${stashID.endpoint}${stashID.stash_id}`}
|
||||||
className="small d-block"
|
className="small d-block"
|
||||||
href={`${base}scenes/${stashID.stash_id}`}
|
href={`${base}scenes/${stashID.stash_id}`}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
{stashID.stash_id}
|
{stashID.stash_id}
|
||||||
</a>
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<div className="small">{stashID.stash_id}</div>
|
<div className="small">{stashID.stash_id}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import StudioModal from "../scenes/StudioModal";
|
|||||||
import { useUpdateStudio } from "../queries";
|
import { useUpdateStudio } from "../queries";
|
||||||
import { apolloError } from "src/utils";
|
import { apolloError } from "src/utils";
|
||||||
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
import { faStar, faTags } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
|
|
||||||
type JobFragment = Pick<
|
type JobFragment = Pick<
|
||||||
GQL.Job,
|
GQL.Job,
|
||||||
@@ -515,14 +516,12 @@ const StudioTaggerList: React.FC<IStudioTaggerListProps> = ({
|
|||||||
if (stashID !== undefined) {
|
if (stashID !== undefined) {
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
const link = base ? (
|
const link = base ? (
|
||||||
<a
|
<ExternalLink
|
||||||
className="small d-block"
|
className="small d-block"
|
||||||
href={`${base}studios/${stashID.stash_id}`}
|
href={`${base}studios/${stashID.stash_id}`}
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
>
|
||||||
{stashID.stash_id}
|
{stashID.stash_id}
|
||||||
</a>
|
</ExternalLink>
|
||||||
) : (
|
) : (
|
||||||
<div className="small">{stashID.stash_id}</div>
|
<div className="small">{stashID.stash_id}</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -205,6 +205,15 @@ dd {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.btn.link {
|
||||||
|
color: $link-color;
|
||||||
|
|
||||||
|
&:hover:not(:disabled),
|
||||||
|
&:active:not(:disabled) {
|
||||||
|
color: $link-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.detail-header.edit {
|
.detail-header.edit {
|
||||||
background-color: unset;
|
background-color: unset;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
@@ -691,12 +700,11 @@ div.dropdown-menu {
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
row-gap: 10px;
|
row-gap: 10px;
|
||||||
|
|
||||||
/* stylelint-disable */
|
|
||||||
.badge {
|
.badge {
|
||||||
white-space: normal !important;
|
|
||||||
margin: unset;
|
margin: unset;
|
||||||
|
// stylelint-disable declaration-no-important
|
||||||
|
white-space: normal !important;
|
||||||
}
|
}
|
||||||
/* stylelint-enable */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-item {
|
.tag-item {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||||
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
import { TruncatedText } from "src/components/Shared/TruncatedText";
|
||||||
|
|
||||||
interface ITextField {
|
interface ITextField {
|
||||||
@@ -44,8 +46,8 @@ interface IURLField {
|
|||||||
url?: string | null;
|
url?: string | null;
|
||||||
truncate?: boolean | null;
|
truncate?: boolean | null;
|
||||||
target?: string;
|
target?: string;
|
||||||
// use for internal links
|
// an internal link (uses <Link to={url}>)
|
||||||
trusted?: boolean;
|
internal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const URLField: React.FC<IURLField> = ({
|
export const URLField: React.FC<IURLField> = ({
|
||||||
@@ -55,11 +57,10 @@ export const URLField: React.FC<IURLField> = ({
|
|||||||
url,
|
url,
|
||||||
abbr,
|
abbr,
|
||||||
truncate,
|
truncate,
|
||||||
children,
|
target = "_blank",
|
||||||
target,
|
internal,
|
||||||
trusted,
|
|
||||||
}) => {
|
}) => {
|
||||||
if (!value && !children) {
|
if (!value) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,26 +68,30 @@ export const URLField: React.FC<IURLField> = ({
|
|||||||
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const rel = !trusted ? "noopener noreferrer" : undefined;
|
function maybeRenderUrl() {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
const children = truncate ? <TruncatedText text={value} /> : value;
|
||||||
|
|
||||||
|
if (internal) {
|
||||||
|
return (
|
||||||
|
<Link to={url} target={target}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ExternalLink href={url} target={target}>
|
||||||
|
{children}
|
||||||
|
</ExternalLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||||
<dd>
|
<dd>{maybeRenderUrl()}</dd>
|
||||||
{url ? (
|
|
||||||
<a href={url} target={target || "_blank"} rel={rel}>
|
|
||||||
{value ? (
|
|
||||||
truncate ? (
|
|
||||||
<TruncatedText text={value} />
|
|
||||||
) : (
|
|
||||||
value
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
) : undefined}
|
|
||||||
</dd>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -98,8 +103,8 @@ interface IURLsField {
|
|||||||
urls?: string[] | null;
|
urls?: string[] | null;
|
||||||
truncate?: boolean | null;
|
truncate?: boolean | null;
|
||||||
target?: string;
|
target?: string;
|
||||||
// use for internal links
|
// an internal link (uses <Link to={url}>)
|
||||||
trusted?: boolean;
|
internal?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const URLsField: React.FC<IURLsField> = ({
|
export const URLsField: React.FC<IURLsField> = ({
|
||||||
@@ -108,11 +113,10 @@ export const URLsField: React.FC<IURLsField> = ({
|
|||||||
urls,
|
urls,
|
||||||
abbr,
|
abbr,
|
||||||
truncate,
|
truncate,
|
||||||
target,
|
target = "_blank",
|
||||||
trusted,
|
internal,
|
||||||
}) => {
|
}) => {
|
||||||
const values = urls ?? [];
|
if (!urls || !urls.length) {
|
||||||
if (!values.length) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,19 +124,33 @@ export const URLsField: React.FC<IURLsField> = ({
|
|||||||
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const rel = !trusted ? "noopener noreferrer" : undefined;
|
const renderUrls = () => {
|
||||||
|
return urls.map((url, i) => {
|
||||||
|
if (!url) return;
|
||||||
|
|
||||||
|
const children = truncate ? <TruncatedText text={url} /> : url;
|
||||||
|
|
||||||
|
if (internal) {
|
||||||
|
return (
|
||||||
|
<Link key={i} to={url} target={target}>
|
||||||
|
{children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<ExternalLink key={i} href={url} target={target}>
|
||||||
|
{children}
|
||||||
|
</ExternalLink>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||||
<dd>
|
<dd>
|
||||||
<dl>
|
<dl>{renderUrls()}</dl>
|
||||||
{values.map((url, i) => (
|
|
||||||
<a key={i} href={url} target={target || "_blank"} rel={rel}>
|
|
||||||
{truncate ? <TruncatedText text={url} /> : url}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</dl>
|
|
||||||
</dd>
|
</dd>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user