From 59c7dd622ba98536516f47b774ba5eedd87195e7 Mon Sep 17 00:00:00 2001
From: gitgiggety <79809426+gitgiggety@users.noreply.github.com>
Date: Tue, 10 Aug 2021 06:39:09 +0200
Subject: [PATCH] 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 (`
`). 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.
---
.../src/components/Changelog/versions/v090.md | 1 +
.../GalleryDetails/GalleryFileInfoPanel.tsx | 52 ++-
.../ImageDetails/ImageFileInfoPanel.tsx | 74 ++--
.../Movies/MovieDetails/MovieDetailsPanel.tsx | 14 +-
.../PerformerDetailsPanel.tsx | 28 +-
.../SceneDetails/SceneFileInfoPanel.tsx | 336 ++++++------------
ui/v2.5/src/components/Shared/styles.scss | 4 +
ui/v2.5/src/index.scss | 6 +
ui/v2.5/src/utils/field.tsx | 70 +++-
9 files changed, 227 insertions(+), 358 deletions(-)
diff --git a/ui/v2.5/src/components/Changelog/versions/v090.md b/ui/v2.5/src/components/Changelog/versions/v090.md
index 29105fbb2..b2157a772 100644
--- a/ui/v2.5/src/components/Changelog/versions/v090.md
+++ b/ui/v2.5/src/components/Changelog/versions/v090.md
@@ -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))
diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx
index 456896d16..e03e96178 100644
--- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx
+++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryFileInfoPanel.tsx
@@ -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 = (
props: IGalleryFileInfoPanelProps
) => {
- function renderChecksum() {
- return (
-
-
-
-
-
-
- );
- }
-
- function renderPath() {
- const filePath = `file://${props.gallery.path}`;
-
- return (
-
- );
- }
-
return (
-
- {renderChecksum()}
- {renderPath()}
-
+
+
+
+
+
);
};
diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx
index ba80d14e6..cefacb959 100644
--- a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx
+++ b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx
@@ -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 = (
props: IImageFileInfoPanelProps
) => {
- function renderChecksum() {
- return (
-
-
-
-
-
-
- );
- }
-
- function renderPath() {
- const {
- image: { path },
- } = props;
- return (
-
- );
- }
-
function renderFileSize() {
if (props.image.file.size === undefined) {
return;
@@ -46,9 +19,8 @@ export const ImageFileInfoPanel: React.FC = (
const { size, unit } = TextUtils.fileSize(props.image.file.size ?? 0);
return (
-
- File Size
-
+
+
= (
maximumFractionDigits={2}
/>
-
+
);
}
- function renderDimensions() {
- if (props.image.file.height && props.image.file.width) {
- return (
-
- Dimensions
-
- {props.image.file.width} x {props.image.file.height}
-
-
- );
- }
- }
-
return (
-
- {renderChecksum()}
- {renderPath()}
+
+
+
{renderFileSize()}
- {renderDimensions()}
-
+
+
);
};
diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx
index 1de2007af..2635e99fb 100644
--- a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx
+++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx
@@ -32,14 +32,12 @@ export const MovieDetailsPanel: React.FC = ({ movie }) => {
}
return (
-
- -
- {intl.formatMessage({ id: "rating" })}
-
- -
+ <>
+
- {intl.formatMessage({ id: "rating" })}
+ -
-
+ >
);
}
@@ -52,7 +50,7 @@ export const MovieDetailsPanel: React.FC = ({ movie }) => {
{maybeRenderAliases()}
-
+
= ({ movie }) => {
/>
-
+
);
};
diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
index eaeb024bd..4ea7de9bc 100644
--- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
+++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx
@@ -23,18 +23,18 @@ export const PerformerDetailsPanel: React.FC = ({
}
return (
-
- -
+ <>
+
-
- -
+
-
{(performer.tags ?? []).map((tag) => (
))}
-
+ >
);
}
@@ -44,14 +44,14 @@ export const PerformerDetailsPanel: React.FC = ({
}
return (
-
- -
+ <>
+
-
:
- -
+
-
-
+ >
);
}
@@ -61,9 +61,9 @@ export const PerformerDetailsPanel: React.FC = ({
}
return (
-
- - StashIDs
- -
+ <>
+
- StashIDs
+ -
{performer.stash_ids.map((stashID) => {
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
@@ -86,7 +86,7 @@ export const PerformerDetailsPanel: React.FC = ({
})}
-
+ >
);
}
@@ -113,7 +113,7 @@ export const PerformerDetailsPanel: React.FC = ({
};
return (
- <>
+
= ({
{renderRating()}
{renderTagsField()}
{renderStashIDs()}
- >
+
);
};
diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
index b0a78fda0..31b3ab384 100644
--- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
+++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx
@@ -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 = (
props: ISceneFileInfoPanelProps
) => {
- function renderOSHash() {
- if (props.scene.oshash) {
- return (
-
-
-
-
-
-
- );
- }
- }
-
- function renderChecksum() {
- if (props.scene.checksum) {
- return (
-
-
-
-
-
-
- );
- }
- }
-
- function renderPath() {
- const {
- scene: { path },
- } = props;
- return (
-
- );
- }
-
- function renderStream() {
- return (
-
- );
- }
-
function renderFileSize() {
if (props.scene.file.size === undefined) {
return;
@@ -76,11 +21,8 @@ export const SceneFileInfoPanel: React.FC = (
);
return (
-
-
-
-
-
+
+
= (
maximumFractionDigits={2}
/>
-
- );
- }
-
- function renderDuration() {
- if (props.scene.file.duration === undefined) {
- return;
- }
- return (
-
-
-
-
-
-
- );
- }
-
- function renderDimensions() {
- if (props.scene.file.duration === undefined) {
- return;
- }
- return (
-
-
-
-
-
-
- );
- }
-
- function renderFrameRate() {
- if (props.scene.file.framerate === undefined) {
- return;
- }
- return (
-
-
-
-
-
- frames per
- second
-
-
- );
- }
-
- function renderbitrate() {
- // TODO: An upcoming react-intl version will support compound units, megabits-per-second
- if (props.scene.file.bitrate === undefined) {
- return;
- }
- return (
-
-
-
-
-
-
- megabits per second
-
-
- );
- }
-
- function renderVideoCodec() {
- if (props.scene.file.video_codec === undefined) {
- return;
- }
- return (
-
-
-
-
-
-
- );
- }
-
- function renderAudioCodec() {
- if (props.scene.file.audio_codec === undefined) {
- return;
- }
- return (
-
-
-
-
-
-
- );
- }
-
- function renderUrl() {
- if (!props.scene.url || props.scene.url === "") {
- return;
- }
- return (
-
+
);
}
@@ -216,76 +42,114 @@ export const SceneFileInfoPanel: React.FC = (
}
return (
-
-
StashIDs
-
- {props.scene.stash_ids.map((stashID) => {
- const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
- const link = base ? (
-
- {stashID.stash_id}
-
- ) : (
- stashID.stash_id
- );
- return (
- -
- {link}
-
- );
- })}
-
-
+ <>
+ StashIDs
+
+
+ {props.scene.stash_ids.map((stashID) => {
+ const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
+ const link = base ? (
+
+ {stashID.stash_id}
+
+ ) : (
+ stashID.stash_id
+ );
+ return (
+ -
+ {link}
+
+ );
+ })}
+
+
+ >
);
}
- function renderPhash() {
- if (props.scene.phash) {
- return (
-
- );
- }
- }
-
function renderFunscript() {
if (props.scene.interactive) {
return (
-
-
Funscript
-
-
- {" "}
-
+
);
}
}
return (
-
- {renderOSHash()}
- {renderChecksum()}
- {renderPhash()}
- {renderPath()}
- {renderStream()}
+
+
+
+
+
+
{renderFunscript()}
{renderFileSize()}
- {renderDuration()}
- {renderDimensions()}
- {renderFrameRate()}
- {renderbitrate()}
- {renderVideoCodec()}
- {renderAudioCodec()}
- {renderUrl()}
+
+
+
+ frames per
+ second
+
+
+ {" "}
+ frames per second
+
+
+
+
{renderStashIDs()}
-
+
);
};
diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss
index 99a51bcb8..bff6a5907 100644
--- a/ui/v2.5/src/components/Shared/styles.scss
+++ b/ui/v2.5/src/components/Shared/styles.scss
@@ -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 {
diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss
index 58c81d61c..3fbad4278 100755
--- a/ui/v2.5/src/index.scss
+++ b/ui/v2.5/src/index.scss
@@ -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;
+}
diff --git a/ui/v2.5/src/utils/field.tsx b/ui/v2.5/src/utils/field.tsx
index 7eaad5a12..68c8a8230 100644
--- a/ui/v2.5/src/utils/field.tsx
+++ b/ui/v2.5/src/utils/field.tsx
@@ -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 = ({ id, name, value }) => {
- if (!value) {
+export const TextField: React.FC = ({
+ id,
+ name,
+ value,
+ abbr,
+ truncate,
+ children,
+}) => {
+ if (!value && !children) {
return null;
}
+
+ const message = (
+ <>{id ? : name}:>
+ );
+
return (
-
- -
- {id ? : name}:
-
- - {value ?? undefined}
-
+ <>
+ {abbr ? {message} : message}
+
+ {value ? truncate ? : value : children}
+
+ >
);
};
interface IURLField {
id?: string;
name?: string;
+ abbr?: string | null;
value?: string | null;
url?: string | null;
+ truncate?: boolean | null;
}
-export const URLField: React.FC = ({ id, name, value, url }) => {
- if (!value) {
+export const URLField: React.FC = ({
+ id,
+ name,
+ value,
+ url,
+ abbr,
+ truncate,
+ children,
+}) => {
+ if (!value && !children) {
return null;
}
+
+ const message = (
+ <>{id ? : name}:>
+ );
+
return (
-
- -
- {id ? : name}:
-
- -
+ <>
+
- {abbr ? {message} : message}
+ -
{url ? (
- {value}
+ {value ? (
+ truncate ? (
+
+ ) : (
+ value
+ )
+ ) : (
+ children
+ )}
) : undefined}
-
+ >
);
};