Merge branch 'main' into ml-alpha

This commit is contained in:
Abhinav 2023-01-14 21:41:35 +05:30
parent cbdae5b1e8
commit 3b468cb154
221 changed files with 7271 additions and 5523 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,29 +1,17 @@
{
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
"parserOptions": {
"project": ["./tsconfig.json"]
},
"extends": [
"plugin:react/recommended",
"next/core-web-vitals",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"google",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"@typescript-eslint"
],
"plugins": ["@typescript-eslint"],
"rules": {
"indent": "off",
"class-methods-use-this": "off",
@ -31,30 +19,47 @@
"react/display-name": "off",
"react/no-unescaped-entities": "off",
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"error"
],
"@typescript-eslint/no-unused-vars": ["error"],
"require-jsdoc": "off",
"valid-jsdoc": "off",
"max-len": "off",
"new-cap": "off",
"no-invalid-this": "off",
"eqeqeq": "error",
"object-curly-spacing": [
"error",
"always"
],
"object-curly-spacing": ["error", "always"],
"space-before-function-paren": "off",
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
},
"settings": {
"react": {
"version": "detect"
"operator-linebreak": [
"error",
"after",
{ "overrides": { "?": "before", ":": "before" } }
],
"import/no-anonymous-default-export": [
"error",
{
"allowNew": true
}
},
"globals": {
"JSX": "readonly",
"NodeJS": "readonly",
"ReadableStreamDefaultController": "readonly"
],
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@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"
}
}

View file

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx lint-staged
yarn lint-staged

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

@ -2,9 +2,16 @@
**ente** is a cloud storage provider that provides end-to-end encryption for your data.
We have open-source apps across [Android](https://github.com/ente-io/frame), [iOS](https://github.com/ente-io/frame), [web](https://github.com/ente-io/bada-frame) and [desktop](https://github.com/ente-io/bhari-frame) that automatically backup your photos and videos.
We have open-source apps across
[Android](https://github.com/ente-io/photos-app),
[iOS](https://github.com/ente-io/photos-app),
[web](https://github.com/ente-io/photos-web) and
[desktop](https://github.com/ente-io/photos-desktop) that automatically backup
your photos and videos.
This repository contains the code for our web app, built with a lot of ❤️, and a
little bit of JavaScript.
This repository contains the code for our web app, built with a lot of ❤️, and a little bit of JavaScript.
<br/><br/><br/>
![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png)
@ -30,7 +37,7 @@ The deployed application is accessible @ [web.ente.io](https://web.ente.io).
## 🧑‍💻 Building from source
1. Clone this repository with `git clone git@github.com:ente-io/bada-frame.git`
1. Clone this repository with `git clone https://github.com/ente-io/photos-web.git`
2. Pull in all submodules with `git submodule update --init --recursive`
3. Install dependencies with `yarn install`
4. Finally, run the development server with `yarn dev`
@ -55,7 +62,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
If you like this project, please consider upgrading to a paid subscription.
If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/bada-frame/stargazers) this project.
If you would like to motivate us to keep building, you can do so by
[starring](https://github.com/ente-io/photos-web/stargazers) this project.
<br/>

View file

@ -26,7 +26,7 @@ module.exports = {
'style-src': "'self' 'unsafe-inline'",
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
'connect-src':
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/",
'base-uri ': "'self'",
// to allow worker
'child-src': "'self' blob:",
@ -37,11 +37,6 @@ module.exports = {
'report-to': ' https://csp-reporter.ente.io/local',
},
WORKBOX_CONFIG: {
swSrc: 'src/serviceWorker.js',
exclude: [/manifest\.json$/i],
},
ALL_ROUTES: '/(.*)',
buildCSPHeader: (directives) => ({

View file

@ -1,7 +1,6 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
const withWorkbox = require('@ente-io/next-with-workbox');
const { withSentryConfig } = require('@sentry/nextjs');
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
@ -19,7 +18,6 @@ const {
COOP_COEP_HEADERS,
WEB_SECURITY_HEADERS,
CSP_DIRECTIVES,
WORKBOX_CONFIG,
ALL_ROUTES,
getIsSentryEnabled,
} = require('./configUtil');
@ -30,14 +28,17 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
module.exports = (phase) =>
withSentryConfig(
withWorkbox(
withBundleAnalyzer(
withTM({
compiler: {
styledComponents: {
ssr: true,
displayName: true,
},
},
env: {
SENTRY_RELEASE: GIT_SHA,
NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA,
},
workbox: WORKBOX_CONFIG,
headers() {
return [
@ -60,7 +61,6 @@ module.exports = (phase) =>
return config;
},
})
)
),
{
release: GIT_SHA,

View file

@ -5,8 +5,7 @@
"scripts": {
"dev": "next dev",
"albums": "next dev -p 3002",
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
"prebuild": "yarn lint",
"lint": "next lint",
"build": "next build",
"postbuild": "next export",
"build-analyze": "ANALYZE=true next build",
@ -15,7 +14,6 @@
},
"dependencies": {
"@date-io/date-fns": "^2.14.0",
"@ente-io/next-with-workbox": "^1.0.3",
"@mui/icons-material": "^5.6.2",
"@mui/material": "^5.6.2",
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
@ -53,16 +51,16 @@
"libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0",
"ml-matrix": "^6.8.2",
"next": "^12.1.0",
"next-transpile-modules": "^9.0.0",
"next": "^13.0.6",
"next-transpile-modules": "^10.0.0",
"p-queue": "^7.1.0",
"photoswipe": "file:./thirdparty/photoswipe",
"piexifjs": "^1.0.6",
"react": "^17.0.2",
"react": "^18.2.0",
"react-bootstrap": "^1.3.0",
"react-d3-tree": "^3.1.1",
"react-datepicker": "^4.3.0",
"react-dom": "^17.0.2",
"react-dom": "^18.2.0",
"react-dropzone": "^11.2.4",
"react-otp-input": "^2.3.1",
"react-select": "^4.3.1",
@ -70,6 +68,7 @@
"react-top-loading-bar": "^2.0.1",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.8.6",
"sanitize-filename": "^1.6.3",
"similarity-transformation": "^0.0.1",
"styled-components": "^5.3.5",
"tesseract.js": "file:./thirdparty/tesseract",
@ -85,6 +84,7 @@
},
"devDependencies": {
"@next/bundle-analyzer": "^9.5.3",
"@types/bs58": "^4.0.1",
"@types/debounce-promise": "^3.1.3",
"@types/libsodium-wrappers": "^0.7.8",
"@types/node": "^14.6.4",
@ -98,17 +98,10 @@
"@types/styled-components": "^5.1.25",
"@types/wicg-file-system-access": "^2020.9.5",
"@types/yup": "^0.29.7",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"babel-plugin-styled-components": "^1.11.1",
"eslint": "^7.27.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",
"@typescript-eslint/eslint-plugin": "^5.43.0",
"eslint": "^8.28.0",
"eslint-config-next": "^13.0.6",
"eslint-config-prettier": "^8.5.0",
"husky": "^7.0.1",
"lint-staged": "^11.1.2",
"prettier": "2.3.2",
@ -117,12 +110,6 @@
"standard": {
"parser": "babel-eslint"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": [
"eslint --fix",
"prettier --write --ignore-unknown"
]
},
"resolutions": {
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
}

View file

@ -8,5 +8,5 @@
X-Frame-Options: deny
X-XSS-Protection: 1; mode=block
Referrer-Policy: same-origin
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;

View file

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

View file

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

View file

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

View file

@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
export const Badge = styled(Paper)(({ theme }) => ({
padding: '2px 4px',
backgroundColor: theme.palette.glass.main,
color: theme.palette.glass.contrastText,
backgroundColor: theme.palette.backdrop.main,
backdropFilter: `blur(${theme.palette.blur.muted})`,
color: theme.palette.primary.contrastText,
textTransform: 'uppercase',
...(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 { CopyButtonWrapper } from './styledComponents';
import DoneIcon from '@mui/icons-material/Done';
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 (
<Tooltip arrow open={copied} title={constants.COPIED}>
<CopyButtonWrapper onClick={copyToClipboardHelper(code)}>
<Tooltip
arrow
open={copied}
title={constants.COPIED}
PopperProps={{ sx: { zIndex: 2000 } }}>
<IconButton onClick={copyToClipboardHelper(code)} color={color}>
{copied ? (
<DoneIcon fontSize="small" />
<DoneIcon fontSize={size ?? 'small'} />
) : (
<ContentCopyIcon fontSize="small" />
<ContentCopyIcon fontSize={size ?? 'small'} />
)}
</CopyButtonWrapper>
</IconButton>
</Tooltip>
);
}

View file

@ -1,7 +1,7 @@
import { FreeFlowText } from '../Container';
import React, { useState } from 'react';
import React from 'react';
import EnteSpinner from '../EnteSpinner';
import { Wrapper, CodeWrapper } from './styledComponents';
import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
import CopyButton from './CopyButton';
import { BoxProps } from '@mui/material';
@ -15,14 +15,6 @@ export default function CodeBlock({
wordBreak,
...props
}: 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) {
return (
<Wrapper>
@ -37,11 +29,9 @@ export default function CodeBlock({
{code}
</FreeFlowText>
</CodeWrapper>
<CopyButton
code={code}
copied={copied}
copyToClipboardHelper={copyToClipboardHelper}
/>
<CopyButtonWrapper>
<CopyButton code={code} />
</CopyButtonWrapper>
</Wrapper>
);
}

View file

@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) {
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
{constants.SORT_BY_NAME}
</SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
{constants.SORT_BY_CREATION_TIME_DESCENDING}
</SortByOption>
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
{constants.SORT_BY_CREATION_TIME_ASCENDING}
</SortByOption>

View file

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

View file

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

View file

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

View file

@ -0,0 +1,25 @@
import { OverflowMenuOption } from 'components/OverflowMenu/option';
import React from 'react';
import LogoutIcon from '@mui/icons-material/Logout';
import constants from 'utils/strings/constants';
import { CollectionActions } from '.';
interface Iprops {
handleCollectionAction: (
action: CollectionActions,
loader?: boolean
) => (...args: any[]) => Promise<void>;
}
export function SharedCollectionOption({ handleCollectionAction }: Iprops) {
return (
<OverflowMenuOption
startIcon={<LogoutIcon />}
onClick={handleCollectionAction(
CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM,
false
)}>
{constants.LEAVE_ALBUM}
</OverflowMenuOption>
);
}

View file

@ -17,6 +17,7 @@ import { AppContext } from 'pages/_app';
import OverflowMenu from 'components/OverflowMenu/menu';
import { CollectionSummaryType } from 'constants/collection';
import { TrashCollectionOption } from './TrashCollectionOption';
import { SharedCollectionOption } from './SharedCollectionOption';
import MoreHoriz from '@mui/icons-material/MoreHoriz';
interface CollectionOptionsProps {
@ -39,6 +40,8 @@ export enum CollectionActions {
SHOW_SHARE_DIALOG,
CONFIRM_EMPTY_TRASH,
EMPTY_TRASH,
CONFIRM_LEAVE_SHARED_ALBUM,
LEAVE_SHARED_ALBUM,
}
const CollectionOptions = (props: CollectionOptionsProps) => {
@ -93,6 +96,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
case CollectionActions.EMPTY_TRASH:
callback = emptyTrash;
break;
case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
callback = confirmLeaveSharedAlbum;
break;
case CollectionActions.LEAVE_SHARED_ALBUM:
callback = leaveSharedAlbum;
break;
default:
logError(
Error('invalid collection action '),
@ -130,6 +139,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
redirectToAll();
};
const leaveSharedAlbum = async () => {
await CollectionAPI.leaveSharedAlbum(activeCollection.id);
redirectToAll();
};
const archiveCollection = () => {
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
};
@ -200,6 +214,23 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
close: { text: constants.CANCEL },
});
const confirmLeaveSharedAlbum = () => {
setDialogMessage({
title: constants.LEAVE_SHARED_ALBUM_TITLE,
content: constants.LEAVE_SHARED_ALBUM_MESSAGE,
proceed: {
text: constants.LEAVE_SHARED_ALBUM,
action: handleCollectionAction(
CollectionActions.LEAVE_SHARED_ALBUM
),
variant: 'danger',
},
close: {
text: constants.CANCEL,
},
});
};
return (
<OverflowMenu
ariaControls={'collection-options'}
@ -213,6 +244,10 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
<TrashCollectionOption
handleCollectionAction={handleCollectionAction}
/>
) : collectionSummaryType === CollectionSummaryType.shared ? (
<SharedCollectionOption
handleCollectionAction={handleCollectionAction}
/>
) : (
<AlbumCollectionOption
IsArchived={IsArchived(activeCollection)}

View file

@ -14,11 +14,12 @@ export interface CollectionSelectorAttributes {
showNextModal: () => void;
title: string;
fromCollection?: number;
onCancel?: () => void;
}
interface Props {
open: boolean;
onClose: (closeBtnClick?: boolean) => void;
onClose: () => void;
attributes: CollectionSelectorAttributes;
collections: Collection[];
collectionSummaries: CollectionSummaries;
@ -61,15 +62,18 @@ function CollectionSelector({
props.onClose();
};
const onCloseButtonClick = () => props.onClose(true);
const onUserTriggeredClose = () => {
attributes.onCancel?.();
props.onClose();
};
return (
<AllCollectionDialog
onClose={props.onClose}
onClose={onUserTriggeredClose}
open={props.open}
position="center"
fullScreen={appContext.isMobile}>
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
<DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
{attributes.title}
</DialogTitleWithCloseButton>
<DialogContent>

View file

@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
import React, { useContext } from 'react';
import { shareCollection } from 'services/collectionService';
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 constants from 'utils/strings/constants';
import { CollectionShareSharees } from './sharees';

View file

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

View file

@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material';
import React from 'react';
import Select from 'react-select';
import { DropdownStyle } from 'styles/dropdown';
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
import { getDeviceLimitOptions } from 'utils/collection';
import constants from 'utils/strings/constants';
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageDeviceLimit({
publicShareProp,
collection,
updatePublicShareURLHelper,
}) {
}: Iprops) {
const updateDeviceLimit = async (newLimit: number) => {
return updatePublicShareURLHelper({
collectionID: collection.id,

View file

@ -1,14 +1,21 @@
import { Box, Typography } from '@mui/material';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageDownloadAccess({
publicShareProp,
updatePublicShareURLHelper,
collection,
}) {
}: Iprops) {
const appContext = useContext(AppContext);
const handleFileDownloadSetting = () => {
@ -34,7 +41,7 @@ export function ManageDownloadAccess({
collectionID: collection.id,
enableDownload: false,
}),
variant: ButtonVariant.danger,
variant: 'danger',
},
});
};
@ -42,7 +49,7 @@ export function ManageDownloadAccess({
<Box>
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
<PublicShareSwitch
checked={publicShareProp?.enableDownload ?? false}
checked={publicShareProp?.enableDownload ?? true}
onChange={handleFileDownloadSetting}
/>
</Box>

View file

@ -1,33 +1,37 @@
import { ManageLinkPassword } from './linkPassword';
import { ManageDeviceLimit } from './deviceLimit';
import { ManageLinkExpiry } from './linkExpiry';
import { PublicLinkSetPassword } from '../setPassword';
import { Stack, Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
import React, { useContext, useState } from 'react';
import { updateShareableURL } from 'services/collectionService';
import { UpdatePublicURL } from 'types/collection';
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
import { sleep } from 'utils/common';
import { handleSharingErrors } from 'utils/error';
import constants from 'utils/strings/constants';
import {
ManageSectionLabel,
ManageSectionOptions,
} from '../../styledComponents';
import { ManageDownloadAccess } from './downloadAccess';
import { handleSharingErrors } from 'utils/error/ui';
import { SetPublicShareProp } from 'types/publicCollection';
import { ManagePublicCollect } from './publicCollect';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
setPublicShareProp: SetPublicShareProp;
}
export default function PublicShareManage({
publicShareProp,
collection,
setPublicShareProp,
}) {
}: Iprops) {
const galleryContext = useContext(GalleryContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const [sharableLinkError, setSharableLinkError] = useState(null);
const closeConfigurePassword = () => setChangePasswordView(false);
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
try {
galleryContext.setBlockingLoad(true);
@ -73,6 +77,13 @@ export default function PublicShareManage({
updatePublicShareURLHelper
}
/>
<ManagePublicCollect
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={
updatePublicShareURLHelper
}
/>
<ManageDownloadAccess
collection={collection}
publicShareProp={publicShareProp}
@ -81,7 +92,6 @@ export default function PublicShareManage({
}
/>
<ManageLinkPassword
setChangePasswordView={setChangePasswordView}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={
@ -102,14 +112,6 @@ export default function PublicShareManage({
)}
</ManageSectionOptions>
</details>
<PublicLinkSetPassword
open={changePasswordView}
onClose={closeConfigurePassword}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={updatePublicShareURLHelper}
setChangePasswordView={setChangePasswordView}
/>
</>
);
}

View file

@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material';
import React from 'react';
import Select from 'react-select';
import { linkExpiryStyle } from 'styles/linkExpiry';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import { shareExpiryOptions } from 'utils/collection';
import constants from 'utils/strings/constants';
import { dateStringWithMMH } from 'utils/time';
import { formatDateTime } from 'utils/time/format';
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageLinkExpiry({
publicShareProp,
collection,
updatePublicShareURLHelper,
}) {
}: Iprops) {
const updateDeviceExpiry = async (optionFn) => {
return updatePublicShareURLHelper({
collectionID: collection.id,
@ -31,7 +38,7 @@ export function ManageLinkExpiry({
}}
placeholder={
publicShareProp?.validTill
? dateStringWithMMH(publicShareProp?.validTill)
? formatDateTime(publicShareProp?.validTill / 1000)
: 'never'
}
onChange={(e) => {

View file

@ -1,49 +0,0 @@
import { Box, Typography } from '@mui/material';
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch';
export function ManageLinkPassword({
collection,
publicShareProp,
updatePublicShareURLHelper,
setChangePasswordView,
}) {
const appContext = useContext(AppContext);
const handlePasswordChangeSetting = async () => {
if (publicShareProp.passwordEnabled) {
await confirmDisablePublicUrlPassword();
} else {
setChangePasswordView(true);
}
};
const confirmDisablePublicUrlPassword = async () => {
appContext.setDialogMessage({
title: constants.DISABLE_PASSWORD,
content: constants.DISABLE_PASSWORD_MESSAGE,
close: { text: constants.CANCEL },
proceed: {
text: constants.DISABLE,
action: () =>
updatePublicShareURLHelper({
collectionID: collection.id,
disablePassword: true,
}),
variant: ButtonVariant.danger,
},
});
};
return (
<Box>
<Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
<PublicShareSwitch
checked={!!publicShareProp?.passwordEnabled}
onChange={handlePasswordChangeSetting}
/>
</Box>
);
}

View file

@ -0,0 +1,72 @@
import { Box, Typography } from '@mui/material';
import { AppContext } from 'pages/_app';
import React, { useContext, useState } from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants';
import { PublicLinkSetPassword } from './setPassword';
import PublicShareSwitch from '../../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManageLinkPassword({
collection,
publicShareProp,
updatePublicShareURLHelper,
}: Iprops) {
const appContext = useContext(AppContext);
const [changePasswordView, setChangePasswordView] = useState(false);
const closeConfigurePassword = () => setChangePasswordView(false);
const handlePasswordChangeSetting = async () => {
if (publicShareProp.passwordEnabled) {
await confirmDisablePublicUrlPassword();
} else {
setChangePasswordView(true);
}
};
const confirmDisablePublicUrlPassword = async () => {
appContext.setDialogMessage({
title: constants.DISABLE_PASSWORD,
content: constants.DISABLE_PASSWORD_MESSAGE,
close: { text: constants.CANCEL },
proceed: {
text: constants.DISABLE,
action: () =>
updatePublicShareURLHelper({
collectionID: collection.id,
disablePassword: true,
}),
variant: 'danger',
},
});
};
return (
<>
<Box>
<Typography mb={0.5}>
{' '}
{constants.LINK_PASSWORD_LOCK}
</Typography>
<PublicShareSwitch
checked={!!publicShareProp?.passwordEnabled}
onChange={handlePasswordChangeSetting}
/>
</Box>
<PublicLinkSetPassword
open={changePasswordView}
onClose={closeConfigurePassword}
collection={collection}
publicShareProp={publicShareProp}
updatePublicShareURLHelper={updatePublicShareURLHelper}
setChangePasswordView={setChangePasswordView}
/>
</>
);
}

View file

@ -3,7 +3,7 @@ import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
import React from 'react';
import CryptoWorker from 'utils/crypto';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
import constants from 'utils/strings/constants';
export function PublicLinkSetPassword({
@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
};
const enablePublicUrlPassword = async (password: string) => {
const cryptoWorker = await new CryptoWorker();
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
return updatePublicShareURLHelper({

View file

@ -0,0 +1,34 @@
import { Box, Typography } from '@mui/material';
import React from 'react';
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
import constants from 'utils/strings/constants';
import PublicShareSwitch from '../switch';
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
}
export function ManagePublicCollect({
publicShareProp,
updatePublicShareURLHelper,
collection,
}: Iprops) {
const handleFileDownloadSetting = () => {
updatePublicShareURLHelper({
collectionID: collection.id,
enableCollect: !publicShareProp.enableCollect,
});
};
return (
<Box>
<Typography mb={0.5}>{constants.PUBLIC_COLLECT}</Typography>
<PublicShareSwitch
checked={publicShareProp?.enableCollect}
onChange={handleFileDownloadSetting}
/>
</Box>
);
}

View file

@ -96,7 +96,7 @@ export default function Collections(props: Iprops) {
itemType: ITEM_TYPE.OTHER,
height: 68,
});
}, [collectionSummaries, activeCollectionID]);
}, [collectionSummaries, activeCollectionID, isInSearchMode]);
if (shouldBeHidden) {
return <></>;

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

View file

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

View file

@ -4,7 +4,6 @@ import {
MIN_EDITED_CREATION_TIME,
MAX_EDITED_CREATION_TIME,
} from 'constants/file';
import { TextField } from '@mui/material';
import {
LocalizationProvider,
MobileDateTimePicker,
@ -60,14 +59,7 @@ const EnteDateTimePicker = ({
},
},
}}
renderInput={(params) => (
<TextField
{...params}
hiddenLabel
margin="none"
variant="standard"
/>
)}
renderInput={() => <></>}
/>
</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 React from 'react';
import { ExportStats } from 'types/export';
import { formatDateTime } from 'utils/time';
import constants from 'utils/strings/constants';
import { formatDateTime } from 'utils/time/format';
import { FlexWrapper, Label, Value } from './Container';
import { ComfySpan } from './ExportInProgress';

View file

@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
<div
style={{
marginBottom: '10px',
padding: '0 5%',
display: 'flex',
flexDirection: 'column',
...(fixState === FIX_STATE.RUNNING

View file

@ -23,7 +23,6 @@ const Option = ({
color: value !== Number(selected) ? '#aaa' : '#fff',
}}>
<Form.Check.Input
style={{ marginTop: '6px' }}
id={value.toString()}
type="radio"
value={value}

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

@ -0,0 +1,22 @@
import { Box } from '@mui/material';
import Ente from 'components/icons/ente';
import Link from 'next/link';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
export function EnteLinkLogo() {
return (
<Link href={ENTE_WEBSITE_LINK}>
<Box
sx={(theme) => ({
':hover': {
cursor: 'pointer',
svg: {
fill: theme.palette.text.secondary,
},
},
})}>
<Ente />
</Box>
</Link>
);
}

View file

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

View file

@ -1,12 +1,12 @@
import { GalleryContext } from 'pages/gallery';
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 { styled } from '@mui/material';
import DownloadManager from 'services/downloadManager';
import constants from 'utils/strings/constants';
import AutoSizer from 'react-virtualized-auto-sizer';
import PhotoSwipe from 'components/PhotoSwipe';
import PhotoViewer from 'components/PhotoViewer';
import {
ALL_SECTION,
ARCHIVE_SECTION,
@ -15,7 +15,7 @@ import {
import { isSharedFile } from 'utils/file';
import { isPlaybackPossible } from 'utils/photoFrame';
import { PhotoList } from './PhotoList';
import { SetFiles, SelectedState } from 'types/gallery';
import { SelectedState } from 'types/gallery';
import { FILE_TYPE } from 'constants/file';
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
@ -30,6 +30,8 @@ import { logError } from 'utils/sentry';
import { CustomError } from 'utils/error';
import { User } from 'types/user';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { useMemo } from 'react';
import { Collection } from 'types/collection';
const Container = styled('div')`
display: block;
@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
interface Props {
files: EnteFile[];
setFiles: SetFiles;
collections?: Collection[];
syncWithRemote: () => Promise<void>;
favItemIds?: Set<number>;
archivedCollections?: Set<number>;
@ -60,7 +62,8 @@ interface Props {
openUploader?;
isInSearchMode?: boolean;
search?: Search;
deleted?: number[];
deletedFileIds?: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void;
activeCollection: number;
isSharedCollection?: boolean;
enableDownload?: boolean;
@ -69,13 +72,15 @@ interface Props {
}
type SourceURL = {
imageURL?: string;
videoURL?: string;
originalImageURL?: string;
originalVideoURL?: string;
convertedImageURL?: string;
convertedVideoURL?: string;
};
const PhotoFrame = ({
files,
setFiles,
collections,
syncWithRemote,
favItemIds,
archivedCollections,
@ -86,7 +91,8 @@ const PhotoFrame = ({
isInSearchMode,
search,
resetSearch,
deleted,
deletedFileIds,
setDeletedFileIds,
activeCollection,
isSharedCollection,
enableDownload,
@ -104,75 +110,26 @@ const PhotoFrame = ({
const [rangeStart, setRangeStart] = useState(null);
const [currentHover, setCurrentHover] = useState(null);
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
const filteredDataRef = useRef<EnteFile[]>([]);
const filteredData = filteredDataRef?.current ?? [];
const router = useRouter();
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(() => {
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 filteredData = useMemo(() => {
const idSet = new Set();
const user: User = getData(LS_KEYS.USER);
filteredDataRef.current = files
return files
.map((item, index) => ({
...item,
dataIndex: index,
w: window.innerWidth,
h: window.innerHeight,
title: item.pubMagicMetadata?.data.caption,
}))
.filter((item) => {
if (deleted?.includes(item.id)) {
if (
deletedFileIds?.has(item.id) &&
activeCollection !== TRASH_SECTION
) {
return false;
}
if (
@ -236,7 +193,8 @@ const PhotoFrame = ({
activeCollection === ALL_SECTION ||
activeCollection === ARCHIVE_SECTION ||
activeCollection === TRASH_SECTION ||
activeCollection === item.collectionID
activeCollection === item.collectionID ||
isInSearchMode
) {
idSet.add(item.id);
return true;
@ -245,7 +203,37 @@ const PhotoFrame = ({
}
return false;
});
}, [files, deleted, search, activeCollection]);
}, [
files,
deletedFileIds,
search?.date,
search?.location,
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(() => {
const currentURL = new URL(window.location.href);
@ -262,6 +250,59 @@ const PhotoFrame = ({
}
}, [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 index = files.findIndex((file) => file.id === id);
if (index === -1) {
@ -272,12 +313,10 @@ const PhotoFrame = ({
const updateURL = (id: number) => (url: string) => {
const updateFile = (file: EnteFile) => {
file = {
...file,
msrc: url,
w: window.innerWidth,
h: window.innerHeight,
};
file.msrc = url;
file.w = window.innerWidth;
file.h = window.innerHeight;
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
file.html = `
<div class="pswp-item-container">
@ -307,29 +346,30 @@ const PhotoFrame = ({
}
return file;
};
setFiles((files) => {
const index = getFileIndexFromID(files, id);
files[index] = updateFile(files[index]);
return files;
});
const index = getFileIndexFromID(files, id);
return updateFile(files[index]);
};
const updateSrcURL = async (id: number, srcURL: SourceURL) => {
const { videoURL, imageURL } = srcURL;
const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
const {
originalImageURL,
convertedImageURL,
originalVideoURL,
convertedVideoURL,
} = srcURL;
const isPlayable =
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
const updateFile = (file: EnteFile) => {
file = {
...file,
w: window.innerWidth,
h: window.innerHeight,
};
file.w = window.innerWidth;
file.h = window.innerHeight;
file.isSourceLoaded = true;
file.originalImageURL = originalImageURL;
file.originalVideoURL = originalVideoURL;
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
if (isPlayable) {
file.html = `
<video controls onContextMenu="return false;">
<source src="${videoURL}" />
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
`;
@ -339,7 +379,7 @@ const PhotoFrame = ({
<img src="${file.msrc}" onContextMenu="return false;"/>
<div class="download-banner" >
${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>
`;
@ -348,9 +388,9 @@ const PhotoFrame = ({
if (isPlayable) {
file.html = `
<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;">
<source src="${videoURL}" />
<source src="${convertedVideoURL}" />
Your browser does not support the video tag.
</video>
</div>
@ -367,15 +407,10 @@ const PhotoFrame = ({
`;
}
} else {
file.src = imageURL;
file.src = convertedImageURL;
}
return file;
};
setFiles((files) => {
const index = getFileIndexFromID(files, id);
files[index] = updateFile(files[index]);
return files;
});
setIsSourceLoaded(true);
const index = getFileIndexFromID(files, id);
return updateFile(files[index]);
@ -441,7 +476,11 @@ const PhotoFrame = ({
handleSelect(filteredData[index].id, index)(!checked);
}
};
const getThumbnail = (files: EnteFile[], index: number) =>
const getThumbnail = (
files: EnteFile[],
index: number,
isScrolling: boolean
) =>
files[index] ? (
<PreviewCard
key={`tile-${files[index].id}-selected-${
@ -465,6 +504,7 @@ const PhotoFrame = ({
(index >= currentHover && index <= rangeStart)
}
activeCollection={activeCollection}
showPlaceholder={isScrolling}
/>
) : (
<></>
@ -499,6 +539,9 @@ const PhotoFrame = ({
item.msrc = newFile.msrc;
item.html = newFile.html;
item.src = newFile.src;
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL;
item.originalVideoURL = newFile.originalVideoURL;
item.w = newFile.w;
item.h = newFile.h;
@ -521,10 +564,13 @@ const PhotoFrame = ({
if (!fetching[item.id]) {
try {
fetching[item.id] = true;
let urls: string[];
let urls: { original: string[]; converted: string[] };
if (galleryContext.files.has(item.id)) {
const mergedURL = galleryContext.files.get(item.id);
urls = mergedURL.split(',');
urls = {
original: mergedURL.original.split(','),
converted: mergedURL.converted.split(','),
};
} else {
appContext.startLoading();
if (
@ -540,26 +586,40 @@ const PhotoFrame = ({
urls = await DownloadManager.getFile(item, true);
}
appContext.finishLoading();
const mergedURL = urls.join(',');
const mergedURL = {
original: urls.original.join(','),
converted: urls.converted.join(','),
};
galleryContext.files.set(item.id, mergedURL);
}
let imageURL;
let videoURL;
let originalImageURL;
let originalVideoURL;
let convertedImageURL;
let convertedVideoURL;
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
[imageURL, videoURL] = urls;
[originalImageURL, originalVideoURL] = urls.original;
[convertedImageURL, convertedVideoURL] = urls.converted;
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
[videoURL] = urls;
[originalVideoURL] = urls.original;
[convertedVideoURL] = urls.converted;
} else {
[imageURL] = urls;
[originalImageURL] = urls.original;
[convertedImageURL] = urls.converted;
}
setIsSourceLoaded(false);
const newFile = await updateSrcURL(item.id, {
imageURL,
videoURL,
originalImageURL,
originalVideoURL,
convertedImageURL,
convertedVideoURL,
});
item.msrc = newFile.msrc;
item.html = newFile.html;
item.src = newFile.src;
item.isSourceLoaded = newFile.isSourceLoaded;
item.originalImageURL = newFile.originalImageURL;
item.originalVideoURL = newFile.originalVideoURL;
item.w = newFile.w;
item.h = newFile.h;
try {
@ -606,17 +666,21 @@ const PhotoFrame = ({
/>
)}
</AutoSizer>
<PhotoSwipe
<PhotoViewer
isOpen={open}
items={filteredData}
currentIndex={currentIndex}
onClose={handleClose}
gettingData={getSlideData}
favItemIds={favItemIds}
deletedFileIds={deletedFileIds}
setDeletedFileIds={setDeletedFileIds}
isSharedCollection={isSharedCollection}
isTrashCollection={activeCollection === TRASH_SECTION}
enableDownload={enableDownload}
isSourceLoaded={isSourceLoaded}
fileToCollectionsMap={fileToCollectionsMap}
collectionNameMap={collectionNameMap}
/>
</Container>
)}

View file

@ -1,6 +1,6 @@
import React, { useRef, useEffect, useContext } from 'react';
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 {
IMAGE_CONTAINER_MAX_HEIGHT,
@ -15,17 +15,17 @@ import {
import constants from 'utils/strings/constants';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
import { convertBytesToHumanReadable } from 'utils/file/size';
import { DeduplicateContext } from 'pages/deduplicate';
import { FlexWrapper } from './Container';
import { Typography } from '@mui/material';
import { GalleryContext } from 'pages/gallery';
import { SpecialPadding } from 'styles/SpecialPadding';
import { formatDate } from 'utils/time/format';
const A_DAY = 24 * 60 * 60 * 1000;
const NO_OF_PAGES = 2;
const FOOTER_HEIGHT = 90;
const ALBUM_FOOTER_HEIGHT = 75;
export enum ITEM_TYPE {
TIME = 'TIME',
@ -129,7 +129,6 @@ const SizeAndCountContainer = styled(DateContainer)`
`;
const FooterContainer = styled(ListItemContainer)`
font-size: 14px;
margin-bottom: 0.75rem;
@media (max-width: 540px) {
font-size: 12px;
@ -142,6 +141,13 @@ const FooterContainer = styled(ListItemContainer)`
margin-top: calc(2rem + 20px);
`;
const AlbumFooterContainer = styled(ListItemContainer)`
margin-top: 48px;
margin-bottom: 10px;
text-align: center;
justify-content: center;
`;
const NothingContainer = styled(ListItemContainer)`
color: #979797;
text-align: center;
@ -153,7 +159,11 @@ interface Props {
width: number;
filteredData: EnteFile[];
showAppDownloadBanner: boolean;
getThumbnail: (files: EnteFile[], index: number) => JSX.Element;
getThumbnail: (
files: EnteFile[],
index: number,
isScrolling?: boolean
) => JSX.Element;
activeCollection: number;
resetFetching: () => void;
}
@ -223,11 +233,18 @@ export function PhotoList({
if (timeStampList.length === 1) {
timeStampList.push(getEmptyListItem());
}
timeStampList.push(getVacuumItem(timeStampList));
if (publicCollectionGalleryContext.photoListFooter) {
timeStampList.push(
getPhotoListFooter(
publicCollectionGalleryContext.photoListFooter
)
);
}
if (
showAppDownloadBanner ||
publicCollectionGalleryContext.accessedThroughSharedURL
) {
timeStampList.push(getVacuumItem(timeStampList));
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
timeStampList.push(getAlbumsFooter());
} else {
@ -244,6 +261,11 @@ export function PhotoList({
filteredData,
showAppDownloadBanner,
publicCollectionGalleryContext.accessedThroughSharedURL,
galleryContext.photoListHeader,
publicCollectionGalleryContext.photoListFooter,
publicCollectionGalleryContext.photoListHeader,
deduplicateContext.isOnDeduplicatePage,
deduplicateContext.fileSizeMap,
]);
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
@ -288,32 +310,27 @@ export function PhotoList({
const groupByTime = (timeStampList: TimeStampListItem[]) => {
let listItemIndex = 0;
let currentDate = -1;
let currentDate;
filteredData.forEach((item, index) => {
if (
!currentDate ||
!isSameDay(
new Date(item.metadata.creationTime / 1000),
new Date(currentDate)
)
) {
currentDate = item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
});
timeStampList.push({
itemType: ITEM_TYPE.TIME,
date: isSameDay(new Date(currentDate), new Date())
? 'Today'
? constants.TODAY
: isSameDay(
new Date(currentDate),
new Date(Date.now() - A_DAY)
)
? 'Yesterday'
: dateTimeFormat.format(currentDate),
? constants.YESTERDAY
: formatDate(currentDate),
id: currentDate.toString(),
});
timeStampList.push({
@ -336,10 +353,13 @@ export function PhotoList({
});
};
const isSameDay = (first, second) =>
const isSameDay = (first, second) => {
return (
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate();
first.getDate() === second.getDate()
);
};
const getPhotoListHeader = (photoListHeader) => {
return {
@ -352,6 +372,17 @@ export function PhotoList({
};
};
const getPhotoListFooter = (photoListFooter) => {
return {
...photoListFooter,
item: (
<ListItemContainer span={columns}>
{photoListFooter.item}
</ListItemContainer>
),
};
};
const getEmptyListItem = () => {
return {
itemType: ITEM_TYPE.OTHER,
@ -365,12 +396,17 @@ export function PhotoList({
};
};
const getVacuumItem = (timeStampList) => {
const footerHeight =
publicCollectionGalleryContext.accessedThroughSharedURL
? ALBUM_FOOTER_HEIGHT +
(publicCollectionGalleryContext.photoListFooter?.height ?? 0)
: FOOTER_HEIGHT;
const photoFrameHeight = (() => {
let sum = 0;
const getCurrentItemSize = getItemSize(timeStampList);
for (let i = 0; i < timeStampList.length; i++) {
sum += getCurrentItemSize(i);
if (height - sum <= FOOTER_HEIGHT) {
if (height - sum <= footerHeight) {
break;
}
}
@ -379,7 +415,7 @@ export function PhotoList({
return {
itemType: ITEM_TYPE.OTHER,
item: <></>,
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
height: Math.max(height - photoFrameHeight - footerHeight, 0),
};
};
@ -389,7 +425,9 @@ export function PhotoList({
height: FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
<Typography variant="body2">
{constants.INSTALL_MOBILE_APP()}
</Typography>
</FooterContainer>
),
};
@ -398,22 +436,16 @@ export function PhotoList({
const getAlbumsFooter = () => {
return {
itemType: ITEM_TYPE.OTHER,
height: FOOTER_HEIGHT,
height: ALBUM_FOOTER_HEIGHT,
item: (
<FooterContainer span={columns}>
<p>
{constants.PRESERVED_BY}{' '}
<a
target="_blank"
style={{
color: getVariantColor(ButtonVariant.success),
}}
href={ENTE_WEBSITE_LINK}
rel="noreferrer">
<AlbumFooterContainer span={columns}>
<Typography variant="body2">
{constants.SHARED_USING}{' '}
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
{constants.ENTE_IO}
</a>
</p>
</FooterContainer>
</Link>
</Typography>
</AlbumFooterContainer>
),
};
};
@ -453,9 +485,10 @@ export function PhotoList({
date: currItem.date,
span: items[index + 1].items.length,
});
newList[newIndex + 1].items = newList[
newIndex + 1
].items.concat(items[index + 1].items);
newList[newIndex + 1].items = [
...newList[newIndex + 1].items,
...items[index + 1].items,
];
index += 2;
} else {
// Adding items would exceed the number of columns.
@ -512,10 +545,6 @@ export function PhotoList({
}
};
const extraRowsToRender = Math.ceil(
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
);
const generateKey = (index) => {
switch (timeStampList[index].itemType) {
case ITEM_TYPE.FILE:
@ -527,7 +556,10 @@ export function PhotoList({
}
};
const renderListItem = (listItem: TimeStampListItem) => {
const renderListItem = (
listItem: TimeStampListItem,
isScrolling: boolean
) => {
switch (listItem.itemType) {
case ITEM_TYPE.TIME:
return listItem.dates ? (
@ -553,7 +585,8 @@ export function PhotoList({
const ret = listItem.items.map((item, idx) =>
getThumbnail(
filteredDataCopy,
listItem.itemStartIndex + idx
listItem.itemStartIndex + idx,
isScrolling
)
);
if (listItem.groups) {
@ -584,14 +617,15 @@ export function PhotoList({
width={width}
itemCount={timeStampList.length}
itemKey={generateKey}
overscanCount={extraRowsToRender}>
{({ index, style }) => (
overscanCount={0}
useIsScrolling>
{({ index, style, isScrolling }) => (
<ListItem style={style}>
<ListContainer
columns={columns}
shrinkRatio={shrinkRatio}
groups={timeStampList[index].groups}>
{renderListItem(timeStampList[index])}
{renderListItem(timeStampList[index], isScrolling)}
</ListContainer>
</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,175 +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';
import {
PhotoPeopleList,
UnidentifiedFaces,
} from 'components/MachineLearning/PeopleList';
import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
import { WordList } from 'components/MachineLearning/WordList';
import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
zIndex: 1501,
'& .MuiDialog-container': {
alignItems: 'flex-start',
},
'& .MuiDialog-paper': {
padding: theme.spacing(2),
},
}));
const Legend = styled('span')`
font-size: 20px;
color: #ddd;
display: inline;
`;
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);
const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
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>
)}
{appContext.mlSearchEnabled && (
<>
<div>
<Legend>{constants.PEOPLE}</Legend>
</div>
<PhotoPeopleList
file={items[photoSwipe?.getCurrentIndex()]}
updateMLDataIndex={updateMLDataIndex}
/>
<div>
<Legend>{constants.UNIDENTIFIED_FACES}</Legend>
</div>
<UnidentifiedFaces
file={items[photoSwipe?.getCurrentIndex()]}
updateMLDataIndex={updateMLDataIndex}
/>
<div>
<Legend>{constants.OBJECTS}</Legend>
<ObjectLabelList
file={items[photoSwipe?.getCurrentIndex()]}
updateMLDataIndex={updateMLDataIndex}
/>
</div>
<div>
<Legend>{constants.TEXT}</Legend>
<WordList
file={items[photoSwipe?.getCurrentIndex()]}
updateMLDataIndex={updateMLDataIndex}
/>
</div>
<MLServiceFileInfoButton
file={items[photoSwipe?.getCurrentIndex()]}
updateMLDataIndex={updateMLDataIndex}
setUpdateMLDataIndex={setUpdateMLDataIndex}
/>
</>
)}
{exif && (
<>
<ExifData exif={exif} />
</>
)}
</DialogContent>
</FileInfoDialog>
);
}

View file

@ -0,0 +1,175 @@
export {}; // 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';
// import {
// PhotoPeopleList,
// UnidentifiedFaces,
// } from 'components/MachineLearning/PeopleList';
// import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
// import { WordList } from 'components/MachineLearning/WordList';
// import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
// const FileInfoDialog = styled(Dialog)(({ theme }) => ({
// zIndex: 1501,
// '& .MuiDialog-container': {
// alignItems: 'flex-start',
// },
// '& .MuiDialog-paper': {
// padding: theme.spacing(2),
// },
// }));
// const Legend = styled('span')`
// font-size: 20px;
// color: #ddd;
// display: inline;
// `;
// 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);
// const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
// 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>
// )}
// {appContext.mlSearchEnabled && (
// <>
// <div>
// <Legend>{constants.PEOPLE}</Legend>
// </div>
// <PhotoPeopleList
// file={items[photoSwipe?.getCurrentIndex()]}
// updateMLDataIndex={updateMLDataIndex}
// />
// <div>
// <Legend>{constants.UNIDENTIFIED_FACES}</Legend>
// </div>
// <UnidentifiedFaces
// file={items[photoSwipe?.getCurrentIndex()]}
// updateMLDataIndex={updateMLDataIndex}
// />
// <div>
// <Legend>{constants.OBJECTS}</Legend>
// <ObjectLabelList
// file={items[photoSwipe?.getCurrentIndex()]}
// updateMLDataIndex={updateMLDataIndex}
// />
// </div>
// <div>
// <Legend>{constants.TEXT}</Legend>
// <WordList
// file={items[photoSwipe?.getCurrentIndex()]}
// updateMLDataIndex={updateMLDataIndex}
// />
// </div>
// <MLServiceFileInfoButton
// file={items[photoSwipe?.getCurrentIndex()]}
// updateMLDataIndex={updateMLDataIndex}
// setUpdateMLDataIndex={setUpdateMLDataIndex}
// />
// </>
// )}
// {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,47 @@
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 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/Done';
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 { updateFilePublicMagicMetadata } from 'services/fileService';
import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants';
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import {
changeFileCreationTime,
updateExistingFilePubMetadata,
} from 'utils/file';
import { formatDateTime } from 'utils/time';
import EditIcon from '@mui/icons-material/Edit';
import { Label, Row, Value } from 'components/Container';
import { formatDate, formatTime } from 'utils/time/format';
import { FlexWrapper } from 'components/Container';
import { logError } from 'utils/sentry';
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
import EnteDateTimePicker from 'components/EnteDateTimePicker';
import { IconButton } from '@mui/material';
import InfoItem from './InfoItem';
export function RenderCreationTime({
shouldDisableEdits,
@ -59,39 +57,24 @@ export function RenderCreationTime({
return (
<>
<Row>
<Label width="30%">{constants.CREATION_TIME}</Label>
<Value
width={
!shouldDisableEdits ? !isInEditMode && '60%' : '70%'
}>
{isInEditMode ? (
<FlexWrapper>
<InfoItem
icon={<CalendarTodayIcon />}
title={formatDate(originalCreationTime)}
caption={formatTime(originalCreationTime)}
openEditor={openEditMode}
loading={loading}
hideEditOption={shouldDisableEdits || isInEditMode}
/>
{isInEditMode && (
<EnteDateTimePicker
initialValue={originalCreationTime}
disabled={loading}
onSubmit={saveEdits}
onClose={closeEditMode}
/>
) : (
formatDateTime(originalCreationTime)
)}
</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,122 @@
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 InfoItem from './InfoItem';
import { makeHumanReadableStorage } from 'utils/billing';
import Box from '@mui/material/Box';
import { FileNameEditDialog } from './FileNameEditDialog';
import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
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.VIDEO ? (
<VideocamOutlined />
) : (
<PhotoOutlined />
)
}
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,329 @@
import React, { useContext, 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 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';
import CameraOutlined from '@mui/icons-material/CameraOutlined';
import LocationOnOutlined from '@mui/icons-material/LocationOnOutlined';
import TextSnippetOutlined from '@mui/icons-material/TextSnippetOutlined';
import FolderOutlined from '@mui/icons-material/FolderOutlined';
import BackupOutlined from '@mui/icons-material/BackupOutlined';
import {
PhotoPeopleList,
UnidentifiedFaces,
} from 'components/MachineLearning/PeopleList';
import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
import { WordList } from 'components/MachineLearning/WordList';
import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
import { AppContext } from 'pages/_app';
import { Legend } from '../styledComponents/Legend';
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 appContext = useContext(AppContext);
const [location, setLocation] = useState<Location>(null);
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
const [showExif, setShowExif] = useState(false);
const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
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>
)}
{appContext.mlSearchEnabled && (
<>
<div>
<Legend>{constants.PEOPLE}</Legend>
</div>
<PhotoPeopleList
file={file}
updateMLDataIndex={updateMLDataIndex}
/>
<div>
<Legend>{constants.UNIDENTIFIED_FACES}</Legend>
</div>
<UnidentifiedFaces
file={file}
updateMLDataIndex={updateMLDataIndex}
/>
<div>
<Legend>{constants.OBJECTS}</Legend>
<ObjectLabelList
file={file}
updateMLDataIndex={updateMLDataIndex}
/>
</div>
<div>
<Legend>{constants.TEXT}</Legend>
<WordList
file={file}
updateMLDataIndex={updateMLDataIndex}
/>
</div>
<MLServiceFileInfoButton
file={file}
updateMLDataIndex={updateMLDataIndex}
setUpdateMLDataIndex={setUpdateMLDataIndex}
/>
</>
)}
</Stack>
<ExifData
exif={exif}
open={showExif}
onClose={closeExif}
onInfoClose={handleCloseInfo}
filename={file.metadata.title}
/>
</FileInfoSidebar>
);
}

View file

@ -9,26 +9,54 @@ import {
import { EnteFile } from 'types/file';
import constants from 'utils/strings/constants';
import exifr from 'exifr';
import events from './events';
import { downloadFile } from 'utils/file';
import { prettyPrintExif } from 'utils/exif';
import {
downloadFile,
copyFileToClipboard,
getFileExtension,
} from 'utils/file';
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
import { logError } from 'utils/sentry';
import { FILE_TYPE } from 'constants/file';
import { sleep } from 'utils/common';
import { isClipboardItemPresent } from 'utils/common';
import { playVideo, pauseVideo } from 'utils/photoFrame';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { AppContext } from 'pages/_app';
import { FileInfo } from './InfoDialog';
import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe';
import { FileInfo } from './FileInfo';
import {
defaultLivePhotoDefaultOptions,
photoSwipeV4Events,
} from 'constants/photoViewer';
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
import DownloadIcon from '@mui/icons-material/Download';
import InfoIcon from '@mui/icons-material/InfoOutlined';
import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
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 { styled } from '@mui/material';
import { addLocalLog } from 'utils/logging';
import ContentCopy from '@mui/icons-material/ContentCopy';
import ChevronLeft from '@mui/icons-material/ChevronLeft';
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 {
isOpen: boolean;
items: any[];
@ -38,13 +66,17 @@ interface Iprops {
id?: string;
className?: string;
favItemIds: Set<number>;
deletedFileIds: Set<number>;
setDeletedFileIds?: (value: Set<number>) => void;
isSharedCollection: boolean;
isTrashCollection: boolean;
enableDownload: boolean;
isSourceLoaded: boolean;
fileToCollectionsMap: Map<number, number[]>;
collectionNameMap: Map<number, string>;
}
function PhotoSwipe(props: Iprops) {
function PhotoViewer(props: Iprops) {
const pswpElement = useRef<HTMLDivElement>();
const [photoSwipe, setPhotoSwipe] =
useState<Photoswipe<Photoswipe.Options>>();
@ -52,8 +84,9 @@ function PhotoSwipe(props: Iprops) {
const { isOpen, items, isSourceLoaded } = props;
const [isFav, setIsFav] = useState(false);
const [showInfo, setShowInfo] = useState(false);
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
const [exif, setExif] = useState<any>(null);
const [exif, setExif] =
useState<{ key: string; value: Record<string, any> }>();
const exifCopy = useRef(null);
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
defaultLivePhotoDefaultOptions
);
@ -63,6 +96,9 @@ function PhotoSwipe(props: Iprops) {
);
const appContext = useContext(AppContext);
const exifExtractionInProgress = useRef<string>(null);
const [shouldShowCopyOption] = useState(isClipboardItemPresent());
useEffect(() => {
if (!pswpElement) return;
if (isOpen) {
@ -76,6 +112,57 @@ function PhotoSwipe(props: Iprops) {
};
}, [isOpen]);
useEffect(() => {
if (!photoSwipe) return;
function handleCopyEvent() {
copyToClipboardHelper(photoSwipe.currItem as EnteFile);
}
function handleKeyUp(event: KeyboardEvent) {
if (!isOpen || showInfo) {
return;
}
addLocalLog(() => 'Event: ' + event.key);
switch (event.key) {
case 'i':
case 'I':
setShowInfo(true);
break;
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(() => {
updateItems(items);
}, [items]);
@ -152,8 +239,12 @@ function PhotoSwipe(props: Iprops) {
}
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
function updateFavButton() {
setIsFav(isInFav(this?.currItem));
useEffect(() => {
exifCopy.current = exif;
}, [exif]);
function updateFavButton(file: EnteFile) {
setIsFav(isInFav(file));
}
const openPhotoSwipe = () => {
@ -198,12 +289,12 @@ function PhotoSwipe(props: Iprops) {
items,
options
);
events.forEach((event) => {
photoSwipeV4Events.forEach((event) => {
const callback = props[event];
if (callback || event === 'destroy') {
photoSwipe.listen(event, function (...args) {
if (callback) {
args.unshift(this);
args.unshift(photoSwipe);
callback(...args);
}
if (event === 'destroy') {
@ -215,11 +306,39 @@ function PhotoSwipe(props: Iprops) {
});
}
});
photoSwipe.listen('beforeChange', function () {
updateInfo.call(this);
updateFavButton.call(this);
photoSwipe.listen('beforeChange', () => {
const currItem = photoSwipe?.currItem as EnteFile;
updateFavButton(currItem);
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
setExif({ key: currItem.src, value: null });
return;
}
if (
!currItem ||
!exifCopy?.current?.value === null ||
exifCopy?.current?.key === currItem.src
) {
return;
}
setExif({ key: currItem.src, value: undefined });
checkExifAvailable(currItem);
});
photoSwipe.listen('resize', () => {
const currItem = photoSwipe?.currItem as EnteFile;
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
setExif({ key: currItem.src, value: null });
return;
}
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();
needUpdate.current = false;
setPhotoSwipe(photoSwipe);
@ -240,7 +359,7 @@ function PhotoSwipe(props: Iprops) {
}
handleCloseInfo();
};
const isInFav = (file) => {
const isInFav = (file: EnteFile) => {
const { favItemIds } = props;
if (favItemIds && file) {
return favItemIds.has(file.id);
@ -248,7 +367,7 @@ function PhotoSwipe(props: Iprops) {
return false;
};
const onFavClick = async (file) => {
const onFavClick = async (file: EnteFile) => {
const { favItemIds } = props;
if (!isInFav(file)) {
favItemIds.add(file.id);
@ -262,46 +381,80 @@ function PhotoSwipe(props: Iprops) {
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 = []) => {
if (photoSwipe) {
if (items.length === 0) {
photoSwipe.close();
}
photoSwipe.items.length = 0;
items.forEach((item) => {
photoSwipe.items.push(item);
});
photoSwipe.invalidateCurrItems();
// photoSwipe.updateSize(true);
if (isOpen) {
photoSwipe.updateSize(true);
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
photoSwipe.goTo(0);
}
}
}
};
const checkExifAvailable = async () => {
setExif(null);
await sleep(100);
const refreshPhotoswipe = () => {
photoSwipe.invalidateCurrItems();
if (isOpen) {
photoSwipe.updateSize(true);
}
};
const checkExifAvailable = async (file: EnteFile) => {
try {
const img: HTMLImageElement = document.querySelector(
'.pswp__img:not(.pswp__img--placeholder)'
);
if (img) {
const exifData = await exifr.parse(img);
if (!exifData) {
if (exifExtractionInProgress.current === file.src) {
return;
}
exifData.raw = prettyPrintExif(exifData);
setExif(exifData);
try {
if (file.isSourceLoaded) {
exifExtractionInProgress.current = file.src;
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 });
}
}
}
} finally {
exifExtractionInProgress.current = null;
}
} catch (e) {
logError(e, 'exifr parsing failed');
setExif({ key: file.src, value: null });
const fileExtension = getFileExtension(file.metadata.title);
logError(e, 'exifr parsing failed', { extension: fileExtension });
}
};
function updateInfo() {
const file: EnteFile = this?.currItem;
if (file?.metadata) {
setMetaData(file.metadata);
setExif(null);
checkExifAvailable();
}
}
const handleCloseInfo = () => {
setShowInfo(false);
};
@ -310,6 +463,7 @@ function PhotoSwipe(props: Iprops) {
};
const downloadFileHelper = async (file) => {
if (props.enableDownload) {
appContext.startLoading();
await downloadFile(
file,
@ -317,8 +471,29 @@ function PhotoSwipe(props: Iprops) {
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
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 { id } = props;
@ -355,33 +530,74 @@ function PhotoSwipe(props: Iprops) {
<button
className="pswp__button pswp__button--close"
title={constants.CLOSE}
title={constants.CLOSE_OPTION}
/>
{props.enableDownload && (
<button
className="pswp__button pswp__button--custom"
title={constants.DOWNLOAD}
title={constants.DOWNLOAD_OPTION}
onClick={() =>
downloadFileHelper(photoSwipe.currItem)
}>
<DownloadIcon fontSize="small" />
</button>
)}
{props.enableDownload && shouldShowCopyOption && (
<button
className="pswp__button pswp__button--fs"
title={constants.TOGGLE_FULLSCREEN}
/>
<button
className="pswp__button pswp__button--zoom"
title={constants.ZOOM_IN_OUT}
/>
className="pswp__button pswp__button--custom"
title={constants.COPY_OPTION}
onClick={() =>
copyToClipboardHelper(
photoSwipe.currItem as EnteFile
)
}>
<ContentCopy fontSize="small" />
</button>
)}
{!props.isSharedCollection &&
!props.isTrashCollection && (
<button
className="pswp__button pswp__button--custom"
title={constants.DELETE_OPTION}
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 ? (
<FavoriteIcon fontSize="small" />
@ -390,14 +606,7 @@ function PhotoSwipe(props: Iprops) {
)}
</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__icn">
<div className="pswp__preloader__cut">
@ -411,36 +620,34 @@ function PhotoSwipe(props: Iprops) {
</div>
<button
className="pswp__button pswp__button--arrow--left"
title={constants.PREVIOUS}
onClick={photoSwipe?.prev}>
<ChevronRight
sx={{ transform: 'rotate(180deg)' }}
/>
title={constants.PREVIOUS}>
<ChevronLeft sx={{ pointerEvents: 'none' }} />
</button>
<button
className="pswp__button pswp__button--arrow--right"
title={constants.NEXT}
onClick={photoSwipe?.next}>
<ChevronRight />
title={constants.NEXT}>
<ChevronRight sx={{ pointerEvents: 'none' }} />
</button>
<div className="pswp__caption">
<div />
<div className="pswp__caption pswp-custom-caption-container">
<CaptionContainer />
</div>
</div>
</div>
</div>
<FileInfo
isTrashCollection={props.isTrashCollection}
shouldDisableEdits={props.isSharedCollection}
showInfo={showInfo}
handleCloseInfo={handleCloseInfo}
items={items}
photoSwipe={photoSwipe}
metadata={metadata}
exif={exif}
file={photoSwipe?.currItem as EnteFile}
exif={exif?.value}
scheduleUpdate={scheduleUpdate}
refreshPhotoswipe={refreshPhotoswipe}
fileToCollectionsMap={props.fileToCollectionsMap}
collectionNameMap={props.collectionNameMap}
/>
</>
);
}
export default PhotoSwipe;
export default PhotoViewer;

View file

@ -1,6 +1,5 @@
import React, { useContext } from 'react';
import { PeopleList } from 'components/MachineLearning/PeopleList';
import { Legend } from 'components/PhotoSwipe/styledComponents/Legend';
import { IndexStatus } from 'types/machineLearning/ui';
import { SuggestionType, Suggestion } from 'types/search';
import { components } from 'react-select';
@ -18,6 +17,12 @@ const LegendRow = styled(Row)`
margin-bottom: 0px;
`;
const Legend = styled('span')`
font-size: 20px;
color: #ddd;
display: inline;
`;
const Caption = styled('span')`
font-size: 12px;
display: inline;

View file

@ -44,7 +44,9 @@ export default function SearchInput(props: Iprops) {
};
const [defaultOptions, setDefaultOptions] = useState([]);
useEffect(() => search(value), [value]);
useEffect(() => {
search(value);
}, [value]);
useEffect(() => {
refreshDefaultOptions();

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,84 @@
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 Typography from '@mui/material/Typography';
import { isInternalUser } from 'utils/user';
import { testUpload } from '../../../tests/upload.test';
import {
testZipFileReading,
testZipWithRootFileReadingTest,
} from '../../../tests/zip-file-reading.test';
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}>
Test Upload
</SidebarButton>
<SidebarButton onClick={testZipFileReading}>
Test Zip file reading
</SidebarButton>
<SidebarButton onClick={testZipWithRootFileReadingTest}>
Zip with Root file Test
</SidebarButton>
</>
)}
</>
);
}

View file

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

View file

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

View file

@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({
<Box position={'relative'} width="100%">
<Progressbar
sx={{ backgroundColor: 'transparent' }}
value={(userUsage * 100) / totalStorage}
value={Math.min((userUsage * 100) / totalStorage, 100)}
/>
<Progressbar
sx={{
@ -28,7 +28,7 @@ export function FamilyUsageProgressBar({
},
width: '100%',
}}
value={(totalUsage * 100) / totalStorage}
value={Math.min((totalUsage * 100) / totalStorage, 100)}
/>
</Box>
);

View file

@ -13,7 +13,7 @@ interface Iprops {
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
return (
<Box width="100%">
<Progressbar value={(usage * 100) / storage} />
<Progressbar value={Math.min((usage * 100) / storage, 100)} />
<SpaceBetweenFlex
sx={{
marginTop: 1.5,

View file

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

View file

@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
import UtilitySection from './UtilitySection';
import HelpSection from './HelpSection';
import ExitSection from './ExitSection';
import DebugLogs from './DebugLogs';
import DebugSection from './DebugSection';
import { DrawerSidebar } from './styledComponents';
import HeaderSection from './Header';
import { CollectionSummaries } from 'types/collection';
@ -37,7 +37,7 @@ export default function Sidebar({
<Divider />
<ExitSection />
<Divider />
<DebugLogs />
<DebugSection />
</Stack>
</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 { EnteDrawer } from 'components/EnteDrawer';
export const DrawerSidebar = styled(Drawer)(({ theme }) => ({
export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
'& .MuiPaper-root': {
maxWidth: '375px',
width: '100%',
scrollbarWidth: 'thin',
padding: theme.spacing(1.5),
},
}));

View file

@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) {
{ email, passphrase, confirm }: FormValues,
{ setFieldError }: FormikHelpers<FormValues>
) => {
setLoading(true);
try {
if (passphrase !== confirm) {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
return;
}
setLoading(true);
try {
setData(LS_KEYS.USER, { email });
await sendOtt(email);
@ -60,7 +64,6 @@ export default function SignUp(props: SignUpProps) {
throw e;
}
try {
if (passphrase === confirm) {
const { keyAttributes, masterKey } =
await generateKeyAttributes(passphrase);
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
@ -76,14 +79,8 @@ export default function SignUp(props: SignUpProps) {
);
setJustSignedUp(true);
router.push(PAGES.VERIFY);
} else {
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
}
} catch (e) {
setFieldError(
'passphrase',
constants.PASSWORD_GENERATION_FAILED
);
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
throw e;
}
} catch (err) {

View file

@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton';
import TextField from '@mui/material/TextField';
import ShowHidePassword from './Form/ShowHidePassword';
import { FlexWrapper } from './Container';
import { Button } from '@mui/material';
import { Button, FormHelperText } from '@mui/material';
interface formValues {
inputValue: string;
@ -24,8 +24,11 @@ export interface SingleInputFormProps {
secondaryButtonAction?: () => void;
disableAutoFocus?: boolean;
hiddenPreInput?: any;
caption?: any;
hiddenPostInput?: any;
autoComplete?: string;
blockButton?: boolean;
hiddenLabel?: boolean;
}
export default function SingleInputForm(props: SingleInputFormProps) {
@ -86,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
<form noValidate onSubmit={handleSubmit}>
{props.hiddenPreInput}
<TextField
hiddenLabel={props.hiddenLabel}
variant="filled"
fullWidth
type={showPassword ? 'text' : props.fieldType}
id={props.fieldType}
name={props.fieldType}
label={props.placeholder}
{...(props.hiddenLabel
? { placeholder: props.placeholder }
: { label: props.placeholder })}
value={values.inputValue}
onChange={handleChange('inputValue')}
error={Boolean(errors.inputValue)}
@ -113,20 +119,45 @@ 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}
<FlexWrapper justifyContent={'flex-end'}>
<FlexWrapper
justifyContent={'flex-end'}
flexWrap={
props.blockButton ? 'wrap-reverse' : 'nowrap'
}>
{props.secondaryButtonAction && (
<Button
onClick={props.secondaryButtonAction}
size="large"
color="secondary"
sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }}
sx={{
'&&&': {
mt: !props.blockButton ? 2 : 0.5,
mb: !props.blockButton ? 4 : 0,
mr: !props.blockButton ? 1 : 0,
...buttonSx,
},
}}
{...restSubmitButtonProps}>
{constants.CANCEL}
</Button>
)}
<SubmitButton
sx={{ mt: 2, ...buttonSx }}
sx={{
'&&&': {
mt: 2,
...buttonSx,
},
}}
buttonText={props.buttonText}
loading={loading}
{...restSubmitButtonProps}

View file

@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
disabled={disabled || loading || success}
sx={{
my: 4,
...(loading
? {
'&.Mui-disabled': {
backgroundColor: (theme) => theme.palette.accent.main,
backgroundColor: (theme) =>
theme.palette.accent.main,
color: (theme) => theme.palette.text.primary,
},
}
: {}),
...sx,
}}
{...props}>

View file

@ -0,0 +1,57 @@
import Close from '@mui/icons-material/Close';
import ArrowBack from '@mui/icons-material/ArrowBack';
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

@ -1,11 +1,11 @@
import React from 'react';
import { IconButton, styled } from '@mui/material';
import { ButtonProps, IconButton, styled } from '@mui/material';
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
import { Button } from '@mui/material';
import constants from 'utils/strings/constants';
import uploadManager from 'services/upload/uploadManager';
const Wrapper = styled('div')`
const Wrapper = styled('div')<{ $disableShrink: boolean }>`
display: flex;
align-items: center;
justify-content: center;
@ -14,22 +14,35 @@ const Wrapper = styled('div')`
& .mobile-button {
display: none;
}
@media (max-width: 624px) {
${({ $disableShrink }) =>
!$disableShrink &&
`@media (max-width: 624px) {
& .mobile-button {
display: block;
}
& .desktop-button {
display: none;
}
}
}`}
`;
interface Iprops {
openUploader: () => void;
text?: string;
color?: ButtonProps['color'];
disableShrink?: boolean;
icon?: JSX.Element;
}
function UploadButton({ openUploader }: Iprops) {
function UploadButton({
openUploader,
text,
color,
disableShrink,
icon,
}: Iprops) {
return (
<Wrapper
$disableShrink={disableShrink}
style={{
cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
}}>
@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) {
onClick={openUploader}
disabled={!uploadManager.shouldAllowNewUpload()}
className="desktop-button"
color="secondary"
startIcon={<FileUploadOutlinedIcon />}>
{constants.UPLOAD}
color={color ?? 'secondary'}
startIcon={icon ?? <FileUploadOutlinedIcon />}>
{text ?? constants.UPLOAD}
</Button>
<IconButton

View file

@ -7,9 +7,10 @@ import { UploadProgressHeader } from './header';
import { InProgressSection } from './inProgressSection';
import { ResultSection } from './resultSection';
import { NotUploadSectionHeader } from './styledComponents';
import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
import UploadProgressContext from 'contexts/uploadProgress';
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
import { APP_DOWNLOAD_URL } from 'utils/common';
import { ENTE_WEBSITE_LINK } from 'constants/urls';
export function UploadProgressDialog() {
const { open, onClose, uploadStage, finishedUploads } = useContext(
@ -26,7 +27,8 @@ export function UploadProgressDialog() {
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0
finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 ||
finishedUploads.get(UPLOAD_RESULT.SKIPPED_VIDEOS)?.length > 0
) {
setHasUnUploadedFiles(true);
} else {
@ -40,12 +42,16 @@ export function UploadProgressDialog() {
<Dialog maxWidth="xs" open={open} onClose={handleClose}>
<UploadProgressHeader />
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.FINISH) && (
uploadStage === UPLOAD_STAGES.FINISH ||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
<DialogContent sx={{ '&&&': { px: 0 } }}>
{uploadStage === UPLOAD_STAGES.UPLOADING && (
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
<InProgressSection />
)}
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
uploadStage === UPLOAD_STAGES.FINISH) && (
<>
<ResultSection
uploadResult={UPLOAD_RESULT.UPLOADED}
sectionTitle={constants.SUCCESSFUL_UPLOADS}
@ -57,7 +63,9 @@ export function UploadProgressDialog() {
sectionTitle={
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
}
sectionInfo={constants.THUMBNAIL_GENERATION_FAILED_INFO}
sectionInfo={
constants.THUMBNAIL_GENERATION_FAILED_INFO
}
/>
{uploadStage === UPLOAD_STAGES.FINISH &&
@ -71,13 +79,20 @@ export function UploadProgressDialog() {
uploadResult={UPLOAD_RESULT.BLOCKED}
sectionTitle={constants.BLOCKED_UPLOADS}
sectionInfo={constants.ETAGS_BLOCKED(
getOSSpecificDesktopAppDownloadLink()
APP_DOWNLOAD_URL
)}
/>
<ResultSection
uploadResult={UPLOAD_RESULT.FAILED}
sectionTitle={constants.FAILED_UPLOADS}
/>
<ResultSection
uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
sectionTitle={constants.SKIPPED_VIDEOS}
sectionInfo={constants.SKIPPED_VIDEOS_INFO(
ENTE_WEBSITE_LINK
)}
/>
<ResultSection
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
sectionTitle={constants.SKIPPED_FILES}
@ -104,6 +119,8 @@ export function UploadProgressDialog() {
sectionTitle={constants.TOO_LARGE_UPLOADS}
sectionInfo={constants.TOO_LARGE_INFO}
/>
</>
)}
</DialogContent>
)}
{uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}

View file

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

View file

@ -20,6 +20,8 @@ function UploadProgressSubtitleText() {
return (
<Typography color="text.secondary">
{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]}
</Typography>

View file

@ -1,19 +1,48 @@
import React from 'react';
import React, { useContext, useEffect, useRef } from 'react';
import constants from 'utils/strings/constants';
import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
import GoogleIcon from '@mui/icons-material/Google';
import { UploadTypeOption } from './option';
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
import DialogTitleWithCloseButton, {
dialogCloseHandler,
} from 'components/DialogBox/TitleWithCloseButton';
import { Box, Dialog, Stack, Typography } from '@mui/material';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import { isMobileOrTable } from 'utils/common/deviceDetection';
interface Iprops {
onClose: () => void;
show: boolean;
uploadFiles: () => void;
uploadFolders: () => void;
uploadGoogleTakeoutZips: () => void;
hideZipUploadOption?: boolean;
}
export default function UploadTypeSelector({
onHide,
onClose,
show,
uploadFiles,
uploadFolders,
uploadGoogleTakeoutZips,
}) {
hideZipUploadOption,
}: Iprops) {
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const directlyShowUploadFiles = useRef(isMobileOrTable());
useEffect(() => {
if (
show &&
directlyShowUploadFiles.current &&
publicCollectionGalleryContext.accessedThroughSharedURL
) {
uploadFiles();
onClose();
}
}, [show]);
return (
<Dialog
open={show}
@ -24,9 +53,11 @@ export default function UploadTypeSelector({
[theme.breakpoints.down(360)]: { p: 0 },
}),
}}
onClose={onHide}>
<DialogTitleWithCloseButton onClose={onHide}>
{constants.UPLOAD}
onClose={dialogCloseHandler({ onClose })}>
<DialogTitleWithCloseButton onClose={onClose}>
{publicCollectionGalleryContext.accessedThroughSharedURL
? constants.SELECT_PHOTOS
: constants.UPLOAD}
</DialogTitleWithCloseButton>
<Box p={1.5} pt={0.5}>
<Stack spacing={0.5}>
@ -40,11 +71,13 @@ export default function UploadTypeSelector({
startIcon={<FolderUploadIcon />}>
{constants.UPLOAD_DIRS}
</UploadTypeOption>
{!hideZipUploadOption && (
<UploadTypeOption
onClick={uploadGoogleTakeoutZips}
startIcon={<GoogleIcon />}>
{constants.UPLOAD_GOOGLE_TAKEOUT}
</UploadTypeOption>
)}
</Stack>
<Typography p={1.5} pt={4} color="text.secondary">
{constants.DRAG_AND_DROP_HINT}

View file

@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
import { GalleryContext } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry';
import UploadManager from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager';
import ImportService from 'services/importService';
import isElectron from 'is-electron';
@ -40,7 +39,10 @@ import {
PICKED_UPLOAD_TYPE,
} from 'constants/upload';
import importService from 'services/importService';
import { getDownloadAppMessage } from 'utils/ui';
import {
getDownloadAppMessage,
getRootLevelFileWithFolderNotAllowMessage,
} from 'utils/ui';
import UploadTypeSelector from './UploadTypeSelector';
import {
filterOutSystemFiles,
@ -49,21 +51,29 @@ import {
} from 'utils/upload';
import { getUserOwnedCollections } from 'utils/collection';
import billingService from 'services/billingService';
import { addLogLine } from 'utils/logging';
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
import UserNameInputDialog from 'components/UserNameInputDialog';
import {
getPublicCollectionUID,
getPublicCollectionUploaderName,
savePublicCollectionUploaderName,
} from 'services/publicCollectionService';
const FIRST_ALBUM_NAME = 'My First Album';
interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
closeCollectionSelector: () => void;
closeCollectionSelector?: () => void;
closeUploadTypeSelector: () => void;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
setCollectionNamerAttributes: SetCollectionNamerAttributes;
setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
setCollectionNamerAttributes?: SetCollectionNamerAttributes;
setLoading: SetLoading;
setShouldDisableDropzone: (value: boolean) => void;
showCollectionSelector: () => void;
showCollectionSelector?: () => void;
setFiles: SetFiles;
setCollections: SetCollections;
isFirstUpload: boolean;
setCollections?: SetCollections;
isFirstUpload?: boolean;
uploadTypeSelectorView: boolean;
showSessionExpiredMessage: () => void;
showUploadFilesDialog: () => void;
@ -71,9 +81,17 @@ interface Props {
webFolderSelectorFiles: File[];
webFileSelectorFiles: File[];
dragAndDropFiles: File[];
zipUploadDisabled?: boolean;
uploadCollection?: Collection;
}
export default function Uploader(props: Props) {
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const publicCollectionGalleryContext = useContext(
PublicCollectionGalleryContext
);
const [uploadProgressView, setUploadProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
UPLOAD_STAGES.START
@ -92,11 +110,13 @@ export default function Uploader(props: Props) {
const [hasLivePhotos, setHasLivePhotos] = useState(false);
const [choiceModalView, setChoiceModalView] = useState(false);
const [userNameInputDialogView, setUserNameInputDialogView] =
useState(false);
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
DEFAULT_IMPORT_SUGGESTION
);
const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const [webFiles, setWebFiles] = useState([]);
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
const isPendingDesktopUpload = useRef(false);
@ -105,20 +125,33 @@ export default function Uploader(props: Props) {
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
const zipPaths = useRef<string[]>(null);
const currentUploadPromise = useRef<Promise<void>>(null);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
const [webFiles, setWebFiles] = useState([]);
const uploadRunning = useRef(false);
const uploaderNameRef = useRef<string>(null);
const closeUploadProgress = () => setUploadProgressView(false);
const showUserNameInputDialog = () => setUserNameInputDialogView(true);
const setCollectionName = (collectionName: string) => {
isPendingDesktopUpload.current = true;
pendingDesktopUploadCollectionName.current = collectionName;
};
const uploadRunning = useRef(false);
const handleChoiceModalClose = () => {
setChoiceModalView(false);
uploadRunning.current = false;
};
const handleCollectionSelectorCancel = () => {
uploadRunning.current = false;
appContext.resetSharedFiles();
};
const handleUserNameInputDialogClose = () => {
setUserNameInputDialogView(false);
uploadRunning.current = false;
};
useEffect(() => {
UploadManager.init(
uploadManager.init(
{
setPercentComplete,
setUploadCounter,
@ -128,12 +161,16 @@ export default function Uploader(props: Props) {
setUploadFilenames: setUploadFileNames,
setHasLivePhotos,
},
props.setFiles
props.setFiles,
publicCollectionGalleryContext
);
if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
ImportService.getPendingUploads().then(
({ files: electronFiles, collectionName, type }) => {
addLogLine(
`found pending desktop upload, resuming uploads`
);
resumeDesktopUpload(type, electronFiles, collectionName);
}
);
@ -144,7 +181,11 @@ export default function Uploader(props: Props) {
appContext.setIsFolderSyncRunning
);
}
}, []);
}, [
publicCollectionGalleryContext.accessedThroughSharedURL,
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken,
]);
// this handles the change of selectorFiles changes on web when user selects
// files for upload through the opened file/folder selector or dragAndDrop them
@ -159,13 +200,16 @@ export default function Uploader(props: Props) {
pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
props.webFolderSelectorFiles?.length > 0
) {
addLogLine(`received folder upload request`);
setWebFiles(props.webFolderSelectorFiles);
} else if (
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
props.webFileSelectorFiles?.length > 0
) {
addLogLine(`received file upload request`);
setWebFiles(props.webFileSelectorFiles);
} else if (props.dragAndDropFiles?.length > 0) {
addLogLine(`received drag and drop upload request`);
setWebFiles(props.dragAndDropFiles);
}
}, [
@ -180,17 +224,37 @@ export default function Uploader(props: Props) {
webFiles?.length > 0 ||
appContext.sharedFiles?.length > 0
) {
addLogLine(
`upload request type:${
electronFiles?.length > 0
? 'electronFiles'
: webFiles?.length > 0
? 'webFiles'
: 'sharedFiles'
} count ${
electronFiles?.length ??
webFiles?.length ??
appContext?.sharedFiles.length
}`
);
if (uploadRunning.current) {
if (watchFolderService.isUploadRunning()) {
addLogLine(
'watchFolder upload was running, pausing it to run user upload'
);
// pause watch folder service on user upload
watchFolderService.pauseRunningSync();
} else {
addLogLine(
'an upload is already running, rejecting new upload request'
);
// no-op
// a user upload is already in progress
return;
}
}
if (isCanvasBlocked()) {
addLogLine('canvas blocked, blocking upload');
appContext.setDialogMessage({
title: constants.CANVAS_BLOCKED_TITLE,
@ -235,7 +299,8 @@ export default function Uploader(props: Props) {
handleCollectionCreationAndUpload(
importSuggestion,
props.isFirstUpload,
pickedUploadType.current
pickedUploadType.current,
publicCollectionGalleryContext.accessedThroughSharedURL
);
pickedUploadType.current = null;
props.setLoading(false);
@ -256,14 +321,20 @@ export default function Uploader(props: Props) {
};
const preCollectionCreationAction = async () => {
props.closeCollectionSelector();
props.closeCollectionSelector?.();
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
setUploadStage(UPLOAD_STAGES.START);
setUploadProgressView(true);
};
const uploadFilesToExistingCollection = async (collection: Collection) => {
const uploadFilesToExistingCollection = async (
collection: Collection,
uploaderName?: string
) => {
try {
addLogLine(
`upload file to an existing collection - "${collection.name}"`
);
await preCollectionCreationAction();
const filesWithCollectionToUpload: FileWithCollection[] =
toUploadFiles.current.map((file, index) => ({
@ -271,10 +342,11 @@ export default function Uploader(props: Props) {
localID: index,
collectionID: collection.id,
}));
waitInQueueAndUploadFiles(filesWithCollectionToUpload, [
collection,
]);
toUploadFiles.current = null;
waitInQueueAndUploadFiles(
filesWithCollectionToUpload,
[collection],
uploaderName
);
} catch (e) {
logError(e, 'Failed to upload files to existing collections');
}
@ -285,8 +357,11 @@ export default function Uploader(props: Props) {
collectionName?: string
) => {
try {
addLogLine(
`upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
);
await preCollectionCreationAction();
const filesWithCollectionToUpload: FileWithCollection[] = [];
let filesWithCollectionToUpload: FileWithCollection[] = [];
const collections: Collection[] = [];
let collectionNameToFilesMap = new Map<
string,
@ -302,6 +377,9 @@ export default function Uploader(props: Props) {
toUploadFiles.current
);
}
addLogLine(
`upload collections - [${[...collectionNameToFilesMap.keys()]}]`
);
try {
const existingCollection = getUserOwnedCollections(
await syncCollections()
@ -320,13 +398,14 @@ export default function Uploader(props: Props) {
...existingCollection,
...collections,
]);
filesWithCollectionToUpload.push(
filesWithCollectionToUpload = [
...filesWithCollectionToUpload,
...files.map((file) => ({
localID: index++,
collectionID: collection.id,
file,
}))
);
})),
];
}
} catch (e) {
closeUploadProgress();
@ -348,13 +427,18 @@ export default function Uploader(props: Props) {
const waitInQueueAndUploadFiles = (
filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[]
collections: Collection[],
uploaderName?: string
) => {
const currentPromise = currentUploadPromise.current;
currentUploadPromise.current = waitAndRun(
currentPromise,
async () =>
await uploadFiles(filesWithCollectionToUploadIn, collections)
await uploadFiles(
filesWithCollectionToUploadIn,
collections,
uploaderName
)
);
};
@ -372,9 +456,11 @@ export default function Uploader(props: Props) {
const uploadFiles = async (
filesWithCollectionToUploadIn: FileWithCollection[],
collections: Collection[]
collections: Collection[],
uploaderName?: string
) => {
try {
addLogLine('uploadFiles called');
preUploadAction();
if (
isElectron() &&
@ -399,7 +485,8 @@ export default function Uploader(props: Props) {
const shouldCloseUploadProgress =
await uploadManager.queueFilesForUpload(
filesWithCollectionToUploadIn,
collections
collections,
uploaderName
);
if (shouldCloseUploadProgress) {
closeUploadProgress();
@ -416,6 +503,7 @@ export default function Uploader(props: Props) {
}
}
} catch (err) {
logError(err, 'failed to upload files');
showUserFacingError(err.message);
closeUploadProgress();
throw err;
@ -426,14 +514,18 @@ export default function Uploader(props: Props) {
const retryFailed = async () => {
try {
addLogLine('user retrying failed upload');
const filesWithCollections =
await uploadManager.getFailedFilesWithCollections();
uploadManager.getFailedFilesWithCollections();
const uploaderName = uploadManager.getUploaderName();
await preUploadAction();
await uploadManager.queueFilesForUpload(
filesWithCollections.files,
filesWithCollections.collections
filesWithCollections.collections,
uploaderName
);
} catch (err) {
logError(err, 'retry failed files failed');
showUserFacingError(err.message);
closeUploadProgress();
} finally {
@ -449,35 +541,33 @@ export default function Uploader(props: Props) {
case CustomError.SUBSCRIPTION_EXPIRED:
notification = {
variant: 'danger',
message: constants.SUBSCRIPTION_EXPIRED,
action: {
text: constants.RENEW_NOW,
callback: billingService.redirectToCustomerPortal,
},
subtext: constants.SUBSCRIPTION_EXPIRED,
message: constants.RENEW_NOW,
onClick: () => billingService.redirectToCustomerPortal(),
};
break;
case CustomError.STORAGE_QUOTA_EXCEEDED:
notification = {
variant: 'danger',
message: constants.STORAGE_QUOTA_EXCEEDED,
action: {
text: constants.UPGRADE_NOW,
callback: galleryContext.showPlanSelectorModal,
},
icon: <DiscFullIcon fontSize="large" />,
subtext: constants.STORAGE_QUOTA_EXCEEDED,
message: constants.UPGRADE_NOW,
onClick: () => galleryContext.showPlanSelectorModal(),
startIcon: <DiscFullIcon />,
};
break;
default:
notification = {
variant: 'danger',
message: constants.UNKNOWN_ERROR,
onClick: () => null,
};
}
galleryContext.setNotificationAttributes(notification);
appContext.setNotificationAttributes(notification);
}
const uploadToSingleNewCollection = (collectionName: string) => {
if (collectionName) {
addLogLine(`upload to single collection - "${collectionName}"`);
uploadFilesToNewCollections(
UPLOAD_STRATEGY.SINGLE_COLLECTION,
collectionName
@ -495,19 +585,38 @@ export default function Uploader(props: Props) {
});
};
const handleCollectionCreationAndUpload = (
const handleCollectionCreationAndUpload = async (
importSuggestion: ImportSuggestion,
isFirstUpload: boolean,
pickedUploadType: PICKED_UPLOAD_TYPE
pickedUploadType: PICKED_UPLOAD_TYPE,
accessedThroughSharedURL?: boolean
) => {
try {
if (accessedThroughSharedURL) {
addLogLine(
`uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}`
);
const uploaderName = await getPublicCollectionUploaderName(
getPublicCollectionUID(publicCollectionGalleryContext.token)
);
uploaderNameRef.current = uploaderName;
showUserNameInputDialog();
return;
}
if (isPendingDesktopUpload.current) {
isPendingDesktopUpload.current = false;
if (pendingDesktopUploadCollectionName.current) {
addLogLine(
`upload pending files to collection - ${pendingDesktopUploadCollectionName.current}`
);
uploadToSingleNewCollection(
pendingDesktopUploadCollectionName.current
);
pendingDesktopUploadCollectionName.current = null;
} else {
addLogLine(
`pending upload - strategy - "multiple collections" `
);
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
);
@ -515,7 +624,10 @@ export default function Uploader(props: Props) {
return;
}
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
addLogLine('uploading zip files');
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
);
return;
}
if (isFirstUpload && !importSuggestion.rootFolderName) {
@ -523,17 +635,25 @@ export default function Uploader(props: Props) {
}
let showNextModal = () => {};
if (importSuggestion.hasNestedFolders) {
addLogLine(`nested folders detected`);
showNextModal = () => setChoiceModalView(true);
} else {
showNextModal = () =>
uploadToSingleNewCollection(importSuggestion.rootFolderName);
uploadToSingleNewCollection(
importSuggestion.rootFolderName
);
}
props.setCollectionSelectorAttributes({
callback: uploadFilesToExistingCollection,
onCancel: handleCollectionSelectorCancel,
showNextModal,
title: constants.UPLOAD_TO_COLLECTION,
});
} catch (e) {
logError(e, 'handleCollectionCreationAndUpload failed');
}
};
const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => {
let files: ElectronFile[];
pickedUploadType.current = type;
@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
zipPaths.current = response.zipPaths;
}
if (files?.length > 0) {
addLogLine(
` desktop upload for type:${type} and fileCount: ${files?.length} requested`
);
setElectronFiles(files);
props.closeUploadTypeSelector();
}
@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS);
const handlePublicUpload = async (
uploaderName: string,
skipSave?: boolean
) => {
try {
if (!skipSave) {
savePublicCollectionUploaderName(
getPublicCollectionUID(
publicCollectionGalleryContext.token
),
uploaderName
);
}
await uploadFilesToExistingCollection(
props.uploadCollection,
uploaderName
);
} catch (e) {
logError(e, 'public upload failed ');
}
};
const handleUploadToSingleCollection = () => {
uploadToSingleNewCollection(importSuggestion.rootFolderName);
};
const handleUploadToMultipleCollections = () => {
if (importSuggestion.hasRootLevelFileWithFolder) {
appContext.setDialogMessage(
getRootLevelFileWithFolderNotAllowMessage()
);
return;
}
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
};
return (
<>
<UploadStrategyChoiceModal
open={choiceModalView}
onClose={() => setChoiceModalView(false)}
uploadToSingleCollection={() =>
uploadToSingleNewCollection(importSuggestion.rootFolderName)
}
uploadToMultipleCollection={() =>
uploadFilesToNewCollections(
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
)
}
onClose={handleChoiceModalClose}
uploadToSingleCollection={handleUploadToSingleCollection}
uploadToMultipleCollection={handleUploadToMultipleCollections}
/>
<UploadTypeSelector
show={props.uploadTypeSelectorView}
onHide={props.closeUploadTypeSelector}
onClose={props.closeUploadTypeSelector}
uploadFiles={handleFileUpload}
uploadFolders={handleFolderUpload}
uploadGoogleTakeoutZips={handleZipUpload}
hideZipUploadOption={props.zipUploadDisabled}
/>
<UploadProgress
open={uploadProgressView}
@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
finishedUploads={finishedUploads}
cancelUploads={cancelUploads}
/>
<UserNameInputDialog
open={userNameInputDialogView}
onClose={handleUserNameInputDialogClose}
onNameSubmit={handlePublicUpload}
toUploadFilesCount={toUploadFiles.current?.length}
uploaderName={uploaderNameRef.current}
/>
</>
);
}

View file

@ -0,0 +1,43 @@
import React from 'react';
import constants from 'utils/strings/constants';
import DialogBox from './DialogBox';
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
import { Typography } from '@mui/material';
import SingleInputForm from './SingleInputForm';
export default function UserNameInputDialog({
open,
onClose,
onNameSubmit,
toUploadFilesCount,
uploaderName,
}) {
const handleSubmit = async (inputValue: string) => {
onClose();
await onNameSubmit(inputValue);
};
return (
<DialogBox
size="xs"
open={open}
onClose={onClose}
attributes={{
title: constants.ENTER_NAME,
icon: <AutoAwesomeOutlinedIcon />,
}}>
<Typography color={'text.secondary'} pb={1}>
{constants.PUBLIC_UPLOADER_NAME_MESSAGE}
</Typography>
<SingleInputForm
hiddenLabel
initialValue={uploaderName}
callback={handleSubmit}
placeholder={constants.NAME_PLACEHOLDER}
buttonText={constants.ADD_X_PHOTOS(toUploadFilesCount)}
fieldType="text"
blockButton
secondaryButtonAction={onClose}
/>
</DialogBox>
);
}

View file

@ -1,7 +1,6 @@
import React from 'react';
import constants from 'utils/strings/constants';
import CryptoWorker from 'utils/crypto';
import SingleInputForm, {
SingleInputFormProps,
} from 'components/SingleInputForm';
@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
import { Input } from '@mui/material';
import { KeyAttributes, User } from 'types/user';
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
export interface VerifyMasterPasswordFormProps {
user: User;
@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
setFieldError
) => {
try {
const cryptoWorker = await new CryptoWorker();
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
let kek: string = null;
try {
kek = await cryptoWorker.deriveKey(
@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
throw Error(CustomError.WEAK_DEVICE);
}
try {
const key: string = await cryptoWorker.decryptB64(
const key = await cryptoWorker.decryptB64(
keyAttributes.encryptedKey,
keyAttributes.keyDecryptionNonce,
kek

View file

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

View file

@ -0,0 +1,14 @@
import React from 'react';
export default function Ente() {
return (
<svg
width="50"
height="26"
viewBox="0 0 43 13"
fill="#fff"
xmlns="http://www.w3.org/2000/svg">
<path d="M6.102 12.144C4.998 12.144 4.026 11.928 3.186 11.496C2.358 11.064 1.716 10.476 1.26 9.732C0.804 8.976 0.576 8.118 0.576 7.158C0.576 6.186 0.798 5.328 1.242 4.584C1.698 3.828 2.316 3.24 3.096 2.82C3.876 2.388 4.758 2.172 5.742 2.172C6.69 2.172 7.542 2.376 8.298 2.784C9.066 3.18 9.672 3.756 10.116 4.512C10.56 5.256 10.782 6.15 10.782 7.194C10.782 7.302 10.776 7.428 10.764 7.572C10.752 7.704 10.74 7.83 10.728 7.95H2.862V6.312H9.252L8.172 6.798C8.172 6.294 8.07 5.856 7.866 5.484C7.662 5.112 7.38 4.824 7.02 4.62C6.66 4.404 6.24 4.296 5.76 4.296C5.28 4.296 4.854 4.404 4.482 4.62C4.122 4.824 3.84 5.118 3.636 5.502C3.432 5.874 3.33 6.318 3.33 6.834V7.266C3.33 7.794 3.444 8.262 3.672 8.67C3.912 9.066 4.242 9.372 4.662 9.588C5.094 9.792 5.598 9.894 6.174 9.894C6.69 9.894 7.14 9.816 7.524 9.66C7.92 9.504 8.28 9.27 8.604 8.958L10.098 10.578C9.654 11.082 9.096 11.472 8.424 11.748C7.752 12.012 6.978 12.144 6.102 12.144ZM18.5375 2.172C19.3055 2.172 19.9895 2.328 20.5895 2.64C21.2015 2.94 21.6815 3.408 22.0295 4.044C22.3775 4.668 22.5515 5.472 22.5515 6.456V12H19.7435V6.888C19.7435 6.108 19.5695 5.532 19.2215 5.16C18.8855 4.788 18.4055 4.602 17.7815 4.602C17.3375 4.602 16.9355 4.698 16.5755 4.89C16.2275 5.07 15.9515 5.352 15.7475 5.736C15.5555 6.12 15.4595 6.612 15.4595 7.212V12H12.6515V2.316H15.3335V4.998L14.8295 4.188C15.1775 3.54 15.6755 3.042 16.3235 2.694C16.9715 2.346 17.7095 2.172 18.5375 2.172ZM29.0568 12.144C27.9168 12.144 27.0288 11.856 26.3928 11.28C25.7568 10.692 25.4388 9.822 25.4388 8.67V0.174H28.2468V8.634C28.2468 9.042 28.3548 9.36 28.5708 9.588C28.7868 9.804 29.0808 9.912 29.4528 9.912C29.8968 9.912 30.2748 9.792 30.5868 9.552L31.3428 11.532C31.0548 11.736 30.7068 11.892 30.2988 12C29.9028 12.096 29.4888 12.144 29.0568 12.144ZM23.9448 4.692V2.532H30.6588V4.692H23.9448ZM37.4262 12.144C36.3222 12.144 35.3502 11.928 34.5102 11.496C33.6822 11.064 33.0402 10.476 32.5842 9.732C32.1282 8.976 31.9002 8.118 31.9002 7.158C31.9002 6.186 32.1222 5.328 32.5662 4.584C33.0222 3.828 33.6402 3.24 34.4202 2.82C35.2002 2.388 36.0822 2.172 37.0662 2.172C38.0142 2.172 38.8662 2.376 39.6222 2.784C40.3902 3.18 40.9962 3.756 41.4402 4.512C41.8842 5.256 42.1062 6.15 42.1062 7.194C42.1062 7.302 42.1002 7.428 42.0882 7.572C42.0762 7.704 42.0642 7.83 42.0522 7.95H34.1862V6.312H40.5762L39.4962 6.798C39.4962 6.294 39.3942 5.856 39.1902 5.484C38.9862 5.112 38.7042 4.824 38.3442 4.62C37.9842 4.404 37.5642 4.296 37.0842 4.296C36.6042 4.296 36.1782 4.404 35.8062 4.62C35.4462 4.824 35.1642 5.118 34.9602 5.502C34.7562 5.874 34.6542 6.318 34.6542 6.834V7.266C34.6542 7.794 34.7682 8.262 34.9962 8.67C35.2362 9.066 35.5662 9.372 35.9862 9.588C36.4182 9.792 36.9222 9.894 37.4982 9.894C38.0142 9.894 38.4642 9.816 38.8482 9.66C39.2442 9.504 39.6042 9.27 39.9282 8.958L41.4222 10.578C40.9782 11.082 40.4202 11.472 39.7482 11.748C39.0762 12.012 38.3022 12.144 37.4262 12.144Z" />
</svg>
);
}

View file

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

View file

@ -1,34 +1,12 @@
import Link, { LinkProps } from '@mui/material/Link';
import { ButtonProps, Link, LinkProps } from '@mui/material';
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<{
onClick: () => void;
variant?: string;
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'] }>> = ({
children,
sx,
@ -41,6 +19,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
sx={{
color: 'text.primary',
textDecoration: 'underline rgba(255, 255, 255, 0.4)',
paddingBottom: 0.5,
'&:hover': {
color: `${color}.main`,
textDecoration: `underline `,

View file

@ -1,7 +1,6 @@
import React from 'react';
import NavbarBase from 'components/Navbar/base';
import SidebarToggler from 'components/Navbar/SidebarToggler';
import { getNonTrashedUniqueUserFiles } from 'utils/file';
import SearchBar from 'components/Search/SearchBar';
import { Collection } from 'types/collection';
import { EnteFile } from 'types/file';
@ -36,7 +35,7 @@ export function GalleryNavbar({
isInSearchMode={isInSearchMode}
setIsInSearchMode={setIsInSearchMode}
collections={collections}
files={getNonTrashedUniqueUserFiles(files)}
files={files}
setActiveCollection={setActiveCollection}
updateSearch={updateSearch}
/>

View file

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

View file

@ -2,6 +2,7 @@ import { Stack } from '@mui/material';
import { AppContext } from 'pages/_app';
import React, { useContext } from 'react';
import { Subscription } from 'types/billing';
import { SetLoading } from 'types/gallery';
import {
activateSubscription,
cancelSubscription,
@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button';
interface Iprops {
subscription: Subscription;
closeModal: () => void;
setLoading: (value: boolean) => void;
setLoading: SetLoading;
}
export function ManageSubscription({

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