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", "string-quotes": "double",
"time-min-milliseconds": 100, "time-min-milliseconds": 100,
"value-list-comma-space-after": "always-single-line", "value-list-comma-space-after": "always-single-line",
"value-list-comma-space-before": "never", "value-list-comma-space-before": "never"
"value-no-vendor-prefix": true
}, },
} }

View File

@@ -1,4 +1,5 @@
### 🎨 Improvements ### 🎨 Improvements
* Truncate long text and show on hover.
* Show scene studio as text where image is missing. * Show scene studio as text where image is missing.
* Use natural sort for titles and movie names. * Use natural sort for titles and movie names.
* Support optional preview and sprite generation during scanning. * Support optional preview and sprite generation during scanning.

View File

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

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
import { FormattedDate } from "react-intl"; import { FormattedDate } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; 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 { PerformerCard } from "src/components/Performers/PerformerCard";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; 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 // filename should use entire row if there is no studio
const galleryDetailsWidth = props.gallery.studio ? "col-9" : "col-12"; const galleryDetailsWidth = props.gallery.studio ? "col-9" : "col-12";
const title =
props.gallery.title ?? TextUtils.fileNameFromPath(props.gallery.path ?? "");
return ( return (
<> <>
<div className="row"> <div className="row">
<div className={`${galleryDetailsWidth} col-xl-12 gallery-details`}> <div className={`${galleryDetailsWidth} col-xl-12 gallery-details`}>
<div className="gallery-header d-xl-none"> <h3 className="gallery-header d-xl-none">
<h3 className="text-truncate"> <TruncatedText text={title} />
{props.gallery.title ?? </h3>
TextUtils.fileNameFromPath(props.gallery.path ?? "")}
</h3>
</div>
{props.gallery.date ? ( {props.gallery.date ? (
<h5> <h5>
<FormattedDate <FormattedDate

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import React from "react";
import { FormattedNumber } from "react-intl"; import { FormattedNumber } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { TruncatedText } from "src/components/Shared";
interface IImageFileInfoPanelProps { interface IImageFileInfoPanelProps {
image: GQL.ImageDataFragment; image: GQL.ImageDataFragment;
@@ -14,7 +15,7 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Checksum</span> <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> </div>
); );
} }
@@ -26,9 +27,9 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Path</span> <span className="col-4">Path</span>
<span className="col-8 text-truncate"> <a href={`file://${path}`} className="col-8">
<a href={`file://${path}`}>{`file://${props.image.path}`}</a>{" "} <TruncatedText text={`file://${props.image.path}`} />
</span> </a>{" "}
</div> </div>
); );
} }

View File

@@ -1,7 +1,7 @@
import React, { FunctionComponent } from "react"; import React, { FunctionComponent } from "react";
import { FormattedPlural } from "react-intl"; import { FormattedPlural } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { BasicCard } from "../Shared/BasicCard"; import { BasicCard, TruncatedText } from "src/components/Shared";
interface IProps { interface IProps {
movie: GQL.MovieDataFragment; movie: GQL.MovieDataFragment;
@@ -61,7 +61,9 @@ export const MovieCard: FunctionComponent<IProps> = (props: IProps) => {
} }
details={ details={
<> <>
<h5 className="text-truncate">{props.movie.name}</h5> <h5>
<TruncatedText text={props.movie.name} />
</h5>
{maybeRenderSceneNumber()} {maybeRenderSceneNumber()}
</> </>
} }

View File

@@ -3,8 +3,7 @@ import { Link } from "react-router-dom";
import { FormattedNumber, FormattedPlural, FormattedMessage } from "react-intl"; import { FormattedNumber, FormattedPlural, FormattedMessage } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils } from "src/utils";
import { CountryFlag } from "src/components/Shared"; import { BasicCard, CountryFlag, TruncatedText } from "src/components/Shared";
import { BasicCard } from "../Shared/BasicCard";
interface IPerformerCardProps { interface IPerformerCardProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
@@ -51,7 +50,9 @@ export const PerformerCard: React.FC<IPerformerCardProps> = ({
} }
details={ details={
<> <>
<h5 className="text-truncate">{performer.name}</h5> <h5>
<TruncatedText text={performer.name} />
</h5>
{age !== 0 ? <div className="text-muted">{ageString}</div> : ""} {age !== 0 ? <div className="text-muted">{ageString}</div> : ""}
<Link to={NavUtils.makePerformersCountryUrl(performer)}> <Link to={NavUtils.makePerformersCountryUrl(performer)}>
<CountryFlag country={performer.country} /> <CountryFlag country={performer.country} />

View File

@@ -4,7 +4,7 @@ import React from "react";
import { Button, Table } from "react-bootstrap"; import { Button, Table } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; 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"; import { NavUtils } from "src/utils";
interface IPerformerListTableProps { interface IPerformerListTableProps {
@@ -27,7 +27,9 @@ export const PerformerListTable: React.FC<IPerformerListTableProps> = (
</td> </td>
<td className="text-left"> <td className="text-left">
<Link to={`/performers/${performer.id}`}> <Link to={`/performers/${performer.id}`}>
<h5 className="text-truncate">{performer.name}</h5> <h5>
<TruncatedText text={performer.name} />
</h5>
</Link> </Link>
</td> </td>
<td>{performer.aliases ? performer.aliases : ""}</td> <td>{performer.aliases ? performer.aliases : ""}</td>

View File

@@ -4,7 +4,13 @@ import { Link } from "react-router-dom";
import cx from "classnames"; import cx from "classnames";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { useConfiguration } from "src/core/StashService"; 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"; import { TextUtils } from "src/utils";
interface IScenePreviewProps { interface IScenePreviewProps {
@@ -363,14 +369,18 @@ export const SceneCard: React.FC<ISceneCardProps> = (
</div> </div>
<div className="card-section"> <div className="card-section">
<h5 className="card-section-title"> <h5 className="card-section-title">
{props.scene.title <TruncatedText
? props.scene.title text={
: TextUtils.fileNameFromPath(props.scene.path)} props.scene.title
? props.scene.title
: TextUtils.fileNameFromPath(props.scene.path)
}
lineCount={2}
/>
</h5> </h5>
<span>{props.scene.date}</span> <span>{props.scene.date}</span>
<p> <p>
{props.scene.details && <TruncatedText text={props.scene.details} lineCount={3} />
TextUtils.truncate(props.scene.details, 100, "... (continued)")}
</p> </p>
</div> </div>

View File

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

View File

@@ -3,7 +3,7 @@ import { Link } from "react-router-dom";
import { FormattedDate } from "react-intl"; import { FormattedDate } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; 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 { PerformerCard } from "src/components/Performers/PerformerCard";
import { RatingStars } from "./RatingStars"; import { RatingStars } from "./RatingStars";
@@ -63,9 +63,13 @@ export const SceneDetailPanel: React.FC<ISceneDetailProps> = (props) => {
<div className="row"> <div className="row">
<div className={`${sceneDetailsWidth} col-xl-12 scene-details`}> <div className={`${sceneDetailsWidth} col-xl-12 scene-details`}>
<div className="scene-header d-xl-none"> <div className="scene-header d-xl-none">
<h3 className="text-truncate"> <h3>
{props.scene.title ?? <TruncatedText
TextUtils.fileNameFromPath(props.scene.path)} text={
props.scene.title ??
TextUtils.fileNameFromPath(props.scene.path)
}
/>
</h3> </h3>
</div> </div>
{props.scene.date ? ( {props.scene.date ? (

View File

@@ -2,6 +2,7 @@ import React from "react";
import { FormattedNumber } from "react-intl"; import { FormattedNumber } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { TruncatedText } from "src/components/Shared";
interface ISceneFileInfoPanelProps { interface ISceneFileInfoPanelProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
@@ -15,7 +16,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Hash</span> <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> </div>
); );
} }
@@ -26,7 +27,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Checksum</span> <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> </div>
); );
} }
@@ -39,9 +40,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Path</span> <span className="col-4">Path</span>
<span className="col-8 text-truncate"> <a href={`file://${path}`} className="col-8">
<a href={`file://${path}`}>{`file://${props.scene.path}`}</a>{" "} <TruncatedText text={`file://${props.scene.path}`} />
</span> </a>{" "}
</div> </div>
); );
} }
@@ -50,11 +51,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Stream</span> <span className="col-4">Stream</span>
<span className="col-8 text-truncate"> <a href={props.scene.paths.stream ?? ""} className="col-8">
<a href={props.scene.paths.stream ?? ""}> <TruncatedText text={props.scene.paths.stream} />
{props.scene.paths.stream} </a>{" "}
</a>{" "}
</span>
</div> </div>
); );
} }
@@ -92,9 +91,10 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Duration</span> <span className="col-4">Duration</span>
<span className="col-8 text-truncate"> <TruncatedText
{TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)} className="col-8"
</span> text={TextUtils.secondsToTimestamp(props.scene.file.duration ?? 0)}
/>
</div> </div>
); );
} }
@@ -106,9 +106,10 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Dimensions</span> <span className="col-4">Dimensions</span>
<span className="col-8 text-truncate"> <TruncatedText
{props.scene.file.width} x {props.scene.file.height} className="col-8"
</span> text={`${props.scene.file.width} x ${props.scene.file.height}`}
/>
</div> </div>
); );
} }
@@ -154,9 +155,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Video Codec</span> <span className="col-4">Video Codec</span>
<span className="col-8 text-truncate"> <TruncatedText className="col-8" text={props.scene.file.video_codec} />
{props.scene.file.video_codec}
</span>
</div> </div>
); );
} }
@@ -168,9 +167,7 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Audio Codec</span> <span className="col-4">Audio Codec</span>
<span className="col-8 text-truncate"> <TruncatedText className="col-8" text={props.scene.file.audio_codec} />
{props.scene.file.audio_codec}
</span>
</div> </div>
); );
} }
@@ -182,9 +179,9 @@ export const SceneFileInfoPanel: React.FC<ISceneFileInfoPanelProps> = (
return ( return (
<div className="row"> <div className="row">
<span className="col-4">Downloaded From</span> <span className="col-4">Downloaded From</span>
<span className="col-8 text-truncate"> <a href={TextUtils.sanitiseURL(props.scene.url)} className="col-8">
<a href={TextUtils.sanitiseURL(props.scene.url)}>{props.scene.url}</a> <TruncatedText text={props.scene.url} />
</span> </a>
</div> </div>
); );
} }

View File

@@ -1,7 +1,8 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Button, Form } from "react-bootstrap";
import { JWUtils } from "../../../utils"; import { TruncatedText } from "src/components/Shared";
import * as GQL from "../../../core/generated-graphql"; import { JWUtils } from "src/utils";
import * as GQL from "src/core/generated-graphql";
interface ISceneVideoFilterPanelProps { interface ISceneVideoFilterPanelProps {
scene: GQL.SceneDataFragment; scene: GQL.SceneDataFragment;
@@ -328,12 +329,12 @@ export const SceneVideoFilterPanel: React.FC<ISceneVideoFilterPanelProps> = (
/> />
</span> </span>
<span <span
className="col-sm-2 text-truncate" className="col-sm-2"
role="presentation" role="presentation"
onClick={() => sliderProps.setValue(sliderProps.range.default)} onClick={() => sliderProps.setValue(sliderProps.range.default)}
onKeyPress={() => sliderProps.setValue(sliderProps.range.default)} onKeyPress={() => sliderProps.setValue(sliderProps.range.default)}
> >
{sliderProps.displayValue} <TruncatedText text={sliderProps.displayValue} />
</span> </span>
</div> </div>
); );

View File

@@ -5,7 +5,7 @@ import { Table, Button } from "react-bootstrap";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils, TextUtils } from "src/utils"; import { NavUtils, TextUtils } from "src/utils";
import { Icon } from "src/components/Shared"; import { Icon, TruncatedText } from "src/components/Shared";
interface ISceneListTableProps { interface ISceneListTableProps {
scenes: GQL.SlimSceneDataFragment[]; scenes: GQL.SlimSceneDataFragment[];
@@ -50,8 +50,10 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
</td> </td>
<td className="text-left"> <td className="text-left">
<Link to={`/scenes/${scene.id}`}> <Link to={`/scenes/${scene.id}`}>
<h5 className="text-truncate"> <h5>
{scene.title ?? TextUtils.fileNameFromPath(scene.path)} <TruncatedText
text={scene.title ?? TextUtils.fileNameFromPath(scene.path)}
/>
</h5> </h5>
</Link> </Link>
</td> </td>

View File

@@ -20,12 +20,6 @@
.card-section { .card-section {
margin-bottom: 0; margin-bottom: 0;
padding: 0.5rem 1rem 0 1rem; padding: 0.5rem 1rem 0 1rem;
&-title {
overflow: hidden;
overflow-wrap: normal;
text-overflow: ellipsis;
}
} }
.performer-tag-container, .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 CountryFlag } from "./CountryFlag";
export { default as SuccessIcon } from "./SuccessIcon"; export { default as SuccessIcon } from "./SuccessIcon";
export { default as ErrorMessage } from "./ErrorMessage"; 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; 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 * as GQL from "src/core/generated-graphql";
import { FormattedPlural } from "react-intl"; import { FormattedPlural } from "react-intl";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { BasicCard } from "../Shared/BasicCard"; import { BasicCard, TruncatedText } from "src/components/Shared";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@@ -65,7 +65,9 @@ export const StudioCard: React.FC<IProps> = ({
} }
details={ details={
<> <>
<h5 className="text-truncate">{studio.name}</h5> <h5>
<TruncatedText text={studio.name} />
</h5>
<span> <span>
{studio.scene_count}&nbsp; {studio.scene_count}&nbsp;
<FormattedPlural <FormattedPlural

View File

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

View File

@@ -5,7 +5,11 @@ import { uniq } from "lodash";
import { blobToBase64 } from "base64-blob"; import { blobToBase64 } from "base64-blob";
import * as GQL from "src/core/generated-graphql"; 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 PerformerResult, { PerformerOperation } from "./PerformerResult";
import StudioResult, { StudioOperation } from "./StudioResult"; import StudioResult, { StudioOperation } from "./StudioResult";
import { IStashBoxScene } from "./utils"; import { IStashBoxScene } from "./utils";
@@ -324,10 +328,10 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
rel="noopener noreferrer" rel="noopener noreferrer"
className="scene-link" className="scene-link"
> >
{scene?.title} <TruncatedText text={scene?.title} />
</a> </a>
) : ( ) : (
<span>{scene?.title}</span> <TruncatedText text={scene?.title} />
); );
const saveEnabled = const saveEnabled =
@@ -358,9 +362,7 @@ const StashSearchResult: React.FC<IStashSearchResultProps> = ({
/> />
</a> </a>
<div className="d-flex flex-column justify-content-center scene-metadata"> <div className="d-flex flex-column justify-content-center scene-metadata">
<h4 className="text-truncate" title={scene?.title ?? ""}> <h4>{sceneTitle}</h4>
{sceneTitle}
</h4>
<h5> <h5>
{scene?.studio?.name} {scene?.date} {scene?.studio?.name} {scene?.date}
</h5> </h5>

View File

@@ -5,7 +5,7 @@ import { HashLink } from "react-router-hash-link";
import { ScenePreview } from "src/components/Scenes/SceneCard"; import { ScenePreview } from "src/components/Scenes/SceneCard";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { LoadingIndicator } from "src/components/Shared"; import { LoadingIndicator, TruncatedText } from "src/components/Shared";
import { import {
stashBoxQuery, stashBoxQuery,
stashBoxBatchQuery, stashBoxBatchQuery,
@@ -396,12 +396,12 @@ const TaggerList: React.FC<ITaggerListProps> = ({
</div> </div>
<Link <Link
to={`/scenes/${scene.id}`} to={`/scenes/${scene.id}`}
className="scene-link text-truncate w-100" className="scene-link overflow-hidden"
title={scene.path}
> >
{originalDir} <TruncatedText
<wbr /> text={`${originalDir}\u200B${file}${ext}`}
{`${file}.${ext}`} lineCount={2}
/>
</Link> </Link>
</div> </div>
<div className="col-md-6 my-1 align-self-center"> <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 { Link } from "react-router-dom";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { NavUtils } from "src/utils"; import { NavUtils } from "src/utils";
import { Icon } from "../Shared"; import { Icon, TruncatedText } from "../Shared";
import { BasicCard } from "../Shared/BasicCard"; import { BasicCard } from "../Shared/BasicCard";
interface IProps { interface IProps {
@@ -73,7 +73,11 @@ export const TagCard: React.FC<IProps> = ({
src={tag.image_path ?? ""} src={tag.image_path ?? ""}
/> />
} }
details={<h5 className="text-truncate">{tag.name}</h5>} details={
<h5>
<TruncatedText text={tag.name} />
</h5>
}
popovers={maybeRenderPopoverButtonGroup()} popovers={maybeRenderPopoverButtonGroup()}
selected={selected} selected={selected}
selecting={selecting} selecting={selecting}

View File

@@ -18,15 +18,6 @@ const Units: Unit[] = [
]; ];
const shortUnits = ["B", "KB", "MB", "GB", "TB", "PB"]; 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) => { const fileSize = (bytes: number = 0) => {
if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes)) if (Number.isNaN(parseFloat(String(bytes))) || !Number.isFinite(bytes))
return { size: 0, unit: Units[0] }; return { size: 0, unit: Units[0] };
@@ -145,7 +136,6 @@ const formatDate = (intl: IntlShape, date?: string) => {
}; };
const TextUtils = { const TextUtils = {
truncate,
fileSize, fileSize,
formatFileSizeUnit, formatFileSizeUnit,
secondsToTimestamp, secondsToTimestamp,