[web] dynamic free storage - prepare for changes (#1637)
This commit is contained in:
commit
ee8a6cfb55
13 changed files with 149 additions and 87 deletions
39
.github/workflows/web-crowdin-update.yml
vendored
Normal file
39
.github/workflows/web-crowdin-update.yml
vendored
Normal file
|
@ -0,0 +1,39 @@
|
|||
name: "Update Crowdin translations (web)"
|
||||
|
||||
# This is a variant of web-crowdin.yml that also uploads the translated strings
|
||||
# (in addition to the source strings). This allows us to change the strings in
|
||||
# our source code for an automated refactoring (e.g. renaming a key), and then
|
||||
# run this workflow to update the data in Crowdin taking our source code as the
|
||||
# source of truth.
|
||||
|
||||
on:
|
||||
# Only allow running manually.
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
synchronize-with-crowdin:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Crowdin's action
|
||||
uses: crowdin/github-action@v1
|
||||
with:
|
||||
base_path: "web/"
|
||||
config: "web/crowdin.yml"
|
||||
upload_sources: true
|
||||
# This is what differs from web-crowdin.yml
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
localization_branch_name: translations/web
|
||||
create_pull_request: true
|
||||
skip_untranslated_strings: true
|
||||
pull_request_title: "[web] Updated translations"
|
||||
pull_request_body: "Updated translations from [Crowdin](https://crowdin.com/project/ente-photos-web)"
|
||||
pull_request_base_branch_name: "main"
|
||||
project_id: 569613
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
|
@ -83,7 +83,8 @@ export default function Index() {
|
|||
fontWeight: "normal",
|
||||
}}
|
||||
>
|
||||
Enter this code on <b>Ente Photos</b> to pair this screen
|
||||
Enter this code on <b>Ente Photos</b> to pair this
|
||||
screen
|
||||
</h1>
|
||||
<div
|
||||
style={{
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from "react-window";
|
||||
import { Duplicate } from "services/deduplicationService";
|
||||
import { EnteFile } from "types/file";
|
||||
import { convertBytesToHumanReadable } from "utils/file";
|
||||
import { formattedByteSize } from "utils/units";
|
||||
|
||||
export enum ITEM_TYPE {
|
||||
TIME = "TIME",
|
||||
|
@ -310,8 +310,7 @@ export function DedupePhotoList({
|
|||
*/
|
||||
<SizeAndCountContainer span={columns}>
|
||||
{listItem.fileCount} {t("FILES")},{" "}
|
||||
{convertBytesToHumanReadable(listItem.fileSize || 0)}{" "}
|
||||
{t("EACH")}
|
||||
{formattedByteSize(listItem.fileSize || 0)} {t("EACH")}
|
||||
</SizeAndCountContainer>
|
||||
);
|
||||
case ITEM_TYPE.FILE: {
|
||||
|
|
|
@ -22,9 +22,9 @@ import {
|
|||
areEqual,
|
||||
} from "react-window";
|
||||
import { EnteFile } from "types/file";
|
||||
import { convertBytesToHumanReadable } from "utils/file";
|
||||
import { handleSelectCreator } from "utils/photoFrame";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
import { formattedByteSize } from "utils/units";
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
const FOOTER_HEIGHT = 90;
|
||||
|
@ -829,8 +829,7 @@ export function PhotoList({
|
|||
return (
|
||||
<SizeAndCountContainer span={columns}>
|
||||
{listItem.fileCount} {t("FILES")},{" "}
|
||||
{convertBytesToHumanReadable(listItem.fileSize || 0)}{" "}
|
||||
{t("EACH")}
|
||||
{formattedByteSize(listItem.fileSize || 0)} {t("EACH")}
|
||||
</SizeAndCountContainer>
|
||||
);
|
||||
case ITEM_TYPE.FILE: {
|
||||
|
|
|
@ -7,8 +7,8 @@ import VideocamOutlined from "@mui/icons-material/VideocamOutlined";
|
|||
import Box from "@mui/material/Box";
|
||||
import { useEffect, useState } from "react";
|
||||
import { EnteFile } from "types/file";
|
||||
import { makeHumanReadableStorage } from "utils/billing";
|
||||
import { changeFileName, updateExistingFilePubMetadata } from "utils/file";
|
||||
import { formattedByteSize } from "utils/units";
|
||||
import { FileNameEditDialog } from "./FileNameEditDialog";
|
||||
import InfoItem from "./InfoItem";
|
||||
|
||||
|
@ -33,7 +33,7 @@ const getCaption = (file: EnteFile, parsedExifData) => {
|
|||
captionParts.push(resolution);
|
||||
}
|
||||
if (fileSize) {
|
||||
captionParts.push(makeHumanReadableStorage(fileSize));
|
||||
captionParts.push(formattedByteSize(fileSize));
|
||||
}
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||
import { Box, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { makeHumanReadableStorage } from "utils/billing";
|
||||
import { formattedStorageByteSize } from "utils/units";
|
||||
|
||||
import { Progressbar } from "../../styledComponents";
|
||||
|
||||
|
@ -19,7 +19,7 @@ export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
|
|||
marginTop: 1.5,
|
||||
}}
|
||||
>
|
||||
<Typography variant="mini">{`${makeHumanReadableStorage(
|
||||
<Typography variant="mini">{`${formattedStorageByteSize(
|
||||
storage - usage,
|
||||
)} ${t("FREE")}`}</Typography>
|
||||
<Typography variant="mini" fontWeight={"bold"}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, styled, Typography } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { convertBytesToGBs, makeHumanReadableStorage } from "utils/billing";
|
||||
import { bytesInGB, formattedStorageByteSize } from "utils/units";
|
||||
|
||||
const MobileSmallBox = styled(Box)`
|
||||
display: none;
|
||||
|
@ -30,9 +30,9 @@ export default function StorageSection({ usage, storage }: Iprops) {
|
|||
fontWeight={"bold"}
|
||||
sx={{ fontSize: "24px", lineHeight: "30px" }}
|
||||
>
|
||||
{`${makeHumanReadableStorage(usage, { roundUp: true })} ${t(
|
||||
{`${formattedStorageByteSize(usage, { round: true })} ${t(
|
||||
"OF",
|
||||
)} ${makeHumanReadableStorage(storage)} ${t("USED")}`}
|
||||
)} ${formattedStorageByteSize(storage)} ${t("USED")}`}
|
||||
</Typography>
|
||||
</DefaultBox>
|
||||
<MobileSmallBox>
|
||||
|
@ -40,9 +40,7 @@ export default function StorageSection({ usage, storage }: Iprops) {
|
|||
fontWeight={"bold"}
|
||||
sx={{ fontSize: "24px", lineHeight: "30px" }}
|
||||
>
|
||||
{`${convertBytesToGBs(usage)} / ${convertBytesToGBs(
|
||||
storage,
|
||||
)} ${t("GB")} ${t("USED")}`}
|
||||
{`${bytesInGB(usage)} / ${bytesInGB(storage)} ${t("GB")} ${t("USED")}`}
|
||||
</Typography>
|
||||
</MobileSmallBox>
|
||||
</Box>
|
||||
|
|
|
@ -5,11 +5,8 @@ import Box from "@mui/material/Box";
|
|||
import Typography from "@mui/material/Typography";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import {
|
||||
convertBytesToGBs,
|
||||
hasAddOnBonus,
|
||||
isSubscriptionCancelled,
|
||||
} from "utils/billing";
|
||||
import { hasAddOnBonus, isSubscriptionCancelled } from "utils/billing";
|
||||
import { bytesInGB } from "utils/units";
|
||||
import { ManageSubscription } from "../manageSubscription";
|
||||
import { PeriodToggler } from "../periodToggler";
|
||||
import Plans from "../plans";
|
||||
|
@ -35,8 +32,7 @@ export default function PaidSubscriptionPlanSelectorCard({
|
|||
{t("SUBSCRIPTION")}
|
||||
</Typography>
|
||||
<Typography variant="small" color={"text.muted"}>
|
||||
{convertBytesToGBs(subscription.storage, 2)}{" "}
|
||||
{t("GB")}
|
||||
{bytesInGB(subscription.storage, 2)} {t("GB")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<IconButton onClick={closeModal} color="secondary">
|
||||
|
@ -50,7 +46,7 @@ export default function PaidSubscriptionPlanSelectorCard({
|
|||
<Trans
|
||||
i18nKey="CURRENT_USAGE"
|
||||
values={{
|
||||
usage: `${convertBytesToGBs(usage, 2)} ${t("GB")}`,
|
||||
usage: `${bytesInGB(usage, 2)} ${t("GB")}`,
|
||||
}}
|
||||
/>
|
||||
</Typography>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
|||
import { Box, styled, Typography } from "@mui/material";
|
||||
|
||||
import { Trans } from "react-i18next";
|
||||
import { makeHumanReadableStorage } from "utils/billing";
|
||||
import { formattedStorageByteSize } from "utils/units";
|
||||
|
||||
const RowContainer = styled(SpaceBetweenFlex)(({ theme }) => ({
|
||||
// gap: theme.spacing(1.5),
|
||||
|
@ -24,7 +24,7 @@ export function BFAddOnRow({ bonusData, closeModal }) {
|
|||
<Trans
|
||||
i18nKey={"ADD_ON_AVAILABLE_TILL"}
|
||||
values={{
|
||||
storage: makeHumanReadableStorage(
|
||||
storage: formattedStorageByteSize(
|
||||
bonus.storage,
|
||||
),
|
||||
date: bonus.validTill,
|
||||
|
|
|
@ -6,11 +6,8 @@ import { Badge } from "components/Badge";
|
|||
import { PLAN_PERIOD } from "constants/gallery";
|
||||
import { t } from "i18next";
|
||||
import { Plan, Subscription } from "types/billing";
|
||||
import {
|
||||
convertBytesToGBs,
|
||||
hasPaidSubscription,
|
||||
isUserSubscribedPlan,
|
||||
} from "utils/billing";
|
||||
import { hasPaidSubscription, isUserSubscribedPlan } from "utils/billing";
|
||||
import { bytesInGB } from "utils/units";
|
||||
|
||||
interface Iprops {
|
||||
plan: Plan;
|
||||
|
@ -66,7 +63,7 @@ export function PlanRow({
|
|||
<PlanRowContainer>
|
||||
<TopAlignedFluidContainer>
|
||||
<Typography variant="h1" fontWeight={"bold"}>
|
||||
{convertBytesToGBs(plan.storage)}
|
||||
{bytesInGB(plan.storage)}
|
||||
</Typography>
|
||||
<FlexWrapper flexWrap={"wrap"} gap={1}>
|
||||
<Typography variant="h3" color="text.muted">
|
||||
|
|
|
@ -31,44 +31,6 @@ enum RESPONSE_STATUS {
|
|||
fail = "fail",
|
||||
}
|
||||
|
||||
const StorageUnits = ["B", "KB", "MB", "GB", "TB"];
|
||||
|
||||
const ONE_GB = 1024 * 1024 * 1024;
|
||||
|
||||
export function convertBytesToGBs(bytes: number, precision = 0): string {
|
||||
return (bytes / (1024 * 1024 * 1024)).toFixed(precision);
|
||||
}
|
||||
|
||||
export function makeHumanReadableStorage(
|
||||
bytes: number,
|
||||
{ roundUp } = { roundUp: false },
|
||||
): string {
|
||||
if (bytes <= 0) {
|
||||
return `0 ${t("STORAGE_UNITS.MB")}`;
|
||||
}
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
let quantity = bytes / Math.pow(1024, i);
|
||||
let unit = StorageUnits[i];
|
||||
|
||||
if (quantity > 100 && unit !== "GB") {
|
||||
quantity /= 1024;
|
||||
unit = StorageUnits[i + 1];
|
||||
}
|
||||
|
||||
quantity = Number(quantity.toFixed(1));
|
||||
|
||||
if (bytes >= 10 * ONE_GB) {
|
||||
if (roundUp) {
|
||||
quantity = Math.ceil(quantity);
|
||||
} else {
|
||||
quantity = Math.round(quantity);
|
||||
}
|
||||
}
|
||||
|
||||
return `${quantity} ${t(`STORAGE_UNITS.${unit}`)}`;
|
||||
}
|
||||
|
||||
export function hasPaidSubscription(subscription: Subscription) {
|
||||
return (
|
||||
subscription &&
|
||||
|
@ -160,9 +122,8 @@ export function isSubscriptionPastDue(subscription: Subscription) {
|
|||
);
|
||||
}
|
||||
|
||||
export function isPopularPlan(plan: Plan) {
|
||||
return plan.storage === 100 * ONE_GB;
|
||||
}
|
||||
export const isPopularPlan = (plan: Plan) =>
|
||||
plan.storage === 100 * 1024 * 1024 * 1024; /* 100 GB */
|
||||
|
||||
export async function updateSubscription(
|
||||
plan: Plan,
|
||||
|
|
|
@ -103,19 +103,6 @@ export async function getUpdatedEXIFFileForDownload(
|
|||
}
|
||||
}
|
||||
|
||||
export function convertBytesToHumanReadable(
|
||||
bytes: number,
|
||||
precision = 2,
|
||||
): string {
|
||||
if (bytes === 0 || isNaN(bytes)) {
|
||||
return "0 MB";
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i];
|
||||
}
|
||||
|
||||
export async function downloadFile(file: EnteFile) {
|
||||
try {
|
||||
const fileReader = new FileReader();
|
||||
|
|
85
web/apps/photos/src/utils/units.ts
Normal file
85
web/apps/photos/src/utils/units.ts
Normal file
|
@ -0,0 +1,85 @@
|
|||
import { t } from "i18next";
|
||||
|
||||
const StorageUnits = ["B", "KB", "MB", "GB", "TB"];
|
||||
|
||||
/**
|
||||
* Convert the given number of {@link bytes} to their equivalent GB string with
|
||||
* {@link precision}.
|
||||
*
|
||||
* The returned string does not have the GB suffix.
|
||||
*/
|
||||
export const bytesInGB = (bytes: number, precision = 0): string =>
|
||||
(bytes / (1024 * 1024 * 1024)).toFixed(precision);
|
||||
|
||||
/**
|
||||
* Convert the given number of {@link bytes} to a user visible string in an
|
||||
* appropriately sized unit.
|
||||
*
|
||||
* The returned string includes the (localized) unit suffix, e.g. "TB".
|
||||
*
|
||||
* @param precision Modify the number of digits after the decimal point.
|
||||
* Defaults to 2.
|
||||
*/
|
||||
export function formattedByteSize(bytes: number, precision = 2): string {
|
||||
if (bytes === 0 || isNaN(bytes)) {
|
||||
return "0 MB";
|
||||
}
|
||||
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
|
||||
return (bytes / Math.pow(1024, i)).toFixed(precision) + " " + sizes[i];
|
||||
}
|
||||
|
||||
interface FormattedStorageByteSizeOptions {
|
||||
/**
|
||||
* If `true` then round up the fractional quantity we obtain when dividing
|
||||
* the number of bytes by the number of bytes in the unit that got chosen.
|
||||
*
|
||||
* The default behaviour is to take the ceiling.
|
||||
*/
|
||||
round?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the given number of storage {@link bytes} to a user visible string in
|
||||
* an appropriately sized unit.
|
||||
*
|
||||
* This differs from {@link formattedByteSize} in that while
|
||||
* {@link formattedByteSize} is meant for arbitrary byte sizes, this function
|
||||
* has a few additional beautification heuristics that we want to apply when
|
||||
* displaying the "storage size" (in different contexts) as opposed to, say, a
|
||||
* generic "file size".
|
||||
*
|
||||
* @param options
|
||||
*
|
||||
* @return A user visible string, including the localized unit suffix.
|
||||
*/
|
||||
export const formattedStorageByteSize = (
|
||||
bytes: number,
|
||||
options?: FormattedStorageByteSizeOptions,
|
||||
): string => {
|
||||
if (bytes <= 0) {
|
||||
return `0 ${t("STORAGE_UNITS.MB")}`;
|
||||
}
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
|
||||
let quantity = bytes / Math.pow(1024, i);
|
||||
let unit = StorageUnits[i];
|
||||
|
||||
if (quantity > 100 && unit !== "GB") {
|
||||
quantity /= 1024;
|
||||
unit = StorageUnits[i + 1];
|
||||
}
|
||||
|
||||
quantity = Number(quantity.toFixed(1));
|
||||
|
||||
if (bytes >= 10 * 1024 * 1024 * 1024 /* 10 GB */) {
|
||||
if (options?.round) {
|
||||
quantity = Math.ceil(quantity);
|
||||
} else {
|
||||
quantity = Math.round(quantity);
|
||||
}
|
||||
}
|
||||
|
||||
return `${quantity} ${t(`STORAGE_UNITS.${unit}`)}`;
|
||||
};
|
Loading…
Add table
Reference in a new issue