[web] Light mode WIP - Part x/x (#4962)
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
14
web/packages/base/date.ts
Normal 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);
|
||||
38
web/packages/base/i18n-date.ts
Normal file
38
web/packages/base/i18n-date.ts
Normal 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");
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user