Add FileSize component and refactor file size rendering in various components (#5695)

This commit is contained in:
WithoutPants 2025-03-03 18:38:19 +11:00 committed by GitHub
parent a391fa4345
commit ce2d779dbc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 53 additions and 125 deletions

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { Accordion, Button, Card } from "react-bootstrap"; import { Accordion, Button, Card } from "react-bootstrap";
import { FormattedMessage, FormattedNumber, FormattedTime } from "react-intl"; import { FormattedMessage, FormattedTime } from "react-intl";
import { TruncatedText } from "src/components/Shared/TruncatedText"; import { TruncatedText } from "src/components/Shared/TruncatedText";
import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog"; import { DeleteFilesDialog } from "src/components/Shared/DeleteFilesDialog";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@ -8,6 +8,7 @@ import { mutateImageSetPrimaryFile } from "src/core/StashService";
import { useToast } from "src/hooks/Toast"; import { useToast } from "src/hooks/Toast";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { TextField, URLField, URLsField } from "src/utils/field"; import { TextField, URLField, URLsField } from "src/utils/field";
import { FileSize } from "src/components/Shared/FileSize";
interface IFileInfoPanelProps { interface IFileInfoPanelProps {
file: GQL.ImageFileDataFragment | GQL.VideoFileDataFragment; file: GQL.ImageFileDataFragment | GQL.VideoFileDataFragment;
@ -21,29 +22,6 @@ interface IFileInfoPanelProps {
const FileInfoPanel: React.FC<IFileInfoPanelProps> = ( const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
props: IFileInfoPanelProps props: IFileInfoPanelProps
) => { ) => {
function renderFileSize() {
if (props.file.size === undefined) {
return;
}
const { size, unit } = TextUtils.fileSize(props.file.size ?? 0);
return (
<TextField id="filesize">
<span className="text-truncate">
<FormattedNumber
value={size}
// eslint-disable-next-line react/style-prop-object
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
</span>
</TextField>
);
}
const checksum = props.file.fingerprints.find((f) => f.type === "md5"); const checksum = props.file.fingerprints.find((f) => f.type === "md5");
return ( return (
@ -64,7 +42,11 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
value={`file://${props.file.path}`} value={`file://${props.file.path}`}
truncate truncate
/> />
{renderFileSize()} <TextField id="filesize">
<span className="text-truncate">
<FileSize size={props.file.size} />
</span>
</TextField>
<TextField id="file_mod_time"> <TextField id="file_mod_time">
<FormattedTime <FormattedTime
dateStyle="medium" dateStyle="medium"

View file

@ -23,11 +23,11 @@ import "flexbin/flexbin.css";
import Gallery from "react-photo-gallery"; import Gallery from "react-photo-gallery";
import { ExportDialog } from "../Shared/ExportDialog"; import { ExportDialog } from "../Shared/ExportDialog";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text";
import { ConfigurationContext } from "src/hooks/Config"; import { ConfigurationContext } from "src/hooks/Config";
import { ImageGridCard } from "./ImageGridCard"; import { ImageGridCard } from "./ImageGridCard";
import { View } from "../List/views"; import { View } from "../List/views";
import { IItemListOperation } from "../List/FilteredListToolbar"; import { IItemListOperation } from "../List/FilteredListToolbar";
import { FileSize } from "../Shared/FileSize";
interface IImageWallProps { interface IImageWallProps {
images: GQL.SlimImageDataFragment[]; images: GQL.SlimImageDataFragment[];
@ -230,7 +230,6 @@ function getCount(result: GQL.FindImagesQueryResult) {
function renderMetadataByline(result: GQL.FindImagesQueryResult) { function renderMetadataByline(result: GQL.FindImagesQueryResult) {
const megapixels = result?.data?.findImages?.megapixels; const megapixels = result?.data?.findImages?.megapixels;
const size = result?.data?.findImages?.filesize; const size = result?.data?.findImages?.filesize;
const filesize = size ? TextUtils.fileSize(size) : undefined;
if (!megapixels && !size) { if (!megapixels && !size) {
return; return;
@ -247,15 +246,9 @@ function renderMetadataByline(result: GQL.FindImagesQueryResult) {
</span> </span>
) : undefined} ) : undefined}
{separator} {separator}
{size && filesize ? ( {size ? (
<span className="images-size"> <span className="images-size">
<FormattedNumber <FileSize size={size} />
value={filesize.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
filesize.unit
)}
/>
{` ${TextUtils.formatFileSizeUnit(filesize.unit)}`}
</span> </span>
) : undefined} ) : undefined}
) )

View file

@ -44,6 +44,7 @@ import {
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { SceneMergeModal } from "../Scenes/SceneMergeDialog"; import { SceneMergeModal } from "../Scenes/SceneMergeDialog";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { FileSize } from "../Shared/FileSize";
const CLASSNAME = "duplicate-checker"; const CLASSNAME = "duplicate-checker";
@ -326,19 +327,6 @@ export const SceneDuplicateChecker: React.FC = () => {
resetCheckboxSelection(); resetCheckboxSelection();
} }
const renderFilesize = (filesize: number | null | undefined) => {
const { size: parsedSize, unit } = TextUtils.fileSize(filesize ?? 0);
return (
<FormattedNumber
value={parsedSize}
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
);
};
function maybeRenderMissingPhashWarning() { function maybeRenderMissingPhashWarning() {
const missingPhashes = missingPhash?.findScenes.count ?? 0; const missingPhashes = missingPhash?.findScenes.count ?? 0;
if (missingPhashes > 0) { if (missingPhashes > 0) {
@ -917,7 +905,9 @@ export const SceneDuplicateChecker: React.FC = () => {
{file?.duration && {file?.duration &&
TextUtils.secondsToTimestamp(file.duration)} TextUtils.secondsToTimestamp(file.duration)}
</td> </td>
<td>{renderFilesize(file?.size ?? 0)}</td> <td>
<FileSize size={file?.size ?? 0} />
</td>
<td>{`${file?.width ?? 0}x${file?.height ?? 0}`}</td> <td>{`${file?.width ?? 0}x${file?.height ?? 0}`}</td>
<td> <td>
<FormattedNumber <FormattedNumber

View file

@ -15,7 +15,7 @@ import { ConfigurationContext } from "src/hooks/Config";
import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton"; import { PerformerPopoverButton } from "../Shared/PerformerPopoverButton";
import { GridCard, calculateCardWidth } from "../Shared/GridCard/GridCard"; import { GridCard, calculateCardWidth } from "../Shared/GridCard/GridCard";
import { RatingBanner } from "../Shared/RatingBanner"; import { RatingBanner } from "../Shared/RatingBanner";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage } from "react-intl";
import { import {
faBox, faBox,
faCopy, faCopy,
@ -30,6 +30,7 @@ import { PatchComponent } from "src/patch";
import ScreenUtils from "src/utils/screen"; import ScreenUtils from "src/utils/screen";
import { StudioOverlay } from "../Shared/GridCard/StudioOverlay"; import { StudioOverlay } from "../Shared/GridCard/StudioOverlay";
import { GroupTag } from "../Groups/GroupTag"; import { GroupTag } from "../Groups/GroupTag";
import { FileSize } from "../Shared/FileSize";
interface IScenePreviewProps { interface IScenePreviewProps {
isPortrait: boolean; isPortrait: boolean;
@ -362,21 +363,11 @@ const SceneCardImage = PatchComponent(
); );
function maybeRenderSceneSpecsOverlay() { function maybeRenderSceneSpecsOverlay() {
let sizeObj = null;
if (file?.size) {
sizeObj = TextUtils.fileSize(file.size);
}
return ( return (
<div className="scene-specs-overlay"> <div className="scene-specs-overlay">
{sizeObj != null ? ( {file?.size !== undefined ? (
<span className="overlay-filesize extra-scene-info"> <span className="overlay-filesize extra-scene-info">
<FormattedNumber <FileSize size={file.size} />
value={sizeObj.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
sizeObj.unit
)}
/>
{TextUtils.formatFileSizeUnit(sizeObj.unit)}
</span> </span>
) : ( ) : (
"" ""

View file

@ -18,6 +18,7 @@ import TextUtils from "src/utils/text";
import { TextField, URLField, URLsField } from "src/utils/field"; import { TextField, URLField, URLsField } from "src/utils/field";
import { StashIDPill } from "src/components/Shared/StashID"; import { StashIDPill } from "src/components/Shared/StashID";
import { PatchComponent } from "../../../patch"; import { PatchComponent } from "../../../patch";
import { FileSize } from "src/components/Shared/FileSize";
interface IFileInfoPanelProps { interface IFileInfoPanelProps {
sceneID: string; sceneID: string;
@ -36,25 +37,6 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
function renderFileSize() {
const { size, unit } = TextUtils.fileSize(props.file.size);
return (
<TextField id="filesize">
<span className="text-truncate">
<FormattedNumber
value={size}
// eslint-disable-next-line react/style-prop-object
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
</span>
</TextField>
);
}
// TODO - generalise fingerprints // TODO - generalise fingerprints
const oshash = props.file.fingerprints.find((f) => f.type === "oshash"); const oshash = props.file.fingerprints.find((f) => f.type === "oshash");
const phash = props.file.fingerprints.find((f) => f.type === "phash"); const phash = props.file.fingerprints.find((f) => f.type === "phash");
@ -94,7 +76,11 @@ const FileInfoPanel: React.FC<IFileInfoPanelProps> = (
value={`file://${props.file.path}`} value={`file://${props.file.path}`}
truncate truncate
/> />
{renderFileSize()} <TextField id="filesize">
<span className="text-truncate">
<FileSize size={props.file.size} />
</span>
</TextField>
<TextField id="file_mod_time"> <TextField id="file_mod_time">
<FormattedTime <FormattedTime
dateStyle="medium" dateStyle="medium"

View file

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import cloneDeep from "lodash-es/cloneDeep"; import cloneDeep from "lodash-es/cloneDeep";
import { FormattedNumber, useIntl } from "react-intl"; import { useIntl } from "react-intl";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import Mousetrap from "mousetrap"; import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
@ -25,6 +25,7 @@ import { SceneMergeModal } from "./SceneMergeDialog";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { View } from "../List/views"; import { View } from "../List/views";
import { FileSize } from "../Shared/FileSize";
function getItems(result: GQL.FindScenesQueryResult) { function getItems(result: GQL.FindScenesQueryResult) {
return result?.data?.findScenes?.scenes ?? []; return result?.data?.findScenes?.scenes ?? [];
@ -37,7 +38,6 @@ function getCount(result: GQL.FindScenesQueryResult) {
function renderMetadataByline(result: GQL.FindScenesQueryResult) { function renderMetadataByline(result: GQL.FindScenesQueryResult) {
const duration = result?.data?.findScenes?.duration; const duration = result?.data?.findScenes?.duration;
const size = result?.data?.findScenes?.filesize; const size = result?.data?.findScenes?.filesize;
const filesize = size ? TextUtils.fileSize(size) : undefined;
if (!duration && !size) { if (!duration && !size) {
return; return;
@ -54,15 +54,9 @@ function renderMetadataByline(result: GQL.FindScenesQueryResult) {
</span> </span>
) : undefined} ) : undefined}
{separator} {separator}
{size && filesize ? ( {size ? (
<span className="scenes-size"> <span className="scenes-size">
<FormattedNumber <FileSize size={size} />
value={filesize.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
filesize.unit
)}
/>
{` ${TextUtils.formatFileSizeUnit(filesize.unit)}`}
</span> </span>
) : undefined} ) : undefined}
) )

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 NavUtils from "src/utils/navigation"; import NavUtils from "src/utils/navigation";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { objectTitle } from "src/core/files"; import { objectTitle } from "src/core/files";
import { galleryTitle } from "src/core/galleries"; import { galleryTitle } from "src/core/galleries";
import SceneQueue from "src/models/sceneQueue"; import SceneQueue from "src/models/sceneQueue";
@ -11,6 +11,7 @@ import { RatingSystem } from "../Shared/Rating/RatingSystem";
import { useSceneUpdate } from "src/core/StashService"; import { useSceneUpdate } from "src/core/StashService";
import { IColumn, ListTable } from "../List/ListTable"; import { IColumn, ListTable } from "../List/ListTable";
import { useTableColumns } from "src/hooks/useTableColumns"; import { useTableColumns } from "src/hooks/useTableColumns";
import { FileSize } from "../Shared/FileSize";
interface ISceneListTableProps { interface ISceneListTableProps {
scenes: GQL.SlimSceneDataFragment[]; scenes: GQL.SlimSceneDataFragment[];
@ -169,24 +170,12 @@ export const SceneListTable: React.FC<ISceneListTableProps> = (
</ul> </ul>
); );
function renderFileSize(file: { size: number | undefined }) {
const { size, unit } = TextUtils.fileSize(file.size);
return (
<FormattedNumber
value={size}
style="unit"
unit={unit}
unitDisplay="narrow"
maximumFractionDigits={2}
/>
);
}
const FileSizeCell = (scene: GQL.SlimSceneDataFragment) => ( const FileSizeCell = (scene: GQL.SlimSceneDataFragment) => (
<ul className="comma-list"> <ul className="comma-list">
{scene.files.map((file) => ( {scene.files.map((file) => (
<li key={file.id}>{renderFileSize(file)}</li> <li key={file.id}>
<FileSize size={file.size} />
</li>
))} ))}
</ul> </ul>
); );

View file

@ -0,0 +1,17 @@
import React from "react";
import { FormattedNumber } from "react-intl";
import TextUtils from "src/utils/text";
export const FileSize: React.FC<{ size: number }> = ({ size: fileSize }) => {
const { size, unit } = TextUtils.fileSize(fileSize);
return (
<>
<FormattedNumber
value={size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(unit)}
/>
{` ${TextUtils.formatFileSizeUnit(unit)}`}
</>
);
};

View file

@ -3,6 +3,7 @@ import { useStats } from "src/core/StashService";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator"; import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import TextUtils from "src/utils/text"; import TextUtils from "src/utils/text";
import { FileSize } from "./Shared/FileSize";
export const Stats: React.FC = () => { export const Stats: React.FC = () => {
const { data, error, loading } = useStats(); const { data, error, loading } = useStats();
@ -10,9 +11,6 @@ export const Stats: React.FC = () => {
if (error) return <span>{error.message}</span>; if (error) return <span>{error.message}</span>;
if (loading || !data) return <LoadingIndicator />; if (loading || !data) return <LoadingIndicator />;
const scenesSize = TextUtils.fileSize(data.stats.scenes_size);
const imagesSize = TextUtils.fileSize(data.stats.images_size);
const scenesDuration = TextUtils.secondsAsTimeString( const scenesDuration = TextUtils.secondsAsTimeString(
data.stats.scenes_duration, data.stats.scenes_duration,
3 3
@ -28,13 +26,7 @@ export const Stats: React.FC = () => {
<div className="col col-sm-8 m-sm-auto row stats"> <div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber <FileSize size={data.stats.scenes_size} />
value={scenesSize.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
scenesSize.unit
)}
/>
{` ${TextUtils.formatFileSizeUnit(scenesSize.unit)}`}
</p> </p>
<p className="heading"> <p className="heading">
<FormattedMessage id="stats.scenes_size" /> <FormattedMessage id="stats.scenes_size" />
@ -74,13 +66,7 @@ export const Stats: React.FC = () => {
<div className="col col-sm-8 m-sm-auto row stats"> <div className="col col-sm-8 m-sm-auto row stats">
<div className="stats-element"> <div className="stats-element">
<p className="title"> <p className="title">
<FormattedNumber <FileSize size={data.stats.images_size} />
value={imagesSize.size}
maximumFractionDigits={TextUtils.fileSizeFractionalDigits(
imagesSize.unit
)}
/>
{` ${TextUtils.formatFileSizeUnit(imagesSize.unit)}`}
</p> </p>
<p className="heading"> <p className="heading">
<FormattedMessage id="stats.image_size" /> <FormattedMessage id="stats.image_size" />