mirror of
https://github.com/stashapp/stash.git
synced 2025-12-06 08:26:00 +01:00
Fix links and styling of File Info tabs (#1622)
* Refactor HTML of movie and performer details panels Refactor HTML of the movie and performer details panels so that the items are contained in a single list (`<dl/>`). This allows using a grid layout which means that the styling is easier to get right for multiple form factors, fixing issues where "values" would overlap the "labels" (for instance on my phone performers "Measurements" almost overlaps the actual value, while there is a lot of space for the value itself). This refactor also allows reusing the `TextField` and `URLField` components as they don't have any styling related classes anymore (i.e.: the `col-` classes are gone). Which means they can be used in more dense places, like the SceneFileInfoPanel, as well. As the width of the label / value doesn't rely on the viewport size anymore (as happened due to the `col-xl` usage, but for example the scene sidebar being small, and 16% being to small). * Rebuild SceneFileInfoPanel Completely rebuild the SceneFileInfoPanel to make use of the `TextField` and `URLField` components. Using these components means that the URLs automatically get `target="_blank" rel="noopener noreferrer"`. Furhermore this should also improve the styling a bit, as described in the previous commit. * Rebuild ImageFileInfoPanel Completely rebuild the ImageFileInfoPanel to make use of `TextField` and `URLField` components. Furthermore it should resolve some small styling issues. * Rebuild GalleryFileInfoPanel Rebuild the GalleryFileInfoPanel to make use of `TextField` and `URLField` components. Using these components means that for example the URLs automatically get `target="blank" rel="noopener noreferrer"`. Also adds the url property as 1. at the moment it is nowhere accessible, and 2. scenes also has it in this panel. * Truncate links on the file info tabs at latest opportunity On the File Info tabs links always have the link destination as text for the link as well. But these texts can be long and without whitespace. This means that the default applied `word-wrap: break-word` doesn't really work as URLs (and paths) don't contain spaces that ofter. So apply `word-break: break-all` instead so that the text will be as long as possible and just cut off in the middle, instead of only at whitespace. This thus means that the fully available width will be used to display the URL.
This commit is contained in:
parent
d31b6841d0
commit
59c7dd622b
9 changed files with 227 additions and 358 deletions
|
|
@ -6,6 +6,7 @@
|
|||
* Added not equals/greater than/less than modifiers for resolution criteria. ([#1568](https://github.com/stashapp/stash/pull/1568))
|
||||
|
||||
### 🎨 Improvements
|
||||
* Improve link styling and ensure links open in a new tab. ([#1622](https://github.com/stashapp/stash/pull/1622))
|
||||
* Added zh-CN language option. ([#1620](https://github.com/stashapp/stash/pull/1620))
|
||||
* Moved scraping settings into the Scraping settings page. ([#1548](https://github.com/stashapp/stash/pull/1548))
|
||||
* Show current scene details in tagger view. ([#1605](https://github.com/stashapp/stash/pull/1605))
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import React from "react";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IGalleryFileInfoPanelProps {
|
||||
gallery: GQL.GalleryDataFragment;
|
||||
|
|
@ -10,36 +9,25 @@ interface IGalleryFileInfoPanelProps {
|
|||
export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
||||
props: IGalleryFileInfoPanelProps
|
||||
) => {
|
||||
function renderChecksum() {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.checksum" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.gallery.checksum} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPath() {
|
||||
const filePath = `file://${props.gallery.path}`;
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={filePath} className="col-8">
|
||||
<TruncatedText text={filePath} />
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container gallery-file-info">
|
||||
{renderChecksum()}
|
||||
{renderPath()}
|
||||
</div>
|
||||
<dl className="container gallery-file-info details-list">
|
||||
<TextField
|
||||
id="media_info.checksum"
|
||||
value={props.gallery.checksum}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="path"
|
||||
url={`file://${props.gallery.path}`}
|
||||
value={`file://${props.gallery.path}`}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="path"
|
||||
url={props.gallery.url}
|
||||
value={props.gallery.url}
|
||||
truncate
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { FormattedNumber } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface IImageFileInfoPanelProps {
|
||||
image: GQL.ImageDataFragment;
|
||||
|
|
@ -11,33 +11,6 @@ interface IImageFileInfoPanelProps {
|
|||
export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
||||
props: IImageFileInfoPanelProps
|
||||
) => {
|
||||
function renderChecksum() {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.checksum" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.image.checksum} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPath() {
|
||||
const {
|
||||
image: { path },
|
||||
} = props;
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={`file://${path}`} className="col-8">
|
||||
<TruncatedText text={`file://${props.image.path}`} />
|
||||
</a>{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFileSize() {
|
||||
if (props.image.file.size === undefined) {
|
||||
return;
|
||||
|
|
@ -46,9 +19,8 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
|||
const { size, unit } = TextUtils.fileSize(props.image.file.size ?? 0);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">File Size</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<TextField id="filesize">
|
||||
<span className="text-truncate">
|
||||
<FormattedNumber
|
||||
value={size}
|
||||
// eslint-disable-next-line react/style-prop-object
|
||||
|
|
@ -58,29 +30,29 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
|||
maximumFractionDigits={2}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDimensions() {
|
||||
if (props.image.file.height && props.image.file.width) {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Dimensions</span>
|
||||
<span className="col-8 text-truncate">
|
||||
{props.image.file.width} x {props.image.file.height}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container image-file-info">
|
||||
{renderChecksum()}
|
||||
{renderPath()}
|
||||
<dl className="container image-file-info details-list">
|
||||
<TextField
|
||||
id="media_info.checksum"
|
||||
value={props.image.checksum}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="path"
|
||||
url={`file://${props.image.path}`}
|
||||
value={`file://${props.image.path}`}
|
||||
truncate
|
||||
/>
|
||||
{renderFileSize()}
|
||||
{renderDimensions()}
|
||||
</div>
|
||||
<TextField
|
||||
id="dimensions"
|
||||
value={`${props.image.file.width} x ${props.image.file.height}`}
|
||||
truncate
|
||||
/>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -32,14 +32,12 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<dl className="row">
|
||||
<dt className="col-3 col-xl-2">
|
||||
{intl.formatMessage({ id: "rating" })}
|
||||
</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<>
|
||||
<dt>{intl.formatMessage({ id: "rating" })}</dt>
|
||||
<dd>
|
||||
<RatingStars value={movie.rating} disabled />
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -52,7 +50,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||
|
||||
{maybeRenderAliases()}
|
||||
|
||||
<div>
|
||||
<dl className="details-list">
|
||||
<TextField
|
||||
id="duration"
|
||||
value={
|
||||
|
|
@ -79,7 +77,7 @@ export const MovieDetailsPanel: React.FC<IMovieDetailsPanel> = ({ movie }) => {
|
|||
/>
|
||||
|
||||
<TextField id="synopsis" value={movie.synopsis} />
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -23,18 +23,18 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<dl className="row">
|
||||
<dt className="col-3 col-xl-2">
|
||||
<>
|
||||
<dt>
|
||||
<FormattedMessage id="tags" />
|
||||
</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<dd>
|
||||
<ul className="pl-0">
|
||||
{(performer.tags ?? []).map((tag) => (
|
||||
<TagLink key={tag.id} tagType="performer" tag={tag} />
|
||||
))}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -44,14 +44,14 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<dl className="row mb-0">
|
||||
<dt className="col-3 col-xl-2">
|
||||
<>
|
||||
<dt>
|
||||
<FormattedMessage id="rating" />:
|
||||
</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<dd>
|
||||
<RatingStars value={performer.rating} />
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -61,9 +61,9 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<dl className="row">
|
||||
<dt className="col-3 col-xl-2">StashIDs</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<>
|
||||
<dt>StashIDs</dt>
|
||||
<dd>
|
||||
<ul className="pl-0">
|
||||
{performer.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
|
|
@ -86,7 +86,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
})}
|
||||
</ul>
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -113,7 +113,7 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<dl className="details-list">
|
||||
<TextField
|
||||
id="gender"
|
||||
value={genderToString(performer.gender ?? undefined)}
|
||||
|
|
@ -162,6 +162,6 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
|||
{renderRating()}
|
||||
{renderTagsField()}
|
||||
{renderStashIDs()}
|
||||
</>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import React from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { FormattedNumber } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { TextUtils } from "src/utils";
|
||||
import { TruncatedText } from "src/components/Shared";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
|
||||
interface ISceneFileInfoPanelProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
|
|
@ -11,61 +11,6 @@ interface ISceneFileInfoPanelProps {
|
|||
export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
||||
props: ISceneFileInfoPanelProps
|
||||
) => {
|
||||
function renderOSHash() {
|
||||
if (props.scene.oshash) {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.hash" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.scene.oshash} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderChecksum() {
|
||||
if (props.scene.checksum) {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.checksum" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.scene.checksum} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPath() {
|
||||
const {
|
||||
scene: { path },
|
||||
} = props;
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="path" />
|
||||
</span>
|
||||
<a href={`file://${path}`} className="col-8">
|
||||
<TruncatedText text={`file://${props.scene.path}`} />
|
||||
</a>{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStream() {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.stream" />
|
||||
</span>
|
||||
<a href={props.scene.paths.stream ?? ""} className="col-8">
|
||||
<TruncatedText text={props.scene.paths.stream} />
|
||||
</a>{" "}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFileSize() {
|
||||
if (props.scene.file.size === undefined) {
|
||||
return;
|
||||
|
|
@ -76,11 +21,8 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="filesize" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<TextField id="filesize">
|
||||
<span className="text-truncate">
|
||||
<FormattedNumber
|
||||
value={size}
|
||||
// eslint-disable-next-line react/style-prop-object
|
||||
|
|
@ -90,123 +32,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||
maximumFractionDigits={2}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDuration() {
|
||||
if (props.scene.file.duration === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="duration" />
|
||||
</span>
|
||||
<TruncatedText
|
||||
className="col-8"
|
||||
text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderDimensions() {
|
||||
if (props.scene.file.duration === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="dimensions" />
|
||||
</span>
|
||||
<TruncatedText
|
||||
className="col-8"
|
||||
text={`${props.scene.file.width} x ${props.scene.file.height}`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderFrameRate() {
|
||||
if (props.scene.file.framerate === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="framerate" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
|
||||
second
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderbitrate() {
|
||||
// TODO: An upcoming react-intl version will support compound units, megabits-per-second
|
||||
if (props.scene.file.bitrate === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="bitrate" />
|
||||
</span>
|
||||
<span className="col-8 text-truncate">
|
||||
<FormattedNumber
|
||||
value={(props.scene.file.bitrate ?? 0) / 1000000}
|
||||
maximumFractionDigits={2}
|
||||
/>
|
||||
megabits per second
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderVideoCodec() {
|
||||
if (props.scene.file.video_codec === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.video_codec" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.scene.file.video_codec} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderAudioCodec() {
|
||||
if (props.scene.file.audio_codec === undefined) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.audio_codec" />
|
||||
</span>
|
||||
<TruncatedText className="col-8" text={props.scene.file.audio_codec} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUrl() {
|
||||
if (!props.scene.url || props.scene.url === "") {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">
|
||||
<FormattedMessage id="media_info.downloaded_from" />
|
||||
</span>
|
||||
<a href={TextUtils.sanitiseURL(props.scene.url)} className="col-8">
|
||||
<TruncatedText text={props.scene.url} />
|
||||
</a>
|
||||
</div>
|
||||
</TextField>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -216,76 +42,114 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">StashIDs</span>
|
||||
<ul className="col-8">
|
||||
{props.scene.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
stashID.stash_id
|
||||
);
|
||||
return (
|
||||
<li key={stashID.stash_id} className="row no-gutters">
|
||||
{link}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<>
|
||||
<dt>StashIDs</dt>
|
||||
<dd>
|
||||
<ul>
|
||||
{props.scene.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
stashID.stash_id
|
||||
);
|
||||
return (
|
||||
<li key={stashID.stash_id} className="row no-gutters">
|
||||
{link}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPhash() {
|
||||
if (props.scene.phash) {
|
||||
return (
|
||||
<div className="row">
|
||||
<abbr className="col-4" title="Perceptual hash">
|
||||
<FormattedMessage id="media_info.phash" />
|
||||
</abbr>
|
||||
<TruncatedText className="col-8" text={props.scene.phash} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderFunscript() {
|
||||
if (props.scene.interactive) {
|
||||
return (
|
||||
<div className="row">
|
||||
<span className="col-4">Funscript</span>
|
||||
<a href={props.scene.paths.funscript ?? ""} className="col-8">
|
||||
<TruncatedText text={props.scene.paths.funscript} />
|
||||
</a>{" "}
|
||||
</div>
|
||||
<URLField
|
||||
name="Funscript"
|
||||
url={props.scene.paths.funscript}
|
||||
value={props.scene.paths.funscript}
|
||||
truncate
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container scene-file-info">
|
||||
{renderOSHash()}
|
||||
{renderChecksum()}
|
||||
{renderPhash()}
|
||||
{renderPath()}
|
||||
{renderStream()}
|
||||
<dl className="container scene-file-info details-list">
|
||||
<TextField id="media_info.hash" value={props.scene.oshash} truncate />
|
||||
<TextField
|
||||
id="media_info.checksum"
|
||||
value={props.scene.checksum}
|
||||
truncate
|
||||
/>
|
||||
<TextField
|
||||
id="media_info.phash"
|
||||
abbr="Perceptual hash"
|
||||
value={props.scene.phash}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="path"
|
||||
url={`file://${props.scene.path}`}
|
||||
value={`file://${props.scene.path}`}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="media_info.stream"
|
||||
url={props.scene.paths.stream}
|
||||
value={props.scene.paths.stream}
|
||||
truncate
|
||||
/>
|
||||
{renderFunscript()}
|
||||
{renderFileSize()}
|
||||
{renderDuration()}
|
||||
{renderDimensions()}
|
||||
{renderFrameRate()}
|
||||
{renderbitrate()}
|
||||
{renderVideoCodec()}
|
||||
{renderAudioCodec()}
|
||||
{renderUrl()}
|
||||
<TextField
|
||||
id="duration"
|
||||
value={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
|
||||
truncate
|
||||
/>
|
||||
<TextField
|
||||
id="dimensions"
|
||||
value={`${props.scene.file.width} x ${props.scene.file.height}`}
|
||||
truncate
|
||||
/>
|
||||
<TextField id="framerate">
|
||||
<FormattedNumber value={props.scene.file.framerate ?? 0} /> frames per
|
||||
second
|
||||
</TextField>
|
||||
<TextField id="bitrate">
|
||||
<FormattedNumber
|
||||
value={(props.scene.file.bitrate ?? 0) / 1000000}
|
||||
maximumFractionDigits={2}
|
||||
/>{" "}
|
||||
frames per second
|
||||
</TextField>
|
||||
<TextField
|
||||
id="media_info.video_codec"
|
||||
value={props.scene.file.video_codec}
|
||||
truncate
|
||||
/>
|
||||
<TextField
|
||||
id="media_info.audio_codec"
|
||||
value={props.scene.file.audio_codec}
|
||||
truncate
|
||||
/>
|
||||
<URLField
|
||||
id="media_info.downloaded_from"
|
||||
url={props.scene.url}
|
||||
value={props.scene.url}
|
||||
truncate
|
||||
/>
|
||||
{renderStashIDs()}
|
||||
</div>
|
||||
</dl>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -186,6 +186,10 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active {
|
|||
max-width: 300px;
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.file-info-panel a > & {
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.RatingStars {
|
||||
|
|
|
|||
|
|
@ -643,3 +643,9 @@ div.dropdown-menu {
|
|||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
dl.details-list {
|
||||
display: grid;
|
||||
grid-column-gap: 10px;
|
||||
grid-template-columns: minmax(16.67%, auto) 1fr;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,49 +1,85 @@
|
|||
import React from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { TruncatedText } from "../components/Shared";
|
||||
|
||||
interface ITextField {
|
||||
id?: string;
|
||||
name?: string;
|
||||
abbr?: string | null;
|
||||
value?: string | null;
|
||||
truncate?: boolean | null;
|
||||
}
|
||||
|
||||
export const TextField: React.FC<ITextField> = ({ id, name, value }) => {
|
||||
if (!value) {
|
||||
export const TextField: React.FC<ITextField> = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
abbr,
|
||||
truncate,
|
||||
children,
|
||||
}) => {
|
||||
if (!value && !children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = (
|
||||
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
||||
);
|
||||
|
||||
return (
|
||||
<dl className="row mb-0">
|
||||
<dt className="col-3 col-xl-2">
|
||||
{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:
|
||||
</dt>
|
||||
<dd className="col-9 col-xl-10">{value ?? undefined}</dd>
|
||||
</dl>
|
||||
<>
|
||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||
<dd>
|
||||
{value ? truncate ? <TruncatedText text={value} /> : value : children}
|
||||
</dd>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface IURLField {
|
||||
id?: string;
|
||||
name?: string;
|
||||
abbr?: string | null;
|
||||
value?: string | null;
|
||||
url?: string | null;
|
||||
truncate?: boolean | null;
|
||||
}
|
||||
|
||||
export const URLField: React.FC<IURLField> = ({ id, name, value, url }) => {
|
||||
if (!value) {
|
||||
export const URLField: React.FC<IURLField> = ({
|
||||
id,
|
||||
name,
|
||||
value,
|
||||
url,
|
||||
abbr,
|
||||
truncate,
|
||||
children,
|
||||
}) => {
|
||||
if (!value && !children) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const message = (
|
||||
<>{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:</>
|
||||
);
|
||||
|
||||
return (
|
||||
<dl className="row mb-0">
|
||||
<dt className="col-3 col-xl-2">
|
||||
{id ? <FormattedMessage id={id} defaultMessage={name} /> : name}:
|
||||
</dt>
|
||||
<dd className="col-9 col-xl-10">
|
||||
<>
|
||||
<dt>{abbr ? <abbr title={abbr}>{message}</abbr> : message}</dt>
|
||||
<dd>
|
||||
{url ? (
|
||||
<a href={url} target="_blank" rel="noopener noreferrer">
|
||||
{value}
|
||||
{value ? (
|
||||
truncate ? (
|
||||
<TruncatedText text={value} />
|
||||
) : (
|
||||
value
|
||||
)
|
||||
) : (
|
||||
children
|
||||
)}
|
||||
</a>
|
||||
) : undefined}
|
||||
</dd>
|
||||
</dl>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in a new issue