mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
* #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:
@@ -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<IProps> = ({ performer }) => {
|
||||
const intl = useIntl();
|
||||
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 [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
@@ -195,9 +203,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(performer.scene_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.scene_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -208,9 +217,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(performer.gallery_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.gallery_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -221,9 +231,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(performer.image_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.image_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -234,9 +245,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "movies" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(performer.movie_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={performer.movie_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -135,6 +135,14 @@ export const SettingsInterfacePanel: React.FC = () => {
|
||||
onChange={(v) => saveInterface({ menuItems: v })}
|
||||
/>
|
||||
</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 headingID="config.ui.desktop_integration.desktop_integration">
|
||||
|
||||
37
ui/v2.5/src/components/Shared/Counter.tsx
Normal file
37
ui/v2.5/src/components/Shared/Counter.tsx
Normal 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;
|
||||
@@ -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<IProps> = ({
|
||||
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<IProps> = ({
|
||||
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 (
|
||||
<Link className={className} to={url} title={getTitle()}>
|
||||
<Button className="minimal">
|
||||
<Icon icon={getIcon()} />
|
||||
<span>{count}</span>
|
||||
<span>{countEl}</span>
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<IProps> = ({ studio }) => {
|
||||
const intl = useIntl();
|
||||
const { tab = "details" } = useParams<IStudioParams>();
|
||||
|
||||
// Configuration settings
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const abbreviateCounter =
|
||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
||||
|
||||
// Editing state
|
||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
@@ -222,9 +230,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(studio.scene_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={studio.scene_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -235,9 +244,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(studio.gallery_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={studio.gallery_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -248,9 +258,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(studio.image_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={studio.image_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -267,9 +278,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "movies" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(studio.movie_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={studio.movie_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -280,9 +292,10 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "subsidiary_studios" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(studio.child_studios?.length)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={false}
|
||||
count={studio.child_studios?.length ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -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<IProps> = ({ 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<ITabParams>();
|
||||
|
||||
// Editing state
|
||||
@@ -309,9 +318,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "scenes" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(tag.scene_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={tag.scene_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -322,9 +332,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "images" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(tag.image_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={tag.image_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -335,9 +346,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "galleries" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(tag.gallery_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={tag.gallery_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -348,9 +360,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "markers" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(tag.scene_marker_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={tag.scene_marker_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
@@ -361,9 +374,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||
title={
|
||||
<React.Fragment>
|
||||
{intl.formatMessage({ id: "performers" })}
|
||||
<Badge className="left-spacing" pill variant="secondary">
|
||||
{intl.formatNumber(tag.performer_count ?? 0)}
|
||||
</Badge>
|
||||
<Counter
|
||||
abbreviateCounter={abbreviateCounter}
|
||||
count={tag.performer_count ?? 0}
|
||||
/>
|
||||
</React.Fragment>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface IUIConfig {
|
||||
showChildTagContent?: boolean;
|
||||
showChildStudioContent?: boolean;
|
||||
showTagCardOnHover?: boolean;
|
||||
abbreviateCounters?: boolean;
|
||||
}
|
||||
|
||||
function recentlyReleased(
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)"
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user