Add TruncatedText component (#932)

This commit is contained in:
InfiniteTF
2020-11-27 03:01:37 +01:00
committed by GitHub
parent 54c9f167ba
commit a45c1111be
27 changed files with 244 additions and 147 deletions

View File

@@ -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"
},
}

View File

@@ -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.

View File

@@ -108,17 +108,16 @@ export const Gallery: React.FC = () => {
</div>
<Tab.Content>
<Tab.Pane eventKey="gallery-details-panel" title="Details">
<Tab.Pane eventKey="gallery-details-panel">
<GalleryDetailPanel gallery={gallery} />
</Tab.Pane>
<Tab.Pane
className="file-info-panel"
eventKey="gallery-file-info-panel"
title="File Info"
>
<GalleryFileInfoPanel gallery={gallery} />
</Tab.Pane>
<Tab.Pane eventKey="gallery-edit-panel" title="Edit">
<Tab.Pane eventKey="gallery-edit-panel">
<GalleryEditPanel
isVisible={activeTabKey === "gallery-edit-panel"}
isNew={false}
@@ -154,11 +153,11 @@ export const Gallery: React.FC = () => {
</div>
<Tab.Content>
<Tab.Pane eventKey="images" title="Images">
<Tab.Pane eventKey="images">
{/* <GalleryViewer gallery={gallery} /> */}
<GalleryImagesPanel gallery={gallery} />
</Tab.Pane>
<Tab.Pane eventKey="add" title="Add">
<Tab.Pane eventKey="add">
<GalleryAddPanel gallery={gallery} />
</Tab.Pane>
</Tab.Content>

View File

@@ -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<IGalleryDetailProps> = (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 (
<>
<div className="row">
<div className={`${galleryDetailsWidth} col-xl-12 gallery-details`}>
<div className="gallery-header d-xl-none">
<h3 className="text-truncate">
{props.gallery.title ??
TextUtils.fileNameFromPath(props.gallery.path ?? "")}
</h3>
</div>
<h3 className="gallery-header d-xl-none">
<TruncatedText text={title} />
</h3>
{props.gallery.date ? (
<h5>
<FormattedDate

View File

@@ -1,5 +1,6 @@
import React from "react";
import * as GQL from "src/core/generated-graphql";
import { TruncatedText } from "src/components/Shared";
interface IGalleryFileInfoPanelProps {
gallery: GQL.GalleryDataFragment;
@@ -12,21 +13,20 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Checksum</span>
<span className="col-8 text-truncate">{props.gallery.checksum}</span>
<TruncatedText className="col-8" text={props.gallery.checksum} />
</div>
);
}
function renderPath() {
const {
gallery: { path },
} = props;
const filePath = `file://${props.gallery.path}`;
return (
<div className="row">
<span className="col-4">Path</span>
<span className="col-8 text-truncate">
<a href={`file://${path}`}>{`file://${props.gallery.path}`}</a>{" "}
</span>
<a href={filePath} className="col-8">
<TruncatedText text={filePath} />
</a>
</div>
);
}

View File

@@ -144,17 +144,16 @@ export const Image: React.FC = () => {
</div>
<Tab.Content>
<Tab.Pane eventKey="image-details-panel" title="Details">
<Tab.Pane eventKey="image-details-panel">
<ImageDetailPanel image={image} />
</Tab.Pane>
<Tab.Pane
className="file-info-panel"
eventKey="image-file-info-panel"
title="File Info"
>
<ImageFileInfoPanel image={image} />
</Tab.Pane>
<Tab.Pane eventKey="image-edit-panel" title="Edit">
<Tab.Pane eventKey="image-edit-panel">
<ImageEditPanel
isVisible={activeTabKey === "image-edit-panel"}
image={image}

View File

@@ -2,7 +2,7 @@ import React from "react";
import { Link } from "react-router-dom";
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";
@@ -61,9 +61,13 @@ export const ImageDetailPanel: React.FC<IImageDetailProps> = (props) => {
<div className="row">
<div className={`${imageDetailsWidth} col-xl-12 image-details`}>
<div className="image-header d-xl-none">
<h3 className="text-truncate">
{props.image.title ??
TextUtils.fileNameFromPath(props.image.path)}
<h3>
<TruncatedText
text={
props.image.title ??
TextUtils.fileNameFromPath(props.image.path)
}
/>
</h3>
</div>
{props.image.rating ? (

View File

@@ -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<IImageFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Checksum</span>
<span className="col-8 text-truncate">{props.image.checksum}</span>
<TruncatedText className="col-8" text={props.image.checksum} />
</div>
);
}
@@ -26,9 +27,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Path</span>
<span className="col-8 text-truncate">
<a href={`file://${path}`}>{`file://${props.image.path}`}</a>{" "}
</span>
<a href={`file://${path}`} className="col-8">
<TruncatedText text={`file://${props.image.path}`} />
</a>{" "}
</div>
);
}

View File

@@ -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<IProps> = (props: IProps) => {
}
details={
<>
<h5 className="text-truncate">{props.movie.name}</h5>
<h5>
<TruncatedText text={props.movie.name} />
</h5>
{maybeRenderSceneNumber()}
</>
}

View File

@@ -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<IPerformerCardProps> = ({
}
details={
<>
<h5 className="text-truncate">{performer.name}</h5>
<h5>
<TruncatedText text={performer.name} />
</h5>
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<Link to={NavUtils.makePerformersCountryUrl(performer)}>
<CountryFlag country={performer.country} />

View File

@@ -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<IPerformerListTableProps> = (
</td>
<td className="text-left">
<Link to={`/performers/${performer.id}`}>
<h5 className="text-truncate">{performer.name}</h5>
<h5>
<TruncatedText text={performer.name} />
</h5>
</Link>
</td>
<td>{performer.aliases ? performer.aliases : ""}</td>

View File

@@ -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<ISceneCardProps> = (
</div>
<div className="card-section">
<h5 className="card-section-title">
{props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)}
<TruncatedText
text={
props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)
}
lineCount={2}
/>
</h5>
<span>{props.scene.date}</span>
<p>
{props.scene.details &&
TextUtils.truncate(props.scene.details, 100, "... (continued)")}
<TruncatedText text={props.scene.details} lineCount={3} />
</p>
</div>

View File

@@ -252,37 +252,36 @@ export const Scene: React.FC = () => {
</div>
<Tab.Content>
<Tab.Pane eventKey="scene-details-panel" title="Details">
<Tab.Pane eventKey="scene-details-panel">
<SceneDetailPanel scene={scene} />
</Tab.Pane>
<Tab.Pane eventKey="scene-markers-panel" title="Markers">
<Tab.Pane eventKey="scene-markers-panel">
<SceneMarkersPanel
scene={scene}
onClickMarker={onClickMarker}
isVisible={activeTabKey === "scene-markers-panel"}
/>
</Tab.Pane>
<Tab.Pane eventKey="scene-movie-panel" title="Movies">
<Tab.Pane eventKey="scene-movie-panel">
<SceneMoviePanel scene={scene} />
</Tab.Pane>
{scene.gallery ? (
<Tab.Pane eventKey="scene-gallery-panel" title="Gallery">
<Tab.Pane eventKey="scene-gallery-panel">
<GalleryViewer gallery={scene.gallery} />
</Tab.Pane>
) : (
""
)}
<Tab.Pane eventKey="scene-video-filter-panel" title="Filter">
<Tab.Pane eventKey="scene-video-filter-panel">
<SceneVideoFilterPanel scene={scene} />
</Tab.Pane>
<Tab.Pane
className="file-info-panel"
eventKey="scene-file-info-panel"
title="File Info"
>
<SceneFileInfoPanel scene={scene} />
</Tab.Pane>
<Tab.Pane eventKey="scene-edit-panel" title="Edit">
<Tab.Pane eventKey="scene-edit-panel">
<SceneEditPanel
isVisible={activeTabKey === "scene-edit-panel"}
scene={scene}

View File

@@ -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 "./RatingStars";
@@ -63,9 +63,13 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
<div className="row">
<div className={`${sceneDetailsWidth} col-xl-12 scene-details`}>
<div className="scene-header d-xl-none">
<h3 className="text-truncate">
{props.scene.title ??
TextUtils.fileNameFromPath(props.scene.path)}
<h3>
<TruncatedText
text={
props.scene.title ??
TextUtils.fileNameFromPath(props.scene.path)
}
/>
</h3>
</div>
{props.scene.date ? (

View File

@@ -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<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Hash</span>
<span className="col-8 text-truncate">{props.scene.oshash}</span>
<TruncatedText className="col-8" text={props.scene.oshash} />
</div>
);
}
@@ -26,7 +27,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Checksum</span>
<span className="col-8 text-truncate">{props.scene.checksum}</span>
<TruncatedText className="col-8" text={props.scene.checksum} />
</div>
);
}
@@ -39,9 +40,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Path</span>
<span className="col-8 text-truncate">
<a href={`file://${path}`}>{`file://${props.scene.path}`}</a>{" "}
</span>
<a href={`file://${path}`} className="col-8">
<TruncatedText text={`file://${props.scene.path}`} />
</a>{" "}
</div>
);
}
@@ -50,11 +51,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Stream</span>
<span className="col-8 text-truncate">
<a href={props.scene.paths.stream ?? ""}>
{props.scene.paths.stream}
</a>{" "}
</span>
<a href={props.scene.paths.stream ?? ""} className="col-8">
<TruncatedText text={props.scene.paths.stream} />
</a>{" "}
</div>
);
}
@@ -92,9 +91,10 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Duration</span>
<span className="col-8 text-truncate">
{TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
</span>
<TruncatedText
className="col-8"
text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
/>
</div>
);
}
@@ -106,9 +106,10 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Dimensions</span>
<span className="col-8 text-truncate">
{props.scene.file.width} x {props.scene.file.height}
</span>
<TruncatedText
className="col-8"
text={`${props.scene.file.width} x ${props.scene.file.height}`}
/>
</div>
);
}
@@ -154,9 +155,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Video Codec</span>
<span className="col-8 text-truncate">
{props.scene.file.video_codec}
</span>
<TruncatedText className="col-8" text={props.scene.file.video_codec} />
</div>
);
}
@@ -168,9 +167,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Audio Codec</span>
<span className="col-8 text-truncate">
{props.scene.file.audio_codec}
</span>
<TruncatedText className="col-8" text={props.scene.file.audio_codec} />
</div>
);
}
@@ -182,9 +179,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return (
<div className="row">
<span className="col-4">Downloaded From</span>
<span className="col-8 text-truncate">
<a href={TextUtils.sanitiseURL(props.scene.url)}>{props.scene.url}</a>
</span>
<a href={TextUtils.sanitiseURL(props.scene.url)} className="col-8">
<TruncatedText text={props.scene.url} />
</a>
</div>
);
}

View File

@@ -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<ISceneVideoFilterPanelProps> = (
/>
</span>
<span
className="col-sm-2 text-truncate"
className="col-sm-2"
role="presentation"
onClick={() => sliderProps.setValue(sliderProps.range.default)}
onKeyPress={() => sliderProps.setValue(sliderProps.range.default)}
>
{sliderProps.displayValue}
<TruncatedText text={sliderProps.displayValue} />
</span>
</div>
);

View File

@@ -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<ISceneListTableProps> = (
</td>
<td className="text-left">
<Link to={`/scenes/${scene.id}`}>
<h5 className="text-truncate">
{scene.title ?? TextUtils.fileNameFromPath(scene.path)}
<h5>
<TruncatedText
text={scene.title ?? TextUtils.fileNameFromPath(scene.path)}
/>
</h5>
</Link>
</td>

View File

@@ -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,

View File

@@ -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<ITruncatedTextProps> = ({
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 = (
<Overlay target={target.current} show={showTooltip} placement={placement}>
<Tooltip id={CLASSNAME} className={CLASSNAME_TOOLTIP}>
{text}
</Tooltip>
</Overlay>
);
return (
<div
className={cx(CLASSNAME, className)}
style={{ WebkitLineClamp: lineCount }}
ref={target}
onMouseEnter={(e) => handleFocus(e.currentTarget)}
onFocus={(e) => handleFocus(e.currentTarget)}
onMouseLeave={handleBlur}
onBlur={handleBlur}
>
{text}
{overlay}
</div>
);
};
export default TruncatedText;

View File

@@ -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";

View File

@@ -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;
}
}

View File

@@ -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<IProps> = ({
}
details={
<>
<h5 className="text-truncate">{studio.name}</h5>
<h5>
<TruncatedText text={studio.name} />
</h5>
<span>
{studio.scene_count}&nbsp;
<FormattedPlural

View File

@@ -2,7 +2,12 @@ import React, { useState } from "react";
import { Button } from "react-bootstrap";
import cx from "classnames";
import { LoadingIndicator, Icon, Modal } from "src/components/Shared";
import {
LoadingIndicator,
Icon,
Modal,
TruncatedText,
} from "src/components/Shared";
import * as GQL from "src/core/generated-graphql";
import { genderToString } from "src/core/StashService";
import { IStashBoxPerformer } from "./utils";
@@ -64,67 +69,65 @@ const PerformerModal: React.FC<IPerformerModalProps> = ({
</div>
<div className="row no-gutters">
<strong className="col-6">Name:</strong>
<span className="col-6 text-truncate">{performer.name}</span>
<TruncatedText className="col-6" text={performer.name} />
</div>
<div className="row no-gutters">
<strong className="col-6">Gender:</strong>
<span className="col-6 text-truncate text-capitalize">
{performer.gender && genderToString(performer.gender)}
</span>
<TruncatedText
className="col-6 text-capitalize"
text={performer.gender && genderToString(performer.gender)}
/>
</div>
<div className="row no-gutters">
<strong className="col-6">Birthdate:</strong>
<span className="col-6 text-truncate">
{performer.birthdate ?? "Unknown"}
</span>
<TruncatedText
className="col-6"
text={performer.birthdate ?? "Unknown"}
/>
</div>
<div className="row no-gutters">
<strong className="col-6">Ethnicity:</strong>
<span className="col-6 text-truncate text-capitalize">
{performer.ethnicity}
</span>
<TruncatedText
className="col-6 text-capitalize"
text={performer.ethnicity}
/>
</div>
<div className="row no-gutters">
<strong className="col-6">Country:</strong>
<span className="col-6 text-truncate">
{performer.country ?? ""}
</span>
<TruncatedText className="col-6" text={performer.country ?? ""} />
</div>
<div className="row no-gutters">
<strong className="col-6">Eye Color:</strong>
<span className="col-6 text-truncate text-capitalize">
{performer.eye_color}
</span>
<TruncatedText
className="col-6 text-capitalize"
text={performer.eye_color}
/>
</div>
<div className="row no-gutters">
<strong className="col-6">Height:</strong>
<span className="col-6 text-truncate">{performer.height}</span>
<TruncatedText className="col-6" text={performer.height} />
</div>
<div className="row no-gutters">
<strong className="col-6">Measurements:</strong>
<span className="col-6 text-truncate">
{performer.measurements}
</span>
<TruncatedText className="col-6" text={performer.measurements} />
</div>
{performer?.gender !== GQL.GenderEnum.Male && (
<div className="row no-gutters">
<strong className="col-6">Fake Tits:</strong>
<span className="col-6 text-truncate">{performer.fake_tits}</span>
<TruncatedText className="col-6" text={performer.fake_tits} />
</div>
)}
<div className="row no-gutters">
<strong className="col-6">Career Length:</strong>
<span className="col-6 text-truncate">
{performer.career_length}
</span>
<TruncatedText className="col-6" text={performer.career_length} />
</div>
<div className="row no-gutters">
<strong className="col-6">Tattoos:</strong>
<span className="col-6 text-truncate">{performer.tattoos}</span>
<TruncatedText className="col-6" text={performer.tattoos} />
</div>
<div className="row no-gutters ">
<strong className="col-6">Piercings:</strong>
<span className="col-6 text-truncate">{performer.piercings}</span>
<TruncatedText className="col-6" text={performer.piercings} />
</div>
</div>
{images.length > 0 && (

View File

@@ -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<IStashSearchResultProps> = ({
rel="noopener noreferrer"
className="scene-link"
>
{scene?.title}
<TruncatedText text={scene?.title} />
</a>
) : (
<span>{scene?.title}</span>
<TruncatedText text={scene?.title} />
);
const saveEnabled =
@@ -358,9 +362,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
/>
</a>
<div className="d-flex flex-column justify-content-center scene-metadata">
<h4 className="text-truncate" title={scene?.title ?? ""}>
{sceneTitle}
</h4>
<h4>{sceneTitle}</h4>
<h5>
{scene?.studio?.name} {scene?.date}
</h5>

View File

@@ -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<ITaggerListProps> = ({
</div>
<Link
to={`/scenes/${scene.id}`}
className="scene-link text-truncate w-100"
title={scene.path}
className="scene-link overflow-hidden"
>
{originalDir}
<wbr />
{`${file}.${ext}`}
<TruncatedText
text={`${originalDir}\u200B${file}${ext}`}
lineCount={2}
/>
</Link>
</div>
<div className="col-md-6 my-1 align-self-center">

View File

@@ -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<IProps> = ({
src={tag.image_path ?? ""}
/>
}
details={<h5 className="text-truncate">{tag.name}</h5>}
details={
<h5>
<TruncatedText text={tag.name} />
</h5>
}
popovers={maybeRenderPopoverButtonGroup()}
selected={selected}
selecting={selecting}

View File

@@ -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,