Merge branch 'main' into add-comlink-types

This commit is contained in:
Abhinav 2022-12-04 19:47:30 +05:30
commit fb32c6de5b
152 changed files with 4274 additions and 4286 deletions

View file

@ -1,13 +0,0 @@
{
"presets": ["next/babel"],
"plugins": [
[
"styled-components",
{
"ssr": true,
"displayName": true,
"preprocess": false
}
]
]
}

View file

@ -1 +0,0 @@
thirdparty

View file

@ -1,60 +1,59 @@
{ {
"root": true, "root": true,
"env": { "parserOptions": {
"browser": true, "project": ["./tsconfig.json"]
"es2021": true,
"node": true
}, },
"extends": [ "extends": [
"plugin:react/recommended", "next/core-web-vitals",
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended",
"google", "plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier" "prettier"
], ],
"parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"rules": { "rules": {
"indent":"off", "indent": "off",
"class-methods-use-this": "off", "class-methods-use-this": "off",
"react/prop-types": "off", "react/prop-types": "off",
"react/display-name": "off", "react/display-name": "off",
"react/no-unescaped-entities": "off", "react/no-unescaped-entities": "off",
"no-unused-vars": "off", "no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [ "@typescript-eslint/no-unused-vars": ["error"],
"error"
],
"require-jsdoc": "off", "require-jsdoc": "off",
"valid-jsdoc": "off", "valid-jsdoc": "off",
"max-len": "off", "max-len": "off",
"new-cap": "off", "new-cap": "off",
"no-invalid-this": "off", "no-invalid-this": "off",
"eqeqeq": "error", "eqeqeq": "error",
"object-curly-spacing": [ "object-curly-spacing": ["error", "always"],
"error",
"always"
],
"space-before-function-paren": "off", "space-before-function-paren": "off",
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }] "operator-linebreak": [
}, "error",
"settings": { "after",
"react": { { "overrides": { "?": "before", ":": "before" } }
"version": "detect" ],
} "@typescript-eslint/no-unsafe-member-access": "off",
}, "@typescript-eslint/no-unsafe-return": "off",
"globals": { "@typescript-eslint/no-unsafe-assignment": "off",
"JSX": "readonly", "@typescript-eslint/no-inferrable-types": "off",
"NodeJS": "readonly", "@typescript-eslint/restrict-template-expressions": "off",
"ReadableStreamDefaultController": "readonly" "@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-floating-promises": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-empty-interface": "off",
"@typescript-eslint/no-misused-promises": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"react-hooks/rules-of-hooks": "off",
"react-hooks/exhaustive-deps": "off",
"@next/next/no-img-element": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"jsx-a11y/alt-text": "off"
} }
} }

13
.lintstagedrc.js Normal file
View file

@ -0,0 +1,13 @@
const path = require('path');
const buildEslintCommand = (filenames) =>
`next lint --fix --file ${filenames
.map((f) => path.relative(process.cwd(), f))
.join(' --file ')}`;
const buildPrettierCommand = (filenames) =>
`yarn prettier --write --ignore-unknown ${filenames.join(' ')}`;
module.exports = {
'*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand],
};

1
.yarnrc Normal file
View file

@ -0,0 +1 @@
network-timeout 500000

View file

@ -37,11 +37,6 @@ module.exports = {
'report-to': ' https://csp-reporter.ente.io/local', 'report-to': ' https://csp-reporter.ente.io/local',
}, },
WORKBOX_CONFIG: {
swSrc: 'src/serviceWorker.js',
exclude: [/manifest\.json$/i],
},
ALL_ROUTES: '/(.*)', ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({ buildCSPHeader: (directives) => ({

View file

@ -1,7 +1,6 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === 'true',
}); });
const withWorkbox = require('@ente-io/next-with-workbox');
const { withSentryConfig } = require('@sentry/nextjs'); const { withSentryConfig } = require('@sentry/nextjs');
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants'); const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
@ -19,7 +18,6 @@ const {
COOP_COEP_HEADERS, COOP_COEP_HEADERS,
WEB_SECURITY_HEADERS, WEB_SECURITY_HEADERS,
CSP_DIRECTIVES, CSP_DIRECTIVES,
WORKBOX_CONFIG,
ALL_ROUTES, ALL_ROUTES,
getIsSentryEnabled, getIsSentryEnabled,
} = require('./configUtil'); } = require('./configUtil');
@ -30,37 +28,39 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
module.exports = (phase) => module.exports = (phase) =>
withSentryConfig( withSentryConfig(
withWorkbox( withBundleAnalyzer(
withBundleAnalyzer( withTM({
withTM({ compiler: {
env: { styledComponents: {
SENTRY_RELEASE: GIT_SHA, ssr: true,
NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA, displayName: true,
}, },
workbox: WORKBOX_CONFIG, },
env: {
SENTRY_RELEASE: GIT_SHA,
},
headers() { headers() {
return [ return [
{ {
// Apply these headers to all routes in your application.... // Apply these headers to all routes in your application....
source: ALL_ROUTES, source: ALL_ROUTES,
headers: convertToNextHeaderFormat({ headers: convertToNextHeaderFormat({
...COOP_COEP_HEADERS, ...COOP_COEP_HEADERS,
...WEB_SECURITY_HEADERS, ...WEB_SECURITY_HEADERS,
...buildCSPHeader(CSP_DIRECTIVES), ...buildCSPHeader(CSP_DIRECTIVES),
}), }),
}, },
]; ];
}, },
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j // https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
webpack: (config, { isServer }) => { webpack: (config, { isServer }) => {
if (!isServer) { if (!isServer) {
config.resolve.fallback.fs = false; config.resolve.fallback.fs = false;
} }
return config; return config;
}, },
}) })
)
), ),
{ {
release: GIT_SHA, release: GIT_SHA,

View file

@ -5,7 +5,7 @@
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"albums": "next dev -p 3002", "albums": "next dev -p 3002",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"", "lint": "next lint",
"prebuild": "yarn lint", "prebuild": "yarn lint",
"build": "next build", "build": "next build",
"postbuild": "next export", "postbuild": "next export",
@ -15,7 +15,6 @@
}, },
"dependencies": { "dependencies": {
"@date-io/date-fns": "^2.14.0", "@date-io/date-fns": "^2.14.0",
"@ente-io/next-with-workbox": "^1.0.3",
"@mui/icons-material": "^5.6.2", "@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.2", "@mui/material": "^5.6.2",
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest", "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
@ -39,7 +38,7 @@
"jszip": "3.7.1", "jszip": "3.7.1",
"libsodium-wrappers": "^0.7.8", "libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"next": "^12.1.0", "next": "^12.3.1",
"next-transpile-modules": "^9.0.0", "next-transpile-modules": "^9.0.0",
"photoswipe": "file:./thirdparty/photoswipe", "photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6", "piexifjs": "^1.0.6",
@ -52,12 +51,8 @@
"react-top-loading-bar": "^2.0.1", "react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2", "react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6", "react-window": "^1.8.6",
"sanitize-filename": "^1.6.3",
"styled-components": "^5.3.5", "styled-components": "^5.3.5",
"workbox-precaching": "^6.1.5",
"workbox-recipes": "^6.1.5",
"workbox-routing": "^6.1.5",
"workbox-strategies": "^6.1.5",
"workbox-window": "^6.1.5",
"xml-js": "^1.6.11", "xml-js": "^1.6.11",
"yup": "^0.29.3" "yup": "^0.29.3"
}, },
@ -75,17 +70,10 @@
"@types/react-window-infinite-loader": "^1.0.3", "@types/react-window-infinite-loader": "^1.0.3",
"@types/styled-components": "^5.1.25", "@types/styled-components": "^5.1.25",
"@types/yup": "^0.29.7", "@types/yup": "^0.29.7",
"@typescript-eslint/eslint-plugin": "^4.25.0", "@typescript-eslint/eslint-plugin": "^5.43.0",
"@typescript-eslint/parser": "^4.25.0", "eslint": "^8.28.0",
"babel-plugin-styled-components": "^1.11.1", "eslint-config-next": "^13.0.4",
"eslint": "^7.27.0", "eslint-config-prettier": "^8.5.0",
"eslint-config-airbnb": "^18.2.1",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-import": "^2.23.3",
"eslint-plugin-jsx-a11y": "^6.4.1",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.2.0",
"husky": "^7.0.1", "husky": "^7.0.1",
"lint-staged": "^11.1.2", "lint-staged": "^11.1.2",
"prettier": "2.3.2", "prettier": "2.3.2",
@ -95,12 +83,6 @@
"standard": { "standard": {
"parser": "babel-eslint" "parser": "babel-eslint"
}, },
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write --ignore-unknown"
]
},
"resolutions": { "resolutions": {
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest" "@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
} }

View file

@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV();
const SENTRY_RELEASE = getSentryRelease(); const SENTRY_RELEASE = getSentryRelease();
const IS_ENABLED = getIsSentryEnabled(); const IS_ENABLED = getIsSentryEnabled();
Sentry.setUser({ id: getSentryUserID() });
Sentry.init({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
enabled: IS_ENABLED, enabled: IS_ENABLED,
@ -39,3 +38,9 @@ Sentry.init({
// `release` value here - use the environment variable `SENTRY_RELEASE`, so // `release` value here - use the environment variable `SENTRY_RELEASE`, so
// that it will also get attached to your source maps // that it will also get attached to your source maps
}); });
const main = async () => {
Sentry.setUser({ id: await getSentryUserID() });
};
main();

3
sentry.properties Normal file
View file

@ -0,0 +1,3 @@
defaults.url=https://sentry.ente.io/
defaults.org=ente
defaults.project=bada-frame

View file

@ -6,6 +6,8 @@ import {
getIsSentryEnabled, getIsSentryEnabled,
} from 'constants/sentry'; } from 'constants/sentry';
import { getSentryUserID } from 'utils/user';
const SENTRY_DSN = getSentryDSN(); const SENTRY_DSN = getSentryDSN();
const SENTRY_ENV = getSentryENV(); const SENTRY_ENV = getSentryENV();
const SENTRY_RELEASE = getSentryRelease(); const SENTRY_RELEASE = getSentryRelease();
@ -18,3 +20,9 @@ Sentry.init({
release: SENTRY_RELEASE, release: SENTRY_RELEASE,
autoSessionTracking: false, autoSessionTracking: false,
}); });
const main = async () => {
Sentry.setUser({ id: await getSentryUserID() });
};
main();

View file

@ -1,10 +1,11 @@
const ENV_DEVELOPMENT = 'development';
module.exports.getIsSentryEnabled = () => { module.exports.getIsSentryEnabled = () => {
if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) { if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes'; return false;
} else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
return false;
} else { } else {
if (process.env.NEXT_PUBLIC_SENTRY_ENV) { return true;
return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development';
}
} }
return false;
}; };

View file

@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
export const Badge = styled(Paper)(({ theme }) => ({ export const Badge = styled(Paper)(({ theme }) => ({
padding: '2px 4px', padding: '2px 4px',
backgroundColor: theme.palette.glass.main, backgroundColor: theme.palette.backdrop.main,
color: theme.palette.glass.contrastText, backdropFilter: `blur(${theme.palette.blur.muted})`,
color: theme.palette.primary.contrastText,
textTransform: 'uppercase', textTransform: 'uppercase',
...(theme.typography.mini as CSSProperties), ...(theme.typography.mini as CSSProperties),
})); }));

10
src/components/Chip.tsx Normal file
View file

@ -0,0 +1,10 @@
import { Box, styled } from '@mui/material';
import { CSSProperties } from 'react';
export const Chip = styled(Box)(({ theme }) => ({
...(theme.typography.body2 as CSSProperties),
padding: '8px 12px',
borderRadius: '4px',
backgroundColor: theme.palette.fill.dark,
fontWeight: 'bold',
}));

View file

@ -1,20 +1,43 @@
import React from 'react'; import React, { useState } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { CopyButtonWrapper } from './styledComponents';
import DoneIcon from '@mui/icons-material/Done'; import DoneIcon from '@mui/icons-material/Done';
import ContentCopyIcon from '@mui/icons-material/ContentCopy'; import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import { Tooltip } from '@mui/material'; import {
IconButton,
IconButtonProps,
SvgIconProps,
Tooltip,
} from '@mui/material';
export default function CopyButton({ code, copied, copyToClipboardHelper }) { export default function CopyButton({
code,
color,
size,
}: {
code: string;
color?: IconButtonProps['color'];
size?: SvgIconProps['fontSize'];
}) {
const [copied, setCopied] = useState<boolean>(false);
const copyToClipboardHelper = (text: string) => () => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
return ( return (
<Tooltip arrow open={copied} title={constants.COPIED}> <Tooltip
<CopyButtonWrapper onClick={copyToClipboardHelper(code)}> arrow
open={copied}
title={constants.COPIED}
PopperProps={{ sx: { zIndex: 2000 } }}>
<IconButton onClick={copyToClipboardHelper(code)} color={color}>
{copied ? ( {copied ? (
<DoneIcon fontSize="small" /> <DoneIcon fontSize={size ?? 'small'} />
) : ( ) : (
<ContentCopyIcon fontSize="small" /> <ContentCopyIcon fontSize={size ?? 'small'} />
)} )}
</CopyButtonWrapper> </IconButton>
</Tooltip> </Tooltip>
); );
} }

View file

@ -1,7 +1,7 @@
import { FreeFlowText } from '../Container'; import { FreeFlowText } from '../Container';
import React, { useState } from 'react'; import React from 'react';
import EnteSpinner from '../EnteSpinner'; import EnteSpinner from '../EnteSpinner';
import { Wrapper, CodeWrapper } from './styledComponents'; import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
import CopyButton from './CopyButton'; import CopyButton from './CopyButton';
import { BoxProps } from '@mui/material'; import { BoxProps } from '@mui/material';
@ -15,14 +15,6 @@ export default function CodeBlock({
wordBreak, wordBreak,
...props ...props
}: BoxProps<'div', Iprops>) { }: BoxProps<'div', Iprops>) {
const [copied, setCopied] = useState<boolean>(false);
const copyToClipboardHelper = (text: string) => () => {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 1000);
};
if (!code) { if (!code) {
return ( return (
<Wrapper> <Wrapper>
@ -37,11 +29,9 @@ export default function CodeBlock({
{code} {code}
</FreeFlowText> </FreeFlowText>
</CodeWrapper> </CodeWrapper>
<CopyButton <CopyButtonWrapper>
code={code} <CopyButton code={code} />
copied={copied} </CopyButtonWrapper>
copyToClipboardHelper={copyToClipboardHelper}
/>
</Wrapper> </Wrapper>
); );
} }

View file

@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents';
import { shouldShowOptions } from 'utils/collection'; import { shouldShowOptions } from 'utils/collection';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded'; import Favorite from '@mui/icons-material/FavoriteRounded';
import VisibilityOff from '@mui/icons-material/VisibilityOff';
import Delete from '@mui/icons-material/Delete'; import Delete from '@mui/icons-material/Delete';
import { ArchiveOutlined } from '@mui/icons-material';
interface Iprops { interface Iprops {
activeCollection: Collection; activeCollection: Collection;
@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({
return <Favorite />; return <Favorite />;
case CollectionSummaryType.archived: case CollectionSummaryType.archived:
case CollectionSummaryType.archive: case CollectionSummaryType.archive:
return <VisibilityOff />; return <ArchiveOutlined />;
case CollectionSummaryType.trash: case CollectionSummaryType.trash:
return <Delete />; return <Delete />;
default: default:

View file

@ -11,7 +11,7 @@ import TruncateText from 'components/TruncateText';
import { Box } from '@mui/material'; import { Box } from '@mui/material';
import { CollectionSummaryType } from 'constants/collection'; import { CollectionSummaryType } from 'constants/collection';
import Favorite from '@mui/icons-material/FavoriteRounded'; import Favorite from '@mui/icons-material/FavoriteRounded';
import VisibilityOff from '@mui/icons-material/VisibilityOff'; import { ArchiveOutlined } from '@mui/icons-material';
interface Iprops { interface Iprops {
active: boolean; active: boolean;
@ -50,7 +50,7 @@ function CollectionCardIcon({ collectionType }) {
<CollectionBarTileIcon> <CollectionBarTileIcon>
{collectionType === CollectionSummaryType.favorites && <Favorite />} {collectionType === CollectionSummaryType.favorites && <Favorite />}
{collectionType === CollectionSummaryType.archived && ( {collectionType === CollectionSummaryType.archived && (
<VisibilityOff /> <ArchiveOutlined />
)} )}
</CollectionBarTileIcon> </CollectionBarTileIcon>
); );

View file

@ -4,11 +4,10 @@ import React from 'react';
import EditIcon from '@mui/icons-material/Edit'; import EditIcon from '@mui/icons-material/Edit';
import IosShareIcon from '@mui/icons-material/IosShare'; import IosShareIcon from '@mui/icons-material/IosShare';
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined'; import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined'; import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { CollectionActions } from '.'; import { CollectionActions } from '.';
import { ArchiveOutlined, Unarchive } from '@mui/icons-material';
interface Iprops { interface Iprops {
IsArchived: boolean; IsArchived: boolean;
@ -53,13 +52,13 @@ export function AlbumCollectionOption({
onClick={handleCollectionAction( onClick={handleCollectionAction(
CollectionActions.UNARCHIVE CollectionActions.UNARCHIVE
)} )}
startIcon={<VisibilityOnOutlinedIcon />}> startIcon={<Unarchive />}>
{constants.UNARCHIVE} {constants.UNARCHIVE}
</OverflowMenuOption> </OverflowMenuOption>
) : ( ) : (
<OverflowMenuOption <OverflowMenuOption
onClick={handleCollectionAction(CollectionActions.ARCHIVE)} onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
startIcon={<VisibilityOffOutlinedIcon />}> startIcon={<ArchiveOutlined />}>
{constants.ARCHIVE} {constants.ARCHIVE}
</OverflowMenuOption> </OverflowMenuOption>
)} )}

View file

@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { shareCollection } from 'services/collectionService'; import { shareCollection } from 'services/collectionService';
import { User } from 'types/user'; import { User } from 'types/user';
import { handleSharingErrors } from 'utils/error'; import { handleSharingErrors } from 'utils/error/ui';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { CollectionShareSharees } from './sharees'; import { CollectionShareSharees } from './sharees';

View file

@ -1,6 +1,5 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { FlexWrapper } from 'components/Container'; import { FlexWrapper } from 'components/Container';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext, useState } from 'react'; import React, { useContext, useState } from 'react';
import { import {
@ -8,7 +7,7 @@ import {
deleteShareableURL, deleteShareableURL,
} from 'services/collectionService'; } from 'services/collectionService';
import { Collection, PublicURL } from 'types/collection'; import { Collection, PublicURL } from 'types/collection';
import { handleSharingErrors } from 'utils/error'; import { handleSharingErrors } from 'utils/error/ui';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import PublicShareSwitch from './switch'; import PublicShareSwitch from './switch';
interface Iprops { interface Iprops {
@ -60,7 +59,7 @@ export default function PublicShareControl({
proceed: { proceed: {
text: constants.DISABLE, text: constants.DISABLE,
action: disablePublicSharing, action: disablePublicSharing,
variant: ButtonVariant.danger, variant: 'danger',
}, },
}); });
}; };

View file

@ -1,5 +1,4 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -34,7 +33,7 @@ export function ManageDownloadAccess({
collectionID: collection.id, collectionID: collection.id,
enableDownload: false, enableDownload: false,
}), }),
variant: ButtonVariant.danger, variant: 'danger',
}, },
}); });
}; };

View file

@ -8,13 +8,13 @@ import React, { useContext, useState } from 'react';
import { updateShareableURL } from 'services/collectionService'; import { updateShareableURL } from 'services/collectionService';
import { UpdatePublicURL } from 'types/collection'; import { UpdatePublicURL } from 'types/collection';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { handleSharingErrors } from 'utils/error';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { import {
ManageSectionLabel, ManageSectionLabel,
ManageSectionOptions, ManageSectionOptions,
} from '../../styledComponents'; } from '../../styledComponents';
import { ManageDownloadAccess } from './downloadAccess'; import { ManageDownloadAccess } from './downloadAccess';
import { handleSharingErrors } from 'utils/error/ui';
export default function PublicShareManage({ export default function PublicShareManage({
publicShareProp, publicShareProp,

View file

@ -4,7 +4,7 @@ import Select from 'react-select';
import { linkExpiryStyle } from 'styles/linkExpiry'; import { linkExpiryStyle } from 'styles/linkExpiry';
import { shareExpiryOptions } from 'utils/collection'; import { shareExpiryOptions } from 'utils/collection';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { dateStringWithMMH } from 'utils/time'; import { formatDateTime } from 'utils/time/format';
import { OptionWithDivider } from './selectComponents/OptionWithDivider'; import { OptionWithDivider } from './selectComponents/OptionWithDivider';
export function ManageLinkExpiry({ export function ManageLinkExpiry({
@ -31,7 +31,7 @@ export function ManageLinkExpiry({
}} }}
placeholder={ placeholder={
publicShareProp?.validTill publicShareProp?.validTill
? dateStringWithMMH(publicShareProp?.validTill) ? formatDateTime(publicShareProp?.validTill / 1000)
: 'never' : 'never'
} }
onChange={(e) => { onChange={(e) => {

View file

@ -1,5 +1,4 @@
import { Box, Typography } from '@mui/material'; import { Box, Typography } from '@mui/material';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -32,7 +31,7 @@ export function ManageLinkPassword({
collectionID: collection.id, collectionID: collection.id,
disablePassword: true, disablePassword: true,
}), }),
variant: ButtonVariant.danger, variant: 'danger',
}, },
}); });
}; };

View file

@ -1,39 +0,0 @@
import React from 'react';
import { styled } from '@mui/material';
import constants from 'utils/strings/constants';
import { IconWithMessage } from './IconWithMessage';
const Wrapper = styled('button')`
border: none;
background-color: #ff6666;
position: fixed;
z-index: 1;
bottom: 30px;
right: 30px;
width: 60px;
height: 60px;
border-radius: 50%;
color: #fff;
`;
export default function DeleteBtn(props) {
return (
<IconWithMessage message={constants.EMPTY_TRASH}>
<Wrapper onClick={props.onClick}>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</svg>
</Wrapper>
</IconWithMessage>
);
}
DeleteBtn.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -0,0 +1,18 @@
import { Box } from '@mui/material';
import React from 'react';
export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
return (
<Box
className="DialogIcon"
sx={{
svg: {
width: '48px',
height: '48px',
},
color: 'stroke.secondary',
}}>
{icon}
</Box>
);
}

View file

@ -5,6 +5,12 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
padding: theme.spacing(1, 1.5), padding: theme.spacing(1, 1.5),
maxWidth: '346px', maxWidth: '346px',
}, },
'& .DialogIcon': {
padding: theme.spacing(2),
paddingBottom: theme.spacing(1),
},
'& .MuiDialogTitle-root': { '& .MuiDialogTitle-root': {
padding: theme.spacing(2), padding: theme.spacing(2),
paddingBottom: theme.spacing(1), paddingBottom: theme.spacing(1),
@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
'& .MuiDialogContent-root': { '& .MuiDialogContent-root': {
padding: theme.spacing(2), padding: theme.spacing(2),
}, },
'.DialogIcon + .MuiDialogTitle-root': {
paddingTop: 0,
},
'.MuiDialogTitle-root + .MuiDialogContent-root': { '.MuiDialogTitle-root + .MuiDialogContent-root': {
paddingTop: 0, paddingTop: 0,
}, },

View file

@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, {
} from './TitleWithCloseButton'; } from './TitleWithCloseButton';
import DialogBoxBase from './base'; import DialogBoxBase from './base';
import { DialogBoxAttributes } from 'types/dialogBox'; import { DialogBoxAttributes } from 'types/dialogBox';
import DialogIcon from './DialogIcon';
type IProps = React.PropsWithChildren< type IProps = React.PropsWithChildren<
Omit<DialogProps, 'onClose' | 'maxSize'> & { Omit<DialogProps, 'onClose' | 'maxSize'> & {
@ -48,6 +49,7 @@ export default function DialogBox({
maxWidth={size} maxWidth={size}
onClose={handleClose} onClose={handleClose}
{...props}> {...props}>
{attributes.icon && <DialogIcon icon={attributes.icon} />}
{attributes.title && ( {attributes.title && (
<DialogTitleWithCloseButton <DialogTitleWithCloseButton
onClose={ onClose={

View file

@ -4,7 +4,6 @@ import {
MIN_EDITED_CREATION_TIME, MIN_EDITED_CREATION_TIME,
MAX_EDITED_CREATION_TIME, MAX_EDITED_CREATION_TIME,
} from 'constants/file'; } from 'constants/file';
import { TextField } from '@mui/material';
import { import {
LocalizationProvider, LocalizationProvider,
MobileDateTimePicker, MobileDateTimePicker,
@ -60,14 +59,7 @@ const EnteDateTimePicker = ({
}, },
}, },
}} }}
renderInput={(params) => ( renderInput={() => <></>}
<TextField
{...params}
hiddenLabel
margin="none"
variant="standard"
/>
)}
/> />
</LocalizationProvider> </LocalizationProvider>
); );

View file

@ -0,0 +1,11 @@
import { Drawer } from '@mui/material';
import styled from 'styled-components';
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
'& .MuiPaper-root': {
maxWidth: '375px',
width: '100%',
scrollbarWidth: 'thin',
padding: theme.spacing(1),
},
}));

View file

@ -1,8 +1,8 @@
import { Button, DialogActions, DialogContent, Stack } from '@mui/material'; import { Button, DialogActions, DialogContent, Stack } from '@mui/material';
import React from 'react'; import React from 'react';
import { ExportStats } from 'types/export'; import { ExportStats } from 'types/export';
import { formatDateTime } from 'utils/time';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { formatDateTime } from 'utils/time/format';
import { FlexWrapper, Label, Value } from './Container'; import { FlexWrapper, Label, Value } from './Container';
import { ComfySpan } from './ExportInProgress'; import { ComfySpan } from './ExportInProgress';

View file

@ -1,19 +0,0 @@
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import React from 'react';
interface IconWithMessageProps {
children?: any;
message: string;
}
export const IconWithMessage = (props: IconWithMessageProps) => (
<OverlayTrigger
placement="bottom"
overlay={
<Tooltip id="on-hover-info" style={{ zIndex: 1002 }}>
{props.message}
</Tooltip>
}>
{props.children}
</OverlayTrigger>
);

View file

@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
}; };
const handleClick = () => { const handleClick = () => {
attributes.action?.callback(); attributes.onClick();
onClose(); onClose();
}; };
return ( return (
@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
anchorOrigin={{ anchorOrigin={{
horizontal: 'right', horizontal: 'right',
vertical: 'bottom', vertical: 'bottom',
}}> }}
sx={{ backgroundColor: '#000', width: '320px' }}>
<Paper <Paper
component={Button} component={Button}
color={attributes.variant} color={attributes.variant}
onClick={handleClick} onClick={handleClick}
sx={{ sx={{
textAlign: 'left', textAlign: 'left',
width: '320px', flex: '1',
padding: (theme) => theme.spacing(1.5, 2), padding: (theme) => theme.spacing(1.5, 2),
}}> }}>
<Stack <Stack
@ -55,34 +56,38 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
spacing={2} spacing={2}
direction="row" direction="row"
alignItems={'center'}> alignItems={'center'}>
<Box> <Box sx={{ svg: { fontSize: '36px' } }}>
{attributes?.icon ?? <InfoIcon fontSize="large" />} {attributes.startIcon ?? <InfoIcon />}
</Box> </Box>
<Box sx={{ flex: 1 }}>
<Typography <Stack
variant="body2" direction={'column'}
color="rgba(255, 255, 255, 0.7)" spacing={0.5}
mb={0.5}> flex={1}
{attributes.message}{' '} textAlign="left">
</Typography> {attributes.subtext && (
{attributes?.action && ( <Typography variant="body2">
<Typography {attributes.subtext}
mb={0.5}
variant="button"
fontWeight={'bold'}>
{attributes?.action.text}
</Typography> </Typography>
)} )}
</Box> {attributes.message && (
<Box> <Typography variant="button">
{attributes.message}
</Typography>
)}
</Stack>
{attributes.endIcon ? (
<IconButton <IconButton
onClick={handleClose} onClick={attributes.onClick}
sx={{ sx={{ fontSize: '36px' }}>
backgroundColor: 'rgba(255, 255, 255, 0.1)', {attributes?.endIcon}
}}> </IconButton>
) : (
<IconButton onClick={handleClose}>
<CloseIcon /> <CloseIcon />
</IconButton> </IconButton>
</Box> )}
</Stack> </Stack>
</Paper> </Paper>
</Snackbar> </Snackbar>

View file

@ -1,12 +1,12 @@
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import PreviewCard from './pages/gallery/PreviewCard'; import PreviewCard from './pages/gallery/PreviewCard';
import React, { useContext, useEffect, useRef, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import DownloadManager from 'services/downloadManager'; import DownloadManager from 'services/downloadManager';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import PhotoSwipe from 'components/PhotoSwipe'; import PhotoViewer from 'components/PhotoViewer';
import { import {
ALL_SECTION, ALL_SECTION,
ARCHIVE_SECTION, ARCHIVE_SECTION,
@ -15,7 +15,7 @@ import {
import { isSharedFile } from 'utils/file'; import { isSharedFile } from 'utils/file';
import { isPlaybackPossible } from 'utils/photoFrame'; import { isPlaybackPossible } from 'utils/photoFrame';
import { PhotoList } from './PhotoList'; import { PhotoList } from './PhotoList';
import { SetFiles, SelectedState } from 'types/gallery'; import { SelectedState } from 'types/gallery';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager'; import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
@ -30,6 +30,8 @@ import { logError } from 'utils/sentry';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { User } from 'types/user'; import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useMemo } from 'react';
import { Collection } from 'types/collection';
const Container = styled('div')` const Container = styled('div')`
display: block; display: block;
@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
interface Props { interface Props {
files: EnteFile[]; files: EnteFile[];
setFiles: SetFiles; collections?: Collection[];
syncWithRemote: () => Promise<void>; syncWithRemote: () => Promise<void>;
favItemIds?: Set<number>; favItemIds?: Set<number>;
archivedCollections?: Set<number>; archivedCollections?: Set<number>;
@ -60,7 +62,8 @@ interface Props {
openUploader?; openUploader?;
isInSearchMode?: boolean; isInSearchMode?: boolean;
search?: Search; search?: Search;
deleted?: number[]; deletedFileIds?: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void;
activeCollection: number; activeCollection: number;
isSharedCollection?: boolean; isSharedCollection?: boolean;
enableDownload?: boolean; enableDownload?: boolean;
@ -69,13 +72,15 @@ interface Props {
} }
type SourceURL = { type SourceURL = {
imageURL?: string; originalImageURL?: string;
videoURL?: string; originalVideoURL?: string;
convertedImageURL?: string;
convertedVideoURL?: string;
}; };
const PhotoFrame = ({ const PhotoFrame = ({
files, files,
setFiles, collections,
syncWithRemote, syncWithRemote,
favItemIds, favItemIds,
archivedCollections, archivedCollections,
@ -86,7 +91,8 @@ const PhotoFrame = ({
isInSearchMode, isInSearchMode,
search, search,
resetSearch, resetSearch,
deleted, deletedFileIds,
setDeletedFileIds,
activeCollection, activeCollection,
isSharedCollection, isSharedCollection,
enableDownload, enableDownload,
@ -104,75 +110,26 @@ const PhotoFrame = ({
const [rangeStart, setRangeStart] = useState(null); const [rangeStart, setRangeStart] = useState(null);
const [currentHover, setCurrentHover] = useState(null); const [currentHover, setCurrentHover] = useState(null);
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false); const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
const filteredDataRef = useRef<EnteFile[]>([]);
const filteredData = filteredDataRef?.current ?? [];
const router = useRouter(); const router = useRouter();
const [isSourceLoaded, setIsSourceLoaded] = useState(false); const [isSourceLoaded, setIsSourceLoaded] = useState(false);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setIsShiftKeyPressed(true);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setIsShiftKeyPressed(false);
}
};
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false);
router.events.on('hashChangeComplete', (url: string) => {
const start = url.indexOf('#');
const hash = url.slice(start !== -1 ? start : url.length);
const shouldPhotoSwipeBeOpened = hash.endsWith(
PHOTOSWIPE_HASH_SUFFIX
);
if (shouldPhotoSwipeBeOpened) {
setOpen(true);
} else {
setOpen(false);
}
});
return () => {
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false);
};
}, []);
useEffect(() => { const filteredData = useMemo(() => {
if (!isNaN(search?.file)) {
const filteredDataIdx = filteredData.findIndex((file) => {
return file.id === search.file;
});
if (!isNaN(filteredDataIdx)) {
onThumbnailClick(filteredDataIdx)();
}
resetSearch();
}
}, [search, filteredData]);
const resetFetching = () => {
setFetching({});
};
useEffect(() => {
if (selected.count === 0) {
setRangeStart(null);
}
}, [selected]);
useEffect(() => {
const idSet = new Set(); const idSet = new Set();
const user: User = getData(LS_KEYS.USER); const user: User = getData(LS_KEYS.USER);
filteredDataRef.current = files
return files
.map((item, index) => ({ .map((item, index) => ({
...item, ...item,
dataIndex: index, dataIndex: index,
w: window.innerWidth, w: window.innerWidth,
h: window.innerHeight, h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
})) }))
.filter((item) => { .filter((item) => {
if (deleted?.includes(item.id)) { if (
deletedFileIds?.has(item.id) &&
activeCollection !== TRASH_SECTION
) {
return false; return false;
} }
if ( if (
@ -230,7 +187,31 @@ const PhotoFrame = ({
} }
return false; return false;
}); });
}, [files, deleted, search, activeCollection]); }, [files, deletedFileIds, search, activeCollection]);
const fileToCollectionsMap = useMemo(() => {
const fileToCollectionsMap = new Map<number, number[]>();
files.forEach((file) => {
if (!fileToCollectionsMap.get(file.id)) {
fileToCollectionsMap.set(file.id, []);
}
fileToCollectionsMap.get(file.id).push(file.collectionID);
});
return fileToCollectionsMap;
}, [files]);
const collectionNameMap = useMemo(() => {
if (collections) {
return new Map<number, string>(
collections.map((collection) => [
collection.id,
collection.name,
])
);
} else {
return new Map();
}
}, [collections]);
useEffect(() => { useEffect(() => {
const currentURL = new URL(window.location.href); const currentURL = new URL(window.location.href);
@ -247,6 +228,59 @@ const PhotoFrame = ({
} }
}, [open]); }, [open]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setIsShiftKeyPressed(true);
}
};
const handleKeyUp = (e: KeyboardEvent) => {
if (e.key === 'Shift') {
setIsShiftKeyPressed(false);
}
};
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false);
router.events.on('hashChangeComplete', (url: string) => {
const start = url.indexOf('#');
const hash = url.slice(start !== -1 ? start : url.length);
const shouldPhotoSwipeBeOpened = hash.endsWith(
PHOTOSWIPE_HASH_SUFFIX
);
if (shouldPhotoSwipeBeOpened) {
setOpen(true);
} else {
setOpen(false);
}
});
return () => {
document.addEventListener('keydown', handleKeyDown, false);
document.addEventListener('keyup', handleKeyUp, false);
};
}, []);
useEffect(() => {
if (!isNaN(search?.file)) {
const filteredDataIdx = filteredData.findIndex((file) => {
return file.id === search.file;
});
if (!isNaN(filteredDataIdx)) {
onThumbnailClick(filteredDataIdx)();
}
resetSearch();
}
}, [search, filteredData]);
const resetFetching = () => {
setFetching({});
};
useEffect(() => {
if (selected.count === 0) {
setRangeStart(null);
}
}, [selected]);
const getFileIndexFromID = (files: EnteFile[], id: number) => { const getFileIndexFromID = (files: EnteFile[], id: number) => {
const index = files.findIndex((file) => file.id === id); const index = files.findIndex((file) => file.id === id);
if (index === -1) { if (index === -1) {
@ -257,12 +291,10 @@ const PhotoFrame = ({
const updateURL = (id: number) => (url: string) => { const updateURL = (id: number) => (url: string) => {
const updateFile = (file: EnteFile) => { const updateFile = (file: EnteFile) => {
file = { file.msrc = url;
...file, file.w = window.innerWidth;
msrc: url, file.h = window.innerHeight;
w: window.innerWidth,
h: window.innerHeight,
};
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) { if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
file.html = ` file.html = `
<div class="pswp-item-container"> <div class="pswp-item-container">
@ -292,29 +324,30 @@ const PhotoFrame = ({
} }
return file; return file;
}; };
setFiles((files) => {
const index = getFileIndexFromID(files, id);
files[index] = updateFile(files[index]);
return files;
});
const index = getFileIndexFromID(files, id); const index = getFileIndexFromID(files, id);
return updateFile(files[index]); return updateFile(files[index]);
}; };
const updateSrcURL = async (id: number, srcURL: SourceURL) => { const updateSrcURL = async (id: number, srcURL: SourceURL) => {
const { videoURL, imageURL } = srcURL; const {
const isPlayable = videoURL && (await isPlaybackPossible(videoURL)); originalImageURL,
convertedImageURL,
originalVideoURL,
convertedVideoURL,
} = srcURL;
const isPlayable =
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
const updateFile = (file: EnteFile) => { const updateFile = (file: EnteFile) => {
file = { file.w = window.innerWidth;
...file, file.h = window.innerHeight;
w: window.innerWidth, file.isSourceLoaded = true;
h: window.innerHeight, file.originalImageURL = originalImageURL;
}; file.originalVideoURL = originalVideoURL;
if (file.metadata.fileType === FILE_TYPE.VIDEO) { if (file.metadata.fileType === FILE_TYPE.VIDEO) {
if (isPlayable) { if (isPlayable) {
file.html = ` file.html = `
<video controls onContextMenu="return false;"> <video controls onContextMenu="return false;">
<source src="${videoURL}" /> <source src="${convertedVideoURL}" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
`; `;
@ -324,7 +357,7 @@ const PhotoFrame = ({
<img src="${file.msrc}" onContextMenu="return false;"/> <img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner" > <div class="download-banner" >
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD} ${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
<a class="btn btn-outline-success" href=${videoURL} download="${file.metadata.title}"">Download</a> <a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
</div> </div>
</div> </div>
`; `;
@ -333,9 +366,9 @@ const PhotoFrame = ({
if (isPlayable) { if (isPlayable) {
file.html = ` file.html = `
<div class = 'pswp-item-container'> <div class = 'pswp-item-container'>
<img id = "live-photo-image-${file.id}" src="${imageURL}" onContextMenu="return false;"/> <img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;"> <video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
<source src="${videoURL}" /> <source src="${convertedVideoURL}" />
Your browser does not support the video tag. Your browser does not support the video tag.
</video> </video>
</div> </div>
@ -352,15 +385,10 @@ const PhotoFrame = ({
`; `;
} }
} else { } else {
file.src = imageURL; file.src = convertedImageURL;
} }
return file; return file;
}; };
setFiles((files) => {
const index = getFileIndexFromID(files, id);
files[index] = updateFile(files[index]);
return files;
});
setIsSourceLoaded(true); setIsSourceLoaded(true);
const index = getFileIndexFromID(files, id); const index = getFileIndexFromID(files, id);
return updateFile(files[index]); return updateFile(files[index]);
@ -426,7 +454,11 @@ const PhotoFrame = ({
handleSelect(filteredData[index].id, index)(!checked); handleSelect(filteredData[index].id, index)(!checked);
} }
}; };
const getThumbnail = (files: EnteFile[], index: number) => const getThumbnail = (
files: EnteFile[],
index: number,
isScrolling: boolean
) =>
files[index] ? ( files[index] ? (
<PreviewCard <PreviewCard
key={`tile-${files[index].id}-selected-${ key={`tile-${files[index].id}-selected-${
@ -450,6 +482,7 @@ const PhotoFrame = ({
(index >= currentHover && index <= rangeStart) (index >= currentHover && index <= rangeStart)
} }
activeCollection={activeCollection} activeCollection={activeCollection}
showPlaceholder={isScrolling}
/> />
) : ( ) : (
<></> <></>
@ -484,6 +517,9 @@ const PhotoFrame = ({
item.msrc = newFile.msrc; item.msrc = newFile.msrc;
item.html = newFile.html; item.html = newFile.html;
item.src = newFile.src; item.src = newFile.src;
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL;
item.originalVideoURL = newFile.originalVideoURL;
item.w = newFile.w; item.w = newFile.w;
item.h = newFile.h; item.h = newFile.h;
@ -506,10 +542,13 @@ const PhotoFrame = ({
if (!fetching[item.id]) { if (!fetching[item.id]) {
try { try {
fetching[item.id] = true; fetching[item.id] = true;
let urls: string[]; let urls: { original: string[]; converted: string[] };
if (galleryContext.files.has(item.id)) { if (galleryContext.files.has(item.id)) {
const mergedURL = galleryContext.files.get(item.id); const mergedURL = galleryContext.files.get(item.id);
urls = mergedURL.split(','); urls = {
original: mergedURL.original.split(','),
converted: mergedURL.converted.split(','),
};
} else { } else {
appContext.startLoading(); appContext.startLoading();
if ( if (
@ -525,26 +564,39 @@ const PhotoFrame = ({
urls = await DownloadManager.getFile(item, true); urls = await DownloadManager.getFile(item, true);
} }
appContext.finishLoading(); appContext.finishLoading();
const mergedURL = urls.join(','); const mergedURL = {
original: urls.original.join(','),
converted: urls.converted.join(','),
};
galleryContext.files.set(item.id, mergedURL); galleryContext.files.set(item.id, mergedURL);
} }
let imageURL; let originalImageURL;
let videoURL; let originalVideoURL;
let convertedImageURL;
let convertedVideoURL;
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) { if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[imageURL, videoURL] = urls; [originalImageURL, originalVideoURL] = urls.converted;
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) { } else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
[videoURL] = urls; [originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
} else { } else {
[imageURL] = urls; [originalImageURL] = urls.original;
[convertedImageURL] = urls.converted;
} }
setIsSourceLoaded(false); setIsSourceLoaded(false);
const newFile = await updateSrcURL(item.id, { const newFile = await updateSrcURL(item.id, {
imageURL, originalImageURL,
videoURL, originalVideoURL,
convertedImageURL,
convertedVideoURL,
}); });
item.msrc = newFile.msrc; item.msrc = newFile.msrc;
item.html = newFile.html; item.html = newFile.html;
item.src = newFile.src; item.src = newFile.src;
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL;
item.originalVideoURL = newFile.originalVideoURL;
item.w = newFile.w; item.w = newFile.w;
item.h = newFile.h; item.h = newFile.h;
try { try {
@ -591,17 +643,21 @@ const PhotoFrame = ({
/> />
)} )}
</AutoSizer> </AutoSizer>
<PhotoSwipe <PhotoViewer
isOpen={open} isOpen={open}
items={filteredData} items={filteredData}
currentIndex={currentIndex} currentIndex={currentIndex}
onClose={handleClose} onClose={handleClose}
gettingData={getSlideData} gettingData={getSlideData}
favItemIds={favItemIds} favItemIds={favItemIds}
deletedFileIds={deletedFileIds}
setDeletedFileIds={setDeletedFileIds}
isSharedCollection={isSharedCollection} isSharedCollection={isSharedCollection}
isTrashCollection={activeCollection === TRASH_SECTION} isTrashCollection={activeCollection === TRASH_SECTION}
enableDownload={enableDownload} enableDownload={enableDownload}
isSourceLoaded={isSourceLoaded} isSourceLoaded={isSourceLoaded}
fileToCollectionsMap={fileToCollectionsMap}
collectionNameMap={collectionNameMap}
/> />
</Container> </Container>
)} )}

View file

@ -1,6 +1,6 @@
import React, { useRef, useEffect, useContext } from 'react'; import React, { useRef, useEffect, useContext } from 'react';
import { VariableSizeList as List } from 'react-window'; import { VariableSizeList as List } from 'react-window';
import { Box, styled } from '@mui/material'; import { Box, Link, styled } from '@mui/material';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { import {
IMAGE_CONTAINER_MAX_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
@ -15,16 +15,15 @@ import {
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { ENTE_WEBSITE_LINK } from 'constants/urls'; import { ENTE_WEBSITE_LINK } from 'constants/urls';
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
import { convertBytesToHumanReadable } from 'utils/file/size'; import { convertBytesToHumanReadable } from 'utils/file/size';
import { DeduplicateContext } from 'pages/deduplicate'; import { DeduplicateContext } from 'pages/deduplicate';
import { FlexWrapper } from './Container'; import { FlexWrapper } from './Container';
import { Typography } from '@mui/material'; import { Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { SpecialPadding } from 'styles/SpecialPadding'; import { SpecialPadding } from 'styles/SpecialPadding';
import { formatDate } from 'utils/time/format';
const A_DAY = 24 * 60 * 60 * 1000; const A_DAY = 24 * 60 * 60 * 1000;
const NO_OF_PAGES = 2;
const FOOTER_HEIGHT = 90; const FOOTER_HEIGHT = 90;
export enum ITEM_TYPE { export enum ITEM_TYPE {
@ -153,7 +152,11 @@ interface Props {
width: number; width: number;
filteredData: EnteFile[]; filteredData: EnteFile[];
showAppDownloadBanner: boolean; showAppDownloadBanner: boolean;
getThumbnail: (files: EnteFile[], index: number) => JSX.Element; getThumbnail: (
files: EnteFile[],
index: number,
isScrolling?: boolean
) => JSX.Element;
activeCollection: number; activeCollection: number;
resetFetching: () => void; resetFetching: () => void;
} }
@ -244,6 +247,10 @@ export function PhotoList({
filteredData, filteredData,
showAppDownloadBanner, showAppDownloadBanner,
publicCollectionGalleryContext.accessedThroughSharedURL, publicCollectionGalleryContext.accessedThroughSharedURL,
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListHeader,
deduplicateContext.isOnDeduplicatePage,
deduplicateContext.fileSizeMap,
]); ]);
const groupByFileSize = (timeStampList: TimeStampListItem[]) => { const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
@ -298,22 +305,17 @@ export function PhotoList({
) )
) { ) {
currentDate = item.metadata.creationTime / 1000; currentDate = item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
});
timeStampList.push({ timeStampList.push({
itemType: ITEM_TYPE.TIME, itemType: ITEM_TYPE.TIME,
date: isSameDay(new Date(currentDate), new Date()) date: isSameDay(new Date(currentDate), new Date())
? 'Today' ? constants.TODAY
: isSameDay( : isSameDay(
new Date(currentDate), new Date(currentDate),
new Date(Date.now() - A_DAY) new Date(Date.now() - A_DAY)
) )
? 'Yesterday' ? constants.YESTERDAY
: dateTimeFormat.format(currentDate), : formatDate(currentDate),
id: currentDate.toString(), id: currentDate.toString(),
}); });
timeStampList.push({ timeStampList.push({
@ -403,15 +405,9 @@ export function PhotoList({
<FooterContainer span={columns}> <FooterContainer span={columns}>
<p> <p>
{constants.PRESERVED_BY}{' '} {constants.PRESERVED_BY}{' '}
<a <Link target="_blank" href={ENTE_WEBSITE_LINK}>
target="_blank"
style={{
color: getVariantColor(ButtonVariant.success),
}}
href={ENTE_WEBSITE_LINK}
rel="noreferrer">
{constants.ENTE_IO} {constants.ENTE_IO}
</a> </Link>
</p> </p>
</FooterContainer> </FooterContainer>
), ),
@ -453,9 +449,10 @@ export function PhotoList({
date: currItem.date, date: currItem.date,
span: items[index + 1].items.length, span: items[index + 1].items.length,
}); });
newList[newIndex + 1].items = newList[ newList[newIndex + 1].items = [
newIndex + 1 ...newList[newIndex + 1].items,
].items.concat(items[index + 1].items); ...items[index + 1].items,
];
index += 2; index += 2;
} else { } else {
// Adding items would exceed the number of columns. // Adding items would exceed the number of columns.
@ -512,10 +509,6 @@ export function PhotoList({
} }
}; };
const extraRowsToRender = Math.ceil(
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
);
const generateKey = (index) => { const generateKey = (index) => {
switch (timeStampList[index].itemType) { switch (timeStampList[index].itemType) {
case ITEM_TYPE.FILE: case ITEM_TYPE.FILE:
@ -527,7 +520,10 @@ export function PhotoList({
} }
}; };
const renderListItem = (listItem: TimeStampListItem) => { const renderListItem = (
listItem: TimeStampListItem,
isScrolling: boolean
) => {
switch (listItem.itemType) { switch (listItem.itemType) {
case ITEM_TYPE.TIME: case ITEM_TYPE.TIME:
return listItem.dates ? ( return listItem.dates ? (
@ -556,7 +552,8 @@ export function PhotoList({
const ret = listItem.items.map((item, idx) => const ret = listItem.items.map((item, idx) =>
getThumbnail( getThumbnail(
filteredDataCopy, filteredDataCopy,
listItem.itemStartIndex + idx listItem.itemStartIndex + idx,
isScrolling
) )
); );
if (listItem.groups) { if (listItem.groups) {
@ -587,14 +584,15 @@ export function PhotoList({
width={width} width={width}
itemCount={timeStampList.length} itemCount={timeStampList.length}
itemKey={generateKey} itemKey={generateKey}
overscanCount={extraRowsToRender}> overscanCount={0}
{({ index, style }) => ( useIsScrolling>
{({ index, style, isScrolling }) => (
<ListItem style={style}> <ListItem style={style}>
<ListContainer <ListContainer
columns={columns} columns={columns}
shrinkRatio={shrinkRatio} shrinkRatio={shrinkRatio}
groups={timeStampList[index].groups}> groups={timeStampList[index].groups}>
{renderListItem(timeStampList[index])} {renderListItem(timeStampList[index], isScrolling)}
</ListContainer> </ListContainer>
</ListItem> </ListItem>
)} )}

View file

@ -1,73 +0,0 @@
import React, { useState } from 'react';
import constants from 'utils/strings/constants';
import { RenderInfoItem } from './RenderInfoItem';
import { LegendContainer } from '../styledComponents/LegendContainer';
import { Pre } from '../styledComponents/Pre';
import {
Checkbox,
FormControlLabel,
FormGroup,
Typography,
} from '@mui/material';
export function ExifData(props: { exif: any }) {
const { exif } = props;
const [showAll, setShowAll] = useState(false);
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
setShowAll(e.target.checked);
};
const renderAllValues = () => <Pre>{exif.raw}</Pre>;
const renderSelectedValues = () => (
<>
{exif?.Make &&
exif?.Model &&
RenderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
{exif?.ImageWidth &&
exif?.ImageHeight &&
RenderInfoItem(
constants.IMAGE_SIZE,
`${exif.ImageWidth} x ${exif.ImageHeight}`
)}
{exif?.Flash && RenderInfoItem(constants.FLASH, exif.Flash)}
{exif?.FocalLength &&
RenderInfoItem(
constants.FOCAL_LENGTH,
exif.FocalLength.toString()
)}
{exif?.ApertureValue &&
RenderInfoItem(
constants.APERTURE,
exif.ApertureValue.toString()
)}
{exif?.ISOSpeedRatings &&
RenderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
</>
);
return (
<>
<LegendContainer>
<Typography variant="subtitle" mb={1}>
{constants.EXIF}
</Typography>
<FormGroup>
<FormControlLabel
control={
<Checkbox
size="small"
onChange={changeHandler}
color="accent"
/>
}
label={constants.SHOW_ALL}
/>
</FormGroup>
</LegendContainer>
{showAll ? renderAllValues() : renderSelectedValues()}
</>
);
}

View file

@ -1,100 +0,0 @@
import React, { useState } from 'react';
import constants from 'utils/strings/constants';
import { Col, Form, FormControl } from 'react-bootstrap';
import { FlexWrapper, Value } from 'components/Container';
import CloseIcon from '@mui/icons-material/Close';
import TickIcon from '@mui/icons-material/Done';
import { Formik } from 'formik';
import * as Yup from 'yup';
import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import { IconButton } from '@mui/material';
export interface formValues {
filename: string;
}
export const FileNameEditForm = ({
filename,
saveEdits,
discardEdits,
extension,
}) => {
const [loading, setLoading] = useState(false);
const onSubmit = async (values: formValues) => {
try {
setLoading(true);
await saveEdits(values.filename);
} finally {
setLoading(false);
}
};
return (
<Formik<formValues>
initialValues={{ filename }}
validationSchema={Yup.object().shape({
filename: Yup.string()
.required(constants.REQUIRED)
.max(
MAX_EDITED_FILE_NAME_LENGTH,
constants.FILE_NAME_CHARACTER_LIMIT
),
})}
validateOnBlur={false}
onSubmit={onSubmit}>
{({ values, errors, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Row>
<Form.Group
bsPrefix="ente-form-group"
as={Col}
xs={extension ? 8 : 9}>
<Form.Control
as="textarea"
placeholder={constants.FILE_NAME}
value={values.filename}
onChange={handleChange('filename')}
isInvalid={Boolean(errors.filename)}
autoFocus
disabled={loading}
/>
<FormControl.Feedback
type="invalid"
style={{ textAlign: 'center' }}>
{errors.filename}
</FormControl.Feedback>
</Form.Group>
{extension && (
<Form.Group
bsPrefix="ente-form-group"
as={Col}
xs={1}
controlId="formHorizontalFileName">
<FlexWrapper style={{ padding: '5px' }}>
{`.${extension}`}
</FlexWrapper>
</Form.Group>
)}
<Form.Group bsPrefix="ente-form-group" as={Col} xs={3}>
<Value width={'16.67%'}>
<IconButton type="submit" disabled={loading}>
{loading ? (
<SmallLoadingSpinner />
) : (
<TickIcon />
)}
</IconButton>
<IconButton
onClick={discardEdits}
disabled={loading}>
<CloseIcon />
</IconButton>
</Value>
</Form.Group>
</Form.Row>
</Form>
)}
</Formik>
);
};

View file

@ -1,98 +0,0 @@
import React, { useState } from 'react';
import { updateFilePublicMagicMetadata } from 'services/fileService';
import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants';
import {
changeFileName,
splitFilenameAndExtension,
updateExistingFilePubMetadata,
} from 'utils/file';
import EditIcon from '@mui/icons-material/Edit';
import { FreeFlowText, Label, Row, Value } from 'components/Container';
import { logError } from 'utils/sentry';
import { FileNameEditForm } from './FileNameEditForm';
import { IconButton } from '@mui/material';
export const getFileTitle = (filename, extension) => {
if (extension) {
return filename + '.' + extension;
} else {
return filename;
}
};
export function RenderFileName({
shouldDisableEdits,
file,
scheduleUpdate,
}: {
shouldDisableEdits: boolean;
file: EnteFile;
scheduleUpdate: () => void;
}) {
const originalTitle = file?.metadata.title;
const [isInEditMode, setIsInEditMode] = useState(false);
const [originalFileName, extension] =
splitFilenameAndExtension(originalTitle);
const [filename, setFilename] = useState(originalFileName);
const openEditMode = () => setIsInEditMode(true);
const closeEditMode = () => setIsInEditMode(false);
const saveEdits = async (newFilename: string) => {
try {
if (file) {
if (filename === newFilename) {
closeEditMode();
return;
}
setFilename(newFilename);
const newTitle = getFileTitle(newFilename, extension);
let updatedFile = await changeFileName(file, newTitle);
updatedFile = (
await updateFilePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
scheduleUpdate();
}
} catch (e) {
logError(e, 'failed to update file name');
} finally {
closeEditMode();
}
};
return (
<>
<Row>
<Label width="30%">{constants.FILE_NAME}</Label>
{!isInEditMode ? (
<>
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
<FreeFlowText>
{getFileTitle(filename, extension)}
</FreeFlowText>
</Value>
{!shouldDisableEdits && (
<Value
width="10%"
style={{
cursor: 'pointer',
marginLeft: '10px',
}}>
<IconButton onClick={openEditMode}>
<EditIcon />
</IconButton>
</Value>
)}
</>
) : (
<FileNameEditForm
extension={extension}
filename={filename}
saveEdits={saveEdits}
discardEdits={closeEditMode}
/>
)}
</Row>
</>
);
}

View file

@ -1,123 +0,0 @@
import React, { useContext, useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { formatDateTime } from 'utils/time';
import { RenderFileName } from './RenderFileName';
import { ExifData } from './ExifData';
import { RenderCreationTime } from './RenderCreationTime';
import { RenderInfoItem } from './RenderInfoItem';
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
import { AppContext } from 'pages/_app';
import { Location, Metadata } from 'types/upload';
import Photoswipe from 'photoswipe';
import { getEXIFLocation } from 'services/upload/exifService';
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
zIndex: 1501,
'& .MuiDialog-container': {
alignItems: 'flex-start',
},
'& .MuiDialog-paper': {
padding: theme.spacing(2),
},
}));
interface Iprops {
shouldDisableEdits: boolean;
showInfo: boolean;
handleCloseInfo: () => void;
items: any[];
photoSwipe: Photoswipe<Photoswipe.Options>;
metadata: Metadata;
exif: any;
scheduleUpdate: () => void;
}
export function FileInfo({
shouldDisableEdits,
showInfo,
handleCloseInfo,
items,
photoSwipe,
metadata,
exif,
scheduleUpdate,
}: Iprops) {
const appContext = useContext(AppContext);
const [location, setLocation] = useState<Location>(null);
useEffect(() => {
if (!location && metadata) {
if (metadata.longitude || metadata.longitude === 0) {
setLocation({
latitude: metadata.latitude,
longitude: metadata.longitude,
});
}
}
}, [metadata]);
useEffect(() => {
if (!location && exif) {
const exifLocation = getEXIFLocation(exif);
if (exifLocation.latitude || exifLocation.latitude === 0) {
setLocation(exifLocation);
}
}
}, [exif]);
return (
<FileInfoDialog
open={showInfo}
onClose={handleCloseInfo}
fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={handleCloseInfo}>
{constants.INFO}
</DialogTitleWithCloseButton>
<DialogContent>
<Typography variant="subtitle" mb={1}>
{constants.METADATA}
</Typography>
{RenderInfoItem(
constants.FILE_ID,
items[photoSwipe?.getCurrentIndex()]?.id
)}
{metadata?.title && (
<RenderFileName
shouldDisableEdits={shouldDisableEdits}
file={items[photoSwipe?.getCurrentIndex()]}
scheduleUpdate={scheduleUpdate}
/>
)}
{metadata?.creationTime && (
<RenderCreationTime
shouldDisableEdits={shouldDisableEdits}
file={items[photoSwipe?.getCurrentIndex()]}
scheduleUpdate={scheduleUpdate}
/>
)}
{metadata?.modificationTime &&
RenderInfoItem(
constants.UPDATED_ON,
formatDateTime(metadata.modificationTime / 1000)
)}
{location &&
RenderInfoItem(
constants.LOCATION,
<Link
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
target="_blank"
rel="noopener noreferrer">
{constants.SHOW_MAP}
</Link>
)}
{exif && (
<>
<ExifData exif={exif} />
</>
)}
</DialogContent>
</FileInfoDialog>
);
}

View file

@ -0,0 +1,92 @@
import React from 'react';
import constants from 'utils/strings/constants';
import { Stack, styled, Typography } from '@mui/material';
import { FileInfoSidebar } from '.';
import Titlebar from 'components/Titlebar';
import { Box } from '@mui/system';
import CopyButton from 'components/CodeBlock/CopyButton';
import { formatDateFull } from 'utils/time/format';
const ExifItem = styled(Box)`
padding-left: 8px;
padding-right: 8px;
display: flex;
flex-direction: column;
gap: 4px;
`;
function parseExifValue(value: any) {
switch (typeof value) {
case 'string':
case 'number':
return value;
default:
if (value instanceof Date) {
return formatDateFull(value);
}
try {
return JSON.stringify(Array.from(value));
} catch (e) {
return null;
}
}
}
export function ExifData(props: {
exif: any;
open: boolean;
onClose: () => void;
filename: string;
onInfoClose: () => void;
}) {
const { exif, open, onClose, filename, onInfoClose } = props;
if (!exif) {
return <></>;
}
const handleRootClose = () => {
onClose();
onInfoClose();
};
return (
<FileInfoSidebar open={open} onClose={onClose}>
<Titlebar
onClose={onClose}
title={constants.EXIF}
caption={filename}
onRootClose={handleRootClose}
actionButton={
<CopyButton
code={JSON.stringify(exif)}
color={'secondary'}
/>
}
/>
<Stack py={3} px={1} spacing={2}>
{[...Object.entries(exif)].map(([key, value]) =>
value ? (
<ExifItem key={key}>
<Typography
variant="body2"
color={'text.secondary'}>
{key}
</Typography>
<Typography
sx={{
width: '100%',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
overflow: 'hidden',
}}>
{parseExifValue(value)}
</Typography>
</ExifItem>
) : (
<></>
)
)}
</Stack>
</FileInfoSidebar>
);
}

View file

@ -0,0 +1,51 @@
import React from 'react';
import constants from 'utils/strings/constants';
import { DialogContent, DialogTitle } from '@mui/material';
import DialogBoxBase from 'components/DialogBox/base';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
export interface formValues {
filename: string;
}
export const FileNameEditDialog = ({
isInEditMode,
closeEditMode,
filename,
extension,
saveEdits,
}) => {
const onSubmit: SingleInputFormProps['callback'] = async (
filename,
setFieldError
) => {
try {
await saveEdits(filename);
closeEditMode();
} catch (e) {
setFieldError(constants.UNKNOWN_ERROR);
}
};
return (
<DialogBoxBase
open={isInEditMode}
onClose={closeEditMode}
sx={{ zIndex: 1600 }}>
<DialogTitle>{constants.RENAME_FILE}</DialogTitle>
<DialogContent>
<SingleInputForm
initialValue={filename}
callback={onSubmit}
placeholder={constants.ENTER_FILE_NAME}
buttonText={constants.RENAME}
fieldType="text"
caption={extension}
secondaryButtonAction={closeEditMode}
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
/>
</DialogContent>
</DialogBoxBase>
);
};

View file

@ -0,0 +1,61 @@
import Edit from '@mui/icons-material/Edit';
import { Box, IconButton, Typography } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import React from 'react';
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
interface Iprops {
icon: JSX.Element;
title?: string;
caption?: string | JSX.Element;
openEditor?: any;
loading?: boolean;
hideEditOption?: any;
customEndButton?: any;
children?: any;
}
export default function InfoItem({
icon,
title,
caption,
openEditor,
loading,
hideEditOption,
customEndButton,
children,
}: Iprops): JSX.Element {
return (
<FlexWrapper justifyContent="space-between">
<Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
<IconButton
color="secondary"
sx={{ '&&': { cursor: 'default', m: 0.5 } }}
disableRipple>
{icon}
</IconButton>
<Box py={0.5}>
{children ? (
children
) : (
<>
<Typography sx={{ wordBreak: 'break-all' }}>
{title}
</Typography>
<Typography variant="body2" color="text.secondary">
{caption}
</Typography>
</>
)}
</Box>
</Box>
{customEndButton
? customEndButton
: !hideEditOption && (
<IconButton onClick={openEditor} color="secondary">
{!loading ? <Edit /> : <SmallLoadingSpinner />}
</IconButton>
)}
</FlexWrapper>
);
}

View file

@ -0,0 +1,126 @@
import React, { useState } from 'react';
import { updateFilePublicMagicMetadata } from 'services/fileService';
import { EnteFile } from 'types/file';
import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
import { logError } from 'utils/sentry';
import { Box, IconButton, TextField } from '@mui/material';
import { FlexWrapper } from 'components/Container';
import { MAX_CAPTION_SIZE } from 'constants/file';
import { Formik } from 'formik';
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import * as Yup from 'yup';
import constants from 'utils/strings/constants';
import Close from '@mui/icons-material/Close';
import { Done } from '@mui/icons-material';
export interface formValues {
caption: string;
}
export function RenderCaption({
file,
scheduleUpdate,
refreshPhotoswipe,
}: {
shouldDisableEdits: boolean;
file: EnteFile;
scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
}) {
const [caption, setCaption] = useState(
file?.pubMagicMetadata?.data.caption
);
const [loading, setLoading] = useState(false);
const saveEdits = async (newCaption: string) => {
try {
if (file) {
if (caption === newCaption) {
return;
}
setCaption(newCaption);
let updatedFile = await changeCaption(file, newCaption);
updatedFile = (
await updateFilePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
file.title = file.pubMagicMetadata.data.caption;
refreshPhotoswipe();
scheduleUpdate();
}
} catch (e) {
logError(e, 'failed to update caption');
}
};
const onSubmit = async (values: formValues) => {
try {
setLoading(true);
await saveEdits(values.caption);
} finally {
setLoading(false);
}
};
return (
<Box p={1}>
<Formik<formValues>
initialValues={{ caption }}
validationSchema={Yup.object().shape({
caption: Yup.string().max(
MAX_CAPTION_SIZE,
constants.CAPTION_CHARACTER_LIMIT
),
})}
validateOnBlur={false}
onSubmit={onSubmit}>
{({
values,
errors,
handleChange,
handleSubmit,
resetForm,
}) => (
<form noValidate onSubmit={handleSubmit}>
<TextField
hiddenLabel
fullWidth
id="caption"
name="caption"
type="text"
multiline
placeholder={constants.CAPTION_PLACEHOLDER}
value={values.caption}
onChange={handleChange('caption')}
error={Boolean(errors.caption)}
helperText={errors.caption}
disabled={loading}
/>
{values.caption !== caption && (
<FlexWrapper justifyContent={'flex-end'}>
<IconButton type="submit" disabled={loading}>
{loading ? (
<SmallLoadingSpinner />
) : (
<Done />
)}
</IconButton>
<IconButton
onClick={() =>
resetForm({
values: { caption: caption ?? '' },
touched: { caption: false },
})
}
disabled={loading}>
<Close />
</IconButton>
</FlexWrapper>
)}
</form>
)}
</Formik>
</Box>
);
}

View file

@ -1,18 +1,16 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { updateFilePublicMagicMetadata } from 'services/fileService'; import { updateFilePublicMagicMetadata } from 'services/fileService';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants'; import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import { import {
changeFileCreationTime, changeFileCreationTime,
updateExistingFilePubMetadata, updateExistingFilePubMetadata,
} from 'utils/file'; } from 'utils/file';
import { formatDateTime } from 'utils/time'; import { formatDate, formatTime } from 'utils/time/format';
import EditIcon from '@mui/icons-material/Edit'; import { FlexWrapper } from 'components/Container';
import { Label, Row, Value } from 'components/Container';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import EnteDateTimePicker from 'components/EnteDateTimePicker'; import EnteDateTimePicker from 'components/EnteDateTimePicker';
import { IconButton } from '@mui/material'; import InfoItem from './InfoItem';
export function RenderCreationTime({ export function RenderCreationTime({
shouldDisableEdits, shouldDisableEdits,
@ -59,39 +57,24 @@ export function RenderCreationTime({
return ( return (
<> <>
<Row> <FlexWrapper>
<Label width="30%">{constants.CREATION_TIME}</Label> <InfoItem
<Value icon={<CalendarTodayIcon />}
width={ title={formatDate(originalCreationTime)}
!shouldDisableEdits ? !isInEditMode && '60%' : '70%' caption={formatTime(originalCreationTime)}
}> openEditor={openEditMode}
{isInEditMode ? ( loading={loading}
<EnteDateTimePicker hideEditOption={shouldDisableEdits || isInEditMode}
initialValue={originalCreationTime} />
disabled={loading} {isInEditMode && (
onSubmit={saveEdits} <EnteDateTimePicker
onClose={closeEditMode} initialValue={originalCreationTime}
/> disabled={loading}
) : ( onSubmit={saveEdits}
formatDateTime(originalCreationTime) onClose={closeEditMode}
)} />
</Value>
{!shouldDisableEdits && !isInEditMode && (
<Value
width={'10%'}
style={{ cursor: 'pointer', marginLeft: '10px' }}>
{loading ? (
<IconButton>
<SmallLoadingSpinner />
</IconButton>
) : (
<IconButton onClick={openEditMode}>
<EditIcon />
</IconButton>
)}
</Value>
)} )}
</Row> </FlexWrapper>
</> </>
); );
} }

View file

@ -0,0 +1,121 @@
import React, { useEffect, useState } from 'react';
import { updateFilePublicMagicMetadata } from 'services/fileService';
import { EnteFile } from 'types/file';
import {
changeFileName,
splitFilenameAndExtension,
updateExistingFilePubMetadata,
} from 'utils/file';
import { FlexWrapper } from 'components/Container';
import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
import InfoItem from './InfoItem';
import { makeHumanReadableStorage } from 'utils/billing';
import Box from '@mui/material/Box';
import { FileNameEditDialog } from './FileNameEditDialog';
const getFileTitle = (filename, extension) => {
if (extension) {
return filename + '.' + extension;
} else {
return filename;
}
};
const getCaption = (file: EnteFile, parsedExifData) => {
const megaPixels = parsedExifData?.['megaPixels'];
const resolution = parsedExifData?.['resolution'];
const fileSize = file.info?.fileSize;
const captionParts = [];
if (megaPixels) {
captionParts.push(megaPixels);
}
if (resolution) {
captionParts.push(resolution);
}
if (fileSize) {
captionParts.push(makeHumanReadableStorage(fileSize));
}
return (
<FlexWrapper gap={1}>
{captionParts.map((caption) => (
<Box key={caption}> {caption}</Box>
))}
</FlexWrapper>
);
};
export function RenderFileName({
parsedExifData,
shouldDisableEdits,
file,
scheduleUpdate,
}: {
parsedExifData: Record<string, any>;
shouldDisableEdits: boolean;
file: EnteFile;
scheduleUpdate: () => void;
}) {
const [isInEditMode, setIsInEditMode] = useState(false);
const openEditMode = () => setIsInEditMode(true);
const closeEditMode = () => setIsInEditMode(false);
const [filename, setFilename] = useState<string>();
const [extension, setExtension] = useState<string>();
useEffect(() => {
const [filename, extension] = splitFilenameAndExtension(
file.metadata.title
);
setFilename(filename);
setExtension(extension);
}, []);
const saveEdits = async (newFilename: string) => {
try {
if (file) {
if (filename === newFilename) {
closeEditMode();
return;
}
setFilename(newFilename);
const newTitle = getFileTitle(newFilename, extension);
let updatedFile = await changeFileName(file, newTitle);
updatedFile = (
await updateFilePublicMagicMetadata([updatedFile])
)[0];
updateExistingFilePubMetadata(file, updatedFile);
scheduleUpdate();
}
} catch (e) {
logError(e, 'failed to update file name');
throw e;
}
};
return (
<>
<InfoItem
icon={
file.metadata.fileType === FILE_TYPE.IMAGE ? (
<PhotoOutlined />
) : (
<VideoFileOutlined />
)
}
title={getFileTitle(filename, extension)}
caption={getCaption(file, parsedExifData)}
openEditor={openEditMode}
hideEditOption={shouldDisableEdits || isInEditMode}
/>
<FileNameEditDialog
isInEditMode={isInEditMode}
closeEditMode={closeEditMode}
filename={filename}
extension={extension}
saveEdits={saveEdits}
/>
</>
);
}

View file

@ -0,0 +1,280 @@
import React, { useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { RenderFileName } from './RenderFileName';
import { RenderCreationTime } from './RenderCreationTime';
import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
import { Location } from 'types/upload';
import { getEXIFLocation } from 'services/upload/exifService';
import { RenderCaption } from './RenderCaption';
import {
BackupOutlined,
CameraOutlined,
FolderOutlined,
LocationOnOutlined,
TextSnippetOutlined,
} from '@mui/icons-material';
import CopyButton from 'components/CodeBlock/CopyButton';
import { formatDate, formatTime } from 'utils/time/format';
import Titlebar from 'components/Titlebar';
import InfoItem from './InfoItem';
import { FlexWrapper } from 'components/Container';
import EnteSpinner from 'components/EnteSpinner';
import { EnteFile } from 'types/file';
import { Chip } from 'components/Chip';
import LinkButton from 'components/pages/gallery/LinkButton';
import { ExifData } from './ExifData';
import { EnteDrawer } from 'components/EnteDrawer';
export const FileInfoSidebar = styled((props: DialogProps) => (
<EnteDrawer {...props} anchor="right" />
))({
zIndex: 1501,
'& .MuiPaper-root': {
padding: 8,
},
});
interface Iprops {
shouldDisableEdits: boolean;
showInfo: boolean;
handleCloseInfo: () => void;
file: EnteFile;
exif: any;
scheduleUpdate: () => void;
refreshPhotoswipe: () => void;
fileToCollectionsMap: Map<number, number[]>;
collectionNameMap: Map<number, string>;
isTrashCollection: boolean;
}
function BasicDeviceCamera({
parsedExifData,
}: {
parsedExifData: Record<string, any>;
}) {
return (
<FlexWrapper gap={1}>
<Box>{parsedExifData['fNumber']}</Box>
<Box>{parsedExifData['exposureTime']}</Box>
<Box>{parsedExifData['ISO']}</Box>
</FlexWrapper>
);
}
function getOpenStreetMapLink(location: {
latitude: number;
longitude: number;
}) {
return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
}
export function FileInfo({
shouldDisableEdits,
showInfo,
handleCloseInfo,
file,
exif,
scheduleUpdate,
refreshPhotoswipe,
fileToCollectionsMap,
collectionNameMap,
isTrashCollection,
}: Iprops) {
const [location, setLocation] = useState<Location>(null);
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
const [showExif, setShowExif] = useState(false);
const openExif = () => setShowExif(true);
const closeExif = () => setShowExif(false);
useEffect(() => {
if (!location && file && file.metadata) {
if (file.metadata.longitude || file.metadata.longitude === 0) {
setLocation({
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
});
}
}
}, [file]);
useEffect(() => {
if (!location && exif) {
const exifLocation = getEXIFLocation(exif);
if (exifLocation.latitude || exifLocation.latitude === 0) {
setLocation(exifLocation);
}
}
}, [exif]);
useEffect(() => {
if (!exif) {
setParsedExifData({});
return;
}
const parsedExifData = {};
if (exif['fNumber']) {
parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`;
} else if (exif['ApertureValue'] && exif['FocalLength']) {
parsedExifData['fNumber'] = `f/${Math.ceil(
exif['FocalLength'] / exif['ApertureValue']
)}`;
}
const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth'];
const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight'];
if (imageWidth && imageHeight) {
parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`;
const megaPixels = Math.round((imageWidth * imageHeight) / 1000000);
if (megaPixels) {
parsedExifData['megaPixels'] = `${Math.round(
(imageWidth * imageHeight) / 1000000
)}MP`;
}
}
if (exif['Make'] && exif['Model']) {
parsedExifData[
'takenOnDevice'
] = `${exif['Make']} ${exif['Model']}`;
}
if (exif['ExposureTime']) {
parsedExifData['exposureTime'] = `1/${
1 / parseFloat(exif['ExposureTime'])
}`;
}
if (exif['ISO']) {
parsedExifData['ISO'] = `ISO${exif['ISO']}`;
}
setParsedExifData(parsedExifData);
}, [exif]);
if (!file) {
return <></>;
}
return (
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
<Titlebar
onClose={handleCloseInfo}
title={constants.INFO}
backIsClose
/>
<Stack pt={1} pb={3} spacing={'20px'}>
<RenderCaption
shouldDisableEdits={shouldDisableEdits}
file={file}
scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
/>
<RenderCreationTime
shouldDisableEdits={shouldDisableEdits}
file={file}
scheduleUpdate={scheduleUpdate}
/>
<RenderFileName
parsedExifData={parsedExifData}
shouldDisableEdits={shouldDisableEdits}
file={file}
scheduleUpdate={scheduleUpdate}
/>
{parsedExifData && parsedExifData['takenOnDevice'] && (
<InfoItem
icon={<CameraOutlined />}
title={parsedExifData['takenOnDevice']}
caption={
<BasicDeviceCamera
parsedExifData={parsedExifData}
/>
}
hideEditOption
/>
)}
{location && (
<InfoItem
icon={<LocationOnOutlined />}
title={constants.LOCATION}
caption={
<Link
href={getOpenStreetMapLink({
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
})}
target="_blank"
sx={{ fontWeight: 'bold' }}>
{constants.SHOW_ON_MAP}
</Link>
}
customEndButton={
<CopyButton
code={getOpenStreetMapLink({
latitude: file.metadata.latitude,
longitude: file.metadata.longitude,
})}
color="secondary"
size="medium"
/>
}
/>
)}
<InfoItem
icon={<TextSnippetOutlined />}
title={constants.DETAILS}
caption={
typeof exif === 'undefined' ? (
<EnteSpinner size={11.33} />
) : exif !== null ? (
<LinkButton
onClick={openExif}
sx={{
textDecoration: 'none',
color: 'text.secondary',
fontWeight: 'bold',
}}>
{constants.VIEW_EXIF}
</LinkButton>
) : (
constants.NO_EXIF
)
}
hideEditOption
/>
<InfoItem
icon={<BackupOutlined />}
title={formatDate(file.metadata.modificationTime / 1000)}
caption={formatTime(file.metadata.modificationTime / 1000)}
hideEditOption
/>
{!isTrashCollection && (
<InfoItem icon={<FolderOutlined />} hideEditOption>
<Box
display={'flex'}
gap={1}
flexWrap="wrap"
justifyContent={'flex-start'}
alignItems={'flex-start'}>
{fileToCollectionsMap
.get(file.id)
?.filter((collectionID) =>
collectionNameMap.has(collectionID)
)
?.map((collectionID) => (
<Chip key={collectionID}>
{collectionNameMap.get(collectionID)}
</Chip>
))}
</Box>
</InfoItem>
)}
</Stack>
<ExifData
exif={exif}
open={showExif}
onClose={closeExif}
onInfoClose={handleCloseInfo}
filename={file.metadata.title}
/>
</FileInfoSidebar>
);
}

View file

@ -9,26 +9,49 @@ import {
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import exifr from 'exifr'; import exifr from 'exifr';
import events from './events'; import { downloadFile, copyFileToClipboard } from 'utils/file';
import { downloadFile } from 'utils/file';
import { prettyPrintExif } from 'utils/exif';
import { livePhotoBtnHTML } from 'components/LivePhotoBtn'; import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { sleep } from 'utils/common'; import { isClipboardItemPresent } from 'utils/common';
import { playVideo, pauseVideo } from 'utils/photoFrame'; import { playVideo, pauseVideo } from 'utils/photoFrame';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { FileInfo } from './InfoDialog'; import { FileInfo } from './FileInfo';
import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe'; import {
defaultLivePhotoDefaultOptions,
photoSwipeV4Events,
} from 'constants/photoViewer';
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn'; import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import InfoIcon from '@mui/icons-material/InfoOutlined'; import InfoIcon from '@mui/icons-material/InfoOutlined';
import FavoriteIcon from '@mui/icons-material/FavoriteRounded'; import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded'; import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
import ChevronRight from '@mui/icons-material/ChevronRight'; import ChevronRight from '@mui/icons-material/ChevronRight';
import DeleteIcon from '@mui/icons-material/Delete';
import { trashFiles } from 'services/fileService';
import { getTrashFileMessage } from 'utils/ui';
import { ChevronLeft, ContentCopy } from '@mui/icons-material';
import { styled } from '@mui/material';
import { addLocalLog } from 'utils/logging';
interface PhotoswipeFullscreenAPI {
enter: () => void;
exit: () => void;
isFullscreen: () => boolean;
}
const CaptionContainer = styled('div')(({ theme }) => ({
padding: theme.spacing(2),
wordBreak: 'break-word',
textAlign: 'right',
maxWidth: '375px',
fontSize: '14px',
lineHeight: '17px',
backgroundColor: theme.palette.backdrop.light,
backdropFilter: `blur(${theme.palette.blur.base})`,
}));
interface Iprops { interface Iprops {
isOpen: boolean; isOpen: boolean;
items: any[]; items: any[];
@ -38,13 +61,17 @@ interface Iprops {
id?: string; id?: string;
className?: string; className?: string;
favItemIds: Set<number>; favItemIds: Set<number>;
deletedFileIds: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void;
isSharedCollection: boolean; isSharedCollection: boolean;
isTrashCollection: boolean; isTrashCollection: boolean;
enableDownload: boolean; enableDownload: boolean;
isSourceLoaded: boolean; isSourceLoaded: boolean;
fileToCollectionsMap: Map<number, number[]>;
collectionNameMap: Map<number, string>;
} }
function PhotoSwipe(props: Iprops) { function PhotoViewer(props: Iprops) {
const pswpElement = useRef<HTMLDivElement>(); const pswpElement = useRef<HTMLDivElement>();
const [photoSwipe, setPhotoSwipe] = const [photoSwipe, setPhotoSwipe] =
useState<Photoswipe<Photoswipe.Options>>(); useState<Photoswipe<Photoswipe.Options>>();
@ -52,8 +79,9 @@ function PhotoSwipe(props: Iprops) {
const { isOpen, items, isSourceLoaded } = props; const { isOpen, items, isSourceLoaded } = props;
const [isFav, setIsFav] = useState(false); const [isFav, setIsFav] = useState(false);
const [showInfo, setShowInfo] = useState(false); const [showInfo, setShowInfo] = useState(false);
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null); const [exif, setExif] =
const [exif, setExif] = useState<any>(null); useState<{ key: string; value: Record<string, any> }>();
const exifCopy = useRef(null);
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState( const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
defaultLivePhotoDefaultOptions defaultLivePhotoDefaultOptions
); );
@ -63,6 +91,9 @@ function PhotoSwipe(props: Iprops) {
); );
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const exifExtractionInProgress = useRef<string>(null);
const [shouldShowCopyOption] = useState(isClipboardItemPresent());
useEffect(() => { useEffect(() => {
if (!pswpElement) return; if (!pswpElement) return;
if (isOpen) { if (isOpen) {
@ -76,6 +107,61 @@ function PhotoSwipe(props: Iprops) {
}; };
}, [isOpen]); }, [isOpen]);
useEffect(() => {
if (!photoSwipe) return;
function handleCopyEvent() {
copyToClipboardHelper(photoSwipe.currItem as EnteFile);
}
function handleKeyUp(event: KeyboardEvent) {
if (!isOpen) {
return;
}
addLocalLog(() => 'Event: ' + event.key);
if (event.key === 'i' || event.key === 'I') {
if (!showInfo) {
setShowInfo(true);
} else {
setShowInfo(false);
}
}
if (showInfo) {
return;
}
switch (event.key) {
case 'Backspace':
case 'Delete':
confirmTrashFile(photoSwipe?.currItem as EnteFile);
break;
case 'd':
case 'D':
downloadFileHelper(photoSwipe?.currItem as EnteFile);
break;
case 'f':
case 'F':
toggleFullscreen(photoSwipe);
break;
case 'l':
case 'L':
onFavClick(photoSwipe?.currItem as EnteFile);
break;
default:
break;
}
}
window.addEventListener('keyup', handleKeyUp);
if (shouldShowCopyOption) {
window.addEventListener('copy', handleCopyEvent);
}
return () => {
window.removeEventListener('keyup', handleKeyUp);
if (shouldShowCopyOption) {
window.removeEventListener('copy', handleCopyEvent);
}
};
}, [isOpen, photoSwipe, showInfo]);
useEffect(() => { useEffect(() => {
updateItems(items); updateItems(items);
}, [items]); }, [items]);
@ -152,8 +238,12 @@ function PhotoSwipe(props: Iprops) {
} }
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]); }, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
function updateFavButton() { useEffect(() => {
setIsFav(isInFav(this?.currItem)); exifCopy.current = exif;
}, [exif]);
function updateFavButton(file: EnteFile) {
setIsFav(isInFav(file));
} }
const openPhotoSwipe = () => { const openPhotoSwipe = () => {
@ -198,7 +288,7 @@ function PhotoSwipe(props: Iprops) {
items, items,
options options
); );
events.forEach((event) => { photoSwipeV4Events.forEach((event) => {
const callback = props[event]; const callback = props[event];
if (callback || event === 'destroy') { if (callback || event === 'destroy') {
photoSwipe.listen(event, function (...args) { photoSwipe.listen(event, function (...args) {
@ -215,11 +305,31 @@ function PhotoSwipe(props: Iprops) {
}); });
} }
}); });
photoSwipe.listen('beforeChange', function () { photoSwipe.listen('beforeChange', () => {
updateInfo.call(this); const currItem = photoSwipe?.currItem as EnteFile;
updateFavButton.call(this); if (
!currItem ||
!exifCopy?.current?.value === null ||
exifCopy?.current?.key === currItem.src
) {
return;
}
setExif({ key: currItem.src, value: undefined });
checkExifAvailable(currItem);
updateFavButton(currItem);
});
photoSwipe.listen('resize', () => {
const currItem = photoSwipe?.currItem as EnteFile;
if (
!currItem ||
!exifCopy?.current?.value === null ||
exifCopy?.current?.key === currItem.src
) {
return;
}
setExif({ key: currItem.src, value: undefined });
checkExifAvailable(currItem);
}); });
photoSwipe.listen('resize', checkExifAvailable);
photoSwipe.init(); photoSwipe.init();
needUpdate.current = false; needUpdate.current = false;
setPhotoSwipe(photoSwipe); setPhotoSwipe(photoSwipe);
@ -240,7 +350,7 @@ function PhotoSwipe(props: Iprops) {
} }
handleCloseInfo(); handleCloseInfo();
}; };
const isInFav = (file) => { const isInFav = (file: EnteFile) => {
const { favItemIds } = props; const { favItemIds } = props;
if (favItemIds && file) { if (favItemIds && file) {
return favItemIds.has(file.id); return favItemIds.has(file.id);
@ -248,7 +358,7 @@ function PhotoSwipe(props: Iprops) {
return false; return false;
}; };
const onFavClick = async (file) => { const onFavClick = async (file: EnteFile) => {
const { favItemIds } = props; const { favItemIds } = props;
if (!isInFav(file)) { if (!isInFav(file)) {
favItemIds.add(file.id); favItemIds.add(file.id);
@ -262,46 +372,78 @@ function PhotoSwipe(props: Iprops) {
needUpdate.current = true; needUpdate.current = true;
}; };
const trashFile = async (file: EnteFile) => {
const { deletedFileIds, setDeletedFileIds } = props;
deletedFileIds.add(file.id);
setDeletedFileIds(new Set(deletedFileIds));
await trashFiles([file]);
needUpdate.current = true;
};
const confirmTrashFile = (file: EnteFile) => {
if (props.isSharedCollection || props.isTrashCollection) {
return;
}
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
};
const updateItems = (items = []) => { const updateItems = (items = []) => {
if (photoSwipe) { if (photoSwipe) {
if (items.length === 0) {
photoSwipe.close();
}
photoSwipe.items.length = 0; photoSwipe.items.length = 0;
items.forEach((item) => { items.forEach((item) => {
photoSwipe.items.push(item); photoSwipe.items.push(item);
}); });
photoSwipe.invalidateCurrItems(); photoSwipe.invalidateCurrItems();
// photoSwipe.updateSize(true); if (isOpen) {
photoSwipe.updateSize(true);
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
photoSwipe.goTo(0);
}
}
} }
}; };
const checkExifAvailable = async () => { const refreshPhotoswipe = () => {
setExif(null); photoSwipe.invalidateCurrItems();
await sleep(100); if (isOpen) {
photoSwipe.updateSize(true);
}
};
const checkExifAvailable = async (file: EnteFile) => {
try { try {
const img: HTMLImageElement = document.querySelector( if (exifExtractionInProgress.current === file.src) {
'.pswp__img:not(.pswp__img--placeholder)' return;
); }
if (img) { try {
const exifData = await exifr.parse(img); if (file.isSourceLoaded) {
if (!exifData) { exifExtractionInProgress.current = file.src;
return; const imageBlob = await (
await fetch(file.originalImageURL)
).blob();
const exifData = (await exifr.parse(imageBlob)) as Record<
string,
any
>;
if (exifExtractionInProgress.current === file.src) {
if (exifData) {
setExif({ key: file.src, value: exifData });
} else {
setExif({ key: file.src, value: null });
}
}
} }
exifData.raw = prettyPrintExif(exifData); } finally {
setExif(exifData); exifExtractionInProgress.current = null;
} }
} catch (e) { } catch (e) {
logError(e, 'exifr parsing failed'); logError(e, 'exifr parsing failed');
} }
}; };
function updateInfo() {
const file: EnteFile = this?.currItem;
if (file?.metadata) {
setMetaData(file.metadata);
setExif(null);
checkExifAvailable();
}
}
const handleCloseInfo = () => { const handleCloseInfo = () => {
setShowInfo(false); setShowInfo(false);
}; };
@ -310,15 +452,37 @@ function PhotoSwipe(props: Iprops) {
}; };
const downloadFileHelper = async (file) => { const downloadFileHelper = async (file) => {
appContext.startLoading(); if (props.enableDownload) {
await downloadFile( appContext.startLoading();
file, await downloadFile(
publicCollectionGalleryContext.accessedThroughSharedURL, file,
publicCollectionGalleryContext.token, publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.passwordToken publicCollectionGalleryContext.token,
); publicCollectionGalleryContext.passwordToken
);
appContext.finishLoading();
}
};
appContext.finishLoading(); const copyToClipboardHelper = async (file: EnteFile) => {
if (props.enableDownload && shouldShowCopyOption) {
appContext.startLoading();
await copyFileToClipboard(file.src);
appContext.finishLoading();
}
};
const toggleFullscreen = (photoSwipe) => {
const fullScreenApi: PhotoswipeFullscreenAPI =
photoSwipe?.ui?.getFullscreenAPI();
if (!fullScreenApi) {
return;
}
if (fullScreenApi.isFullscreen()) {
fullScreenApi.exit();
} else {
fullScreenApi.enter();
}
}; };
const scheduleUpdate = () => (needUpdate.current = true); const scheduleUpdate = () => (needUpdate.current = true);
const { id } = props; const { id } = props;
@ -355,33 +519,74 @@ function PhotoSwipe(props: Iprops) {
<button <button
className="pswp__button pswp__button--close" className="pswp__button pswp__button--close"
title={constants.CLOSE} title={constants.CLOSE_OPTION}
/> />
{props.enableDownload && ( {props.enableDownload && (
<button <button
className="pswp__button pswp__button--custom" className="pswp__button pswp__button--custom"
title={constants.DOWNLOAD} title={constants.DOWNLOAD_OPTION}
onClick={() => onClick={() =>
downloadFileHelper(photoSwipe.currItem) downloadFileHelper(photoSwipe.currItem)
}> }>
<DownloadIcon fontSize="small" /> <DownloadIcon fontSize="small" />
</button> </button>
)} )}
<button {props.enableDownload && shouldShowCopyOption && (
className="pswp__button pswp__button--fs" <button
title={constants.TOGGLE_FULLSCREEN} className="pswp__button pswp__button--custom"
/> title={constants.COPY_OPTION}
<button onClick={() =>
className="pswp__button pswp__button--zoom" copyToClipboardHelper(
title={constants.ZOOM_IN_OUT} photoSwipe.currItem as EnteFile
/> )
}>
<ContentCopy fontSize="small" />
</button>
)}
{!props.isSharedCollection && {!props.isSharedCollection &&
!props.isTrashCollection && ( !props.isTrashCollection && (
<button <button
className="pswp__button pswp__button--custom" className="pswp__button pswp__button--custom"
title={constants.DELETE_OPTION}
onClick={() => { onClick={() => {
onFavClick(photoSwipe?.currItem); confirmTrashFile(
photoSwipe?.currItem as EnteFile
);
}}>
<DeleteIcon fontSize="small" />
</button>
)}
<button
className="pswp__button pswp__button--zoom"
title={constants.ZOOM_IN_OUT}
/>
<button
className="pswp__button pswp__button--fs"
title={constants.TOGGLE_FULLSCREEN}
/>
{!props.isSharedCollection && (
<button
className="pswp__button pswp__button--custom"
title={constants.INFO_OPTION}
onClick={handleOpenInfo}>
<InfoIcon fontSize="small" />
</button>
)}
{!props.isSharedCollection &&
!props.isTrashCollection && (
<button
title={
isFav
? constants.UNFAVORITE_OPTION
: constants.FAVORITE_OPTION
}
className="pswp__button pswp__button--custom"
onClick={() => {
onFavClick(
photoSwipe?.currItem as EnteFile
);
}}> }}>
{isFav ? ( {isFav ? (
<FavoriteIcon fontSize="small" /> <FavoriteIcon fontSize="small" />
@ -390,14 +595,7 @@ function PhotoSwipe(props: Iprops) {
)} )}
</button> </button>
)} )}
{!props.isSharedCollection && (
<button
className="pswp__button pswp__button--custom"
title={constants.INFO}
onClick={handleOpenInfo}>
<InfoIcon fontSize="small" />
</button>
)}
<div className="pswp__preloader"> <div className="pswp__preloader">
<div className="pswp__preloader__icn"> <div className="pswp__preloader__icn">
<div className="pswp__preloader__cut"> <div className="pswp__preloader__cut">
@ -411,36 +609,34 @@ function PhotoSwipe(props: Iprops) {
</div> </div>
<button <button
className="pswp__button pswp__button--arrow--left" className="pswp__button pswp__button--arrow--left"
title={constants.PREVIOUS} title={constants.PREVIOUS}>
onClick={photoSwipe?.prev}> <ChevronLeft sx={{ pointerEvents: 'none' }} />
<ChevronRight
sx={{ transform: 'rotate(180deg)' }}
/>
</button> </button>
<button <button
className="pswp__button pswp__button--arrow--right" className="pswp__button pswp__button--arrow--right"
title={constants.NEXT} title={constants.NEXT}>
onClick={photoSwipe?.next}> <ChevronRight sx={{ pointerEvents: 'none' }} />
<ChevronRight />
</button> </button>
<div className="pswp__caption"> <div className="pswp__caption pswp-custom-caption-container">
<div /> <CaptionContainer />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<FileInfo <FileInfo
isTrashCollection={props.isTrashCollection}
shouldDisableEdits={props.isSharedCollection} shouldDisableEdits={props.isSharedCollection}
showInfo={showInfo} showInfo={showInfo}
handleCloseInfo={handleCloseInfo} handleCloseInfo={handleCloseInfo}
items={items} file={photoSwipe?.currItem as EnteFile}
photoSwipe={photoSwipe} exif={exif?.value}
metadata={metadata}
exif={exif}
scheduleUpdate={scheduleUpdate} scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
fileToCollectionsMap={props.fileToCollectionsMap}
collectionNameMap={props.collectionNameMap}
/> />
</> </>
); );
} }
export default PhotoSwipe; export default PhotoViewer;

View file

@ -37,7 +37,9 @@ export default function SearchInput(props: Iprops) {
setValue(value); setValue(value);
}; };
useEffect(() => search(value), [value]); useEffect(() => {
search(value);
}, [value]);
const resetSearch = () => { const resetSearch = () => {
if (props.isOpen) { if (props.isOpen) {

View file

@ -1,47 +0,0 @@
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import { downloadAsFile } from 'utils/file';
import constants from 'utils/strings/constants';
import { addLogLine, getDebugLogs } from 'utils/logging';
import SidebarButton from './Button';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { User } from 'types/user';
import { getSentryUserID } from 'utils/user';
export default function DebugLogs() {
const appContext = useContext(AppContext);
const confirmLogDownload = () =>
appContext.setDialogMessage({
title: constants.DOWNLOAD_LOGS,
content: constants.DOWNLOAD_LOGS_MESSAGE(),
proceed: {
text: constants.DOWNLOAD,
variant: 'accent',
action: downloadDebugLogs,
},
close: {
text: constants.CANCEL,
},
});
const downloadDebugLogs = () => {
addLogLine(
'latest commit id :' + process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH
);
addLogLine(`user sentry id ${getSentryUserID()}`);
addLogLine(`ente userID ${(getData(LS_KEYS.USER) as User)?.id}`);
addLogLine('exporting logs');
const logs = getDebugLogs();
const logString = logs.join('\n');
downloadAsFile(`debug_logs_${Date.now()}.txt`, logString);
};
return (
<SidebarButton
onClick={confirmLogDownload}
typographyVariant="caption"
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
{constants.DOWNLOAD_UPLOAD_LOGS}
</SidebarButton>
);
}

View file

@ -0,0 +1,72 @@
import { AppContext } from 'pages/_app';
import React, { useContext, useEffect, useState } from 'react';
import { downloadAsFile } from 'utils/file';
import constants from 'utils/strings/constants';
import { addLogLine, getDebugLogs } from 'utils/logging';
import SidebarButton from './Button';
import isElectron from 'is-electron';
import ElectronService from 'services/electron/common';
import { testUpload } from 'tests/upload.test';
import Typography from '@mui/material/Typography';
import { isInternalUser } from 'utils/user';
export default function DebugSection() {
const appContext = useContext(AppContext);
const [appVersion, setAppVersion] = useState<string>(null);
useEffect(() => {
const main = async () => {
if (isElectron()) {
const appVersion = await ElectronService.getAppVersion();
setAppVersion(appVersion);
}
};
main();
});
const confirmLogDownload = () =>
appContext.setDialogMessage({
title: constants.DOWNLOAD_LOGS,
content: constants.DOWNLOAD_LOGS_MESSAGE(),
proceed: {
text: constants.DOWNLOAD,
variant: 'accent',
action: downloadDebugLogs,
},
close: {
text: constants.CANCEL,
},
});
const downloadDebugLogs = () => {
addLogLine('exporting logs');
if (isElectron()) {
ElectronService.openLogDirectory();
} else {
const logs = getDebugLogs();
downloadAsFile(`debug_logs_${Date.now()}.txt`, logs);
}
};
return (
<>
<SidebarButton
onClick={confirmLogDownload}
typographyVariant="caption"
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
{constants.DOWNLOAD_UPLOAD_LOGS}
</SidebarButton>
{appVersion && (
<Typography p={1.5} color="text.secondary" variant="caption">
{appVersion}
</Typography>
)}
{isInternalUser() && (
<SidebarButton onClick={testUpload}>
{constants.RUN_TESTS}
</SidebarButton>
)}
</>
);
}

View file

@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { getDownloadAppMessage } from 'utils/ui'; import { getDownloadAppMessage } from 'utils/ui';
import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte'; import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
import { openLink } from 'utils/common';
export default function HelpSection() { export default function HelpSection() {
const [exportModalView, setExportModalView] = useState(false); const [exportModalView, setExportModalView] = useState(false);
@ -20,8 +21,7 @@ export default function HelpSection() {
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent( const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
getToken() getToken()
)}`; )}`;
const win = window.open(feedbackURL, '_blank'); openLink(feedbackURL, true);
win.focus();
} }
function exportFiles() { function exportFiles() {

View file

@ -2,10 +2,9 @@ import React, { useContext } from 'react';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection'; import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
import DeleteIcon from '@mui/icons-material/Delete';
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
import { CollectionSummaries } from 'types/collection'; import { CollectionSummaries } from 'types/collection';
import ShortcutButton from './ShortcutButton'; import ShortcutButton from './ShortcutButton';
import { ArchiveOutlined, DeleteOutline } from '@mui/icons-material';
interface Iprops { interface Iprops {
closeSidebar: () => void; closeSidebar: () => void;
collectionSummaries: CollectionSummaries; collectionSummaries: CollectionSummaries;
@ -30,13 +29,13 @@ export default function ShortcutSection({
return ( return (
<> <>
<ShortcutButton <ShortcutButton
startIcon={<DeleteIcon />} startIcon={<DeleteOutline />}
label={constants.TRASH} label={constants.TRASH}
count={collectionSummaries.get(TRASH_SECTION)?.fileCount} count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
onClick={openTrashSection} onClick={openTrashSection}
/> />
<ShortcutButton <ShortcutButton
startIcon={<VisibilityOffIcon />} startIcon={<ArchiveOutlined />}
label={constants.ARCHIVE_SECTION_NAME} label={constants.ARCHIVE_SECTION_NAME}
count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount} count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
onClick={openArchiveSection} onClick={openArchiveSection}

View file

@ -1,5 +1,5 @@
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
import React, { useContext, useMemo } from 'react'; import React, { MouseEventHandler, useContext, useMemo } from 'react';
import { import {
hasPaidSubscription, hasPaidSubscription,
isFamilyAdmin, isFamilyAdmin,
@ -43,19 +43,23 @@ export default function SubscriptionStatus({
}, [userDetails]); }, [userDetails]);
const handleClick = useMemo(() => { const handleClick = useMemo(() => {
if (userDetails) { const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
if (isSubscriptionActive(userDetails.subscription)) { e.stopPropagation();
if (hasExceededStorageQuota(userDetails)) { if (userDetails) {
return showPlanSelectorModal; if (isSubscriptionActive(userDetails.subscription)) {
} if (hasExceededStorageQuota(userDetails)) {
} else { showPlanSelectorModal();
if (hasStripeSubscription(userDetails.subscription)) { }
return billingService.redirectToCustomerPortal;
} else { } else {
return showPlanSelectorModal; if (hasStripeSubscription(userDetails.subscription)) {
billingService.redirectToCustomerPortal();
} else {
showPlanSelectorModal();
}
} }
} }
} };
return eventHandler;
}, [userDetails]); }, [userDetails]);
if (!hasAMessage) { if (!hasAMessage) {
@ -80,13 +84,9 @@ export default function SubscriptionStatus({
) )
: hasExceededStorageQuota(userDetails) && : hasExceededStorageQuota(userDetails) &&
constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO( constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO(
showPlanSelectorModal handleClick
) )
: constants.SUBSCRIPTION_EXPIRED_MESSAGE( : constants.SUBSCRIPTION_EXPIRED_MESSAGE(handleClick)}
hasStripeSubscription(userDetails.subscription)
? billingService.redirectToCustomerPortal
: showPlanSelectorModal
)}
</Typography> </Typography>
</Box> </Box>
); );

View file

@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
import UtilitySection from './UtilitySection'; import UtilitySection from './UtilitySection';
import HelpSection from './HelpSection'; import HelpSection from './HelpSection';
import ExitSection from './ExitSection'; import ExitSection from './ExitSection';
import DebugLogs from './DebugLogs'; import DebugSection from './DebugSection';
import { DrawerSidebar } from './styledComponents'; import { DrawerSidebar } from './styledComponents';
import HeaderSection from './Header'; import HeaderSection from './Header';
import { CollectionSummaries } from 'types/collection'; import { CollectionSummaries } from 'types/collection';
@ -37,7 +37,7 @@ export default function Sidebar({
<Divider /> <Divider />
<ExitSection /> <ExitSection />
<Divider /> <Divider />
<DebugLogs /> <DebugSection />
</Stack> </Stack>
</DrawerSidebar> </DrawerSidebar>
); );

View file

@ -1,11 +1,9 @@
import { Drawer, styled } from '@mui/material'; import { styled } from '@mui/material';
import CircleIcon from '@mui/icons-material/Circle'; import CircleIcon from '@mui/icons-material/Circle';
import { EnteDrawer } from 'components/EnteDrawer';
export const DrawerSidebar = styled(Drawer)(({ theme }) => ({ export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
'& .MuiPaper-root': { '& .MuiPaper-root': {
maxWidth: '375px',
width: '100%',
scrollbarWidth: 'thin',
padding: theme.spacing(1.5), padding: theme.spacing(1.5),
}, },
})); }));

View file

@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton';
import TextField from '@mui/material/TextField'; import TextField from '@mui/material/TextField';
import ShowHidePassword from './Form/ShowHidePassword'; import ShowHidePassword from './Form/ShowHidePassword';
import { FlexWrapper } from './Container'; import { FlexWrapper } from './Container';
import { Button } from '@mui/material'; import { Button, FormHelperText } from '@mui/material';
interface formValues { interface formValues {
inputValue: string; inputValue: string;
@ -24,6 +24,7 @@ export interface SingleInputFormProps {
secondaryButtonAction?: () => void; secondaryButtonAction?: () => void;
disableAutoFocus?: boolean; disableAutoFocus?: boolean;
hiddenPreInput?: any; hiddenPreInput?: any;
caption?: any;
hiddenPostInput?: any; hiddenPostInput?: any;
autoComplete?: string; autoComplete?: string;
} }
@ -113,6 +114,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
), ),
}} }}
/> />
<FormHelperText
sx={{
position: 'relative',
top: errors.inputValue ? '-22px' : '0',
float: 'right',
padding: '0 8px',
}}>
{props.caption}
</FormHelperText>
{props.hiddenPostInput} {props.hiddenPostInput}
<FlexWrapper justifyContent={'flex-end'}> <FlexWrapper justifyContent={'flex-end'}>
{props.secondaryButtonAction && ( {props.secondaryButtonAction && (
@ -120,13 +130,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
onClick={props.secondaryButtonAction} onClick={props.secondaryButtonAction}
size="large" size="large"
color="secondary" color="secondary"
sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }} sx={{
'&&&': { mt: 2, mb: 4, mr: 1, ...buttonSx },
}}
{...restSubmitButtonProps}> {...restSubmitButtonProps}>
{constants.CANCEL} {constants.CANCEL}
</Button> </Button>
)} )}
<SubmitButton <SubmitButton
sx={{ mt: 2, ...buttonSx }} sx={{ '&&&': { mt: 2, ...buttonSx } }}
buttonText={props.buttonText} buttonText={props.buttonText}
loading={loading} loading={loading}
{...restSubmitButtonProps} {...restSubmitButtonProps}

View file

@ -0,0 +1,56 @@
import { ArrowBack, Close } from '@mui/icons-material';
import { Box, IconButton, Typography } from '@mui/material';
import React from 'react';
import { FlexWrapper } from './Container';
interface Iprops {
title: string;
caption?: string;
onClose: () => void;
backIsClose?: boolean;
onRootClose?: () => void;
actionButton?: JSX.Element;
}
export default function Titlebar({
title,
caption,
onClose,
backIsClose,
actionButton,
onRootClose,
}: Iprops): JSX.Element {
return (
<>
<FlexWrapper
height={48}
alignItems={'center'}
justifyContent="space-between">
<IconButton
onClick={onClose}
color={backIsClose ? 'secondary' : 'primary'}>
{backIsClose ? <Close /> : <ArrowBack />}
</IconButton>
<Box display={'flex'} gap="4px">
{actionButton && actionButton}
{!backIsClose && (
<IconButton onClick={onRootClose} color={'secondary'}>
<Close />
</IconButton>
)}
</Box>
</FlexWrapper>
<Box py={0.5} px={2}>
<Typography variant="h3" fontWeight={'bold'}>
{title}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
{caption}
</Typography>
</Box>
</>
);
}

View file

@ -7,9 +7,9 @@ import { UploadProgressHeader } from './header';
import { InProgressSection } from './inProgressSection'; import { InProgressSection } from './inProgressSection';
import { ResultSection } from './resultSection'; import { ResultSection } from './resultSection';
import { NotUploadSectionHeader } from './styledComponents'; import { NotUploadSectionHeader } from './styledComponents';
import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
import UploadProgressContext from 'contexts/uploadProgress'; import UploadProgressContext from 'contexts/uploadProgress';
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton'; import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
import { APP_DOWNLOAD_URL } from 'utils/common';
export function UploadProgressDialog() { export function UploadProgressDialog() {
const { open, onClose, uploadStage, finishedUploads } = useContext( const { open, onClose, uploadStage, finishedUploads } = useContext(
@ -40,70 +40,78 @@ export function UploadProgressDialog() {
<Dialog maxWidth="xs" open={open} onClose={handleClose}> <Dialog maxWidth="xs" open={open} onClose={handleClose}>
<UploadProgressHeader /> <UploadProgressHeader />
{(uploadStage === UPLOAD_STAGES.UPLOADING || {(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.FINISH) && ( uploadStage === UPLOAD_STAGES.FINISH ||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
<DialogContent sx={{ '&&&': { px: 0 } }}> <DialogContent sx={{ '&&&': { px: 0 } }}>
{uploadStage === UPLOAD_STAGES.UPLOADING && ( {(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
<InProgressSection /> <InProgressSection />
)} )}
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.FINISH) && (
<>
<ResultSection
uploadResult={UPLOAD_RESULT.UPLOADED}
sectionTitle={constants.SUCCESSFUL_UPLOADS}
/>
<ResultSection
uploadResult={
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
}
sectionTitle={
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
}
sectionInfo={
constants.THUMBNAIL_GENERATION_FAILED_INFO
}
/>
<ResultSection {uploadStage === UPLOAD_STAGES.FINISH &&
uploadResult={UPLOAD_RESULT.UPLOADED} hasUnUploadedFiles && (
sectionTitle={constants.SUCCESSFUL_UPLOADS} <NotUploadSectionHeader>
/> {constants.FILE_NOT_UPLOADED_LIST}
<ResultSection </NotUploadSectionHeader>
uploadResult={ )}
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
}
sectionTitle={
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
}
sectionInfo={constants.THUMBNAIL_GENERATION_FAILED_INFO}
/>
{uploadStage === UPLOAD_STAGES.FINISH && <ResultSection
hasUnUploadedFiles && ( uploadResult={UPLOAD_RESULT.BLOCKED}
<NotUploadSectionHeader> sectionTitle={constants.BLOCKED_UPLOADS}
{constants.FILE_NOT_UPLOADED_LIST} sectionInfo={constants.ETAGS_BLOCKED(
</NotUploadSectionHeader> APP_DOWNLOAD_URL
)} )}
/>
<ResultSection <ResultSection
uploadResult={UPLOAD_RESULT.BLOCKED} uploadResult={UPLOAD_RESULT.FAILED}
sectionTitle={constants.BLOCKED_UPLOADS} sectionTitle={constants.FAILED_UPLOADS}
sectionInfo={constants.ETAGS_BLOCKED( />
getOSSpecificDesktopAppDownloadLink() <ResultSection
)} uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
/> sectionTitle={constants.SKIPPED_FILES}
<ResultSection sectionInfo={constants.SKIPPED_INFO}
uploadResult={UPLOAD_RESULT.FAILED} />
sectionTitle={constants.FAILED_UPLOADS} <ResultSection
/> uploadResult={
<ResultSection UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED} }
sectionTitle={constants.SKIPPED_FILES} sectionTitle={
sectionInfo={constants.SKIPPED_INFO} constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS
/> }
<ResultSection sectionInfo={
uploadResult={ constants.LARGER_THAN_AVAILABLE_STORAGE_INFO
UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE }
} />
sectionTitle={ <ResultSection
constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS uploadResult={UPLOAD_RESULT.UNSUPPORTED}
} sectionTitle={constants.UNSUPPORTED_FILES}
sectionInfo={ sectionInfo={constants.UNSUPPORTED_INFO}
constants.LARGER_THAN_AVAILABLE_STORAGE_INFO />
} <ResultSection
/> uploadResult={UPLOAD_RESULT.TOO_LARGE}
<ResultSection sectionTitle={constants.TOO_LARGE_UPLOADS}
uploadResult={UPLOAD_RESULT.UNSUPPORTED} sectionInfo={constants.TOO_LARGE_INFO}
sectionTitle={constants.UNSUPPORTED_FILES} />
sectionInfo={constants.UNSUPPORTED_INFO} </>
/> )}
<ResultSection
uploadResult={UPLOAD_RESULT.TOO_LARGE}
sectionTitle={constants.TOO_LARGE_UPLOADS}
sectionInfo={constants.TOO_LARGE_INFO}
/>
</DialogContent> </DialogContent>
)} )}
{uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />} {uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}

View file

@ -10,17 +10,19 @@ import {
} from './section'; } from './section';
import UploadProgressContext from 'contexts/uploadProgress'; import UploadProgressContext from 'contexts/uploadProgress';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { UPLOAD_STAGES } from 'constants/upload';
export const InProgressSection = () => { export const InProgressSection = () => {
const { inProgressUploads, hasLivePhotos, uploadFileNames } = useContext( const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } =
UploadProgressContext useContext(UploadProgressContext);
);
const fileList = inProgressUploads ?? []; const fileList = inProgressUploads ?? [];
return ( return (
<UploadProgressSection defaultExpanded> <UploadProgressSection>
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}> <UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
{constants.INPROGRESS_UPLOADS} {uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
? constants.INPROGRESS_METADATA_EXTRACTION
: constants.INPROGRESS_UPLOADS}
</UploadProgressSectionTitle> </UploadProgressSectionTitle>
<UploadProgressSectionContent> <UploadProgressSectionContent>
{hasLivePhotos && ( {hasLivePhotos && (
@ -30,8 +32,13 @@ export const InProgressSection = () => {
fileList={fileList.map(({ localFileID, progress }) => ( fileList={fileList.map(({ localFileID, progress }) => (
<InProgressItemContainer key={localFileID}> <InProgressItemContainer key={localFileID}>
<span>{uploadFileNames.get(localFileID)}</span> <span>{uploadFileNames.get(localFileID)}</span>
<span className="separator">{`-`}</span> {uploadStage === UPLOAD_STAGES.UPLOADING && (
<span>{`${progress}%`}</span> <>
{' '}
<span className="separator">{`-`}</span>
<span>{`${progress}%`}</span>
</>
)}
</InProgressItemContainer> </InProgressItemContainer>
))} ))}
/> />

View file

@ -20,6 +20,8 @@ function UploadProgressSubtitleText() {
return ( return (
<Typography color="text.secondary"> <Typography color="text.secondary">
{uploadStage === UPLOAD_STAGES.UPLOADING {uploadStage === UPLOAD_STAGES.UPLOADING
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
: uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter) ? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
: constants.UPLOAD_STAGE_MESSAGE[uploadStage]} : constants.UPLOAD_STAGE_MESSAGE[uploadStage]}
</Typography> </Typography>

View file

@ -49,6 +49,7 @@ import {
} from 'utils/upload'; } from 'utils/upload';
import { getUserOwnedCollections } from 'utils/collection'; import { getUserOwnedCollections } from 'utils/collection';
import billingService from 'services/billingService'; import billingService from 'services/billingService';
import { addLogLine } from 'utils/logging';
const FIRST_ALBUM_NAME = 'My First Album'; const FIRST_ALBUM_NAME = 'My First Album';
@ -286,7 +287,7 @@ export default function Uploader(props: Props) {
) => { ) => {
try { try {
await preCollectionCreationAction(); await preCollectionCreationAction();
const filesWithCollectionToUpload: FileWithCollection[] = []; let filesWithCollectionToUpload: FileWithCollection[] = [];
const collections: Collection[] = []; const collections: Collection[] = [];
let collectionNameToFilesMap = new Map< let collectionNameToFilesMap = new Map<
string, string,
@ -320,13 +321,14 @@ export default function Uploader(props: Props) {
...existingCollection, ...existingCollection,
...collections, ...collections,
]); ]);
filesWithCollectionToUpload.push( filesWithCollectionToUpload = [
...filesWithCollectionToUpload,
...files.map((file) => ({ ...files.map((file) => ({
localID: index++, localID: index++,
collectionID: collection.id, collectionID: collection.id,
file, file,
})) })),
); ];
} }
} catch (e) { } catch (e) {
closeUploadProgress(); closeUploadProgress();
@ -426,6 +428,7 @@ export default function Uploader(props: Props) {
const retryFailed = async () => { const retryFailed = async () => {
try { try {
addLogLine('user retrying failed upload');
const filesWithCollections = const filesWithCollections =
await uploadManager.getFailedFilesWithCollections(); await uploadManager.getFailedFilesWithCollections();
await preUploadAction(); await preUploadAction();
@ -449,31 +452,28 @@ export default function Uploader(props: Props) {
case CustomError.SUBSCRIPTION_EXPIRED: case CustomError.SUBSCRIPTION_EXPIRED:
notification = { notification = {
variant: 'danger', variant: 'danger',
message: constants.SUBSCRIPTION_EXPIRED, subtext: constants.SUBSCRIPTION_EXPIRED,
action: { message: constants.RENEW_NOW,
text: constants.RENEW_NOW, onClick: () => billingService.redirectToCustomerPortal(),
callback: billingService.redirectToCustomerPortal,
},
}; };
break; break;
case CustomError.STORAGE_QUOTA_EXCEEDED: case CustomError.STORAGE_QUOTA_EXCEEDED:
notification = { notification = {
variant: 'danger', variant: 'danger',
message: constants.STORAGE_QUOTA_EXCEEDED, subtext: constants.STORAGE_QUOTA_EXCEEDED,
action: { message: constants.UPGRADE_NOW,
text: constants.UPGRADE_NOW, onClick: () => galleryContext.showPlanSelectorModal(),
callback: galleryContext.showPlanSelectorModal, startIcon: <DiscFullIcon />,
},
icon: <DiscFullIcon fontSize="large" />,
}; };
break; break;
default: default:
notification = { notification = {
variant: 'danger', variant: 'danger',
message: constants.UNKNOWN_ERROR, message: constants.UNKNOWN_ERROR,
onClick: () => null,
}; };
} }
galleryContext.setNotificationAttributes(notification); appContext.setNotificationAttributes(notification);
} }
const uploadToSingleNewCollection = (collectionName: string) => { const uploadToSingleNewCollection = (collectionName: string) => {

View file

@ -3,15 +3,12 @@ import { Box } from '@mui/material';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import VerticallyCentered from 'components/Container'; import VerticallyCentered from 'components/Container';
export const MappingsContainer = styled(Box)(({ theme }) => ({ export const MappingsContainer = styled(Box)(() => ({
height: '278px', height: '278px',
overflow: 'auto', overflow: 'auto',
'&::-webkit-scrollbar': { '&::-webkit-scrollbar': {
width: '4px', width: '4px',
}, },
'&::-webkit-scrollbar-thumb': {
backgroundColor: theme.palette.secondary.main,
},
})); }));
export const NoMappingsContainer = styled(VerticallyCentered)({ export const NoMappingsContainer = styled(VerticallyCentered)({

View file

@ -2,9 +2,8 @@ import { FluidContainer } from 'components/Container';
import { SelectionBar } from '../../Navbar/SelectionBar'; import { SelectionBar } from '../../Navbar/SelectionBar';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { Box, IconButton, styled } from '@mui/material'; import { Box, IconButton, styled, Tooltip } from '@mui/material';
import { DeduplicateContext } from 'pages/deduplicate'; import { DeduplicateContext } from 'pages/deduplicate';
import { IconWithMessage } from 'components/IconWithMessage';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import CloseIcon from '@mui/icons-material/Close'; import CloseIcon from '@mui/icons-material/Close';
import BackButton from '@mui/icons-material/ArrowBackOutlined'; import BackButton from '@mui/icons-material/ArrowBackOutlined';
@ -78,11 +77,11 @@ export default function DeduplicateOptions({
<div> <div>
<VerticalLine /> <VerticalLine />
</div> </div>
<IconWithMessage message={constants.DELETE}> <Tooltip title={constants.DELETE}>
<IconButton onClick={trashHandler}> <IconButton onClick={trashHandler}>
<DeleteIcon /> <DeleteIcon />
</IconButton> </IconButton>
</IconWithMessage> </Tooltip>
</SelectionBar> </SelectionBar>
); );
} }

View file

@ -1,34 +1,12 @@
import { Link, LinkProps } from '@mui/material'; import { ButtonProps, Link, LinkProps } from '@mui/material';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { ButtonProps } from 'react-bootstrap';
export enum ButtonVariant {
success = 'success',
danger = 'danger',
secondary = 'secondary',
warning = 'warning',
}
export type LinkButtonProps = React.PropsWithChildren<{ export type LinkButtonProps = React.PropsWithChildren<{
onClick: () => void; onClick: () => void;
variant?: string; variant?: string;
style?: React.CSSProperties; style?: React.CSSProperties;
}>; }>;
export function getVariantColor(variant: string) {
switch (variant) {
case ButtonVariant.success:
return '#51cd7c';
case ButtonVariant.danger:
return '#c93f3f';
case ButtonVariant.secondary:
return '#858585';
case ButtonVariant.warning:
return '#D7BB63';
default:
return '#d1d1d1';
}
}
const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
children, children,
sx, sx,
@ -41,6 +19,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
sx={{ sx={{
color: 'text.primary', color: 'text.primary',
textDecoration: 'underline rgba(255, 255, 255, 0.4)', textDecoration: 'underline rgba(255, 255, 255, 0.4)',
paddingBottom: 0.5,
'&:hover': { '&:hover': {
color: `${color}.main`, color: `${color}.main`,
textDecoration: `underline `, textDecoration: `underline `,

View file

@ -13,6 +13,7 @@ import {
hasPaidSubscription, hasPaidSubscription,
getTotalFamilyUsage, getTotalFamilyUsage,
isPartOfFamily, isPartOfFamily,
isSubscriptionActive,
} from 'utils/billing'; } from 'utils/billing';
import { reverseString } from 'utils/common'; import { reverseString } from 'utils/common';
import { GalleryContext } from 'pages/gallery'; import { GalleryContext } from 'pages/gallery';
@ -68,18 +69,19 @@ function PlanSelectorCard(props: Props) {
const main = async () => { const main = async () => {
try { try {
props.setLoading(true); props.setLoading(true);
let plans = await billingService.getPlans(); const plans = await billingService.getPlans();
if (isSubscriptionActive(subscription)) {
const planNotListed = const planNotListed =
plans.filter((plan) => plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription) isUserSubscribedPlan(plan, subscription)
).length === 0; ).length === 0;
if ( if (
subscription && subscription &&
!isOnFreePlan(subscription) && !isOnFreePlan(subscription) &&
planNotListed planNotListed
) { ) {
plans = [planForSubscription(subscription), ...plans]; plans.push(planForSubscription(subscription));
}
} }
setPlans(plans); setPlans(plans);
} catch (e) { } catch (e) {

View file

@ -1,4 +1,4 @@
import React, { useContext, useLayoutEffect, useRef, useState } from 'react'; import React, { useContext, useLayoutEffect, useState } from 'react';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined'; import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
@ -14,22 +14,22 @@ import { DeduplicateContext } from 'pages/deduplicate';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { Overlay } from 'components/Container'; import { Overlay } from 'components/Container';
import { TRASH_SECTION } from 'constants/collection'; import { TRASH_SECTION } from 'constants/collection';
import { formatDateRelative } from 'utils/time'; import { formatDateRelative } from 'utils/time/format';
interface IProps { interface IProps {
file: EnteFile; file: EnteFile;
updateURL?: (url: string) => EnteFile; updateURL: (url: string) => EnteFile;
onClick?: () => void; onClick: () => void;
forcedEnable?: boolean; selectable: boolean;
selectable?: boolean; selected: boolean;
selected?: boolean; onSelect: (checked: boolean) => void;
onSelect?: (checked: boolean) => void; onHover: () => void;
onHover?: () => void; onRangeSelect: () => void;
onRangeSelect?: () => void; isRangeSelectActive: boolean;
isRangeSelectActive?: boolean; selectOnClick: boolean;
selectOnClick?: boolean; isInsSelectRange: boolean;
isInsSelectRange?: boolean; activeCollection: number;
activeCollection?: number; showPlaceholder: boolean;
} }
const Check = styled('input')<{ active: boolean }>` const Check = styled('input')<{ active: boolean }>`
@ -203,7 +203,6 @@ export default function PreviewCard(props: IProps) {
file, file,
onClick, onClick,
updateURL, updateURL,
forcedEnable,
selectable, selectable,
selected, selected,
onSelect, onSelect,
@ -213,14 +212,13 @@ export default function PreviewCard(props: IProps) {
isRangeSelectActive, isRangeSelectActive,
isInsSelectRange, isInsSelectRange,
} = props; } = props;
const isMounted = useRef(true);
const publicCollectionGalleryContext = useContext( const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext PublicCollectionGalleryContext
); );
const deduplicateContext = useContext(DeduplicateContext); const deduplicateContext = useContext(DeduplicateContext);
useLayoutEffect(() => { useLayoutEffect(() => {
if (file && !file.msrc) { if (file && !file.msrc && !props.showPlaceholder) {
const main = async () => { const main = async () => {
try { try {
let url; let url;
@ -236,18 +234,14 @@ export default function PreviewCard(props: IProps) {
} else { } else {
url = await DownloadManager.getThumbnail(file); url = await DownloadManager.getThumbnail(file);
} }
if (isMounted.current) { setImgSrc(url);
setImgSrc(url); thumbs.set(file.id, url);
thumbs.set(file.id, url); const newFile = updateURL(url);
if (updateURL) { file.msrc = newFile.msrc;
const newFile = updateURL(url); file.html = newFile.html;
file.msrc = newFile.msrc; file.src = newFile.src;
file.html = newFile.html; file.w = newFile.w;
file.src = newFile.src; file.h = newFile.h;
file.w = newFile.w;
file.h = newFile.h;
}
}
} catch (e) { } catch (e) {
logError(e, 'preview card useEffect failed'); logError(e, 'preview card useEffect failed');
// no-op // no-op
@ -257,17 +251,17 @@ export default function PreviewCard(props: IProps) {
if (thumbs.has(file.id)) { if (thumbs.has(file.id)) {
const thumbImgSrc = thumbs.get(file.id); const thumbImgSrc = thumbs.get(file.id);
setImgSrc(thumbImgSrc); setImgSrc(thumbImgSrc);
file.msrc = thumbImgSrc; const newFile = updateURL(thumbImgSrc);
file.msrc = newFile.msrc;
file.html = newFile.html;
file.src = newFile.src;
file.w = newFile.w;
file.h = newFile.h;
} else { } else {
main(); main();
} }
} }
}, [file, props.showPlaceholder]);
return () => {
// cool cool cool
isMounted.current = false;
};
}, [file]);
const handleClick = () => { const handleClick = () => {
if (selectOnClick) { if (selectOnClick) {
@ -300,10 +294,10 @@ export default function PreviewCard(props: IProps) {
return ( return (
<Cont <Cont
id={`thumb-${file?.id}`} id={`thumb-${file?.id}-${props.showPlaceholder}`}
onClick={handleClick} onClick={handleClick}
onMouseEnter={handleHover} onMouseEnter={handleHover}
disabled={!forcedEnable && !file?.msrc && !imgSrc} disabled={!file?.msrc && !imgSrc}
{...(selectable ? useLongPress(longPressCallback, 500) : {})}> {...(selectable ? useLongPress(longPressCallback, 500) : {})}>
{selectable && ( {selectable && (
<Check <Check

View file

@ -18,8 +18,8 @@ import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete'; import DeleteIcon from '@mui/icons-material/Delete';
import ClockIcon from '@mui/icons-material/AccessTime'; import ClockIcon from '@mui/icons-material/AccessTime';
import DownloadIcon from '@mui/icons-material/Download'; import DownloadIcon from '@mui/icons-material/Download';
import UnArchiveIcon from '@mui/icons-material/Visibility'; import UnArchiveIcon from '@mui/icons-material/Unarchive';
import ArchiveIcon from '@mui/icons-material/VisibilityOff'; import ArchiveIcon from '@mui/icons-material/ArchiveOutlined';
import MoveIcon from '@mui/icons-material/ArrowForward'; import MoveIcon from '@mui/icons-material/ArrowForward';
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline'; import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
import { getTrashFilesMessage } from 'utils/ui'; import { getTrashFilesMessage } from 'utils/ui';

View file

@ -1 +1,4 @@
export const DESKTOP_REDIRECT_URL = 'https://payments.ente.io/desktop-redirect'; import { getPaymentsURL } from 'utils/common/apiUtil';
export const getDesktopRedirectURL = () =>
`${getPaymentsURL()}/desktop-redirect`;

View file

@ -0,0 +1,3 @@
export const INPUT_PATH_PLACEHOLDER = 'INPUT';
export const FFMPEG_PLACEHOLDER = 'FFMPEG';
export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT';

View file

@ -2,6 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
export const MAX_EDITED_CREATION_TIME = new Date(); export const MAX_EDITED_CREATION_TIME = new Date();
export const MAX_EDITED_FILE_NAME_LENGTH = 100; export const MAX_EDITED_FILE_NAME_LENGTH = 100;
export const MAX_CAPTION_SIZE = 280;
export const MAX_TRASH_BATCH_SIZE = 1000; export const MAX_TRASH_BATCH_SIZE = 1000;
export const TYPE_HEIC = 'heic'; export const TYPE_HEIC = 'heic';

View file

@ -1,4 +1,12 @@
export default [ export const defaultLivePhotoDefaultOptions = {
click: () => {},
hide: () => {},
show: () => {},
loading: false,
visible: false,
};
export const photoSwipeV4Events = [
'beforeChange', 'beforeChange',
'afterChange', 'afterChange',
'imageLoadComplete', 'imageLoadComplete',

View file

@ -1,7 +0,0 @@
export const defaultLivePhotoDefaultOptions = {
click: () => {},
hide: () => {},
show: () => {},
loading: false,
visible: false,
};

View file

@ -1,10 +1,15 @@
export const ENV_DEVELOPMENT = 'development';
export const ENV_PRODUCTION = 'production';
export const getSentryDSN = () => export const getSentryDSN = () =>
process.env.NEXT_PUBLIC_SENTRY_DSN ?? process.env.NEXT_PUBLIC_SENTRY_DSN ??
'https://60abb33b597c42f6a3fb27cd82c55101@sentry.ente.io/2'; 'https://60abb33b597c42f6a3fb27cd82c55101@sentry.ente.io/2';
export const getSentryENV = () => export const getSentryENV = () =>
process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development'; process.env.NEXT_PUBLIC_SENTRY_ENV ?? ENV_PRODUCTION;
export const getSentryRelease = () => process.env.SENTRY_RELEASE; export const getSentryRelease = () => process.env.SENTRY_RELEASE;
export { getIsSentryEnabled } from '../../../sentryConfigUtil'; export { getIsSentryEnabled } from '../../../sentryConfigUtil';
export const isDEVSentryENV = () => getSentryENV() === ENV_DEVELOPMENT;

View file

@ -8,12 +8,10 @@ import 'photoswipe/dist/photoswipe.css';
import 'styles/global.css'; import 'styles/global.css';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { logError } from '../utils/sentry'; import { logError } from '../utils/sentry';
// import { Workbox } from 'workbox-window';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import HTTPService from 'services/HTTPService'; import HTTPService from 'services/HTTPService';
import FlashMessageBar from 'components/FlashMessageBar'; import FlashMessageBar from 'components/FlashMessageBar';
import Head from 'next/head'; import Head from 'next/head';
import { addLogLine } from 'utils/logging';
import LoadingBar from 'react-top-loading-bar'; import LoadingBar from 'react-top-loading-bar';
import DialogBox from 'components/DialogBox'; import DialogBox from 'components/DialogBox';
import { styled, ThemeProvider } from '@mui/material/styles'; import { styled, ThemeProvider } from '@mui/material/styles';
@ -27,6 +25,25 @@ import {
getRoadmapRedirectURL, getRoadmapRedirectURL,
} from 'services/userService'; } from 'services/userService';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import {
addLogLine,
clearLogsIfLocalStorageLimitExceeded,
} from 'utils/logging';
import isElectron from 'is-electron';
import ElectronUpdateService from 'services/electron/update';
import {
getUpdateAvailableForDownloadMessage,
getUpdateReadyToInstallMessage,
} from 'utils/ui';
import Notification from 'components/Notification';
import {
NotificationAttributes,
SetNotificationAttributes,
} from 'types/Notification';
import ArrowForward from '@mui/icons-material/ArrowForward';
import { AppUpdateInfo } from 'types/electron';
import { getSentryUserID } from 'utils/user';
import { User } from 'types/user';
export const MessageContainer = styled('div')` export const MessageContainer = styled('div')`
background-color: #111; background-color: #111;
@ -52,6 +69,7 @@ type AppContextType = {
finishLoading: () => void; finishLoading: () => void;
closeMessageDialog: () => void; closeMessageDialog: () => void;
setDialogMessage: SetDialogBoxAttributes; setDialogMessage: SetDialogBoxAttributes;
setNotificationAttributes: SetNotificationAttributes;
isFolderSyncRunning: boolean; isFolderSyncRunning: boolean;
setIsFolderSyncRunning: (isRunning: boolean) => void; setIsFolderSyncRunning: (isRunning: boolean) => void;
watchFolderView: boolean; watchFolderView: boolean;
@ -97,34 +115,12 @@ export default function App({ Component, err }) {
const [watchFolderView, setWatchFolderView] = useState(false); const [watchFolderView, setWatchFolderView] = useState(false);
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null); const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
const isMobile = useMediaQuery('(max-width:428px)'); const isMobile = useMediaQuery('(max-width:428px)');
const [notificationView, setNotificationView] = useState(false);
const closeNotification = () => setNotificationView(false);
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
useEffect(() => { useEffect(() => {
if (
!('serviceWorker' in navigator) ||
process.env.NODE_ENV !== 'production'
) {
console.warn('Progressive Web App support is disabled');
return;
}
// const wb = new Workbox('sw.js', { scope: '/' });
// wb.register();
if ('serviceWorker' in navigator) {
navigator.serviceWorker.onmessage = (event) => {
if (event.data.action === 'upload-files') {
const files = event.data.files;
setSharedFiles(files);
}
};
navigator.serviceWorker
.getRegistrations()
.then(function (registrations) {
for (const registration of registrations) {
registration.unregister();
}
});
}
HTTPService.getInterceptors().response.use( HTTPService.getInterceptors().response.use(
(resp) => resp, (resp) => resp,
(error) => { (error) => {
@ -132,6 +128,34 @@ export default function App({ Component, err }) {
return Promise.reject(error); return Promise.reject(error);
} }
); );
clearLogsIfLocalStorageLimitExceeded();
const main = async () => {
addLogLine(`userID: ${(getData(LS_KEYS.USER) as User)?.id}`);
addLogLine(`sentryID: ${await getSentryUserID()}`);
addLogLine(`sentry release ID: ${process.env.SENTRY_RELEASE}`);
};
main();
}, []);
useEffect(() => {
if (isElectron()) {
const showUpdateDialog = (updateInfo: AppUpdateInfo) => {
if (updateInfo.autoUpdatable) {
setDialogMessage(getUpdateReadyToInstallMessage());
} else {
setNotificationAttributes({
endIcon: <ArrowForward />,
variant: 'secondary',
message: constants.UPDATE_AVAILABLE,
onClick: () =>
setDialogMessage(
getUpdateAvailableForDownloadMessage(updateInfo)
),
});
}
};
ElectronUpdateService.registerUpdateEventListener(showUpdateDialog);
}
}, []); }, []);
const setUserOnline = () => setOffline(false); const setUserOnline = () => setOffline(false);
@ -206,10 +230,12 @@ export default function App({ Component, err }) {
}, [redirectName]); }, [redirectName]);
useEffect(() => { useEffect(() => {
addLogLine(`app started`); setMessageDialogView(true);
}, []); }, [dialogMessage]);
useEffect(() => setMessageDialogView(true), [dialogMessage]); useEffect(() => {
setNotificationView(true);
}, [notificationAttributes]);
const showNavBar = (show: boolean) => setShowNavBar(show); const showNavBar = (show: boolean) => setShowNavBar(show);
const setDisappearingFlashMessage = (flashMessages: FlashMessage) => { const setDisappearingFlashMessage = (flashMessages: FlashMessage) => {
@ -239,7 +265,7 @@ export default function App({ Component, err }) {
</Head> </Head>
<ThemeProvider theme={darkThemeOptions}> <ThemeProvider theme={darkThemeOptions}>
<CssBaseline /> <CssBaseline enableColorScheme />
{showNavbar && <AppNavbar />} {showNavbar && <AppNavbar />}
<MessageContainer> <MessageContainer>
{offline && constants.OFFLINE_MSG} {offline && constants.OFFLINE_MSG}
@ -265,11 +291,17 @@ export default function App({ Component, err }) {
<LoadingBar color="#51cd7c" ref={loadingBar} /> <LoadingBar color="#51cd7c" ref={loadingBar} />
<DialogBox <DialogBox
sx={{ zIndex: 1600 }}
size="xs" size="xs"
open={messageDialogView} open={messageDialogView}
onClose={closeMessageDialog} onClose={closeMessageDialog}
attributes={dialogMessage} attributes={dialogMessage}
/> />
<Notification
open={notificationView}
onClose={closeNotification}
attributes={notificationAttributes}
/>
<AppContext.Provider <AppContext.Provider
value={{ value={{
@ -290,6 +322,7 @@ export default function App({ Component, err }) {
watchFolderFiles, watchFolderFiles,
setWatchFolderFiles, setWatchFolderFiles,
isMobile, isMobile,
setNotificationAttributes,
}}> }}>
{loading ? ( {loading ? (
<VerticallyCentered> <VerticallyCentered>

View file

@ -24,6 +24,9 @@ import router from 'next/router';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { styled } from '@mui/material'; import { styled } from '@mui/material';
import { syncCollections } from 'services/collectionService'; import { syncCollections } from 'services/collectionService';
import EnteSpinner from 'components/EnteSpinner';
import VerticallyCentered from 'components/Container';
import { Collection } from 'types/collection';
export const DeduplicateContext = createContext<DeduplicateContextType>( export const DeduplicateContext = createContext<DeduplicateContextType>(
DefaultDeduplicateContext DefaultDeduplicateContext
@ -42,6 +45,7 @@ export default function Deduplicate() {
setRedirectURL, setRedirectURL,
} = useContext(AppContext); } = useContext(AppContext);
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null); const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
const [collections, setCollection] = useState<Collection[]>([]);
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false); const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>()); const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
const [collectionNameMap, setCollectionNameMap] = useState( const [collectionNameMap, setCollectionNameMap] = useState(
@ -72,6 +76,7 @@ export default function Deduplicate() {
const syncWithRemote = async () => { const syncWithRemote = async () => {
startLoading(); startLoading();
const collections = await syncCollections(); const collections = await syncCollections();
setCollection(collections);
const collectionNameMap = new Map<number, string>(); const collectionNameMap = new Map<number, string>();
for (const collection of collections) { for (const collection of collections) {
collectionNameMap.set(collection.id, collection.name); collectionNameMap.set(collection.id, collection.name);
@ -87,11 +92,12 @@ export default function Deduplicate() {
let toSelectFileIDs: number[] = []; let toSelectFileIDs: number[] = [];
let count = 0; let count = 0;
for (const dupe of duplicates) { for (const dupe of duplicates) {
allDuplicateFiles = allDuplicateFiles.concat(dupe.files); allDuplicateFiles = [...allDuplicateFiles, ...dupe.files];
// select all except first file // select all except first file
toSelectFileIDs = toSelectFileIDs.concat( toSelectFileIDs = [
dupe.files.slice(1).map((f) => f.id) ...toSelectFileIDs,
); ...dupe.files.slice(1).map((f) => f.id),
];
count += dupe.files.length - 1; count += dupe.files.length - 1;
for (const file of dupe.files) { for (const file of dupe.files) {
@ -143,7 +149,13 @@ export default function Deduplicate() {
}; };
if (!duplicateFiles) { if (!duplicateFiles) {
return <></>; return (
<VerticallyCentered>
<EnteSpinner>
<span className="sr-only">Loading...</span>
</EnteSpinner>
</VerticallyCentered>
);
} }
return ( return (
@ -165,7 +177,7 @@ export default function Deduplicate() {
)} )}
<PhotoFrame <PhotoFrame
files={duplicateFiles} files={duplicateFiles}
setFiles={setDuplicateFiles} collections={collections}
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
setSelected={setSelected} setSelected={setSelected}
selected={selected} selected={selected}

View file

@ -27,14 +27,14 @@ import { checkSubscriptionPurchase } from 'utils/billing';
import FullScreenDropZone from 'components/FullScreenDropZone'; import FullScreenDropZone from 'components/FullScreenDropZone';
import Sidebar from 'components/Sidebar'; import Sidebar from 'components/Sidebar';
import { checkConnectivity, preloadImage } from 'utils/common'; import { preloadImage } from 'utils/common';
import { import {
isFirstLogin, isFirstLogin,
justSignedUp, justSignedUp,
setIsFirstLogin, setIsFirstLogin,
setJustSignedUp, setJustSignedUp,
} from 'utils/storage'; } from 'utils/storage';
import { isTokenValid, logoutUser } from 'services/userService'; import { isTokenValid, logoutUser, validateKey } from 'services/userService';
import { useDropzone } from 'react-dropzone'; import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner'; import EnteSpinner from 'components/EnteSpinner';
import { LoadingOverlay } from 'components/LoadingOverlay'; import { LoadingOverlay } from 'components/LoadingOverlay';
@ -89,18 +89,17 @@ import { Collection, CollectionSummaries } from 'types/collection';
import { EnteFile } from 'types/file'; import { EnteFile } from 'types/file';
import { GalleryContextType, SelectedState } from 'types/gallery'; import { GalleryContextType, SelectedState } from 'types/gallery';
import { VISIBILITY_STATE } from 'types/magicMetadata'; import { VISIBILITY_STATE } from 'types/magicMetadata';
import Notification from 'components/Notification';
import Collections from 'components/Collections'; import Collections from 'components/Collections';
import { GalleryNavbar } from 'components/pages/gallery/Navbar'; import { GalleryNavbar } from 'components/pages/gallery/Navbar';
import { Search, SearchResultSummary, UpdateSearch } from 'types/search'; import { Search, SearchResultSummary, UpdateSearch } from 'types/search';
import SearchResultInfo from 'components/Search/SearchResultInfo'; import SearchResultInfo from 'components/Search/SearchResultInfo';
import { NotificationAttributes } from 'types/Notification';
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList'; import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
import UploadInputs from 'components/UploadSelectorInputs'; import UploadInputs from 'components/UploadSelectorInputs';
import useFileInput from 'hooks/useFileInput'; import useFileInput from 'hooks/useFileInput';
import { User } from 'types/user'; import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { CenteredFlex } from 'components/Container'; import { CenteredFlex } from 'components/Container';
import { checkConnectivity } from 'utils/error/ui';
export const DeadCenter = styled('div')` export const DeadCenter = styled('div')`
flex: 1; flex: 1;
@ -117,7 +116,6 @@ const defaultGalleryContext: GalleryContextType = {
showPlanSelectorModal: () => null, showPlanSelectorModal: () => null,
setActiveCollection: () => null, setActiveCollection: () => null,
syncWithRemote: () => null, syncWithRemote: () => null,
setNotificationAttributes: () => null,
setBlockingLoad: () => null, setBlockingLoad: () => null,
photoListHeader: null, photoListHeader: null,
}; };
@ -180,7 +178,9 @@ export default function Gallery() {
useState<SearchResultSummary>(null); useState<SearchResultSummary>(null);
const syncInProgress = useRef(true); const syncInProgress = useRef(true);
const resync = useRef(false); const resync = useRef(false);
const [deleted, setDeleted] = useState<number[]>([]); const [deletedFileIds, setDeletedFileIds] = useState<Set<number>>(
new Set<number>()
);
const { startLoading, finishLoading, setDialogMessage, ...appContext } = const { startLoading, finishLoading, setDialogMessage, ...appContext } =
useContext(AppContext); useContext(AppContext);
const [collectionSummaries, setCollectionSummaries] = const [collectionSummaries, setCollectionSummaries] =
@ -190,13 +190,6 @@ export default function Gallery() {
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null); useState<FixCreationTimeAttributes>(null);
const [notificationView, setNotificationView] = useState(false);
const closeNotification = () => setNotificationView(false);
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
const [archivedCollections, setArchivedCollections] = const [archivedCollections, setArchivedCollections] =
useState<Set<number>>(); useState<Set<number>>();
@ -233,6 +226,10 @@ export default function Gallery() {
return; return;
} }
const main = async () => { const main = async () => {
const valid = await validateKey();
if (!valid) {
return;
}
setActiveCollection(ALL_SECTION); setActiveCollection(ALL_SECTION);
setIsFirstLoad(isFirstLogin()); setIsFirstLoad(isFirstLogin());
setIsFirstFetch(true); setIsFirstFetch(true);
@ -241,10 +238,10 @@ export default function Gallery() {
} }
setIsFirstLogin(false); setIsFirstLogin(false);
const user = getData(LS_KEYS.USER); const user = getData(LS_KEYS.USER);
const files = mergeMetadata(await getLocalFiles()); let files = mergeMetadata(await getLocalFiles());
const collections = await getLocalCollections(); const collections = await getLocalCollections();
const trash = await getLocalTrash(); const trash = await getLocalTrash();
files.push(...getTrashedFiles(trash)); files = [...files, ...getTrashedFiles(trash)];
setUser(user); setUser(user);
setFiles(sortFiles(files)); setFiles(sortFiles(files));
setCollections(collections); setCollections(collections);
@ -264,24 +261,16 @@ export default function Gallery() {
setDerivativeState(user, collections, files); setDerivativeState(user, collections, files);
}, [collections, files]); }, [collections, files]);
useEffect( useEffect(() => {
() => collectionSelectorAttributes && setCollectionSelectorView(true), collectionSelectorAttributes && setCollectionSelectorView(true);
[collectionSelectorAttributes] }, [collectionSelectorAttributes]);
);
useEffect( useEffect(() => {
() => collectionNamerAttributes && setCollectionNamerView(true), collectionNamerAttributes && setCollectionNamerView(true);
[collectionNamerAttributes] }, [collectionNamerAttributes]);
); useEffect(() => {
useEffect( fixCreationTimeAttributes && setFixCreationTimeView(true);
() => fixCreationTimeAttributes && setFixCreationTimeView(true), }, [fixCreationTimeAttributes]);
[fixCreationTimeAttributes]
);
useEffect(
() => notificationAttributes && setNotificationView(true),
[notificationAttributes]
);
useEffect(() => { useEffect(() => {
if (typeof activeCollection === 'undefined') { if (typeof activeCollection === 'undefined') {
@ -341,9 +330,9 @@ export default function Gallery() {
!silent && startLoading(); !silent && startLoading();
const collections = await syncCollections(); const collections = await syncCollections();
setCollections(collections); setCollections(collections);
const files = await syncFiles(collections, setFiles); let files = await syncFiles(collections, setFiles);
const trash = await syncTrash(collections, setFiles, files); const trash = await syncTrash(collections, setFiles, files);
files.push(...getTrashedFiles(trash)); files = [...files, ...getTrashedFiles(trash)];
} catch (e) { } catch (e) {
logError(e, 'syncWithRemote failed'); logError(e, 'syncWithRemote failed');
switch (e.message) { switch (e.message) {
@ -490,12 +479,12 @@ export default function Gallery() {
startLoading(); startLoading();
try { try {
const selectedFiles = getSelectedFiles(selected, files); const selectedFiles = getSelectedFiles(selected, files);
setDeletedFileIds((deletedFileIds) => {
selectedFiles.forEach((file) => deletedFileIds.add(file.id));
return new Set(deletedFileIds);
});
if (permanent) { if (permanent) {
await deleteFromTrash(selectedFiles.map((file) => file.id)); await deleteFromTrash(selectedFiles.map((file) => file.id));
setDeleted([
...deleted,
...selectedFiles.map((file) => file.id),
]);
} else { } else {
await trashFiles(selectedFiles); await trashFiles(selectedFiles);
} }
@ -574,7 +563,6 @@ export default function Gallery() {
showPlanSelectorModal, showPlanSelectorModal,
setActiveCollection, setActiveCollection,
syncWithRemote, syncWithRemote,
setNotificationAttributes,
setBlockingLoad, setBlockingLoad,
photoListHeader: photoListHeader, photoListHeader: photoListHeader,
}}> }}>
@ -602,11 +590,6 @@ export default function Gallery() {
closeModal={() => setPlanModalView(false)} closeModal={() => setPlanModalView(false)}
setLoading={setBlockingLoad} setLoading={setBlockingLoad}
/> />
<Notification
open={notificationView}
onClose={closeNotification}
attributes={notificationAttributes}
/>
<CollectionNamer <CollectionNamer
show={collectionNamerView} show={collectionNamerView}
onHide={setCollectionNamerView.bind(null, false)} onHide={setCollectionNamerView.bind(null, false)}
@ -685,10 +668,9 @@ export default function Gallery() {
sidebarView={sidebarView} sidebarView={sidebarView}
closeSidebar={closeSidebar} closeSidebar={closeSidebar}
/> />
<PhotoFrame <PhotoFrame
files={files} files={files}
setFiles={setFiles} collections={collections}
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
favItemIds={favItemIds} favItemIds={favItemIds}
archivedCollections={archivedCollections} archivedCollections={archivedCollections}
@ -698,7 +680,8 @@ export default function Gallery() {
openUploader={openUploader} openUploader={openUploader}
isInSearchMode={isInSearchMode} isInSearchMode={isInSearchMode}
search={search} search={search}
deleted={deleted} deletedFileIds={deletedFileIds}
setDeletedFileIds={setDeletedFileIds}
activeCollection={activeCollection} activeCollection={activeCollection}
isSharedCollection={isSharedCollection( isSharedCollection={isSharedCollection(
activeCollection, activeCollection,

View file

@ -16,7 +16,6 @@ import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
import VerticallyCentered from 'components/Container'; import VerticallyCentered from 'components/Container';
import { Button } from 'react-bootstrap';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
@ -24,6 +23,7 @@ import { KeyAttributes, User } from 'types/user';
import FormPaper from 'components/Form/FormPaper'; import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');
@ -59,9 +59,15 @@ export default function Recover() {
setFieldError setFieldError
) => { ) => {
try { try {
recoveryKey = recoveryKey
.trim()
.split(' ')
.map((part) => part.trim())
.filter((part) => !!part)
.join(' ');
// check if user is entering mnemonic recovery key // check if user is entering mnemonic recovery key
if (recoveryKey.trim().indexOf(' ') > 0) { if (recoveryKey.indexOf(' ') > 0) {
if (recoveryKey.trim().split(' ').length !== 24) { if (recoveryKey.split(' ').length !== 24) {
throw new Error('recovery code should have 24 words'); throw new Error('recovery code should have 24 words');
} }
recoveryKey = bip39.mnemonicToEntropy(recoveryKey); recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
@ -102,12 +108,12 @@ export default function Recover() {
buttonText={constants.RECOVER} buttonText={constants.RECOVER}
/> />
<FormPaperFooter style={{ justifyContent: 'space-between' }}> <FormPaperFooter style={{ justifyContent: 'space-between' }}>
<Button variant="link" onClick={showNoRecoveryKeyMessage}> <LinkButton onClick={showNoRecoveryKeyMessage}>
{constants.NO_RECOVERY_KEY} {constants.NO_RECOVERY_KEY}
</Button> </LinkButton>
<Button variant="link" onClick={router.back}> <LinkButton onClick={router.back}>
{constants.GO_BACK} {constants.GO_BACK}
</Button> </LinkButton>
</FormPaperFooter> </FormPaperFooter>
</FormPaper> </FormPaper>
</VerticallyCentered> </VerticallyCentered>

View file

@ -58,7 +58,7 @@ export default function PublicCollectionGallery() {
const url = useRef<string>(null); const url = useRef<string>(null);
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null); const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
const [publicCollection, setPublicCollection] = useState<Collection>(null); const [publicCollection, setPublicCollection] = useState<Collection>(null);
const [errorMessage, setErrorMessage] = useState<String>(null); const [errorMessage, setErrorMessage] = useState<string>(null);
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const [abuseReportFormView, setAbuseReportFormView] = useState(false); const [abuseReportFormView, setAbuseReportFormView] = useState(false);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@ -128,9 +128,8 @@ export default function PublicCollectionGallery() {
main(); main();
}, []); }, []);
useEffect( useEffect(() => {
() => publicCollection &&
publicCollection &&
publicFiles && publicFiles &&
setPhotoListHeader({ setPhotoListHeader({
item: ( item: (
@ -143,9 +142,8 @@ export default function PublicCollectionGallery() {
), ),
itemType: ITEM_TYPE.OTHER, itemType: ITEM_TYPE.OTHER,
height: 68, height: 68,
}), });
[publicCollection, publicFiles] }, [publicCollection, publicFiles]);
);
const syncWithRemote = async () => { const syncWithRemote = async () => {
const collectionUID = getPublicCollectionUID(token.current); const collectionUID = getPublicCollectionUID(token.current);
@ -309,16 +307,10 @@ export default function PublicCollectionGallery() {
<SharedAlbumNavbar /> <SharedAlbumNavbar />
<PhotoFrame <PhotoFrame
files={publicFiles} files={publicFiles}
setFiles={setPublicFiles}
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
favItemIds={null}
setSelected={() => null} setSelected={() => null}
selected={{ count: 0, collectionID: null }} selected={{ count: 0, collectionID: null }}
isFirstLoad={true} isFirstLoad={true}
openUploader={() => null}
isInSearchMode={false}
search={{}}
deleted={[]}
activeCollection={ALL_SECTION} activeCollection={ALL_SECTION}
isSharedCollection isSharedCollection
enableDownload={ enableDownload={

View file

@ -7,7 +7,6 @@ import SingleInputForm, {
SingleInputFormProps, SingleInputFormProps,
} from 'components/SingleInputForm'; } from 'components/SingleInputForm';
import VerticallyCentered from 'components/Container'; import VerticallyCentered from 'components/Container';
import { Button } from 'react-bootstrap';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { recoverTwoFactor, removeTwoFactor } from 'services/userService'; import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
@ -15,6 +14,7 @@ import { PAGES } from 'constants/pages';
import FormPaper from 'components/Form/FormPaper'; import FormPaper from 'components/Form/FormPaper';
import FormPaperTitle from 'components/Form/FormPaper/Title'; import FormPaperTitle from 'components/Form/FormPaper/Title';
import FormPaperFooter from 'components/Form/FormPaper/Footer'; import FormPaperFooter from 'components/Form/FormPaper/Footer';
import LinkButton from 'components/pages/gallery/LinkButton';
const bip39 = require('bip39'); const bip39 = require('bip39');
// mobile client library only supports english. // mobile client library only supports english.
bip39.setDefaultWordlist('english'); bip39.setDefaultWordlist('english');
@ -52,9 +52,15 @@ export default function Recover() {
setFieldError setFieldError
) => { ) => {
try { try {
recoveryKey = recoveryKey
.trim()
.split(' ')
.map((part) => part.trim())
.filter((part) => !!part)
.join(' ');
// check if user is entering mnemonic recovery key // check if user is entering mnemonic recovery key
if (recoveryKey.trim().indexOf(' ') > 0) { if (recoveryKey.indexOf(' ') > 0) {
if (recoveryKey.trim().split(' ').length !== 24) { if (recoveryKey.split(' ').length !== 24) {
throw new Error('recovery code should have 24 words'); throw new Error('recovery code should have 24 words');
} }
recoveryKey = bip39.mnemonicToEntropy(recoveryKey); recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
@ -101,12 +107,12 @@ export default function Recover() {
buttonText={constants.RECOVER} buttonText={constants.RECOVER}
/> />
<FormPaperFooter style={{ justifyContent: 'space-between' }}> <FormPaperFooter style={{ justifyContent: 'space-between' }}>
<Button variant="link" onClick={showNoRecoveryKeyMessage}> <LinkButton onClick={showNoRecoveryKeyMessage}>
{constants.NO_RECOVERY_KEY} {constants.NO_RECOVERY_KEY}
</Button> </LinkButton>
<Button variant="link" onClick={router.back}> <LinkButton onClick={router.back}>
{constants.GO_BACK} {constants.GO_BACK}
</Button> </LinkButton>
</FormPaperFooter> </FormPaperFooter>
</FormPaper> </FormPaper>
</VerticallyCentered> </VerticallyCentered>

View file

@ -1,34 +0,0 @@
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
import { registerRoute, setDefaultHandler } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
import { pageCache, offlineFallback } from 'workbox-recipes';
pageCache();
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
registerRoute(
'/share-target',
async ({ event }) => {
event.waitUntil(
(async function () {
const data = await event.request.formData();
const client = await self.clients.get(
event.resultingClientId || event.clientId
);
const files = data.getAll('files');
setTimeout(() => {
client.postMessage({ files, action: 'upload-files' });
}, 1000);
})()
);
return Response.redirect('./');
},
'POST'
);
// Use a stale-while-revalidate strategy for all other requests.
setDefaultHandler(new NetworkOnly());
offlineFallback();

View file

@ -6,7 +6,7 @@ import { logError } from 'utils/sentry';
import { getPaymentToken } from './userService'; import { getPaymentToken } from './userService';
import { Plan, Subscription } from 'types/billing'; import { Plan, Subscription } from 'types/billing';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { DESKTOP_REDIRECT_URL } from 'constants/billing'; import { getDesktopRedirectURL } from 'constants/billing';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
@ -170,12 +170,7 @@ class billingService {
action: string action: string
) { ) {
try { try {
let redirectURL; const redirectURL = this.getRedirectURL();
if (isElectron()) {
redirectURL = DESKTOP_REDIRECT_URL;
} else {
redirectURL = `${window.location.origin}/gallery`;
}
window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`; window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`;
} catch (e) { } catch (e) {
logError(e, 'unable to get payments url'); logError(e, 'unable to get payments url');
@ -185,9 +180,10 @@ class billingService {
public async redirectToCustomerPortal() { public async redirectToCustomerPortal() {
try { try {
const redirectURL = this.getRedirectURL();
const response = await HTTPService.get( const response = await HTTPService.get(
`${ENDPOINT}/billing/stripe/customer-portal`, `${ENDPOINT}/billing/stripe/customer-portal`,
{ redirectURL: `${window.location.origin}/gallery` }, { redirectURL },
{ {
'X-Auth-Token': getToken(), 'X-Auth-Token': getToken(),
} }
@ -198,6 +194,14 @@ class billingService {
throw e; throw e;
} }
} }
public getRedirectURL() {
if (isElectron()) {
return getDesktopRedirectURL();
} else {
return `${window.location.origin}/gallery`;
}
}
} }
export default new billingService(); export default new billingService();

View file

@ -42,6 +42,7 @@ import { EncryptionResult } from 'types/upload';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { IsArchived } from 'utils/magicMetadata'; import { IsArchived } from 'utils/magicMetadata';
import { User } from 'types/user'; import { User } from 'types/user';
import { getNonHiddenCollections } from 'utils/collection';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const COLLECTION_TABLE = 'collections'; const COLLECTION_TABLE = 'collections';
@ -143,7 +144,7 @@ const getCollections = async (
export const getLocalCollections = async (): Promise<Collection[]> => { export const getLocalCollections = async (): Promise<Collection[]> => {
const collections: Collection[] = const collections: Collection[] =
(await localForage.getItem(COLLECTION_TABLE)) ?? []; (await localForage.getItem(COLLECTION_TABLE)) ?? [];
return collections; return getNonHiddenCollections(collections);
}; };
export const getCollectionUpdationTime = async (): Promise<number> => export const getCollectionUpdationTime = async (): Promise<number> =>
@ -188,7 +189,7 @@ export const syncCollections = async () => {
await localForage.setItem(COLLECTION_TABLE, collections); await localForage.setItem(COLLECTION_TABLE, collections);
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime); await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
return collections; return getNonHiddenCollections(collections);
}; };
export const getCollection = async ( export const getCollection = async (

View file

@ -33,7 +33,7 @@ export async function getDuplicateFiles(
fileMap.set(file.id, file); fileMap.set(file.id, file);
} }
const result: DuplicateFiles[] = []; let result: DuplicateFiles[] = [];
for (const dupe of dupes) { for (const dupe of dupes) {
let duplicateFiles: EnteFile[] = []; let duplicateFiles: EnteFile[] = [];
@ -48,12 +48,13 @@ export async function getDuplicateFiles(
); );
if (duplicateFiles.length > 1) { if (duplicateFiles.length > 1) {
result.push( result = [
...result,
...getDupesGroupedBySameFileHashes( ...getDupesGroupedBySameFileHashes(
duplicateFiles, duplicateFiles,
dupe.size dupe.size
) ),
); ];
} }
} }

View file

@ -13,11 +13,22 @@ import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file'; import { FILE_TYPE } from 'constants/file';
import { CustomError } from 'utils/error'; import { CustomError } from 'utils/error';
import { openThumbnailCache } from './cacheService'; import { openThumbnailCache } from './cacheService';
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
const MAX_PARALLEL_DOWNLOADS = 10;
class DownloadManager { class DownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string[]>>(); private fileObjectURLPromise = new Map<
string,
Promise<{ original: string[]; converted: string[] }>
>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>(); private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
private thumbnailDownloadRequestsProcessor = new QueueProcessor<any>(
MAX_PARALLEL_DOWNLOADS,
PROCESSING_STRATEGY.LIFO
);
public async getThumbnail(file: EnteFile) { public async getThumbnail(file: EnteFile) {
try { try {
const token = getToken(); const token = getToken();
@ -34,7 +45,10 @@ class DownloadManager {
if (cacheResp) { if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob()); return URL.createObjectURL(await cacheResp.blob());
} }
const thumb = await this.downloadThumb(token, file); const thumb =
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
() => this.downloadThumb(token, file)
).promise;
const thumbBlob = new Blob([thumb]); const thumbBlob = new Blob([thumb]);
thumbnailCache thumbnailCache
@ -84,12 +98,11 @@ class DownloadManager {
if (forPreview) { if (forPreview) {
return await getRenderableFileURL(file, fileBlob); return await getRenderableFileURL(file, fileBlob);
} else { } else {
return [ const fileURL = await createTypedObjectURL(
await createTypedObjectURL( fileBlob,
fileBlob, file.metadata.title
file.metadata.title );
), return { converted: [fileURL], original: [fileURL] };
];
} }
}; };
if (!this.fileObjectURLPromise.get(fileKey)) { if (!this.fileObjectURLPromise.get(fileKey)) {

View file

@ -1,18 +1,38 @@
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { ElectronAPIs } from 'types/electron'; import { ElectronAPIs } from 'types/electron';
import { runningInBrowser } from 'utils/common';
class ElectronService { class ElectronService {
private electronAPIs: ElectronAPIs; private electronAPIs: ElectronAPIs;
private isBundledApp: boolean = false;
constructor() { constructor() {
this.electronAPIs = runningInBrowser() && window['ElectronAPIs']; this.electronAPIs = globalThis['ElectronAPIs'];
this.isBundledApp = !!this.electronAPIs?.openDiskCache;
} }
checkIsBundledApp() { checkIsBundledApp() {
return isElectron() && this.isBundledApp; return isElectron() && !!this.electronAPIs?.openDiskCache;
}
logToDisk(msg: string) {
if (this.electronAPIs?.logToDisk) {
this.electronAPIs.logToDisk(msg);
}
}
openLogDirectory() {
if (this.electronAPIs?.openLogDirectory) {
this.electronAPIs.openLogDirectory();
}
}
getSentryUserID() {
if (this.electronAPIs?.getSentryUserID) {
return this.electronAPIs.getSentryUserID();
}
}
getAppVersion() {
if (this.electronAPIs?.getAppVersion) {
return this.electronAPIs.getAppVersion();
}
} }
} }

View file

@ -0,0 +1,26 @@
import { IFFmpeg } from 'services/ffmpeg/ffmpegFactory';
import { ElectronAPIs } from 'types/electron';
import { ElectronFile } from 'types/upload';
import { runningInBrowser } from 'utils/common';
export class ElectronFFmpeg implements IFFmpeg {
private electronAPIs: ElectronAPIs;
constructor() {
this.electronAPIs = runningInBrowser() && globalThis['ElectronAPIs'];
}
async run(
cmd: string[],
inputFile: ElectronFile | File,
outputFilename: string
) {
if (this.electronAPIs?.runFFmpegCmd) {
return this.electronAPIs.runFFmpegCmd(
cmd,
inputFile,
outputFilename
);
}
}
}

View file

@ -0,0 +1,46 @@
import { ElectronAPIs } from 'types/electron';
import { makeHumanReadableStorage } from 'utils/billing';
import { addLogLine } from 'utils/logging';
import { logError } from 'utils/sentry';
class ElectronHEICConverter {
private electronAPIs: ElectronAPIs;
private allElectronAPIExists: boolean;
constructor() {
this.electronAPIs = globalThis['ElectronAPIs'];
this.allElectronAPIExists = !!this.electronAPIs?.convertHEIC;
}
apiExists() {
return this.allElectronAPIExists;
}
async convert(fileBlob: Blob): Promise<Blob> {
try {
if (this.allElectronAPIExists) {
const startTime = Date.now();
const inputFileData = new Uint8Array(
await fileBlob.arrayBuffer()
);
const convertedFileData = await this.electronAPIs.convertHEIC(
inputFileData
);
addLogLine(
`originalFileSize:${makeHumanReadableStorage(
fileBlob?.size
)},convertedFileSize:${makeHumanReadableStorage(
convertedFileData?.length
)}, native heic conversion time: ${
Date.now() - startTime
}ms `
);
return new Blob([convertedFileData]);
}
} catch (e) {
logError(e, 'failed to convert heic natively');
throw e;
}
}
}
export default new ElectronHEICConverter();

View file

@ -0,0 +1,31 @@
import { AppUpdateInfo, ElectronAPIs } from 'types/electron';
class ElectronUpdateService {
private electronAPIs: ElectronAPIs;
constructor() {
this.electronAPIs = globalThis['ElectronAPIs'];
}
registerUpdateEventListener(
showUpdateDialog: (updateInfo: AppUpdateInfo) => void
) {
if (this.electronAPIs?.registerUpdateEventListener) {
this.electronAPIs.registerUpdateEventListener(showUpdateDialog);
}
}
updateAndRestart() {
if (this.electronAPIs?.updateAndRestart) {
this.electronAPIs.updateAndRestart();
}
}
skipAppVersion(version: string) {
if (this.electronAPIs?.skipAppVersion) {
this.electronAPIs.skipAppVersion(version);
}
}
}
export default new ElectronUpdateService();

View file

@ -39,7 +39,6 @@ import {
} from 'utils/file'; } from 'utils/file';
import { updateFileCreationDateInEXIF } from './upload/exifService'; import { updateFileCreationDateInEXIF } from './upload/exifService';
import { Metadata } from 'types/upload';
import QueueProcessor from './queueProcessor'; import QueueProcessor from './queueProcessor';
import { Collection } from 'types/collection'; import { Collection } from 'types/collection';
import { import {
@ -245,10 +244,7 @@ class ExportService {
file, file,
RecordType.FAILED RecordType.FAILED
); );
console.log(
`export failed for fileID:${file.id}, reason:`,
e
);
logError( logError(
e, e,
'download and save failed for file during export' 'download and save failed for file during export'
@ -460,11 +456,7 @@ class ExportService {
await this.exportMotionPhoto(fileStream, file, collectionPath); await this.exportMotionPhoto(fileStream, file, collectionPath);
} else { } else {
this.saveMediaFile(collectionPath, fileSaveName, fileStream); this.saveMediaFile(collectionPath, fileSaveName, fileStream);
await this.saveMetadataFile( await this.saveMetadataFile(collectionPath, fileSaveName, file);
collectionPath,
fileSaveName,
file.metadata
);
} }
} }
@ -483,11 +475,7 @@ class ExportService {
file.id file.id
); );
this.saveMediaFile(collectionPath, imageSaveName, imageStream); this.saveMediaFile(collectionPath, imageSaveName, imageStream);
await this.saveMetadataFile( await this.saveMetadataFile(collectionPath, imageSaveName, file);
collectionPath,
imageSaveName,
file.metadata
);
const videoStream = generateStreamFromArrayBuffer(motionPhoto.video); const videoStream = generateStreamFromArrayBuffer(motionPhoto.video);
const videoSaveName = getUniqueFileSaveName( const videoSaveName = getUniqueFileSaveName(
@ -496,11 +484,7 @@ class ExportService {
file.id file.id
); );
this.saveMediaFile(collectionPath, videoSaveName, videoStream); this.saveMediaFile(collectionPath, videoSaveName, videoStream);
await this.saveMetadataFile( await this.saveMetadataFile(collectionPath, videoSaveName, file);
collectionPath,
videoSaveName,
file.metadata
);
} }
private saveMediaFile( private saveMediaFile(
@ -516,11 +500,11 @@ class ExportService {
private async saveMetadataFile( private async saveMetadataFile(
collectionFolderPath: string, collectionFolderPath: string,
fileSaveName: string, fileSaveName: string,
metadata: Metadata file: EnteFile
) { ) {
await this.electronAPIs.saveFileToDisk( await this.electronAPIs.saveFileToDisk(
getFileMetadataSavePath(collectionFolderPath, fileSaveName), getFileMetadataSavePath(collectionFolderPath, fileSaveName),
getGoogleLikeMetadataFile(fileSaveName, metadata) getGoogleLikeMetadataFile(fileSaveName, file)
); );
} }
@ -637,7 +621,6 @@ class ExportService {
oldFileSavePath, oldFileSavePath,
newFileSavePath newFileSavePath
); );
console.log(oldFileMetadataSavePath, newFileMetadataSavePath);
await this.electronAPIs.checkExistsAndRename( await this.electronAPIs.checkExistsAndRename(
oldFileMetadataSavePath, oldFileMetadataSavePath,
newFileMetadataSavePath newFileMetadataSavePath

View file

@ -1,114 +0,0 @@
import { createFFmpeg, FFmpeg } from 'ffmpeg-wasm';
import { getUint8ArrayView } from 'services/readerService';
import {
parseFFmpegExtractedMetadata,
splitFilenameAndExtension,
} from 'utils/ffmpeg';
class FFmpegClient {
private ffmpeg: FFmpeg;
private ready: Promise<void> = null;
constructor() {
this.ffmpeg = createFFmpeg({
corePath: '/js/ffmpeg/ffmpeg-core.js',
mt: false,
});
this.ready = this.init();
}
private async init() {
if (!this.ffmpeg.isLoaded()) {
await this.ffmpeg.load();
}
}
async generateThumbnail(file: File) {
await this.ready;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ext] = splitFilenameAndExtension(file.name);
const inputFileName = `${Date.now().toString()}-input.${ext}`;
const thumbFileName = `${Date.now().toString()}-thumb.jpeg`;
this.ffmpeg.FS(
'writeFile',
inputFileName,
await getUint8ArrayView(file)
);
let seekTime = 1.0;
let thumb = null;
while (seekTime > 0) {
try {
await this.ffmpeg.run(
'-i',
inputFileName,
'-ss',
`00:00:0${seekTime.toFixed(3)}`,
'-vframes',
'1',
'-vf',
'scale=-1:720',
thumbFileName
);
thumb = this.ffmpeg.FS('readFile', thumbFileName);
this.ffmpeg.FS('unlink', thumbFileName);
break;
} catch (e) {
seekTime = Number((seekTime / 10).toFixed(3));
}
}
this.ffmpeg.FS('unlink', inputFileName);
return thumb;
}
async extractVideoMetadata(file: File) {
await this.ready;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [_, ext] = splitFilenameAndExtension(file.name);
const inputFileName = `${Date.now().toString()}-input.${ext}`;
const outFileName = `${Date.now().toString()}-metadata.txt`;
this.ffmpeg.FS(
'writeFile',
inputFileName,
await getUint8ArrayView(file)
);
let metadata = null;
// https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
// -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
// -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
await this.ffmpeg.run(
'-i',
inputFileName,
'-c',
'copy',
'-map_metadata',
'0',
'-f',
'ffmetadata',
outFileName
);
metadata = this.ffmpeg.FS('readFile', outFileName);
this.ffmpeg.FS('unlink', outFileName);
this.ffmpeg.FS('unlink', inputFileName);
return parseFFmpegExtractedMetadata(metadata);
}
async convertToMP4(file: Uint8Array, inputFileName: string) {
await this.ready;
this.ffmpeg.FS('writeFile', inputFileName, file);
await this.ffmpeg.run(
'-i',
inputFileName,
'-preset',
'ultrafast',
'output.mp4'
);
const convertedFile = this.ffmpeg.FS('readFile', 'output.mp4');
this.ffmpeg.FS('unlink', inputFileName);
this.ffmpeg.FS('unlink', 'output.mp4');
return convertedFile;
}
}
export default FFmpegClient;

View file

@ -0,0 +1,28 @@
import isElectron from 'is-electron';
import { ElectronFFmpeg } from 'services/electron/ffmpeg';
import { ElectronFile } from 'types/upload';
import { FFmpegWorker } from 'utils/comlink';
export interface IFFmpeg {
run: (
cmd: string[],
inputFile: File | ElectronFile,
outputFilename: string
) => Promise<File | ElectronFile>;
}
class FFmpegFactory {
private client: IFFmpeg;
async getFFmpegClient() {
if (!this.client) {
if (isElectron()) {
this.client = new ElectronFFmpeg();
} else {
this.client = await new FFmpegWorker();
}
}
return this.client;
}
}
export default new FFmpegFactory();

View file

@ -1,93 +1,99 @@
import { CustomError } from 'utils/error'; import {
FFMPEG_PLACEHOLDER,
INPUT_PATH_PLACEHOLDER,
OUTPUT_PATH_PLACEHOLDER,
} from 'constants/ffmpeg';
import { ElectronFile } from 'types/upload';
import { parseFFmpegExtractedMetadata } from 'utils/ffmpeg';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import QueueProcessor from 'services/queueProcessor'; import ffmpegFactory from './ffmpegFactory';
import { ParsedExtractedMetadata } from 'types/upload';
import { FFmpegWorker } from 'utils/comlink'; export async function generateVideoThumbnail(
import { promiseWithTimeout } from 'utils/common'; file: File | ElectronFile
): Promise<File | ElectronFile> {
const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000; try {
let seekTime = 1.0;
class FFmpegService { const ffmpegClient = await ffmpegFactory.getFFmpegClient();
private ffmpegWorker = null; while (seekTime > 0) {
private ffmpegTaskQueue = new QueueProcessor<any>(1); try {
return await ffmpegClient.run(
async init() { [
this.ffmpegWorker = await new FFmpegWorker(); FFMPEG_PLACEHOLDER,
} '-i',
INPUT_PATH_PLACEHOLDER,
async generateThumbnail(file: File): Promise<Uint8Array> { '-ss',
if (!this.ffmpegWorker) { `00:00:0${seekTime.toFixed(3)}`,
await this.init(); '-vframes',
} '1',
'-vf',
const response = this.ffmpegTaskQueue.queueUpRequest(() => 'scale=-1:720',
promiseWithTimeout( OUTPUT_PATH_PLACEHOLDER,
this.ffmpegWorker.generateThumbnail(file), ],
FFMPEG_EXECUTION_WAIT_TIME file,
) 'thumb.jpeg'
); );
try { } catch (e) {
return await response.promise; if (seekTime <= 0) {
} catch (e) { throw e;
if (e.message === CustomError.REQUEST_CANCELLED) { }
// ignore
return null;
} else {
logError(e, 'ffmpeg thumbnail generation failed');
throw e;
}
}
}
async extractMetadata(file: File): Promise<ParsedExtractedMetadata> {
if (!this.ffmpegWorker) {
await this.init();
}
const response = this.ffmpegTaskQueue.queueUpRequest(() =>
promiseWithTimeout(
this.ffmpegWorker.extractVideoMetadata(file),
FFMPEG_EXECUTION_WAIT_TIME
)
);
try {
return await response.promise;
} catch (e) {
if (e.message === CustomError.REQUEST_CANCELLED) {
// ignore
return null;
} else {
logError(e, 'ffmpeg metadata extraction failed');
throw e;
}
}
}
async convertToMP4(
file: Uint8Array,
fileName: string
): Promise<Uint8Array> {
if (!this.ffmpegWorker) {
await this.init();
}
const response = this.ffmpegTaskQueue.queueUpRequest(
async () => await this.ffmpegWorker.convertToMP4(file, fileName)
);
try {
return await response.promise;
} catch (e) {
if (e.message === CustomError.REQUEST_CANCELLED) {
// ignore
return null;
} else {
logError(e, 'ffmpeg MP4 conversion failed');
throw e;
} }
seekTime = Number((seekTime / 10).toFixed(3));
} }
} catch (e) {
logError(e, 'ffmpeg generateVideoThumbnail failed');
throw e;
} }
} }
export default new FFmpegService(); export async function extractVideoMetadata(file: File | ElectronFile) {
try {
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
// https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
// -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
// -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
const metadata = await ffmpegClient.run(
[
FFMPEG_PLACEHOLDER,
'-i',
INPUT_PATH_PLACEHOLDER,
'-c',
'copy',
'-map_metadata',
'0',
'-f',
'ffmetadata',
OUTPUT_PATH_PLACEHOLDER,
],
file,
`metadata.txt`
);
return parseFFmpegExtractedMetadata(
new Uint8Array(await metadata.arrayBuffer())
);
} catch (e) {
logError(e, 'ffmpeg extractVideoMetadata failed');
throw e;
}
}
export async function convertToMP4(file: File | ElectronFile) {
try {
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
return await ffmpegClient.run(
[
FFMPEG_PLACEHOLDER,
'-i',
INPUT_PATH_PLACEHOLDER,
'-preset',
'ultrafast',
OUTPUT_PATH_PLACEHOLDER,
],
file,
'output.mp4'
);
} catch (e) {
logError(e, 'ffmpeg convertToMP4 failed');
throw e;
}
}

View file

@ -18,6 +18,8 @@ import { SetFiles } from 'types/gallery';
import { MAX_TRASH_BATCH_SIZE } from 'constants/file'; import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata'; import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
import { addLogLine } from 'utils/logging'; import { addLogLine } from 'utils/logging';
import { isCollectionHidden } from 'utils/collection';
import { CustomError } from 'utils/error';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const FILES_TABLE = 'files'; const FILES_TABLE = 'files';
@ -63,13 +65,16 @@ export const syncFiles = async (
if (!getToken()) { if (!getToken()) {
continue; continue;
} }
if (isCollectionHidden(collection)) {
throw Error(CustomError.HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED);
}
const lastSyncTime = await getCollectionLastSyncTime(collection); const lastSyncTime = await getCollectionLastSyncTime(collection);
if (collection.updationTime === lastSyncTime) { if (collection.updationTime === lastSyncTime) {
continue; continue;
} }
const fetchedFiles = const fetchedFiles =
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? []; (await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
files.push(...fetchedFiles); files = [...files, ...fetchedFiles];
const latestVersionFiles = new Map<string, EnteFile>(); const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => { files.forEach((file) => {
const uid = `${file.collectionID}-${file.id}`; const uid = `${file.collectionID}-${file.id}`;
@ -105,7 +110,7 @@ export const getFiles = async (
setFiles: SetFiles setFiles: SetFiles
): Promise<EnteFile[]> => { ): Promise<EnteFile[]> => {
try { try {
const decryptedFiles: EnteFile[] = []; let decryptedFiles: EnteFile[] = [];
let time = sinceTime; let time = sinceTime;
let resp; let resp;
do { do {
@ -124,7 +129,8 @@ export const getFiles = async (
} }
); );
decryptedFiles.push( decryptedFiles = [
...decryptedFiles,
...(await Promise.all( ...(await Promise.all(
resp.data.diff.map(async (file: EnteFile) => { resp.data.diff.map(async (file: EnteFile) => {
if (!file.isDeleted) { if (!file.isDeleted) {
@ -132,8 +138,8 @@ export const getFiles = async (
} }
return file; return file;
}) as Promise<EnteFile>[] }) as Promise<EnteFile>[]
)) )),
); ];
if (resp.data.diff.length) { if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime; time = resp.data.diff.slice(-1)[0].updationTime;

Some files were not shown because too many files have changed in this diff Show more