diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index 27a22a31e..0c9a58a0b 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -1,5 +1,5 @@ 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 { useParams, useHistory } from "react-router-dom"; import { Helmet } from "react-helmet"; @@ -13,6 +13,7 @@ import { mutateMetadataAutoTag, } from "src/core/StashService"; import { + Counter, CountryFlag, DetailsEditNavbar, ErrorMessage, @@ -20,6 +21,7 @@ import { LoadingIndicator, } from "src/components/Shared"; import { useLightbox, useToast } from "src/hooks"; +import { ConfigurationContext } from "src/hooks/Config"; import { TextUtils } from "src/utils"; import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; import { PerformerDetailsPanel } from "./PerformerDetailsPanel"; @@ -36,6 +38,7 @@ import { faHeart, faLink, } from "@fortawesome/free-solid-svg-icons"; +import { IUIConfig } from "src/core/config"; interface IProps { performer: GQL.PerformerDataFragment; @@ -50,6 +53,11 @@ const PerformerPage: React.FC = ({ performer }) => { const intl = useIntl(); const { tab = "details" } = useParams(); + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const abbreviateCounter = + (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + const [imagePreview, setImagePreview] = useState(); const [imageEncoding, setImageEncoding] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -195,9 +203,10 @@ const PerformerPage: React.FC = ({ performer }) => { title={ {intl.formatMessage({ id: "scenes" })} - - {intl.formatNumber(performer.scene_count ?? 0)} - + } > @@ -208,9 +217,10 @@ const PerformerPage: React.FC = ({ performer }) => { title={ {intl.formatMessage({ id: "galleries" })} - - {intl.formatNumber(performer.gallery_count ?? 0)} - + } > @@ -221,9 +231,10 @@ const PerformerPage: React.FC = ({ performer }) => { title={ {intl.formatMessage({ id: "images" })} - - {intl.formatNumber(performer.image_count ?? 0)} - + } > @@ -234,9 +245,10 @@ const PerformerPage: React.FC = ({ performer }) => { title={ {intl.formatMessage({ id: "movies" })} - - {intl.formatNumber(performer.movie_count ?? 0)} - + } > diff --git a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx index 498b5b33a..05e92458c 100644 --- a/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsInterfacePanel/SettingsInterfacePanel.tsx @@ -135,6 +135,14 @@ export const SettingsInterfacePanel: React.FC = () => { onChange={(v) => saveInterface({ menuItems: v })} /> + + saveUI({ abbreviateCounters: v })} + /> diff --git a/ui/v2.5/src/components/Shared/Counter.tsx b/ui/v2.5/src/components/Shared/Counter.tsx new file mode 100644 index 000000000..d0552605f --- /dev/null +++ b/ui/v2.5/src/components/Shared/Counter.tsx @@ -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 = ({ + abbreviateCounter = false, + count, +}) => { + const intl = useIntl(); + + if (abbreviateCounter) { + const formated = TextUtils.abbreviateCounter(count); + return ( + + + {formated.unit} + + ); + } else { + return ( + + {intl.formatNumber(count)} + + ); + } +}; + +export default Counter; diff --git a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx index 3eb1c731f..901424b98 100644 --- a/ui/v2.5/src/components/Shared/PopoverCountButton.tsx +++ b/ui/v2.5/src/components/Shared/PopoverCountButton.tsx @@ -4,10 +4,13 @@ import { faImages, faPlayCircle, } from "@fortawesome/free-solid-svg-icons"; -import React from "react"; +import React, { useMemo } from "react"; import { Button } from "react-bootstrap"; -import { useIntl } from "react-intl"; +import { FormattedNumber, useIntl } from "react-intl"; 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"; type PopoverLinkType = "scene" | "image" | "gallery" | "movie"; @@ -25,6 +28,10 @@ export const PopoverCountButton: React.FC = ({ type, count, }) => { + const { configuration } = React.useContext(ConfigurationContext); + const abbreviateCounter = + (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + const intl = useIntl(); function getIcon() { @@ -72,11 +79,28 @@ export const PopoverCountButton: React.FC = ({ return `${count} ${plural}`; } + const countEl = useMemo(() => { + if (!abbreviateCounter) { + return count; + } + + const formatted = TextUtils.abbreviateCounter(count); + return ( + + + {formatted.unit} + + ); + }, [count, abbreviateCounter]); + return ( ); diff --git a/ui/v2.5/src/components/Shared/index.ts b/ui/v2.5/src/components/Shared/index.ts index d388a45ca..cfbaf8043 100644 --- a/ui/v2.5/src/components/Shared/index.ts +++ b/ui/v2.5/src/components/Shared/index.ts @@ -9,6 +9,7 @@ export { HoverPopover } from "./HoverPopover"; export { default as LoadingIndicator } from "./LoadingIndicator"; export { ImageInput } from "./ImageInput"; export { SweatDrops } from "./SweatDrops"; +export { default as Counter } from "./Counter"; export { default as CountryFlag } from "./CountryFlag"; export { default as SuccessIcon } from "./SuccessIcon"; export { default as ErrorMessage } from "./ErrorMessage"; diff --git a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx index 6a43ce54e..2d87ddc97 100644 --- a/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx +++ b/ui/v2.5/src/components/Studios/StudioDetails/Studio.tsx @@ -1,4 +1,4 @@ -import { Tabs, Tab, Badge } from "react-bootstrap"; +import { Tabs, Tab } from "react-bootstrap"; import React, { useEffect, useState } from "react"; import { useParams, useHistory } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; @@ -14,12 +14,14 @@ import { } from "src/core/StashService"; import { ImageUtils } from "src/utils"; import { + Counter, DetailsEditNavbar, Modal, LoadingIndicator, ErrorMessage, } from "src/components/Shared"; import { useToast } from "src/hooks"; +import { ConfigurationContext } from "src/hooks/Config"; import { StudioScenesPanel } from "./StudioScenesPanel"; import { StudioGalleriesPanel } from "./StudioGalleriesPanel"; import { StudioImagesPanel } from "./StudioImagesPanel"; @@ -29,6 +31,7 @@ import { StudioEditPanel } from "./StudioEditPanel"; import { StudioDetailsPanel } from "./StudioDetailsPanel"; import { StudioMoviesPanel } from "./StudioMoviesPanel"; import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { IUIConfig } from "src/core/config"; interface IProps { studio: GQL.StudioDataFragment; @@ -44,6 +47,11 @@ const StudioPage: React.FC = ({ studio }) => { const intl = useIntl(); const { tab = "details" } = useParams(); + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const abbreviateCounter = + (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + // Editing state const [isEditing, setIsEditing] = useState(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); @@ -222,9 +230,10 @@ const StudioPage: React.FC = ({ studio }) => { title={ {intl.formatMessage({ id: "scenes" })} - - {intl.formatNumber(studio.scene_count ?? 0)} - + } > @@ -235,9 +244,10 @@ const StudioPage: React.FC = ({ studio }) => { title={ {intl.formatMessage({ id: "galleries" })} - - {intl.formatNumber(studio.gallery_count ?? 0)} - + } > @@ -248,9 +258,10 @@ const StudioPage: React.FC = ({ studio }) => { title={ {intl.formatMessage({ id: "images" })} - - {intl.formatNumber(studio.image_count ?? 0)} - + } > @@ -267,9 +278,10 @@ const StudioPage: React.FC = ({ studio }) => { title={ {intl.formatMessage({ id: "movies" })} - - {intl.formatNumber(studio.movie_count ?? 0)} - + } > @@ -280,9 +292,10 @@ const StudioPage: React.FC = ({ studio }) => { title={ {intl.formatMessage({ id: "subsidiary_studios" })} - - {intl.formatNumber(studio.child_studios?.length)} - + } > diff --git a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx index 7d8adbffb..57665b18d 100644 --- a/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx +++ b/ui/v2.5/src/components/Tags/TagDetails/Tag.tsx @@ -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 { useParams, useHistory } from "react-router-dom"; import { FormattedMessage, useIntl } from "react-intl"; @@ -14,6 +14,7 @@ import { } from "src/core/StashService"; import { ImageUtils } from "src/utils"; import { + Counter, DetailsEditNavbar, ErrorMessage, Modal, @@ -21,6 +22,7 @@ import { Icon, } from "src/components/Shared"; import { useToast } from "src/hooks"; +import { ConfigurationContext } from "src/hooks/Config"; import { tagRelationHook } from "src/core/tags"; import { TagScenesPanel } from "./TagScenesPanel"; import { TagMarkersPanel } from "./TagMarkersPanel"; @@ -35,6 +37,7 @@ import { faSignOutAlt, faTrashAlt, } from "@fortawesome/free-solid-svg-icons"; +import { IUIConfig } from "src/core/config"; interface IProps { tag: GQL.TagDataFragment; @@ -48,6 +51,12 @@ const TagPage: React.FC = ({ tag }) => { const history = useHistory(); const Toast = useToast(); const intl = useIntl(); + + // Configuration settings + const { configuration } = React.useContext(ConfigurationContext); + const abbreviateCounter = + (configuration?.ui as IUIConfig)?.abbreviateCounters ?? false; + const { tab = "scenes" } = useParams(); // Editing state @@ -309,9 +318,10 @@ const TagPage: React.FC = ({ tag }) => { title={ {intl.formatMessage({ id: "scenes" })} - - {intl.formatNumber(tag.scene_count ?? 0)} - + } > @@ -322,9 +332,10 @@ const TagPage: React.FC = ({ tag }) => { title={ {intl.formatMessage({ id: "images" })} - - {intl.formatNumber(tag.image_count ?? 0)} - + } > @@ -335,9 +346,10 @@ const TagPage: React.FC = ({ tag }) => { title={ {intl.formatMessage({ id: "galleries" })} - - {intl.formatNumber(tag.gallery_count ?? 0)} - + } > @@ -348,9 +360,10 @@ const TagPage: React.FC = ({ tag }) => { title={ {intl.formatMessage({ id: "markers" })} - - {intl.formatNumber(tag.scene_marker_count ?? 0)} - + } > @@ -361,9 +374,10 @@ const TagPage: React.FC = ({ tag }) => { title={ {intl.formatMessage({ id: "performers" })} - - {intl.formatNumber(tag.performer_count ?? 0)} - + } > diff --git a/ui/v2.5/src/core/config.ts b/ui/v2.5/src/core/config.ts index fa39470a6..152d3f77e 100644 --- a/ui/v2.5/src/core/config.ts +++ b/ui/v2.5/src/core/config.ts @@ -31,6 +31,7 @@ export interface IUIConfig { showChildTagContent?: boolean; showChildStudioContent?: boolean; showTagCardOnHover?: boolean; + abbreviateCounters?: boolean; } function recentlyReleased( diff --git a/ui/v2.5/src/docs/en/Changelog/v0170.md b/ui/v2.5/src/docs/en/Changelog/v0170.md index ba317a15d..97131977e 100644 --- a/ui/v2.5/src/docs/en/Changelog/v0170.md +++ b/ui/v2.5/src/docs/en/Changelog/v0170.md @@ -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. ### ✨ 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 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)) * 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)) diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index a487867a7..2c98a74e8 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -458,6 +458,10 @@ }, "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": { "description": "Time offset in milliseconds for interactive scripts playback.", "heading": "Funscript Offset (ms)" diff --git a/ui/v2.5/src/utils/text.ts b/ui/v2.5/src/utils/text.ts index 7404ab9db..6e67276e3 100644 --- a/ui/v2.5/src/utils/text.ts +++ b/ui/v2.5/src/utils/text.ts @@ -305,6 +305,29 @@ const capitalize = (val: string) => .replace(/^[-_]*(.)/, (_, 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 = { fileSize, formatFileSizeUnit, @@ -322,6 +345,7 @@ const TextUtils = { formatDateTime, capitalize, secondsAsTimeString, + abbreviateCounter, }; export default TextUtils;