mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Add TruncatedText component (#932)
This commit is contained in:
@@ -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
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 ??
|
|
||||||
TextUtils.fileNameFromPath(props.gallery.path ?? "")}
|
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
{props.gallery.date ? (
|
{props.gallery.date ? (
|
||||||
<h5>
|
<h5>
|
||||||
<FormattedDate
|
<FormattedDate
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
text={
|
||||||
|
props.scene.title
|
||||||
? props.scene.title
|
? props.scene.title
|
||||||
: TextUtils.fileNameFromPath(props.scene.path)}
|
: 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 ? (
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
70
ui/v2.5/src/components/Shared/TruncatedText.tsx
Normal file
70
ui/v2.5/src/components/Shared/TruncatedText.tsx
Normal 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;
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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}
|
{studio.scene_count}
|
||||||
<FormattedPlural
|
<FormattedPlural
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user