[web] Light mode WIP - Part x/x (#4962)

This commit is contained in:
Manav Rathi
2025-02-04 12:44:30 +05:30
committed by GitHub
15 changed files with 178 additions and 137 deletions

View File

@@ -999,7 +999,7 @@ const FreehandCropRegion = forwardRef(
key={index}
sx={{
border: "1px solid",
borderColor: "fixed.white",
borderColor: "white",
boxSizing: "border-box",
pointerEvents: "none",
}}
@@ -1011,9 +1011,9 @@ const FreehandCropRegion = forwardRef(
position: "absolute",
height: "10px",
width: "10px",
backgroundColor: "fixed.white",
backgroundColor: "white",
border: "1px solid",
borderColor: "fixed.black",
borderColor: "black",
right: "-5px",
bottom: "-5px",
cursor: "se-resize",

View File

@@ -114,8 +114,14 @@ const LanguageSelector = () => {
const updateCurrentLocale = (newLocale: SupportedLocale) => {
setLocaleInUse(newLocale);
// [Note: Changing locale causes a full reload]
//
// A full reload is needed because we use the global `t` instance
// instead of the useTranslation hook.
//
// We also rely on this behaviour by caching various formatters in
// module static variables that not get updated if the i18n.language
// changes unless there is a full reload.
window.location.reload();
};

View File

@@ -48,7 +48,7 @@ export const SubscriptionCard: React.FC<SubscriptionCardProps> = ({
sx={{ borderRadius: "8px" }}
/>
) : (
<Box sx={{ position: "relative", color: "fixed.white" }}>
<Box sx={{ position: "relative", color: "white" }}>
<BackgroundOverlay />
<SubscriptionCardContentOverlay userDetails={userDetails} />
<ClickOverlay onClick={onClick} />

View File

@@ -1,23 +1,22 @@
import { Overlay } from "@/base/components/containers";
import { formattedDateRelative } from "@/base/i18n-date";
import log from "@/base/log";
import { downloadManager } from "@/gallery/services/download";
import { enteFileDeletionDate } from "@/media/file";
import { FileType } from "@/media/file-type";
import {
GAP_BTW_TILES,
IMAGE_CONTAINER_MAX_WIDTH,
} from "@/new/photos/components/PhotoList";
import { GAP_BTW_TILES } from "@/new/photos/components/PhotoList";
import {
LoadingThumbnail,
StaticThumbnail,
} from "@/new/photos/components/PlaceholderThumbnails";
import { TileBottomTextOverlay } from "@/new/photos/components/Tiles";
import { TRASH_SECTION } from "@/new/photos/services/collection";
import useLongPress from "@ente/shared/hooks/useLongPress";
import AlbumOutlinedIcon from "@mui/icons-material/AlbumOutlined";
import FavoriteRoundedIcon from "@mui/icons-material/FavoriteRounded";
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
import { styled } from "@mui/material";
import { styled, Typography } from "@mui/material";
import type { DisplayFile } from "components/PhotoFrame";
import i18n from "i18next";
import { GalleryContext } from "pages/gallery";
import React, { useContext, useEffect, useRef, useState } from "react";
import { shouldShowAvatar } from "utils/file";
@@ -82,7 +81,7 @@ const Check = styled("input")<{ $active: boolean }>(
content: "";
background-color: ${theme.vars.palette.accent.main};
border-color: ${theme.vars.palette.accent.main};
color: ${theme.vars.palette.fixed.white};
color: white;
}
/* checkmark foreground (tick) */
&:checked::after {
@@ -112,21 +111,45 @@ const HoverOverlay = styled("div")<{ checked: boolean }>`
"background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))"};
`;
/**
* An overlay showing the avatars of the person who shared the item, at the top
* right.
*/
const AvatarOverlay = styled(Overlay)`
display: flex;
justify-content: flex-end;
align-items: flex-start;
padding-right: 5px;
padding-top: 5px;
padding: 5px;
`;
const FavOverlay = styled(Overlay)`
/**
* An overlay showing the favorite icon at bottom left.
*/
const FavoriteOverlay = styled(Overlay)`
display: flex;
justify-content: flex-start;
align-items: flex-end;
padding-left: 5px;
padding-bottom: 5px;
opacity: 0.9;
padding: 5px;
color: white;
opacity: 0.6;
`;
/**
* An overlay with a gradient, showing the file type indicator (e.g. live photo,
* video) at the bottom right.
*/
const FileTypeIndicatorOverlay = styled(Overlay)`
display: flex;
justify-content: flex-end;
align-items: flex-end;
padding: 5px;
color: white;
background: linear-gradient(
315deg,
rgba(0 0 0 / 0.14) 0%,
rgba(0 0 0 / 0.05) 30%,
transparent 50%
);
`;
const InSelectRangeOverlay = styled(Overlay)(
@@ -137,32 +160,6 @@ const InSelectRangeOverlay = styled(Overlay)(
`,
);
const FileAndCollectionNameOverlay = styled("div")(
({ theme }) => `
width: 100%;
bottom: 0;
left: 0;
max-height: 40%;
width: 100%;
background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 2));
& > p {
max-width: calc(${IMAGE_CONTAINER_MAX_WIDTH}px - 10px);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin: 2px;
text-align: center;
}
padding: 7px;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
color: ${theme.vars.palette.fixed.white};
position: absolute;
`,
);
const SelectedOverlay = styled(Overlay)(
({ theme }) => `
z-index: 5;
@@ -171,23 +168,6 @@ const SelectedOverlay = styled(Overlay)(
`,
);
const FileTypeIndicatorOverlay = styled(Overlay)(({ theme }) => ({
display: "flex",
justifyContent: "flex-end",
alignItems: "flex-end",
padding: "8px",
// TODO(LM): Ditto the dark one until lm is ready.
// background:
// "linear-gradient(315deg, rgba(255, 255, 255, 0.14) 0%, rgba(255, 255,
// 255, 0.05) 29.61%, rgba(255, 255, 255, 0) 49.86%)",
background:
"linear-gradient(315deg, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0.05) 29.61%, rgba(0, 0, 0, 0) 49.86%)",
...theme.applyStyles("dark", {
background:
"linear-gradient(315deg, rgba(0, 0, 0, 0.14) 0%, rgba(0, 0, 0, 0.05) 29.61%, rgba(0, 0, 0, 0) 49.86%)",
}),
}));
const Cont = styled("div")<{ disabled: boolean }>`
display: flex;
width: fit-content;
@@ -241,6 +221,7 @@ export default function PreviewCard(props: IProps) {
onRangeSelect,
isRangeSelectActive,
isInsSelectRange,
isFav,
} = props;
const [imgSrc, setImgSrc] = useState<string>(file.msrc);
@@ -345,10 +326,10 @@ export default function PreviewCard(props: IProps) {
<Avatar file={file} />
</AvatarOverlay>
)}
{props.isFav && (
<FavOverlay>
{isFav && (
<FavoriteOverlay>
<FavoriteRoundedIcon />
</FavOverlay>
</FavoriteOverlay>
)}
<HoverOverlay
@@ -360,34 +341,12 @@ export default function PreviewCard(props: IProps) {
)}
{props?.activeCollectionID === TRASH_SECTION && file.isTrashed && (
<FileAndCollectionNameOverlay>
<p>{formatDateRelative(file.deleteBy / 1000)}</p>
</FileAndCollectionNameOverlay>
<TileBottomTextOverlay>
<Typography variant="small">
{formattedDateRelative(enteFileDeletionDate(file))}
</Typography>
</TileBottomTextOverlay>
)}
</Cont>
);
}
function formatDateRelative(date: number) {
const units = {
year: 24 * 60 * 60 * 1000 * 365,
month: (24 * 60 * 60 * 1000 * 365) / 12,
day: 24 * 60 * 60 * 1000,
hour: 60 * 60 * 1000,
minute: 60 * 1000,
second: 1000,
};
const relativeDateFormat = new Intl.RelativeTimeFormat(i18n.language, {
localeMatcher: "best fit",
numeric: "always",
style: "long",
});
const elapsed = date - Date.now(); // "Math.abs" accounts for both "past" & "future" scenarios
for (const u in units)
if (Math.abs(elapsed) > units[u] || u === "second")
return relativeDateFormat.format(
Math.round(elapsed / units[u]),
u as Intl.RelativeTimeFormatUnit,
);
}

View File

@@ -16,7 +16,7 @@ export const EnteSwitch: React.FC<SwitchProps> = styled((props) => (
transitionDuration: "300ms",
"&.Mui-checked": {
transform: "translateX(16px)",
color: theme.vars.palette.fixed.white,
color: "white",
"& + .MuiSwitch-track": {
opacity: 1,
border: 0,

View File

@@ -46,8 +46,16 @@ export const CenteredFill = styled("div")`
`;
/**
* An absolute positioned div that fills the entire nearest relatively
* positioned ancestor.
* An empty overlay on top of the nearest relative positioned ancestor.
*
* {@link Overlay} is an an absolute positioned div that fills the entire
* nearest relatively positioned ancestor. It is usually used in tandem with a
* derivate of {@link BaseTile} or {@link BaseTileButton} to show various
* indicators on top of thumbnails; but it can be used in any context where we
* want to overlay (usually) transparent content on top of a component.
*
* For filling much larger areas (e.g. showing a translucent overlay on top of
* the entire screen), use the MUI {@link Backdrop} instead.
*/
export const Overlay = styled("div")`
position: absolute;

View File

@@ -120,8 +120,6 @@ declare module "@mui/material/styles" {
* These do not change with the color scheme.
*/
fixed: {
white: string;
black: string;
/**
* Various fixed shades of gray.
* TODO(LM) - audit and rename.

View File

@@ -333,8 +333,10 @@ const getColorSchemes = (colors: ReturnType<typeof getColors>) => ({
FilledInput: {
bg: colors.light.fill.faint,
hoverBg: colors.light.fill.faintHover,
// We don't use this currently.
// disabledBg: colors.light.fill.fainter,
// While we don't specifically have disabled inputs, TextInputs
// do get disabled when the form is submitting, and this value
// comes into play then.
disabledBg: colors.light.fill.fainter,
},
},
},
@@ -396,7 +398,7 @@ const getColorSchemes = (colors: ReturnType<typeof getColors>) => ({
FilledInput: {
bg: colors.dark.fill.faint,
hoverBg: colors.dark.fill.faintHover,
// disabledBg: colors.dark.fill.faint,
disabledBg: colors.dark.fill.fainter,
},
},
},

14
web/packages/base/date.ts Normal file
View File

@@ -0,0 +1,14 @@
/**
* Convert an epoch microsecond value to a JavaScript date.
*
* This is a convenience API for dealing with optional epoch microseconds in
* various data structures. Remote talks in terms of epoch microseconds, but
* JavaScript dates are underlain by epoch milliseconds, and this does a
* conversion, with a convenience of short circuiting undefined values.
*/
export const dateFromEpochMicroseconds = (
epochMicroseconds: number | undefined,
) =>
epochMicroseconds === undefined
? undefined
: new Date(epochMicroseconds / 1000);

View File

@@ -0,0 +1,38 @@
/**
* @file Various date formatters.
*
* Note that we rely on the current behaviour of a full reload on changing the
* language. See: [Note: Changing locale causes a full reload].
*/
import i18n from "i18next";
let _relativeTimeFormat: Intl.RelativeTimeFormat | undefined;
export const formattedDateRelative = (date: Date) => {
const units: [Intl.RelativeTimeFormatUnit, number][] = [
["year", 24 * 60 * 60 * 1000 * 365],
["month", (24 * 60 * 60 * 1000 * 365) / 12],
["day", 24 * 60 * 60 * 1000],
["hour", 60 * 60 * 1000],
["minute", 60 * 1000],
["second", 1000],
];
// Math.abs accounts for both past and future scenarios.
const elapsed = Math.abs(date.getTime() - Date.now());
// Lazily created, then cached, instance of RelativeTimeFormat.
const relativeTimeFormat = (_relativeTimeFormat ??=
new Intl.RelativeTimeFormat(i18n.language, {
localeMatcher: "best fit",
numeric: "always",
style: "short",
}));
for (const [u, d] of units) {
if (elapsed > d)
return relativeTimeFormat.format(Math.round(elapsed / d), u);
}
return relativeTimeFormat.format(Math.round(elapsed / 1000), "second");
};

View File

@@ -228,7 +228,15 @@ export const setLocaleInUse = async (locale: SupportedLocale) => {
return i18n.changeLanguage(locale);
};
const numberFormatter = new Intl.NumberFormat(i18n.language);
let _numberFormat: Intl.NumberFormat | undefined;
/**
* Lazily created, cached, instance of NumberFormat used by
* {@link formattedNumber}.
*
* See: [Note: Changing locale causes a full reload].
*/
const numberFormat = () =>
(_numberFormat ??= new Intl.NumberFormat(i18n.language));
/**
* Return the given {@link value} formatted for the current language and locale.
@@ -238,7 +246,7 @@ const numberFormatter = new Intl.NumberFormat(i18n.language);
* However, in some rare cases, we need to format a standalone number. For such
* scenarios, this function can be used.
*/
export const formattedNumber = (value: number) => numberFormatter.format(value);
export const formattedNumber = (value: number) => numberFormat().format(value);
/**
* A no-op marker for strings that, for various reasons, pending addition to the

View File

@@ -1,4 +1,5 @@
import { sharedCryptoWorker } from "@/base/crypto";
import { dateFromEpochMicroseconds } from "@/base/date";
import log from "@/base/log";
import { type Metadata, ItemVisibility } from "./file-metadata";
@@ -172,7 +173,15 @@ export interface EnteFile
* But its presence is not guaranteed.
*/
pubMagicMetadata?: FilePublicMagicMetadata;
/**
* `true` if this file is in trash (i.e. it has been deleted by the user,
* and will be permanently deleted after 30 days of being moved to trash).
*/
isTrashed?: boolean;
/**
* If this is a file in trash, then {@link deleteBy} contains the epoch
* microseconds when this file will be permanently deleted.
*/
deleteBy?: number;
/**
* The base64 representation of the decrypted encryption key associated with
@@ -285,6 +294,16 @@ export const fileLogID = (file: EnteFile) =>
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
`file ${file.metadata.title ?? "-"} (${file.id})`;
/**
* Return the date when the file will be deleted permanently. Only valid for
* files that are in the user's trash.
*
* This is a convenience wrapper over the {@link deleteBy} property of a file,
* converting that epoch microsecond value into a JavaScript date.
*/
export const enteFileDeletionDate = (file: EnteFile) =>
dateFromEpochMicroseconds(file.deleteBy);
export async function decryptFile(
file: EncryptedEnteFile,
collectionKey: string,

View File

@@ -1,3 +1,4 @@
import { Overlay } from "@/base/components/containers";
import { downloadManager } from "@/gallery/services/download";
import { type EnteFile } from "@/media/file";
import {
@@ -99,12 +100,12 @@ export const ItemCard: React.FC<React.PropsWithChildren<ItemCardProps>> = ({
* A generic "base" tile, meant to be used (after setting dimensions) as the
* {@link TileComponent} provided to an {@link ItemCard}.
*
* Use {@link ItemTileOverlay} (usually via one of its presets) to overlay
* content on top of the tile.
* Use {@link Overlay} (usually via one of its presets) to overlay content on
* top of the tile.
*/
const BaseTile = styled("div")`
display: flex;
/* Act as container for the absolutely positioned ItemTileOverlays. */
/* Act as container for the absolutely positioned 'Overlay's. */
position: relative;
border-radius: 4px;
overflow: hidden;
@@ -130,13 +131,11 @@ export const PreviewItemTile = styled(BaseTile)`
/**
* A rectangular, TV-ish tile used in the gallery bar.
*/
export const BarItemTile = styled(BaseTile)(
({ theme }) => `
export const BarItemTile = styled(BaseTile)`
width: 90px;
height: 64px;
color: ${theme.vars.palette.fixed.white};
`,
);
color: white;
`;
/**
* A square tile used on the duplicates listing.
@@ -158,7 +157,7 @@ export const BaseTileButton = styled(UnstyledButton)`
/* Rest of this is mostly verbatim from BaseTile ... */
display: flex;
/* Act as container for the absolutely positioned ItemTileOverlays. */
/* Act as container for the absolutely positioned 'Overlay's. */
position: relative;
border-radius: 4px;
overflow: hidden;
@@ -180,24 +179,10 @@ export const LargeTileButton = styled(BaseTileButton)`
`;
/**
* An empty overlay on top of the nearest relative positioned ancestor.
*
* This is meant to be used in tandem with a derivate of {@link BaseTile} or
* {@link BaseTileButton}.
* An {@link Overlay} suitable for hosting textual content at the top left of
* small and medium sized tiles.
*/
export const ItemTileOverlay = styled("div")`
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
`;
/**
* An {@link ItemTileOverlay} suitable for hosting textual content at the top
* left of small and medium sized tiles.
*/
export const TileTextOverlay = styled(ItemTileOverlay)`
export const TileTextOverlay = styled(Overlay)`
padding: 4px;
background: linear-gradient(
0deg,
@@ -210,7 +195,7 @@ export const TileTextOverlay = styled(ItemTileOverlay)`
* A variation of {@link TileTextOverlay} for use with larger tiles like the
* {@link CollectionTile}.
*/
export const LargeTileTextOverlay = styled(ItemTileOverlay)`
export const LargeTileTextOverlay = styled(Overlay)`
padding: 8px;
background: linear-gradient(
0deg,
@@ -222,7 +207,7 @@ export const LargeTileTextOverlay = styled(ItemTileOverlay)`
/**
* A container for "+", suitable for use with a {@link LargeTileTextOverlay}.
*/
export const LargeTilePlusOverlay = styled(ItemTileOverlay)(
export const LargeTilePlusOverlay = styled(Overlay)(
({ theme }) => `
display: flex;
justify-content: center;
@@ -233,13 +218,15 @@ export const LargeTilePlusOverlay = styled(ItemTileOverlay)(
);
/**
* An {@link ItemTileOverlay} suitable for holding the collection name shown
* atop the tiles in the duplicates listing.
* An {@link Overlay} suitable for showing text at the bottom center of the
* tile. Used by the tiles in trash (for showing the days until deletion) and
* duplicate listing (for showing the collection name).
*/
export const DuplicateTileTextOverlay = styled(ItemTileOverlay)`
export const TileBottomTextOverlay = styled(Overlay)`
display: flex;
justify-content: center;
align-items: flex-end;
padding: 4px;
background: linear-gradient(transparent 50%, rgba(0, 0, 0, 0.7));
padding: 6px;
background: linear-gradient(transparent 30%, 80%, rgba(0 0 0 / 0.7));
color: white;
`;

View File

@@ -42,8 +42,8 @@ import {
} from "react-window";
import {
DuplicateItemTile,
DuplicateTileTextOverlay,
ItemCard,
TileBottomTextOverlay,
} from "../components/Tiles";
import {
computeThumbnailGridLayoutParams,
@@ -600,11 +600,11 @@ const ListItem: React.FC<ListChildComponentProps<DuplicatesListItemData>> =
TileComponent={DuplicateItemTile}
coverFile={item.file}
>
<DuplicateTileTextOverlay>
<Ellipsized2LineTypography color="text.muted">
<TileBottomTextOverlay>
<Ellipsized2LineTypography variant="small">
{item.collectionName}
</Ellipsized2LineTypography>
</DuplicateTileTextOverlay>
</TileBottomTextOverlay>
</ItemCard>
))}
</ItemGrid>

View File

@@ -1,5 +1,7 @@
import i18n, { t } from "i18next";
// TODO: Move to @/base/date
const dateTimeFullFormatter1 = new Intl.DateTimeFormat(i18n.language, {
weekday: "short",
month: "short",