#1810 Truncate large numbers on buttons (#2781)

* #1810 Truncate large numbers on buttons
* Apply to card popovers as well

Co-authored-by: Roland Karle <roland.karle@aufwind-group.de>
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
Joe Scylla
2022-10-06 03:43:17 +02:00
committed by GitHub
parent b160c3bb97
commit 9083796a42
11 changed files with 188 additions and 49 deletions

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useMemo, useState } from "react"; import React, { useEffect, useMemo, useState } from "react";
import { Button, Tabs, Tab, Badge, Col, Row } from "react-bootstrap"; import { Button, Tabs, Tab, Col, Row } from "react-bootstrap";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@@ -13,6 +13,7 @@ import {
mutateMetadataAutoTag, mutateMetadataAutoTag,
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
Counter,
CountryFlag, CountryFlag,
DetailsEditNavbar, DetailsEditNavbar,
ErrorMessage, ErrorMessage,
@@ -20,6 +21,7 @@ import {
LoadingIndicator, LoadingIndicator,
} from "src/components/Shared"; } from "src/components/Shared";
import { useLightbox, useToast } from "src/hooks"; import { useLightbox, useToast } from "src/hooks";
import { ConfigurationContext } from "src/hooks/Config";
import { TextUtils } from "src/utils"; import { TextUtils } from "src/utils";
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; import { PerformerDetailsPanel } from "./PerformerDetailsPanel";
@@ -36,6 +38,7 @@ import {
faHeart, faHeart,
faLink, faLink,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { IUIConfig } from "src/core/config";
interface IProps { interface IProps {
performer: GQL.PerformerDataFragment; performer: GQL.PerformerDataFragment;
@@ -50,6 +53,11 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
const intl = useIntl(); const intl = useIntl();
const { tab = "details" } = useParams<IPerformerParams>(); const { tab = "details" } = useParams<IPerformerParams>();
// Configuration settings
const { configuration } = React.useContext(ConfigurationContext);
const abbreviateCounter =
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
const [imagePreview, setImagePreview] = useState<string | null>(); const [imagePreview, setImagePreview] = useState<string | null>();
const [imageEncoding, setImageEncoding] = useState<boolean>(false); const [imageEncoding, setImageEncoding] = useState<boolean>(false);
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
@@ -195,9 +203,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "scenes" })} {intl.formatMessage({ id: "scenes" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(performer.scene_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={performer.scene_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -208,9 +217,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "galleries" })} {intl.formatMessage({ id: "galleries" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(performer.gallery_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={performer.gallery_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -221,9 +231,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "images" })} {intl.formatMessage({ id: "images" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(performer.image_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={performer.image_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -234,9 +245,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "movies" })} {intl.formatMessage({ id: "movies" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(performer.movie_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={performer.movie_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >

View File

@@ -135,6 +135,14 @@ export const SettingsInterfacePanel: React.FC = () => {
onChange={(v) => saveInterface({ menuItems: v })} onChange={(v) => saveInterface({ menuItems: v })}
/> />
</div> </div>
<BooleanSetting
id="abbreviate-counters"
headingID="config.ui.abbreviate_counters.heading"
subHeadingID="config.ui.abbreviate_counters.description"
checked={ui.abbreviateCounters ?? undefined}
onChange={(v) => saveUI({ abbreviateCounters: v })}
/>
</SettingSection> </SettingSection>
<SettingSection headingID="config.ui.desktop_integration.desktop_integration"> <SettingSection headingID="config.ui.desktop_integration.desktop_integration">

View File

@@ -0,0 +1,37 @@
import React from "react";
import { Badge } from "react-bootstrap";
import { FormattedNumber, useIntl } from "react-intl";
import { TextUtils } from "src/utils";
interface IProps {
abbreviateCounter?: boolean;
count: number;
}
export const Counter: React.FC<IProps> = ({
abbreviateCounter = false,
count,
}) => {
const intl = useIntl();
if (abbreviateCounter) {
const formated = TextUtils.abbreviateCounter(count);
return (
<Badge className="left-spacing" pill variant="secondary">
<FormattedNumber
value={formated.size}
maximumFractionDigits={formated.digits}
/>
{formated.unit}
</Badge>
);
} else {
return (
<Badge className="left-spacing" pill variant="secondary">
{intl.formatNumber(count)}
</Badge>
);
}
};
export default Counter;

View File

@@ -4,10 +4,13 @@ import {
faImages, faImages,
faPlayCircle, faPlayCircle,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import React from "react"; import React, { useMemo } from "react";
import { Button } from "react-bootstrap"; import { Button } from "react-bootstrap";
import { useIntl } from "react-intl"; import { FormattedNumber, useIntl } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { IUIConfig } from "src/core/config";
import { ConfigurationContext } from "src/hooks/Config";
import { TextUtils } from "src/utils";
import Icon from "./Icon"; import Icon from "./Icon";
type PopoverLinkType = "scene" | "image" | "gallery" | "movie"; type PopoverLinkType = "scene" | "image" | "gallery" | "movie";
@@ -25,6 +28,10 @@ export const PopoverCountButton: React.FC<IProps> = ({
type, type,
count, count,
}) => { }) => {
const { configuration } = React.useContext(ConfigurationContext);
const abbreviateCounter =
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
const intl = useIntl(); const intl = useIntl();
function getIcon() { function getIcon() {
@@ -72,11 +79,28 @@ export const PopoverCountButton: React.FC<IProps> = ({
return `${count} ${plural}`; return `${count} ${plural}`;
} }
const countEl = useMemo(() => {
if (!abbreviateCounter) {
return count;
}
const formatted = TextUtils.abbreviateCounter(count);
return (
<span>
<FormattedNumber
value={formatted.size}
maximumFractionDigits={formatted.digits}
/>
{formatted.unit}
</span>
);
}, [count, abbreviateCounter]);
return ( return (
<Link className={className} to={url} title={getTitle()}> <Link className={className} to={url} title={getTitle()}>
<Button className="minimal"> <Button className="minimal">
<Icon icon={getIcon()} /> <Icon icon={getIcon()} />
<span>{count}</span> <span>{countEl}</span>
</Button> </Button>
</Link> </Link>
); );

View File

@@ -9,6 +9,7 @@ export { HoverPopover } from "./HoverPopover";
export { default as LoadingIndicator } from "./LoadingIndicator"; export { default as LoadingIndicator } from "./LoadingIndicator";
export { ImageInput } from "./ImageInput"; export { ImageInput } from "./ImageInput";
export { SweatDrops } from "./SweatDrops"; export { SweatDrops } from "./SweatDrops";
export { default as Counter } from "./Counter";
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";

View File

@@ -1,4 +1,4 @@
import { Tabs, Tab, Badge } from "react-bootstrap"; import { Tabs, Tab } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@@ -14,12 +14,14 @@ import {
} from "src/core/StashService"; } from "src/core/StashService";
import { ImageUtils } from "src/utils"; import { ImageUtils } from "src/utils";
import { import {
Counter,
DetailsEditNavbar, DetailsEditNavbar,
Modal, Modal,
LoadingIndicator, LoadingIndicator,
ErrorMessage, ErrorMessage,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { ConfigurationContext } from "src/hooks/Config";
import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioScenesPanel } from "./StudioScenesPanel";
import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel";
import { StudioImagesPanel } from "./StudioImagesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel";
@@ -29,6 +31,7 @@ import { StudioEditPanel } from "./StudioEditPanel";
import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel";
import { StudioMoviesPanel } from "./StudioMoviesPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel";
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { IUIConfig } from "src/core/config";
interface IProps { interface IProps {
studio: GQL.StudioDataFragment; studio: GQL.StudioDataFragment;
@@ -44,6 +47,11 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
const intl = useIntl(); const intl = useIntl();
const { tab = "details" } = useParams<IStudioParams>(); const { tab = "details" } = useParams<IStudioParams>();
// Configuration settings
const { configuration } = React.useContext(ConfigurationContext);
const abbreviateCounter =
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
// Editing state // Editing state
const [isEditing, setIsEditing] = useState<boolean>(false); const [isEditing, setIsEditing] = useState<boolean>(false);
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
@@ -222,9 +230,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "scenes" })} {intl.formatMessage({ id: "scenes" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(studio.scene_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={studio.scene_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -235,9 +244,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "galleries" })} {intl.formatMessage({ id: "galleries" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(studio.gallery_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={studio.gallery_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -248,9 +258,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "images" })} {intl.formatMessage({ id: "images" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(studio.image_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={studio.image_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -267,9 +278,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "movies" })} {intl.formatMessage({ id: "movies" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(studio.movie_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={studio.movie_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -280,9 +292,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "subsidiary_studios" })} {intl.formatMessage({ id: "subsidiary_studios" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(studio.child_studios?.length)} abbreviateCounter={false}
</Badge> count={studio.child_studios?.length ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >

View File

@@ -1,4 +1,4 @@
import { Tabs, Tab, Dropdown, Badge } from "react-bootstrap"; import { Tabs, Tab, Dropdown } from "react-bootstrap";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams, useHistory } from "react-router-dom"; import { useParams, useHistory } from "react-router-dom";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@@ -14,6 +14,7 @@ import {
} from "src/core/StashService"; } from "src/core/StashService";
import { ImageUtils } from "src/utils"; import { ImageUtils } from "src/utils";
import { import {
Counter,
DetailsEditNavbar, DetailsEditNavbar,
ErrorMessage, ErrorMessage,
Modal, Modal,
@@ -21,6 +22,7 @@ import {
Icon, Icon,
} from "src/components/Shared"; } from "src/components/Shared";
import { useToast } from "src/hooks"; import { useToast } from "src/hooks";
import { ConfigurationContext } from "src/hooks/Config";
import { tagRelationHook } from "src/core/tags"; import { tagRelationHook } from "src/core/tags";
import { TagScenesPanel } from "./TagScenesPanel"; import { TagScenesPanel } from "./TagScenesPanel";
import { TagMarkersPanel } from "./TagMarkersPanel"; import { TagMarkersPanel } from "./TagMarkersPanel";
@@ -35,6 +37,7 @@ import {
faSignOutAlt, faSignOutAlt,
faTrashAlt, faTrashAlt,
} from "@fortawesome/free-solid-svg-icons"; } from "@fortawesome/free-solid-svg-icons";
import { IUIConfig } from "src/core/config";
interface IProps { interface IProps {
tag: GQL.TagDataFragment; tag: GQL.TagDataFragment;
@@ -48,6 +51,12 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
const history = useHistory(); const history = useHistory();
const Toast = useToast(); const Toast = useToast();
const intl = useIntl(); const intl = useIntl();
// Configuration settings
const { configuration } = React.useContext(ConfigurationContext);
const abbreviateCounter =
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
const { tab = "scenes" } = useParams<ITabParams>(); const { tab = "scenes" } = useParams<ITabParams>();
// Editing state // Editing state
@@ -309,9 +318,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "scenes" })} {intl.formatMessage({ id: "scenes" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(tag.scene_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={tag.scene_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -322,9 +332,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "images" })} {intl.formatMessage({ id: "images" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(tag.image_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={tag.image_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -335,9 +346,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "galleries" })} {intl.formatMessage({ id: "galleries" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(tag.gallery_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={tag.gallery_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -348,9 +360,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "markers" })} {intl.formatMessage({ id: "markers" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(tag.scene_marker_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={tag.scene_marker_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >
@@ -361,9 +374,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
title={ title={
<React.Fragment> <React.Fragment>
{intl.formatMessage({ id: "performers" })} {intl.formatMessage({ id: "performers" })}
<Badge className="left-spacing" pill variant="secondary"> <Counter
{intl.formatNumber(tag.performer_count ?? 0)} abbreviateCounter={abbreviateCounter}
</Badge> count={tag.performer_count ?? 0}
/>
</React.Fragment> </React.Fragment>
} }
> >

View File

@@ -31,6 +31,7 @@ export interface IUIConfig {
showChildTagContent?: boolean; showChildTagContent?: boolean;
showChildStudioContent?: boolean; showChildStudioContent?: boolean;
showTagCardOnHover?: boolean; showTagCardOnHover?: boolean;
abbreviateCounters?: boolean;
} }
function recentlyReleased( function recentlyReleased(

View File

@@ -5,8 +5,9 @@ After migrating, please run a scan on your entire library to populate missing da
* Import/export schema has changed and is incompatible with the previous version. * Import/export schema has changed and is incompatible with the previous version.
### ✨ New Features ### ✨ New Features
* Added Interface option to abbreviate counts on cards and details pages. ([#2781](https://github.com/stashapp/stash/pull/2781))
* Added description field to Tags. ([#2708](https://github.com/stashapp/stash/pull/2708)) * Added description field to Tags. ([#2708](https://github.com/stashapp/stash/pull/2708))
* Added option to include sub-studio/sub-tag content in Studio/Tag page. ([#2832](https://github.com/stashapp/stash/pull/2832)) * Added Interface options to include sub-studio/sub-tag content in Studio/Tag pages. ([#2832](https://github.com/stashapp/stash/pull/2832))
* Added backup location configuration setting. ([#2953](https://github.com/stashapp/stash/pull/2953)) * Added backup location configuration setting. ([#2953](https://github.com/stashapp/stash/pull/2953))
* Allow overriding UI localisation strings. ([#2837](https://github.com/stashapp/stash/pull/2837)) * Allow overriding UI localisation strings. ([#2837](https://github.com/stashapp/stash/pull/2837))
* Populate name from query field when creating new performer/studio/tag/gallery. ([#2701](https://github.com/stashapp/stash/pull/2701)) * Populate name from query field when creating new performer/studio/tag/gallery. ([#2701](https://github.com/stashapp/stash/pull/2701))

View File

@@ -458,6 +458,10 @@
}, },
"heading": "Editing" "heading": "Editing"
}, },
"abbreviate_counters": {
"description": "Abbreviate counters in cards and details view pages, for example \"1831\" will get formated to \"1.8K\".",
"heading": "Abbreviate counters"
},
"funscript_offset": { "funscript_offset": {
"description": "Time offset in milliseconds for interactive scripts playback.", "description": "Time offset in milliseconds for interactive scripts playback.",
"heading": "Funscript Offset (ms)" "heading": "Funscript Offset (ms)"

View File

@@ -305,6 +305,29 @@ const capitalize = (val: string) =>
.replace(/^[-_]*(.)/, (_, c) => c.toUpperCase()) .replace(/^[-_]*(.)/, (_, c) => c.toUpperCase())
.replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`); .replace(/[-_]+(.)/g, (_, c) => ` ${c.toUpperCase()}`);
type CountUnit = "" | "K" | "M" | "B";
const CountUnits: CountUnit[] = ["", "K", "M", "B"];
const abbreviateCounter = (counter: number = 0) => {
if (Number.isNaN(parseFloat(String(counter))) || !Number.isFinite(counter))
return { size: 0, unit: CountUnits[0] };
let unit = 0;
let digits = 0;
let count = counter;
while (count >= 1000 && unit + 1 < CountUnits.length) {
count /= 1000;
unit++;
digits = 1;
}
return {
size: count,
unit: CountUnits[unit],
digits: digits,
};
};
const TextUtils = { const TextUtils = {
fileSize, fileSize,
formatFileSizeUnit, formatFileSizeUnit,
@@ -322,6 +345,7 @@ const TextUtils = {
formatDateTime, formatDateTime,
capitalize, capitalize,
secondsAsTimeString, secondsAsTimeString,
abbreviateCounter,
}; };
export default TextUtils; export default TextUtils;