diff --git a/ui/v2.5/.stylelintrc b/ui/v2.5/.stylelintrc index 7442e1729..430e9a879 100644 --- a/ui/v2.5/.stylelintrc +++ b/ui/v2.5/.stylelintrc @@ -86,7 +86,6 @@ "string-quotes": "double", "time-min-milliseconds": 100, "value-list-comma-space-after": "always-single-line", - "value-list-comma-space-before": "never", - "value-no-vendor-prefix": true + "value-list-comma-space-before": "never" }, } diff --git a/ui/v2.5/src/components/Changelog/versions/v050.md b/ui/v2.5/src/components/Changelog/versions/v050.md index a8e26e1b8..9baae1a1b 100644 --- a/ui/v2.5/src/components/Changelog/versions/v050.md +++ b/ui/v2.5/src/components/Changelog/versions/v050.md @@ -1,4 +1,5 @@ ### 🎨 Improvements +* Truncate long text and show on hover. * Show scene studio as text where image is missing. * Use natural sort for titles and movie names. * Support optional preview and sprite generation during scanning. diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx index 9b3db9a12..6b6100f8e 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/Gallery.tsx @@ -108,17 +108,16 @@ export const Gallery: React.FC = () => { - + - + { - + {/* */} - + diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx index a8901bb36..43203395b 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryDetailPanel.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedDate } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { TextUtils } from "src/utils"; -import { TagLink } from "src/components/Shared"; +import { TagLink, TruncatedText } from "src/components/Shared"; import { PerformerCard } from "src/components/Performers/PerformerCard"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; @@ -58,17 +58,16 @@ export const GalleryDetailPanel: React.FC = (props) => { // filename should use entire row if there is no studio const galleryDetailsWidth = props.gallery.studio ? "col-9" : "col-12"; + const title = + props.gallery.title ?? TextUtils.fileNameFromPath(props.gallery.path ?? ""); return ( <>
-
-

- {props.gallery.title ?? - TextUtils.fileNameFromPath(props.gallery.path ?? "")} -

-
+

+ +

{props.gallery.date ? (
= ( return (
Checksum - {props.gallery.checksum} +
); } function renderPath() { - const { - gallery: { path }, - } = props; + const filePath = `file://${props.gallery.path}`; + return ( ); } diff --git a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx index 0abceb958..f61f2bbba 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/Image.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/Image.tsx @@ -144,17 +144,16 @@ export const Image: React.FC = () => {
- + - + = (props) => {
-

- {props.image.title ?? - TextUtils.fileNameFromPath(props.image.path)} +

+

{props.image.rating ? ( diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx index 05fb698db..9d6d3eded 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageFileInfoPanel.tsx @@ -2,6 +2,7 @@ import React from "react"; import { FormattedNumber } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { TextUtils } from "src/utils"; +import { TruncatedText } from "src/components/Shared"; interface IImageFileInfoPanelProps { image: GQL.ImageDataFragment; @@ -14,7 +15,7 @@ export const ImageFileInfoPanel: React.FC = ( return (
Checksum - {props.image.checksum} +
); } @@ -26,9 +27,9 @@ export const ImageFileInfoPanel: React.FC = ( return (
Path - - {`file://${props.image.path}`}{" "} - + + + {" "}
); } diff --git a/ui/v2.5/src/components/Movies/MovieCard.tsx b/ui/v2.5/src/components/Movies/MovieCard.tsx index 1980f72f0..9dff7cbf9 100644 --- a/ui/v2.5/src/components/Movies/MovieCard.tsx +++ b/ui/v2.5/src/components/Movies/MovieCard.tsx @@ -1,7 +1,7 @@ import React, { FunctionComponent } from "react"; import { FormattedPlural } from "react-intl"; import * as GQL from "src/core/generated-graphql"; -import { BasicCard } from "../Shared/BasicCard"; +import { BasicCard, TruncatedText } from "src/components/Shared"; interface IProps { movie: GQL.MovieDataFragment; @@ -61,7 +61,9 @@ export const MovieCard: FunctionComponent = (props: IProps) => { } details={ <> -
{props.movie.name}
+
+ +
{maybeRenderSceneNumber()} } diff --git a/ui/v2.5/src/components/Performers/PerformerCard.tsx b/ui/v2.5/src/components/Performers/PerformerCard.tsx index 958f2803e..4cb317f32 100644 --- a/ui/v2.5/src/components/Performers/PerformerCard.tsx +++ b/ui/v2.5/src/components/Performers/PerformerCard.tsx @@ -3,8 +3,7 @@ import { Link } from "react-router-dom"; import { FormattedNumber, FormattedPlural, FormattedMessage } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { NavUtils, TextUtils } from "src/utils"; -import { CountryFlag } from "src/components/Shared"; -import { BasicCard } from "../Shared/BasicCard"; +import { BasicCard, CountryFlag, TruncatedText } from "src/components/Shared"; interface IPerformerCardProps { performer: GQL.PerformerDataFragment; @@ -51,7 +50,9 @@ export const PerformerCard: React.FC = ({ } details={ <> -
{performer.name}
+
+ +
{age !== 0 ?
{ageString}
: ""} diff --git a/ui/v2.5/src/components/Performers/PerformerListTable.tsx b/ui/v2.5/src/components/Performers/PerformerListTable.tsx index 97ddad3dd..c746b1e9d 100644 --- a/ui/v2.5/src/components/Performers/PerformerListTable.tsx +++ b/ui/v2.5/src/components/Performers/PerformerListTable.tsx @@ -4,7 +4,7 @@ import React from "react"; import { Button, Table } from "react-bootstrap"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; -import { Icon } from "src/components/Shared"; +import { Icon, TruncatedText } from "src/components/Shared"; import { NavUtils } from "src/utils"; interface IPerformerListTableProps { @@ -27,7 +27,9 @@ export const PerformerListTable: React.FC = ( -
{performer.name}
+
+ +
{performer.aliases ? performer.aliases : ""} diff --git a/ui/v2.5/src/components/Scenes/SceneCard.tsx b/ui/v2.5/src/components/Scenes/SceneCard.tsx index 9e441f4bc..bbcbe4f6a 100644 --- a/ui/v2.5/src/components/Scenes/SceneCard.tsx +++ b/ui/v2.5/src/components/Scenes/SceneCard.tsx @@ -4,7 +4,13 @@ import { Link } from "react-router-dom"; import cx from "classnames"; import * as GQL from "src/core/generated-graphql"; import { useConfiguration } from "src/core/StashService"; -import { Icon, TagLink, HoverPopover, SweatDrops } from "src/components/Shared"; +import { + Icon, + TagLink, + HoverPopover, + SweatDrops, + TruncatedText, +} from "src/components/Shared"; import { TextUtils } from "src/utils"; interface IScenePreviewProps { @@ -363,14 +369,18 @@ export const SceneCard: React.FC = (
- {props.scene.title - ? props.scene.title - : TextUtils.fileNameFromPath(props.scene.path)} +
{props.scene.date}

- {props.scene.details && - TextUtils.truncate(props.scene.details, 100, "... (continued)")} +

diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 6b8352ef3..a042ae651 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -252,37 +252,36 @@ export const Scene: React.FC = () => {
- + - + - + {scene.gallery ? ( - + ) : ( "" )} - + - + = (props) => {
-

- {props.scene.title ?? - TextUtils.fileNameFromPath(props.scene.path)} +

+

{props.scene.date ? ( diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx index 4edff654b..6a41e67ed 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneFileInfoPanel.tsx @@ -2,6 +2,7 @@ import React from "react"; import { FormattedNumber } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { TextUtils } from "src/utils"; +import { TruncatedText } from "src/components/Shared"; interface ISceneFileInfoPanelProps { scene: GQL.SceneDataFragment; @@ -15,7 +16,7 @@ export const SceneFileInfoPanel: React.FC = ( return (
Hash - {props.scene.oshash} +
); } @@ -26,7 +27,7 @@ export const SceneFileInfoPanel: React.FC = ( return (
Checksum - {props.scene.checksum} +
); } @@ -39,9 +40,9 @@ export const SceneFileInfoPanel: React.FC = ( return (
Path - - {`file://${props.scene.path}`}{" "} - + + + {" "}
); } @@ -50,11 +51,9 @@ export const SceneFileInfoPanel: React.FC = ( return (
Stream - - - {props.scene.paths.stream} - {" "} - + + + {" "}
); } @@ -92,9 +91,10 @@ export const SceneFileInfoPanel: React.FC = ( return (
Duration - - {TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)} - +
); } @@ -106,9 +106,10 @@ export const SceneFileInfoPanel: React.FC = ( return (
Dimensions - - {props.scene.file.width} x {props.scene.file.height} - +
); } @@ -154,9 +155,7 @@ export const SceneFileInfoPanel: React.FC = ( return (
Video Codec - - {props.scene.file.video_codec} - +
); } @@ -168,9 +167,7 @@ export const SceneFileInfoPanel: React.FC = ( return (
Audio Codec - - {props.scene.file.audio_codec} - +
); } @@ -182,9 +179,9 @@ export const SceneFileInfoPanel: React.FC = ( return (
Downloaded From - - {props.scene.url} - + + +
); } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx index ab7323f34..814f50abf 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneVideoFilterPanel.tsx @@ -1,7 +1,8 @@ import React, { useState } from "react"; import { Button, Form } from "react-bootstrap"; -import { JWUtils } from "../../../utils"; -import * as GQL from "../../../core/generated-graphql"; +import { TruncatedText } from "src/components/Shared"; +import { JWUtils } from "src/utils"; +import * as GQL from "src/core/generated-graphql"; interface ISceneVideoFilterPanelProps { scene: GQL.SceneDataFragment; @@ -328,12 +329,12 @@ export const SceneVideoFilterPanel: React.FC = ( /> sliderProps.setValue(sliderProps.range.default)} onKeyPress={() => sliderProps.setValue(sliderProps.range.default)} > - {sliderProps.displayValue} +
); diff --git a/ui/v2.5/src/components/Scenes/SceneListTable.tsx b/ui/v2.5/src/components/Scenes/SceneListTable.tsx index f85ddf5ba..518e7f2a5 100644 --- a/ui/v2.5/src/components/Scenes/SceneListTable.tsx +++ b/ui/v2.5/src/components/Scenes/SceneListTable.tsx @@ -5,7 +5,7 @@ import { Table, Button } from "react-bootstrap"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { NavUtils, TextUtils } from "src/utils"; -import { Icon } from "src/components/Shared"; +import { Icon, TruncatedText } from "src/components/Shared"; interface ISceneListTableProps { scenes: GQL.SlimSceneDataFragment[]; @@ -50,8 +50,10 @@ export const SceneListTable: React.FC = ( -
- {scene.title ?? TextUtils.fileNameFromPath(scene.path)} +
+
diff --git a/ui/v2.5/src/components/Scenes/styles.scss b/ui/v2.5/src/components/Scenes/styles.scss index d4b23d796..a9e56ecd1 100644 --- a/ui/v2.5/src/components/Scenes/styles.scss +++ b/ui/v2.5/src/components/Scenes/styles.scss @@ -20,12 +20,6 @@ .card-section { margin-bottom: 0; padding: 0.5rem 1rem 0 1rem; - - &-title { - overflow: hidden; - overflow-wrap: normal; - text-overflow: ellipsis; - } } .performer-tag-container, diff --git a/ui/v2.5/src/components/Shared/TruncatedText.tsx b/ui/v2.5/src/components/Shared/TruncatedText.tsx new file mode 100644 index 000000000..4c450403c --- /dev/null +++ b/ui/v2.5/src/components/Shared/TruncatedText.tsx @@ -0,0 +1,70 @@ +import React, { useRef, useState } from "react"; +import { Overlay, Tooltip } from "react-bootstrap"; +import { Placement } from "react-bootstrap/Overlay"; +import { debounce } from "lodash"; +import cx from "classnames"; + +const CLASSNAME = "TruncatedText"; +const CLASSNAME_TOOLTIP = `${CLASSNAME}-tooltip`; + +interface ITruncatedTextProps { + text?: string | null; + lineCount?: number; + placement?: Placement; + delay?: number; + className?: string; +} + +const TruncatedText: React.FC = ({ + text, + className, + lineCount = 1, + placement = "bottom", + delay = 1000, +}) => { + const [showTooltip, setShowTooltip] = useState(false); + const target = useRef(null); + + if (!text) return <>; + + const startShowingTooltip = debounce(() => setShowTooltip(true), delay); + + const handleFocus = (element: HTMLElement) => { + // Check if visible size is smaller than the content size + if ( + element.offsetWidth < element.scrollWidth || + element.offsetHeight + 10 < element.scrollHeight + ) + startShowingTooltip(); + }; + + const handleBlur = () => { + startShowingTooltip.cancel(); + setShowTooltip(false); + }; + + const overlay = ( + + + {text} + + + ); + + return ( +
handleFocus(e.currentTarget)} + onFocus={(e) => handleFocus(e.currentTarget)} + onMouseLeave={handleBlur} + onBlur={handleBlur} + > + {text} + {overlay} +
+ ); +}; + +export default TruncatedText; diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index eef9380c6..e299a1eb0 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -21,3 +21,5 @@ export { SweatDrops } from "./SweatDrops"; export { default as CountryFlag } from "./CountryFlag"; export { default as SuccessIcon } from "./SuccessIcon"; export { default as ErrorMessage } from "./ErrorMessage"; +export { default as TruncatedText } from "./TruncatedText"; +export { BasicCard } from "./BasicCard"; diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 982eb218a..e99b87f82 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -172,3 +172,13 @@ button.collapse-button.btn-primary:not(:disabled):not(.disabled):active { transition: opacity 0.5s; } } + +.TruncatedText { + -webkit-box-orient: vertical; + display: -webkit-box; + overflow: hidden; + + &-tooltip .tooltip-inner { + max-width: 300px; + } +} diff --git a/ui/v2.5/src/components/Studios/StudioCard.tsx b/ui/v2.5/src/components/Studios/StudioCard.tsx index f521d94c4..5e8db8fa9 100644 --- a/ui/v2.5/src/components/Studios/StudioCard.tsx +++ b/ui/v2.5/src/components/Studios/StudioCard.tsx @@ -3,7 +3,7 @@ import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { FormattedPlural } from "react-intl"; import { NavUtils } from "src/utils"; -import { BasicCard } from "../Shared/BasicCard"; +import { BasicCard, TruncatedText } from "src/components/Shared"; interface IProps { studio: GQL.StudioDataFragment; @@ -65,7 +65,9 @@ export const StudioCard: React.FC = ({ } details={ <> -
{studio.name}
+
+ +
{studio.scene_count}  = ({
Name: - {performer.name} +
Gender: - - {performer.gender && genderToString(performer.gender)} - +
Birthdate: - - {performer.birthdate ?? "Unknown"} - +
Ethnicity: - - {performer.ethnicity} - +
Country: - - {performer.country ?? ""} - +
Eye Color: - - {performer.eye_color} - +
Height: - {performer.height} +
Measurements: - - {performer.measurements} - +
{performer?.gender !== GQL.GenderEnum.Male && (
Fake Tits: - {performer.fake_tits} +
)}
Career Length: - - {performer.career_length} - +
Tattoos: - {performer.tattoos} +
Piercings: - {performer.piercings} +
{images.length > 0 && ( diff --git a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx index cb08f2e47..f3fe70fca 100755 --- a/ui/v2.5/src/components/Tagger/StashSearchResult.tsx +++ b/ui/v2.5/src/components/Tagger/StashSearchResult.tsx @@ -5,7 +5,11 @@ import { uniq } from "lodash"; import { blobToBase64 } from "base64-blob"; import * as GQL from "src/core/generated-graphql"; -import { LoadingIndicator, SuccessIcon } from "src/components/Shared"; +import { + LoadingIndicator, + SuccessIcon, + TruncatedText, +} from "src/components/Shared"; import PerformerResult, { PerformerOperation } from "./PerformerResult"; import StudioResult, { StudioOperation } from "./StudioResult"; import { IStashBoxScene } from "./utils"; @@ -324,10 +328,10 @@ const StashSearchResult: React.FC = ({ rel="noopener noreferrer" className="scene-link" > - {scene?.title} + ) : ( - {scene?.title} + ); const saveEnabled = @@ -358,9 +362,7 @@ const StashSearchResult: React.FC = ({ />
-

- {sceneTitle} -

+

{sceneTitle}

{scene?.studio?.name} • {scene?.date}
diff --git a/ui/v2.5/src/components/Tagger/Tagger.tsx b/ui/v2.5/src/components/Tagger/Tagger.tsx index 41c138bad..fd9354d16 100755 --- a/ui/v2.5/src/components/Tagger/Tagger.tsx +++ b/ui/v2.5/src/components/Tagger/Tagger.tsx @@ -5,7 +5,7 @@ import { HashLink } from "react-router-hash-link"; import { ScenePreview } from "src/components/Scenes/SceneCard"; import * as GQL from "src/core/generated-graphql"; -import { LoadingIndicator } from "src/components/Shared"; +import { LoadingIndicator, TruncatedText } from "src/components/Shared"; import { stashBoxQuery, stashBoxBatchQuery, @@ -396,12 +396,12 @@ const TaggerList: React.FC = ({
- {originalDir} - - {`${file}.${ext}`} +
diff --git a/ui/v2.5/src/components/Tags/TagCard.tsx b/ui/v2.5/src/components/Tags/TagCard.tsx index d157703df..c6943c7f7 100644 --- a/ui/v2.5/src/components/Tags/TagCard.tsx +++ b/ui/v2.5/src/components/Tags/TagCard.tsx @@ -3,7 +3,7 @@ import React from "react"; import { Link } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { NavUtils } from "src/utils"; -import { Icon } from "../Shared"; +import { Icon, TruncatedText } from "../Shared"; import { BasicCard } from "../Shared/BasicCard"; interface IProps { @@ -73,7 +73,11 @@ export const TagCard: React.FC = ({ src={tag.image_path ?? ""} /> } - details={
{tag.name}
} + details={ +
+ +
+ } popovers={maybeRenderPopoverButtonGroup()} selected={selected} selecting={selecting} diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index 1ea04b60e..b2710414b 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -18,15 +18,6 @@ const Units: Unit[] = [ ]; const shortUnits = ["B", "KB", "MB", "GB", "TB", "PB"]; -const truncate = ( - value?: string, - limit: number = 100, - tail: string = "..." -) => { - if (!value) return ""; - return value.length > limit ? value.substring(0, limit) + tail : value; -}; - const fileSize = (bytes: number = 0) => { if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes)) return { size: 0, unit: Units[0] }; @@ -145,7 +136,6 @@ const formatDate = (intl: IntlShape, date?: string) => { }; const TextUtils = { - truncate, fileSize, formatFileSizeUnit, secondsToTimestamp,