Merge branch 'main' into integrate-mute-update-notification-api
This commit is contained in:
commit
f4f1de08eb
|
@ -53,6 +53,7 @@
|
||||||
"react-hooks/rules-of-hooks": "off",
|
"react-hooks/rules-of-hooks": "off",
|
||||||
"react-hooks/exhaustive-deps": "off",
|
"react-hooks/exhaustive-deps": "off",
|
||||||
"@next/next/no-img-element": "off",
|
"@next/next/no-img-element": "off",
|
||||||
"@typescript-eslint/no-unsafe-argument": "off"
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"jsx-a11y/alt-text": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
npx lint-staged
|
yarn lint-staged
|
||||||
|
|
18
README.md
18
README.md
|
@ -2,9 +2,16 @@
|
||||||
|
|
||||||
**ente** is a cloud storage provider that provides end-to-end encryption for your data.
|
**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/>
|
<br/><br/><br/>
|
||||||
|
|
||||||
![App Screenshots](https://user-images.githubusercontent.com/24503581/189914045-9d4e9c44-37c6-4ac6-9e17-d8c37aee1e08.png)
|
![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
|
## 🧑💻 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`
|
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||||
3. Install dependencies with `yarn install`
|
3. Install dependencies with `yarn install`
|
||||||
4. Finally, run the development server with `yarn dev`
|
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 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/>
|
<br/>
|
||||||
|
|
||||||
|
@ -69,5 +77,5 @@ An important part of our journey is to build better software by consistently lis
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Cross-browser testing provided by
|
Cross-browser testing provided by
|
||||||
[<img src="https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780" width="115" height="25">](https://www.browserstack.com/open-source)
|
[<img src="https://d98b8t1nnulk5.cloudfront.net/production/images/layout/logo-header.png?1469004780" width="115" height="25">](https://www.browserstack.com/open-source)
|
||||||
|
|
|
@ -26,7 +26,7 @@ module.exports = {
|
||||||
'style-src': "'self' 'unsafe-inline'",
|
'style-src': "'self' 'unsafe-inline'",
|
||||||
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
|
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
|
||||||
'connect-src':
|
'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'",
|
'base-uri ': "'self'",
|
||||||
// to allow worker
|
// to allow worker
|
||||||
'child-src': "'self' blob:",
|
'child-src': "'self' blob:",
|
||||||
|
|
|
@ -5,12 +5,6 @@ const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
const { withSentryConfig } = require('@sentry/nextjs');
|
const { withSentryConfig } = require('@sentry/nextjs');
|
||||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||||
|
|
||||||
const withTM = require('next-transpile-modules')([
|
|
||||||
'@mui/material',
|
|
||||||
'@mui/system',
|
|
||||||
'@mui/icons-material',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
getGitSha,
|
getGitSha,
|
||||||
convertToNextHeaderFormat,
|
convertToNextHeaderFormat,
|
||||||
|
@ -28,40 +22,44 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
|
||||||
|
|
||||||
module.exports = (phase) =>
|
module.exports = (phase) =>
|
||||||
withSentryConfig(
|
withSentryConfig(
|
||||||
withBundleAnalyzer(
|
withBundleAnalyzer({
|
||||||
withTM({
|
transpilePackages: [
|
||||||
compiler: {
|
'@mui/material',
|
||||||
styledComponents: {
|
'@mui/system',
|
||||||
ssr: true,
|
'@mui/icons-material',
|
||||||
displayName: true,
|
],
|
||||||
},
|
compiler: {
|
||||||
},
|
styledComponents: {
|
||||||
env: {
|
ssr: true,
|
||||||
SENTRY_RELEASE: GIT_SHA,
|
displayName: true,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
env: {
|
||||||
|
SENTRY_RELEASE: GIT_SHA,
|
||||||
|
NEXT_PUBLIC_IS_TEST_APP: process.env.IS_TEST_RELEASE,
|
||||||
|
},
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
// Apply these headers to all routes in your application....
|
// Apply these headers to all routes in your application....
|
||||||
source: ALL_ROUTES,
|
source: ALL_ROUTES,
|
||||||
headers: convertToNextHeaderFormat({
|
headers: convertToNextHeaderFormat({
|
||||||
...COOP_COEP_HEADERS,
|
...COOP_COEP_HEADERS,
|
||||||
...WEB_SECURITY_HEADERS,
|
...WEB_SECURITY_HEADERS,
|
||||||
...buildCSPHeader(CSP_DIRECTIVES),
|
...buildCSPHeader(CSP_DIRECTIVES),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||||
webpack: (config, { isServer }) => {
|
webpack: (config, { isServer }) => {
|
||||||
if (!isServer) {
|
if (!isServer) {
|
||||||
config.resolve.fallback.fs = false;
|
config.resolve.fallback.fs = false;
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
),
|
|
||||||
{
|
{
|
||||||
release: GIT_SHA,
|
release: GIT_SHA,
|
||||||
dryRun: phase === PHASE_DEVELOPMENT_SERVER || !IS_SENTRY_ENABLED,
|
dryRun: phase === PHASE_DEVELOPMENT_SERVER || !IS_SENTRY_ENABLED,
|
||||||
|
|
11
package.json
11
package.json
|
@ -6,7 +6,6 @@
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"albums": "next dev -p 3002",
|
"albums": "next dev -p 3002",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"prebuild": "yarn lint",
|
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"postbuild": "next export",
|
"postbuild": "next export",
|
||||||
"build-analyze": "ANALYZE=true next build",
|
"build-analyze": "ANALYZE=true next build",
|
||||||
|
@ -38,13 +37,12 @@
|
||||||
"jszip": "3.7.1",
|
"jszip": "3.7.1",
|
||||||
"libsodium-wrappers": "^0.7.8",
|
"libsodium-wrappers": "^0.7.8",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"next": "^12.3.1",
|
"next": "^13.1.2",
|
||||||
"next-transpile-modules": "^9.0.0",
|
|
||||||
"photoswipe": "file:./thirdparty/photoswipe",
|
"photoswipe": "file:./thirdparty/photoswipe",
|
||||||
"piexifjs": "^1.0.6",
|
"piexifjs": "^1.0.6",
|
||||||
"react": "^17.0.2",
|
"react": "^18.2.0",
|
||||||
"react-bootstrap": "^1.3.0",
|
"react-bootstrap": "^1.3.0",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^11.2.4",
|
"react-dropzone": "^11.2.4",
|
||||||
"react-otp-input": "^2.3.1",
|
"react-otp-input": "^2.3.1",
|
||||||
"react-select": "^4.3.1",
|
"react-select": "^4.3.1",
|
||||||
|
@ -58,6 +56,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^9.5.3",
|
"@next/bundle-analyzer": "^9.5.3",
|
||||||
|
"@types/bs58": "^4.0.1",
|
||||||
"@types/debounce-promise": "^3.1.3",
|
"@types/debounce-promise": "^3.1.3",
|
||||||
"@types/libsodium-wrappers": "^0.7.8",
|
"@types/libsodium-wrappers": "^0.7.8",
|
||||||
"@types/node": "^14.6.4",
|
"@types/node": "^14.6.4",
|
||||||
|
@ -72,7 +71,7 @@
|
||||||
"@types/yup": "^0.29.7",
|
"@types/yup": "^0.29.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"eslint": "^8.28.0",
|
"eslint": "^8.28.0",
|
||||||
"eslint-config-next": "^13.0.4",
|
"eslint-config-next": "^13.0.6",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"husky": "^7.0.1",
|
"husky": "^7.0.1",
|
||||||
"lint-staged": "^11.1.2",
|
"lint-staged": "^11.1.2",
|
||||||
|
|
|
@ -8,5 +8,5 @@
|
||||||
X-Frame-Options: deny
|
X-Frame-Options: deny
|
||||||
X-XSS-Protection: 1; mode=block
|
X-XSS-Protection: 1; mode=block
|
||||||
Referrer-Policy: same-origin
|
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;
|
||||||
|
|
||||||
|
|
|
@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) {
|
||||||
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
||||||
{constants.SORT_BY_NAME}
|
{constants.SORT_BY_NAME}
|
||||||
</SortByOption>
|
</SortByOption>
|
||||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
|
|
||||||
{constants.SORT_BY_CREATION_TIME_DESCENDING}
|
|
||||||
</SortByOption>
|
|
||||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
|
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
|
||||||
{constants.SORT_BY_CREATION_TIME_ASCENDING}
|
{constants.SORT_BY_CREATION_TIME_ASCENDING}
|
||||||
</SortByOption>
|
</SortByOption>
|
||||||
|
|
|
@ -11,13 +11,25 @@ interface Iprops {
|
||||||
export function CollectionInfo({ name, fileCount, endIcon }: Iprops) {
|
export function CollectionInfo({ name, fileCount, endIcon }: Iprops) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
<Typography variant="h3">{name}</Typography>
|
||||||
|
|
||||||
<FlexWrapper>
|
<FlexWrapper>
|
||||||
<Typography variant="subtitle">{name}</Typography>
|
<Typography variant="body2" color="text.secondary">
|
||||||
{endIcon && <Box ml={1.5}>{endIcon}</Box>}
|
{constants.PHOTO_COUNT(fileCount)}
|
||||||
|
</Typography>
|
||||||
|
{endIcon && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
svg: {
|
||||||
|
fontSize: '17px',
|
||||||
|
color: 'text.secondary',
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
ml={1.5}>
|
||||||
|
{endIcon}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</FlexWrapper>
|
</FlexWrapper>
|
||||||
<Typography variant="body2" color="text.secondary">
|
|
||||||
{constants.PHOTO_COUNT(fileCount)}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,9 @@ import { shouldShowOptions } from 'utils/collection';
|
||||||
import { CollectionSummaryType } from 'constants/collection';
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
import { ArchiveOutlined } from '@mui/icons-material';
|
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||||
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
activeCollection: Collection;
|
activeCollection: Collection;
|
||||||
|
@ -46,6 +48,12 @@ export default function CollectionInfoWithOptions({
|
||||||
return <ArchiveOutlined />;
|
return <ArchiveOutlined />;
|
||||||
case CollectionSummaryType.trash:
|
case CollectionSummaryType.trash:
|
||||||
return <Delete />;
|
return <Delete />;
|
||||||
|
case CollectionSummaryType.incomingShare:
|
||||||
|
return <PeopleIcon />;
|
||||||
|
case CollectionSummaryType.outgoingShare:
|
||||||
|
return <PeopleIcon />;
|
||||||
|
case CollectionSummaryType.sharedOnlyViaLink:
|
||||||
|
return <LinkIcon />;
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,9 @@ import TruncateText from 'components/TruncateText';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { CollectionSummaryType } from 'constants/collection';
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||||
import { ArchiveOutlined } from '@mui/icons-material';
|
import ArchiveIcon from '@mui/icons-material/Archive';
|
||||||
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
@ -50,7 +52,20 @@ function CollectionCardIcon({ collectionType }) {
|
||||||
<CollectionBarTileIcon>
|
<CollectionBarTileIcon>
|
||||||
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
||||||
{collectionType === CollectionSummaryType.archived && (
|
{collectionType === CollectionSummaryType.archived && (
|
||||||
<ArchiveOutlined />
|
<ArchiveIcon
|
||||||
|
sx={(theme) => ({
|
||||||
|
color: theme.palette.fixed.strokeMutedWhite,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{collectionType === CollectionSummaryType.outgoingShare && (
|
||||||
|
<PeopleIcon />
|
||||||
|
)}
|
||||||
|
{collectionType === CollectionSummaryType.incomingShare && (
|
||||||
|
<PeopleIcon />
|
||||||
|
)}
|
||||||
|
{collectionType === CollectionSummaryType.sharedOnlyViaLink && (
|
||||||
|
<LinkIcon />
|
||||||
)}
|
)}
|
||||||
</CollectionBarTileIcon>
|
</CollectionBarTileIcon>
|
||||||
);
|
);
|
||||||
|
|
|
@ -2,12 +2,12 @@ import { OverflowMenuOption } from 'components/OverflowMenu/option';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { CollectionActions } from '.';
|
import { CollectionActions } from '.';
|
||||||
import { ArchiveOutlined, Unarchive } from '@mui/icons-material';
|
import Unarchive from '@mui/icons-material/Unarchive';
|
||||||
|
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
IsArchived: boolean;
|
IsArchived: boolean;
|
||||||
|
@ -23,29 +23,13 @@ export function AlbumCollectionOption({
|
||||||
}: Iprops) {
|
}: Iprops) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<OverflowMenuOption
|
|
||||||
onClick={handleCollectionAction(
|
|
||||||
CollectionActions.SHOW_SHARE_DIALOG,
|
|
||||||
false
|
|
||||||
)}
|
|
||||||
startIcon={<IosShareIcon />}>
|
|
||||||
{constants.SHARE}
|
|
||||||
</OverflowMenuOption>
|
|
||||||
<OverflowMenuOption
|
|
||||||
onClick={handleCollectionAction(
|
|
||||||
CollectionActions.CONFIRM_DOWNLOAD,
|
|
||||||
false
|
|
||||||
)}
|
|
||||||
startIcon={<FileDownloadOutlinedIcon />}>
|
|
||||||
{constants.DOWNLOAD}
|
|
||||||
</OverflowMenuOption>
|
|
||||||
<OverflowMenuOption
|
<OverflowMenuOption
|
||||||
onClick={handleCollectionAction(
|
onClick={handleCollectionAction(
|
||||||
CollectionActions.SHOW_RENAME_DIALOG,
|
CollectionActions.SHOW_RENAME_DIALOG,
|
||||||
false
|
false
|
||||||
)}
|
)}
|
||||||
startIcon={<EditIcon />}>
|
startIcon={<EditIcon />}>
|
||||||
{constants.RENAME}
|
{constants.RENAME_COLLECTION}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
{IsArchived ? (
|
{IsArchived ? (
|
||||||
<OverflowMenuOption
|
<OverflowMenuOption
|
||||||
|
@ -53,13 +37,13 @@ export function AlbumCollectionOption({
|
||||||
CollectionActions.UNARCHIVE
|
CollectionActions.UNARCHIVE
|
||||||
)}
|
)}
|
||||||
startIcon={<Unarchive />}>
|
startIcon={<Unarchive />}>
|
||||||
{constants.UNARCHIVE}
|
{constants.UNARCHIVE_COLLECTION}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
) : (
|
) : (
|
||||||
<OverflowMenuOption
|
<OverflowMenuOption
|
||||||
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
||||||
startIcon={<ArchiveOutlined />}>
|
startIcon={<ArchiveOutlined />}>
|
||||||
{constants.ARCHIVE}
|
{constants.ARCHIVE_COLLECTION}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
)}
|
)}
|
||||||
<OverflowMenuOption
|
<OverflowMenuOption
|
||||||
|
@ -68,7 +52,15 @@ export function AlbumCollectionOption({
|
||||||
CollectionActions.CONFIRM_DELETE,
|
CollectionActions.CONFIRM_DELETE,
|
||||||
false
|
false
|
||||||
)}>
|
)}>
|
||||||
{constants.DELETE}
|
{constants.DELETE_COLLECTION}
|
||||||
|
</OverflowMenuOption>
|
||||||
|
<OverflowMenuOption
|
||||||
|
onClick={handleCollectionAction(
|
||||||
|
CollectionActions.SHOW_SHARE_DIALOG,
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
startIcon={<PeopleIcon />}>
|
||||||
|
{constants.SHARE_COLLECTION}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,81 @@
|
||||||
|
import { CollectionActions } from '.';
|
||||||
|
import React from 'react';
|
||||||
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
|
import PeopleIcon from '@mui/icons-material/People';
|
||||||
|
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||||
|
import { FlexWrapper } from 'components/Container';
|
||||||
|
import { IconButton, Tooltip } from '@mui/material';
|
||||||
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
handleCollectionAction: (
|
||||||
|
action: CollectionActions,
|
||||||
|
loader?: boolean
|
||||||
|
) => (...args: any[]) => Promise<void>;
|
||||||
|
collectionSummaryType: CollectionSummaryType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function QuickOptions({
|
||||||
|
handleCollectionAction,
|
||||||
|
collectionSummaryType,
|
||||||
|
}: Iprops) {
|
||||||
|
return (
|
||||||
|
<FlexWrapper sx={{ gap: '16px' }}>
|
||||||
|
{!(
|
||||||
|
collectionSummaryType === CollectionSummaryType.trash ||
|
||||||
|
collectionSummaryType === CollectionSummaryType.favorites
|
||||||
|
) && (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
collectionSummaryType ===
|
||||||
|
CollectionSummaryType.outgoingShare
|
||||||
|
? constants.MODIFY_SHARING
|
||||||
|
: collectionSummaryType ===
|
||||||
|
CollectionSummaryType.incomingShare
|
||||||
|
? constants.SHARING_DETAILS
|
||||||
|
: constants.SHARE_COLLECTION
|
||||||
|
}>
|
||||||
|
<IconButton>
|
||||||
|
<PeopleIcon
|
||||||
|
onClick={handleCollectionAction(
|
||||||
|
CollectionActions.SHOW_SHARE_DIALOG,
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{!(collectionSummaryType === CollectionSummaryType.trash) && (
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
collectionSummaryType ===
|
||||||
|
CollectionSummaryType.favorites
|
||||||
|
? constants.DOWNLOAD_FAVOURITES
|
||||||
|
: constants.DOWNLOAD_COLLECTION
|
||||||
|
}>
|
||||||
|
<IconButton>
|
||||||
|
<FileDownloadOutlinedIcon
|
||||||
|
onClick={handleCollectionAction(
|
||||||
|
CollectionActions.CONFIRM_DOWNLOAD,
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
{collectionSummaryType === CollectionSummaryType.trash && (
|
||||||
|
<Tooltip title={constants.EMPTY_TRASH}>
|
||||||
|
<IconButton>
|
||||||
|
<DeleteOutlinedIcon
|
||||||
|
onClick={handleCollectionAction(
|
||||||
|
CollectionActions.CONFIRM_DELETE,
|
||||||
|
false
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</FlexWrapper>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -17,7 +17,10 @@ import { AppContext } from 'pages/_app';
|
||||||
import OverflowMenu from 'components/OverflowMenu/menu';
|
import OverflowMenu from 'components/OverflowMenu/menu';
|
||||||
import { CollectionSummaryType } from 'constants/collection';
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
import { TrashCollectionOption } from './TrashCollectionOption';
|
import { TrashCollectionOption } from './TrashCollectionOption';
|
||||||
|
import { SharedCollectionOption } from './SharedCollectionOption';
|
||||||
|
import { QuickOptions } from './QuickOptions';
|
||||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||||
|
import { HorizontalFlex } from 'components/Container';
|
||||||
|
|
||||||
interface CollectionOptionsProps {
|
interface CollectionOptionsProps {
|
||||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||||
|
@ -39,6 +42,8 @@ export enum CollectionActions {
|
||||||
SHOW_SHARE_DIALOG,
|
SHOW_SHARE_DIALOG,
|
||||||
CONFIRM_EMPTY_TRASH,
|
CONFIRM_EMPTY_TRASH,
|
||||||
EMPTY_TRASH,
|
EMPTY_TRASH,
|
||||||
|
CONFIRM_LEAVE_SHARED_ALBUM,
|
||||||
|
LEAVE_SHARED_ALBUM,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionOptions = (props: CollectionOptionsProps) => {
|
const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||||
|
@ -93,6 +98,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||||
case CollectionActions.EMPTY_TRASH:
|
case CollectionActions.EMPTY_TRASH:
|
||||||
callback = emptyTrash;
|
callback = emptyTrash;
|
||||||
break;
|
break;
|
||||||
|
case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
|
||||||
|
callback = confirmLeaveSharedAlbum;
|
||||||
|
break;
|
||||||
|
case CollectionActions.LEAVE_SHARED_ALBUM:
|
||||||
|
callback = leaveSharedAlbum;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
logError(
|
logError(
|
||||||
Error('invalid collection action '),
|
Error('invalid collection action '),
|
||||||
|
@ -130,6 +141,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||||
redirectToAll();
|
redirectToAll();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const leaveSharedAlbum = async () => {
|
||||||
|
await CollectionAPI.leaveSharedAlbum(activeCollection.id);
|
||||||
|
redirectToAll();
|
||||||
|
};
|
||||||
|
|
||||||
const archiveCollection = () => {
|
const archiveCollection = () => {
|
||||||
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
|
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
|
||||||
};
|
};
|
||||||
|
@ -174,7 +190,7 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||||
|
|
||||||
const confirmDownloadCollection = () => {
|
const confirmDownloadCollection = () => {
|
||||||
setDialogMessage({
|
setDialogMessage({
|
||||||
title: constants.CONFIRM_DOWNLOAD_COLLECTION,
|
title: constants.DOWNLOAD_COLLECTION,
|
||||||
content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
|
content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
|
||||||
proceed: {
|
proceed: {
|
||||||
text: constants.DOWNLOAD,
|
text: constants.DOWNLOAD,
|
||||||
|
@ -200,26 +216,56 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||||
close: { text: constants.CANCEL },
|
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 (
|
return (
|
||||||
<OverflowMenu
|
<HorizontalFlex sx={{ display: 'inline-flex', gap: '16px' }}>
|
||||||
ariaControls={'collection-options'}
|
<QuickOptions
|
||||||
triggerButtonIcon={<MoreHoriz />}
|
handleCollectionAction={handleCollectionAction}
|
||||||
triggerButtonProps={{
|
collectionSummaryType={collectionSummaryType}
|
||||||
sx: {
|
/>
|
||||||
background: (theme) => theme.palette.fill.dark,
|
{!(collectionSummaryType === CollectionSummaryType.favorites) && (
|
||||||
},
|
<OverflowMenu
|
||||||
}}>
|
ariaControls={'collection-options'}
|
||||||
{collectionSummaryType === CollectionSummaryType.trash ? (
|
triggerButtonIcon={<MoreHoriz />}
|
||||||
<TrashCollectionOption
|
triggerButtonProps={{
|
||||||
handleCollectionAction={handleCollectionAction}
|
sx: {
|
||||||
/>
|
background: (theme) => theme.palette.fill.dark,
|
||||||
) : (
|
},
|
||||||
<AlbumCollectionOption
|
}}>
|
||||||
IsArchived={IsArchived(activeCollection)}
|
{collectionSummaryType === CollectionSummaryType.trash ? (
|
||||||
handleCollectionAction={handleCollectionAction}
|
<TrashCollectionOption
|
||||||
/>
|
handleCollectionAction={handleCollectionAction}
|
||||||
|
/>
|
||||||
|
) : collectionSummaryType ===
|
||||||
|
CollectionSummaryType.incomingShare ? (
|
||||||
|
<SharedCollectionOption
|
||||||
|
handleCollectionAction={handleCollectionAction}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<AlbumCollectionOption
|
||||||
|
IsArchived={IsArchived(activeCollection)}
|
||||||
|
handleCollectionAction={handleCollectionAction}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OverflowMenu>
|
||||||
)}
|
)}
|
||||||
</OverflowMenu>
|
</HorizontalFlex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,12 @@ export interface CollectionSelectorAttributes {
|
||||||
showNextModal: () => void;
|
showNextModal: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
fromCollection?: number;
|
fromCollection?: number;
|
||||||
|
onCancel?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: (closeBtnClick?: boolean) => void;
|
onClose: () => void;
|
||||||
attributes: CollectionSelectorAttributes;
|
attributes: CollectionSelectorAttributes;
|
||||||
collections: Collection[];
|
collections: Collection[];
|
||||||
collectionSummaries: CollectionSummaries;
|
collectionSummaries: CollectionSummaries;
|
||||||
|
@ -61,15 +62,18 @@ function CollectionSelector({
|
||||||
props.onClose();
|
props.onClose();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCloseButtonClick = () => props.onClose(true);
|
const onUserTriggeredClose = () => {
|
||||||
|
attributes.onCancel?.();
|
||||||
|
props.onClose();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AllCollectionDialog
|
<AllCollectionDialog
|
||||||
onClose={props.onClose}
|
onClose={onUserTriggeredClose}
|
||||||
open={props.open}
|
open={props.open}
|
||||||
position="center"
|
position="center"
|
||||||
fullScreen={appContext.isMobile}>
|
fullScreen={appContext.isMobile}>
|
||||||
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
|
<DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
|
||||||
{attributes.title}
|
{attributes.title}
|
||||||
</DialogTitleWithCloseButton>
|
</DialogTitleWithCloseButton>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
|
|
|
@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import { DropdownStyle } from 'styles/dropdown';
|
import { DropdownStyle } from 'styles/dropdown';
|
||||||
|
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||||
import { getDeviceLimitOptions } from 'utils/collection';
|
import { getDeviceLimitOptions } from 'utils/collection';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
publicShareProp: PublicURL;
|
||||||
|
collection: Collection;
|
||||||
|
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export function ManageDeviceLimit({
|
export function ManageDeviceLimit({
|
||||||
publicShareProp,
|
publicShareProp,
|
||||||
collection,
|
collection,
|
||||||
updatePublicShareURLHelper,
|
updatePublicShareURLHelper,
|
||||||
}) {
|
}: Iprops) {
|
||||||
const updateDeviceLimit = async (newLimit: number) => {
|
const updateDeviceLimit = async (newLimit: number) => {
|
||||||
return updatePublicShareURLHelper({
|
return updatePublicShareURLHelper({
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
|
|
|
@ -1,13 +1,21 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
|
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import PublicShareSwitch from '../switch';
|
import PublicShareSwitch from '../switch';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
publicShareProp: PublicURL;
|
||||||
|
collection: Collection;
|
||||||
|
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export function ManageDownloadAccess({
|
export function ManageDownloadAccess({
|
||||||
publicShareProp,
|
publicShareProp,
|
||||||
updatePublicShareURLHelper,
|
updatePublicShareURLHelper,
|
||||||
collection,
|
collection,
|
||||||
}) {
|
}: Iprops) {
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
const handleFileDownloadSetting = () => {
|
const handleFileDownloadSetting = () => {
|
||||||
|
@ -41,7 +49,7 @@ export function ManageDownloadAccess({
|
||||||
<Box>
|
<Box>
|
||||||
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
|
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
|
||||||
<PublicShareSwitch
|
<PublicShareSwitch
|
||||||
checked={publicShareProp?.enableDownload ?? false}
|
checked={publicShareProp?.enableDownload ?? true}
|
||||||
onChange={handleFileDownloadSetting}
|
onChange={handleFileDownloadSetting}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
|
@ -1,12 +1,11 @@
|
||||||
import { ManageLinkPassword } from './linkPassword';
|
import { ManageLinkPassword } from './linkPassword';
|
||||||
import { ManageDeviceLimit } from './deviceLimit';
|
import { ManageDeviceLimit } from './deviceLimit';
|
||||||
import { ManageLinkExpiry } from './linkExpiry';
|
import { ManageLinkExpiry } from './linkExpiry';
|
||||||
import { PublicLinkSetPassword } from '../setPassword';
|
|
||||||
import { Stack, Typography } from '@mui/material';
|
import { Stack, Typography } from '@mui/material';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { updateShareableURL } from 'services/collectionService';
|
import { updateShareableURL } from 'services/collectionService';
|
||||||
import { UpdatePublicURL } from 'types/collection';
|
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||||
import { sleep } from 'utils/common';
|
import { sleep } from 'utils/common';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import {
|
import {
|
||||||
|
@ -15,19 +14,24 @@ import {
|
||||||
} from '../../styledComponents';
|
} from '../../styledComponents';
|
||||||
import { ManageDownloadAccess } from './downloadAccess';
|
import { ManageDownloadAccess } from './downloadAccess';
|
||||||
import { handleSharingErrors } from 'utils/error/ui';
|
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({
|
export default function PublicShareManage({
|
||||||
publicShareProp,
|
publicShareProp,
|
||||||
collection,
|
collection,
|
||||||
setPublicShareProp,
|
setPublicShareProp,
|
||||||
}) {
|
}: Iprops) {
|
||||||
const galleryContext = useContext(GalleryContext);
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
|
||||||
const [changePasswordView, setChangePasswordView] = useState(false);
|
|
||||||
const [sharableLinkError, setSharableLinkError] = useState(null);
|
const [sharableLinkError, setSharableLinkError] = useState(null);
|
||||||
|
|
||||||
const closeConfigurePassword = () => setChangePasswordView(false);
|
|
||||||
|
|
||||||
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
|
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
|
||||||
try {
|
try {
|
||||||
galleryContext.setBlockingLoad(true);
|
galleryContext.setBlockingLoad(true);
|
||||||
|
@ -73,6 +77,13 @@ export default function PublicShareManage({
|
||||||
updatePublicShareURLHelper
|
updatePublicShareURLHelper
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
<ManagePublicCollect
|
||||||
|
collection={collection}
|
||||||
|
publicShareProp={publicShareProp}
|
||||||
|
updatePublicShareURLHelper={
|
||||||
|
updatePublicShareURLHelper
|
||||||
|
}
|
||||||
|
/>
|
||||||
<ManageDownloadAccess
|
<ManageDownloadAccess
|
||||||
collection={collection}
|
collection={collection}
|
||||||
publicShareProp={publicShareProp}
|
publicShareProp={publicShareProp}
|
||||||
|
@ -81,7 +92,6 @@ export default function PublicShareManage({
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ManageLinkPassword
|
<ManageLinkPassword
|
||||||
setChangePasswordView={setChangePasswordView}
|
|
||||||
collection={collection}
|
collection={collection}
|
||||||
publicShareProp={publicShareProp}
|
publicShareProp={publicShareProp}
|
||||||
updatePublicShareURLHelper={
|
updatePublicShareURLHelper={
|
||||||
|
@ -102,14 +112,6 @@ export default function PublicShareManage({
|
||||||
)}
|
)}
|
||||||
</ManageSectionOptions>
|
</ManageSectionOptions>
|
||||||
</details>
|
</details>
|
||||||
<PublicLinkSetPassword
|
|
||||||
open={changePasswordView}
|
|
||||||
onClose={closeConfigurePassword}
|
|
||||||
collection={collection}
|
|
||||||
publicShareProp={publicShareProp}
|
|
||||||
updatePublicShareURLHelper={updatePublicShareURLHelper}
|
|
||||||
setChangePasswordView={setChangePasswordView}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Select from 'react-select';
|
import Select from 'react-select';
|
||||||
import { linkExpiryStyle } from 'styles/linkExpiry';
|
import { linkExpiryStyle } from 'styles/linkExpiry';
|
||||||
|
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
|
||||||
import { shareExpiryOptions } from 'utils/collection';
|
import { shareExpiryOptions } from 'utils/collection';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { formatDateTime } from 'utils/time/format';
|
import { formatDateTime } from 'utils/time/format';
|
||||||
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
publicShareProp: PublicURL;
|
||||||
|
collection: Collection;
|
||||||
|
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
export function ManageLinkExpiry({
|
export function ManageLinkExpiry({
|
||||||
publicShareProp,
|
publicShareProp,
|
||||||
collection,
|
collection,
|
||||||
updatePublicShareURLHelper,
|
updatePublicShareURLHelper,
|
||||||
}) {
|
}: Iprops) {
|
||||||
const updateDeviceExpiry = async (optionFn) => {
|
const updateDeviceExpiry = async (optionFn) => {
|
||||||
return updatePublicShareURLHelper({
|
return updatePublicShareURLHelper({
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
|
||||||
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: 'danger',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
|
|
||||||
<PublicShareSwitch
|
|
||||||
checked={!!publicShareProp?.passwordEnabled}
|
|
||||||
onChange={handlePasswordChangeSetting}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -3,7 +3,7 @@ import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
export function PublicLinkSetPassword({
|
export function PublicLinkSetPassword({
|
||||||
|
@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
|
||||||
};
|
};
|
||||||
|
|
||||||
const enablePublicUrlPassword = async (password: string) => {
|
const enablePublicUrlPassword = async (password: string) => {
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||||
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
|
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
|
||||||
|
|
||||||
return updatePublicShareURLHelper({
|
return updatePublicShareURLHelper({
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -93,10 +93,10 @@ export default function Collections(props: Iprops) {
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.HEADER,
|
||||||
height: 68,
|
height: 68,
|
||||||
});
|
});
|
||||||
}, [collectionSummaries, activeCollectionID]);
|
}, [collectionSummaries, activeCollectionID, isInSearchMode]);
|
||||||
|
|
||||||
if (shouldBeHidden) {
|
if (shouldBeHidden) {
|
||||||
return <></>;
|
return <></>;
|
||||||
|
|
|
@ -78,7 +78,7 @@ export const CollectionBarTileText = styled(Overlay)`
|
||||||
export const CollectionBarTileIcon = styled(Overlay)`
|
export const CollectionBarTileIcon = styled(Overlay)`
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: flex-end;
|
justify-content: flex-start;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
& > .MuiSvgIcon-root {
|
& > .MuiSvgIcon-root {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
|
|
@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '10px',
|
marginBottom: '10px',
|
||||||
padding: '0 5%',
|
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
...(fixState === FIX_STATE.RUNNING
|
...(fixState === FIX_STATE.RUNNING
|
||||||
|
|
|
@ -23,7 +23,6 @@ const Option = ({
|
||||||
color: value !== Number(selected) ? '#aaa' : '#fff',
|
color: value !== Number(selected) ? '#aaa' : '#fff',
|
||||||
}}>
|
}}>
|
||||||
<Form.Check.Input
|
<Form.Check.Input
|
||||||
style={{ marginTop: '6px' }}
|
|
||||||
id={value.toString()}
|
id={value.toString()}
|
||||||
type="radio"
|
type="radio"
|
||||||
value={value}
|
value={value}
|
||||||
|
|
22
src/components/Navbar/EnteLinkLogo.tsx
Normal file
22
src/components/Navbar/EnteLinkLogo.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import PreviewCard from './pages/gallery/PreviewCard';
|
import PreviewCard from './pages/gallery/PreviewCard';
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import DownloadManager from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
|
@ -32,6 +32,7 @@ import { User } from 'types/user';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
|
||||||
const Container = styled('div')`
|
const Container = styled('div')`
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -113,81 +114,112 @@ const PhotoFrame = ({
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
||||||
|
|
||||||
const filteredData = useMemo(() => {
|
const updateInProgress = useRef(false);
|
||||||
const idSet = new Set();
|
const updateRequired = useRef(false);
|
||||||
const user: User = getData(LS_KEYS.USER);
|
|
||||||
|
|
||||||
return files
|
const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
|
||||||
.map((item, index) => ({
|
|
||||||
...item,
|
|
||||||
dataIndex: index,
|
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
title: item.pubMagicMetadata?.data.caption,
|
|
||||||
}))
|
|
||||||
.filter((item) => {
|
|
||||||
if (
|
|
||||||
deletedFileIds?.has(item.id) &&
|
|
||||||
activeCollection !== TRASH_SECTION
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
search?.date &&
|
|
||||||
!isSameDayAnyYear(search.date)(
|
|
||||||
new Date(item.metadata.creationTime / 1000)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
search?.location &&
|
|
||||||
!isInsideBox(
|
|
||||||
{
|
|
||||||
latitude: item.metadata.latitude,
|
|
||||||
longitude: item.metadata.longitude,
|
|
||||||
},
|
|
||||||
search.location
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
!isDeduplicating &&
|
|
||||||
activeCollection === ALL_SECTION &&
|
|
||||||
(IsArchived(item) ||
|
|
||||||
archivedCollections?.has(item.collectionID))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (activeCollection === ARCHIVE_SECTION && !IsArchived(item)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSharedFile(user, item) && !isSharedCollection) {
|
useEffect(() => {
|
||||||
return false;
|
const main = () => {
|
||||||
}
|
if (updateInProgress.current) {
|
||||||
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
|
updateRequired.current = true;
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
if (activeCollection !== TRASH_SECTION && item.isTrashed) {
|
updateInProgress.current = true;
|
||||||
return false;
|
const idSet = new Set();
|
||||||
}
|
const user: User = getData(LS_KEYS.USER);
|
||||||
if (!idSet.has(item.id)) {
|
|
||||||
|
const filteredData = files
|
||||||
|
.map((item, index) => ({
|
||||||
|
...item,
|
||||||
|
dataIndex: index,
|
||||||
|
w: window.innerWidth,
|
||||||
|
h: window.innerHeight,
|
||||||
|
title: item.pubMagicMetadata?.data.caption,
|
||||||
|
}))
|
||||||
|
.filter((item) => {
|
||||||
if (
|
if (
|
||||||
activeCollection === ALL_SECTION ||
|
deletedFileIds?.has(item.id) &&
|
||||||
activeCollection === ARCHIVE_SECTION ||
|
activeCollection !== TRASH_SECTION
|
||||||
activeCollection === TRASH_SECTION ||
|
|
||||||
activeCollection === item.collectionID
|
|
||||||
) {
|
) {
|
||||||
idSet.add(item.id);
|
return false;
|
||||||
return true;
|
}
|
||||||
|
if (
|
||||||
|
search?.date &&
|
||||||
|
!isSameDayAnyYear(search.date)(
|
||||||
|
new Date(item.metadata.creationTime / 1000)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
search?.location &&
|
||||||
|
!isInsideBox(
|
||||||
|
{
|
||||||
|
latitude: item.metadata.latitude,
|
||||||
|
longitude: item.metadata.longitude,
|
||||||
|
},
|
||||||
|
search.location
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
!isDeduplicating &&
|
||||||
|
activeCollection === ALL_SECTION &&
|
||||||
|
(IsArchived(item) ||
|
||||||
|
archivedCollections?.has(item.collectionID))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
activeCollection === ARCHIVE_SECTION &&
|
||||||
|
!IsArchived(item)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSharedFile(user, item) && !isSharedCollection) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (activeCollection !== TRASH_SECTION && item.isTrashed) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!idSet.has(item.id)) {
|
||||||
|
if (
|
||||||
|
activeCollection === ALL_SECTION ||
|
||||||
|
activeCollection === ARCHIVE_SECTION ||
|
||||||
|
activeCollection === TRASH_SECTION ||
|
||||||
|
activeCollection === item.collectionID ||
|
||||||
|
isInSearchMode
|
||||||
|
) {
|
||||||
|
idSet.add(item.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
});
|
||||||
return false;
|
setFilteredData(filteredData);
|
||||||
});
|
updateInProgress.current = false;
|
||||||
}, [files, deletedFileIds, search, activeCollection]);
|
if (updateRequired.current) {
|
||||||
|
updateRequired.current = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
main();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, [
|
||||||
|
files,
|
||||||
|
deletedFileIds,
|
||||||
|
search?.date,
|
||||||
|
search?.location,
|
||||||
|
activeCollection,
|
||||||
|
]);
|
||||||
|
|
||||||
const fileToCollectionsMap = useMemo(() => {
|
const fileToCollectionsMap = useMemo(() => {
|
||||||
const fileToCollectionsMap = new Map<number, number[]>();
|
const fileToCollectionsMap = new Map<number, number[]>();
|
||||||
|
@ -493,12 +525,26 @@ const PhotoFrame = ({
|
||||||
index: number,
|
index: number,
|
||||||
item: EnteFile
|
item: EnteFile
|
||||||
) => {
|
) => {
|
||||||
|
addLogLine(
|
||||||
|
`[${
|
||||||
|
item.id
|
||||||
|
}] getSlideData called for thumbnail:${!!item.msrc} original:${
|
||||||
|
!!item.msrc && item.src !== item.msrc
|
||||||
|
} inProgress:${fetching[item.id]}`
|
||||||
|
);
|
||||||
if (!item.msrc) {
|
if (!item.msrc) {
|
||||||
|
addLogLine(`[${item.id}] doesn't have thumbnail`);
|
||||||
try {
|
try {
|
||||||
let url: string;
|
let url: string;
|
||||||
if (galleryContext.thumbs.has(item.id)) {
|
if (galleryContext.thumbs.has(item.id)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] gallery context cache hit, using cached thumb`
|
||||||
|
);
|
||||||
url = galleryContext.thumbs.get(item.id);
|
url = galleryContext.thumbs.get(item.id);
|
||||||
} else {
|
} else {
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] gallery context cache miss, calling downloadManager to get thumb`
|
||||||
|
);
|
||||||
if (
|
if (
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
) {
|
) {
|
||||||
|
@ -523,6 +569,9 @@ const PhotoFrame = ({
|
||||||
item.w = newFile.w;
|
item.w = newFile.w;
|
||||||
item.h = newFile.h;
|
item.h = newFile.h;
|
||||||
|
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] calling invalidateCurrItems for thumbnail`
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
instance.invalidateCurrItems();
|
instance.invalidateCurrItems();
|
||||||
if (instance.isOpen()) {
|
if (instance.isOpen()) {
|
||||||
|
@ -540,16 +589,23 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fetching[item.id]) {
|
if (!fetching[item.id]) {
|
||||||
|
addLogLine(`[${item.id}] new file download fetch original request`);
|
||||||
try {
|
try {
|
||||||
fetching[item.id] = true;
|
fetching[item.id] = true;
|
||||||
let urls: { original: string[]; converted: string[] };
|
let urls: { original: string[]; converted: string[] };
|
||||||
if (galleryContext.files.has(item.id)) {
|
if (galleryContext.files.has(item.id)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] gallery context cache hit, using cached file`
|
||||||
|
);
|
||||||
const mergedURL = galleryContext.files.get(item.id);
|
const mergedURL = galleryContext.files.get(item.id);
|
||||||
urls = {
|
urls = {
|
||||||
original: mergedURL.original.split(','),
|
original: mergedURL.original.split(','),
|
||||||
converted: mergedURL.converted.split(','),
|
converted: mergedURL.converted.split(','),
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] gallery context cache miss, calling downloadManager to get file`
|
||||||
|
);
|
||||||
appContext.startLoading();
|
appContext.startLoading();
|
||||||
if (
|
if (
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
|
@ -576,7 +632,8 @@ const PhotoFrame = ({
|
||||||
let convertedVideoURL;
|
let convertedVideoURL;
|
||||||
|
|
||||||
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
[originalImageURL, originalVideoURL] = urls.converted;
|
[originalImageURL, originalVideoURL] = urls.original;
|
||||||
|
[convertedImageURL, convertedVideoURL] = urls.converted;
|
||||||
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
[originalVideoURL] = urls.original;
|
[originalVideoURL] = urls.original;
|
||||||
[convertedVideoURL] = urls.converted;
|
[convertedVideoURL] = urls.converted;
|
||||||
|
@ -600,6 +657,9 @@ const PhotoFrame = ({
|
||||||
item.w = newFile.w;
|
item.w = newFile.w;
|
||||||
item.h = newFile.h;
|
item.h = newFile.h;
|
||||||
try {
|
try {
|
||||||
|
addLogLine(
|
||||||
|
`[${item.id}] calling invalidateCurrItems for src`
|
||||||
|
);
|
||||||
instance.invalidateCurrItems();
|
instance.invalidateCurrItems();
|
||||||
if (instance.isOpen()) {
|
if (instance.isOpen()) {
|
||||||
instance.updateSize(true);
|
instance.updateSize(true);
|
||||||
|
@ -609,13 +669,12 @@ const PhotoFrame = ({
|
||||||
e,
|
e,
|
||||||
'updating photoswipe after src url update failed'
|
'updating photoswipe after src url update failed'
|
||||||
);
|
);
|
||||||
// ignore
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'getSlideData failed get src url failed');
|
logError(e, 'getSlideData failed get src url failed');
|
||||||
// no-op
|
|
||||||
} finally {
|
|
||||||
fetching[item.id] = false;
|
fetching[item.id] = false;
|
||||||
|
// no-op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useRef, useEffect, useContext } from 'react';
|
import React, { useRef, useEffect, useContext, useState } from 'react';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
import { VariableSizeList as List } from 'react-window';
|
||||||
import { Box, Link, styled } from '@mui/material';
|
import { Box, Link, styled } from '@mui/material';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
|
@ -25,11 +25,15 @@ import { formatDate } from 'utils/time/format';
|
||||||
|
|
||||||
const A_DAY = 24 * 60 * 60 * 1000;
|
const A_DAY = 24 * 60 * 60 * 1000;
|
||||||
const FOOTER_HEIGHT = 90;
|
const FOOTER_HEIGHT = 90;
|
||||||
|
const ALBUM_FOOTER_HEIGHT = 75;
|
||||||
|
|
||||||
export enum ITEM_TYPE {
|
export enum ITEM_TYPE {
|
||||||
TIME = 'TIME',
|
TIME = 'TIME',
|
||||||
FILE = 'FILE',
|
FILE = 'FILE',
|
||||||
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
|
SIZE_AND_COUNT = 'SIZE_AND_COUNT',
|
||||||
|
HEADER = 'HEADER',
|
||||||
|
FOOTER = 'FOOTER',
|
||||||
|
MARKETING_FOOTER = 'MARKETING_FOOTER',
|
||||||
OTHER = 'OTHER',
|
OTHER = 'OTHER',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -128,7 +132,6 @@ const SizeAndCountContainer = styled(DateContainer)`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const FooterContainer = styled(ListItemContainer)`
|
const FooterContainer = styled(ListItemContainer)`
|
||||||
font-size: 14px;
|
|
||||||
margin-bottom: 0.75rem;
|
margin-bottom: 0.75rem;
|
||||||
@media (max-width: 540px) {
|
@media (max-width: 540px) {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
@ -141,6 +144,13 @@ const FooterContainer = styled(ListItemContainer)`
|
||||||
margin-top: calc(2rem + 20px);
|
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)`
|
const NothingContainer = styled(ListItemContainer)`
|
||||||
color: #979797;
|
color: #979797;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -171,17 +181,16 @@ export function PhotoList({
|
||||||
resetFetching,
|
resetFetching,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const galleryContext = useContext(GalleryContext);
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
|
||||||
const timeStampListRef = useRef([]);
|
|
||||||
const timeStampList = timeStampListRef?.current ?? [];
|
|
||||||
const filteredDataCopyRef = useRef([]);
|
|
||||||
const filteredDataCopy = filteredDataCopyRef.current ?? [];
|
|
||||||
const listRef = useRef(null);
|
|
||||||
const publicCollectionGalleryContext = useContext(
|
const publicCollectionGalleryContext = useContext(
|
||||||
PublicCollectionGalleryContext
|
PublicCollectionGalleryContext
|
||||||
);
|
);
|
||||||
const deduplicateContext = useContext(DeduplicateContext);
|
const deduplicateContext = useContext(DeduplicateContext);
|
||||||
|
|
||||||
|
const [timeStampList, setTimeStampList] = useState<TimeStampListItem[]>([]);
|
||||||
|
const refreshInProgress = useRef(false);
|
||||||
|
const shouldRefresh = useRef(false);
|
||||||
|
const listRef = useRef(null);
|
||||||
|
|
||||||
const fittableColumns = getFractionFittableColumns(width);
|
const fittableColumns = getFractionFittableColumns(width);
|
||||||
let columns = Math.ceil(fittableColumns);
|
let columns = Math.ceil(fittableColumns);
|
||||||
|
|
||||||
|
@ -200,59 +209,135 @@ export function PhotoList({
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let timeStampList: TimeStampListItem[] = [];
|
const main = () => {
|
||||||
|
if (refreshInProgress.current) {
|
||||||
|
shouldRefresh.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
refreshInProgress.current = true;
|
||||||
|
let timeStampList: TimeStampListItem[] = [];
|
||||||
|
|
||||||
if (galleryContext.photoListHeader) {
|
if (galleryContext.photoListHeader) {
|
||||||
timeStampList.push(
|
timeStampList.push(
|
||||||
getPhotoListHeader(galleryContext.photoListHeader)
|
getPhotoListHeader(galleryContext.photoListHeader)
|
||||||
);
|
);
|
||||||
} else if (publicCollectionGalleryContext.photoListHeader) {
|
} else if (publicCollectionGalleryContext.photoListHeader) {
|
||||||
timeStampList.push(
|
timeStampList.push(
|
||||||
getPhotoListHeader(
|
getPhotoListHeader(
|
||||||
publicCollectionGalleryContext.photoListHeader
|
publicCollectionGalleryContext.photoListHeader
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (deduplicateContext.isOnDeduplicatePage) {
|
if (deduplicateContext.isOnDeduplicatePage) {
|
||||||
skipMerge = true;
|
skipMerge = true;
|
||||||
groupByFileSize(timeStampList);
|
groupByFileSize(timeStampList);
|
||||||
} else {
|
} else {
|
||||||
groupByTime(timeStampList);
|
groupByTime(timeStampList);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!skipMerge) {
|
if (!skipMerge) {
|
||||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
timeStampList = mergeTimeStampList(timeStampList, columns);
|
||||||
}
|
}
|
||||||
if (timeStampList.length === 1) {
|
if (timeStampList.length === 1) {
|
||||||
timeStampList.push(getEmptyListItem());
|
timeStampList.push(getEmptyListItem());
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
showAppDownloadBanner ||
|
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
|
||||||
) {
|
|
||||||
timeStampList.push(getVacuumItem(timeStampList));
|
timeStampList.push(getVacuumItem(timeStampList));
|
||||||
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
||||||
|
if (publicCollectionGalleryContext.photoListFooter) {
|
||||||
|
timeStampList.push(
|
||||||
|
getPhotoListFooter(
|
||||||
|
publicCollectionGalleryContext.photoListFooter
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
timeStampList.push(getAlbumsFooter());
|
timeStampList.push(getAlbumsFooter());
|
||||||
} else {
|
} else if (showAppDownloadBanner) {
|
||||||
timeStampList.push(getAppDownloadFooter());
|
timeStampList.push(getAppDownloadFooter());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
timeStampListRef.current = timeStampList;
|
setTimeStampList(timeStampList);
|
||||||
filteredDataCopyRef.current = filteredData;
|
refreshInProgress.current = false;
|
||||||
refreshList();
|
if (shouldRefresh.current) {
|
||||||
|
shouldRefresh.current = false;
|
||||||
|
setTimeout(main, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
main();
|
||||||
}, [
|
}, [
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
filteredData,
|
filteredData,
|
||||||
showAppDownloadBanner,
|
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
|
||||||
galleryContext.photoListHeader,
|
|
||||||
publicCollectionGalleryContext.photoListHeader,
|
|
||||||
deduplicateContext.isOnDeduplicatePage,
|
deduplicateContext.isOnDeduplicatePage,
|
||||||
deduplicateContext.fileSizeMap,
|
deduplicateContext.fileSizeMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeStampList((timeStampList) => {
|
||||||
|
timeStampList = timeStampList ?? [];
|
||||||
|
const hasHeader =
|
||||||
|
timeStampList.length > 0 &&
|
||||||
|
timeStampList[0].itemType === ITEM_TYPE.HEADER;
|
||||||
|
|
||||||
|
if (hasHeader) {
|
||||||
|
return timeStampList;
|
||||||
|
}
|
||||||
|
if (galleryContext.photoListHeader) {
|
||||||
|
return [
|
||||||
|
getPhotoListHeader(galleryContext.photoListHeader),
|
||||||
|
...timeStampList,
|
||||||
|
];
|
||||||
|
} else if (publicCollectionGalleryContext.photoListHeader) {
|
||||||
|
return [
|
||||||
|
getPhotoListHeader(
|
||||||
|
publicCollectionGalleryContext.photoListHeader
|
||||||
|
),
|
||||||
|
...timeStampList,
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
return timeStampList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
galleryContext.photoListHeader,
|
||||||
|
publicCollectionGalleryContext.photoListHeader,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setTimeStampList((timeStampList) => {
|
||||||
|
timeStampList = timeStampList ?? [];
|
||||||
|
const hasFooter =
|
||||||
|
timeStampList.length > 0 &&
|
||||||
|
timeStampList[timeStampList.length - 1].itemType ===
|
||||||
|
ITEM_TYPE.MARKETING_FOOTER;
|
||||||
|
if (hasFooter) {
|
||||||
|
return timeStampList;
|
||||||
|
}
|
||||||
|
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
||||||
|
if (publicCollectionGalleryContext.photoListFooter) {
|
||||||
|
return [
|
||||||
|
...timeStampList,
|
||||||
|
getPhotoListFooter(
|
||||||
|
publicCollectionGalleryContext.photoListFooter
|
||||||
|
),
|
||||||
|
getAlbumsFooter(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else if (showAppDownloadBanner) {
|
||||||
|
return [...timeStampList, getAppDownloadFooter()];
|
||||||
|
} else {
|
||||||
|
return timeStampList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||||
|
showAppDownloadBanner,
|
||||||
|
publicCollectionGalleryContext.photoListFooter,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshList();
|
||||||
|
}, [timeStampList]);
|
||||||
|
|
||||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < filteredData.length) {
|
while (index < filteredData.length) {
|
||||||
|
@ -295,10 +380,10 @@ export function PhotoList({
|
||||||
|
|
||||||
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
||||||
let listItemIndex = 0;
|
let listItemIndex = 0;
|
||||||
let currentDate = -1;
|
let currentDate;
|
||||||
|
|
||||||
filteredData.forEach((item, index) => {
|
filteredData.forEach((item, index) => {
|
||||||
if (
|
if (
|
||||||
|
!currentDate ||
|
||||||
!isSameDay(
|
!isSameDay(
|
||||||
new Date(item.metadata.creationTime / 1000),
|
new Date(item.metadata.creationTime / 1000),
|
||||||
new Date(currentDate)
|
new Date(currentDate)
|
||||||
|
@ -338,10 +423,13 @@ export function PhotoList({
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const isSameDay = (first, second) =>
|
const isSameDay = (first, second) => {
|
||||||
first.getFullYear() === second.getFullYear() &&
|
return (
|
||||||
first.getMonth() === second.getMonth() &&
|
first.getFullYear() === second.getFullYear() &&
|
||||||
first.getDate() === second.getDate();
|
first.getMonth() === second.getMonth() &&
|
||||||
|
first.getDate() === second.getDate()
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getPhotoListHeader = (photoListHeader) => {
|
const getPhotoListHeader = (photoListHeader) => {
|
||||||
return {
|
return {
|
||||||
|
@ -354,6 +442,17 @@ export function PhotoList({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPhotoListFooter = (photoListFooter) => {
|
||||||
|
return {
|
||||||
|
...photoListFooter,
|
||||||
|
item: (
|
||||||
|
<ListItemContainer span={columns}>
|
||||||
|
{photoListFooter.item}
|
||||||
|
</ListItemContainer>
|
||||||
|
),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const getEmptyListItem = () => {
|
const getEmptyListItem = () => {
|
||||||
return {
|
return {
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.OTHER,
|
||||||
|
@ -367,12 +466,17 @@ export function PhotoList({
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
const getVacuumItem = (timeStampList) => {
|
const getVacuumItem = (timeStampList) => {
|
||||||
|
const footerHeight =
|
||||||
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
|
? ALBUM_FOOTER_HEIGHT +
|
||||||
|
(publicCollectionGalleryContext.photoListFooter?.height ?? 0)
|
||||||
|
: FOOTER_HEIGHT;
|
||||||
const photoFrameHeight = (() => {
|
const photoFrameHeight = (() => {
|
||||||
let sum = 0;
|
let sum = 0;
|
||||||
const getCurrentItemSize = getItemSize(timeStampList);
|
const getCurrentItemSize = getItemSize(timeStampList);
|
||||||
for (let i = 0; i < timeStampList.length; i++) {
|
for (let i = 0; i < timeStampList.length; i++) {
|
||||||
sum += getCurrentItemSize(i);
|
sum += getCurrentItemSize(i);
|
||||||
if (height - sum <= FOOTER_HEIGHT) {
|
if (height - sum <= footerHeight) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -381,17 +485,19 @@ export function PhotoList({
|
||||||
return {
|
return {
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.OTHER,
|
||||||
item: <></>,
|
item: <></>,
|
||||||
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
|
height: Math.max(height - photoFrameHeight - footerHeight, 0),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getAppDownloadFooter = () => {
|
const getAppDownloadFooter = () => {
|
||||||
return {
|
return {
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.MARKETING_FOOTER,
|
||||||
height: FOOTER_HEIGHT,
|
height: FOOTER_HEIGHT,
|
||||||
item: (
|
item: (
|
||||||
<FooterContainer span={columns}>
|
<FooterContainer span={columns}>
|
||||||
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
|
<Typography variant="body2">
|
||||||
|
{constants.INSTALL_MOBILE_APP()}
|
||||||
|
</Typography>
|
||||||
</FooterContainer>
|
</FooterContainer>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
@ -399,17 +505,17 @@ export function PhotoList({
|
||||||
|
|
||||||
const getAlbumsFooter = () => {
|
const getAlbumsFooter = () => {
|
||||||
return {
|
return {
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.MARKETING_FOOTER,
|
||||||
height: FOOTER_HEIGHT,
|
height: ALBUM_FOOTER_HEIGHT,
|
||||||
item: (
|
item: (
|
||||||
<FooterContainer span={columns}>
|
<AlbumFooterContainer span={columns}>
|
||||||
<p>
|
<Typography variant="body2">
|
||||||
{constants.PRESERVED_BY}{' '}
|
{constants.SHARED_USING}{' '}
|
||||||
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
|
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
|
||||||
{constants.ENTE_IO}
|
{constants.ENTE_IO}
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</Typography>
|
||||||
</FooterContainer>
|
</AlbumFooterContainer>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -527,14 +633,14 @@ export function PhotoList({
|
||||||
switch (listItem.itemType) {
|
switch (listItem.itemType) {
|
||||||
case ITEM_TYPE.TIME:
|
case ITEM_TYPE.TIME:
|
||||||
return listItem.dates ? (
|
return listItem.dates ? (
|
||||||
listItem.dates.map((item) => (
|
listItem.dates
|
||||||
<>
|
.map((item) => [
|
||||||
<DateContainer key={item.date} span={item.span}>
|
<DateContainer key={item.date} span={item.span}>
|
||||||
{item.date}
|
{item.date}
|
||||||
</DateContainer>
|
</DateContainer>,
|
||||||
<div />
|
<div key={`${item.date}-gap`} />,
|
||||||
</>
|
])
|
||||||
))
|
.flat()
|
||||||
) : (
|
) : (
|
||||||
<DateContainer span={columns}>
|
<DateContainer span={columns}>
|
||||||
{listItem.date}
|
{listItem.date}
|
||||||
|
@ -551,7 +657,7 @@ export function PhotoList({
|
||||||
case ITEM_TYPE.FILE: {
|
case ITEM_TYPE.FILE: {
|
||||||
const ret = listItem.items.map((item, idx) =>
|
const ret = listItem.items.map((item, idx) =>
|
||||||
getThumbnail(
|
getThumbnail(
|
||||||
filteredDataCopy,
|
filteredData,
|
||||||
listItem.itemStartIndex + idx,
|
listItem.itemStartIndex + idx,
|
||||||
isScrolling
|
isScrolling
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,10 +6,6 @@ import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
|
|
||||||
export interface formValues {
|
|
||||||
filename: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FileNameEditDialog = ({
|
export const FileNameEditDialog = ({
|
||||||
isInEditMode,
|
isInEditMode,
|
||||||
closeEditMode,
|
closeEditMode,
|
||||||
|
|
|
@ -11,9 +11,9 @@ import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||||
import * as Yup from 'yup';
|
import * as Yup from 'yup';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import Close from '@mui/icons-material/Close';
|
import Close from '@mui/icons-material/Close';
|
||||||
import { Done } from '@mui/icons-material';
|
import Done from '@mui/icons-material/Done';
|
||||||
|
|
||||||
export interface formValues {
|
interface formValues {
|
||||||
caption: string;
|
caption: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,11 +9,12 @@ import {
|
||||||
import { FlexWrapper } from 'components/Container';
|
import { FlexWrapper } from 'components/Container';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
|
|
||||||
import InfoItem from './InfoItem';
|
import InfoItem from './InfoItem';
|
||||||
import { makeHumanReadableStorage } from 'utils/billing';
|
import { makeHumanReadableStorage } from 'utils/billing';
|
||||||
import Box from '@mui/material/Box';
|
import Box from '@mui/material/Box';
|
||||||
import { FileNameEditDialog } from './FileNameEditDialog';
|
import { FileNameEditDialog } from './FileNameEditDialog';
|
||||||
|
import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
|
||||||
|
import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
|
||||||
|
|
||||||
const getFileTitle = (filename, extension) => {
|
const getFileTitle = (filename, extension) => {
|
||||||
if (extension) {
|
if (extension) {
|
||||||
|
@ -98,10 +99,10 @@ export function RenderFileName({
|
||||||
<>
|
<>
|
||||||
<InfoItem
|
<InfoItem
|
||||||
icon={
|
icon={
|
||||||
file.metadata.fileType === FILE_TYPE.IMAGE ? (
|
file.metadata.fileType === FILE_TYPE.VIDEO ? (
|
||||||
<PhotoOutlined />
|
<VideocamOutlined />
|
||||||
) : (
|
) : (
|
||||||
<VideoFileOutlined />
|
<PhotoOutlined />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
title={getFileTitle(filename, extension)}
|
title={getFileTitle(filename, extension)}
|
||||||
|
|
|
@ -6,13 +6,7 @@ import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
|
||||||
import { Location } from 'types/upload';
|
import { Location } from 'types/upload';
|
||||||
import { getEXIFLocation } from 'services/upload/exifService';
|
import { getEXIFLocation } from 'services/upload/exifService';
|
||||||
import { RenderCaption } from './RenderCaption';
|
import { RenderCaption } from './RenderCaption';
|
||||||
import {
|
|
||||||
BackupOutlined,
|
|
||||||
CameraOutlined,
|
|
||||||
FolderOutlined,
|
|
||||||
LocationOnOutlined,
|
|
||||||
TextSnippetOutlined,
|
|
||||||
} from '@mui/icons-material';
|
|
||||||
import CopyButton from 'components/CodeBlock/CopyButton';
|
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||||
import { formatDate, formatTime } from 'utils/time/format';
|
import { formatDate, formatTime } from 'utils/time/format';
|
||||||
import Titlebar from 'components/Titlebar';
|
import Titlebar from 'components/Titlebar';
|
||||||
|
@ -24,6 +18,11 @@ import { Chip } from 'components/Chip';
|
||||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
import { ExifData } from './ExifData';
|
import { ExifData } from './ExifData';
|
||||||
import { EnteDrawer } from 'components/EnteDrawer';
|
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';
|
||||||
|
|
||||||
export const FileInfoSidebar = styled((props: DialogProps) => (
|
export const FileInfoSidebar = styled((props: DialogProps) => (
|
||||||
<EnteDrawer {...props} anchor="right" />
|
<EnteDrawer {...props} anchor="right" />
|
||||||
|
|
|
@ -9,7 +9,11 @@ import {
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import { downloadFile, copyFileToClipboard } from 'utils/file';
|
import {
|
||||||
|
downloadFile,
|
||||||
|
copyFileToClipboard,
|
||||||
|
getFileExtension,
|
||||||
|
} from 'utils/file';
|
||||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
|
@ -32,9 +36,10 @@ import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import { trashFiles } from 'services/fileService';
|
import { trashFiles } from 'services/fileService';
|
||||||
import { getTrashFileMessage } from 'utils/ui';
|
import { getTrashFileMessage } from 'utils/ui';
|
||||||
import { ChevronLeft, ContentCopy } from '@mui/icons-material';
|
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { addLocalLog } from 'utils/logging';
|
import { addLocalLog } from 'utils/logging';
|
||||||
|
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||||
|
import ChevronLeft from '@mui/icons-material/ChevronLeft';
|
||||||
|
|
||||||
interface PhotoswipeFullscreenAPI {
|
interface PhotoswipeFullscreenAPI {
|
||||||
enter: () => void;
|
enter: () => void;
|
||||||
|
@ -114,21 +119,17 @@ function PhotoViewer(props: Iprops) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKeyUp(event: KeyboardEvent) {
|
function handleKeyUp(event: KeyboardEvent) {
|
||||||
if (!isOpen) {
|
if (!isOpen || showInfo) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
addLocalLog(() => 'Event: ' + event.key);
|
addLocalLog(() => 'Event: ' + event.key);
|
||||||
if (event.key === 'i' || event.key === 'I') {
|
|
||||||
if (!showInfo) {
|
|
||||||
setShowInfo(true);
|
|
||||||
} else {
|
|
||||||
setShowInfo(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (showInfo) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
switch (event.key) {
|
switch (event.key) {
|
||||||
|
case 'i':
|
||||||
|
case 'I':
|
||||||
|
setShowInfo(true);
|
||||||
|
break;
|
||||||
case 'Backspace':
|
case 'Backspace':
|
||||||
case 'Delete':
|
case 'Delete':
|
||||||
confirmTrashFile(photoSwipe?.currItem as EnteFile);
|
confirmTrashFile(photoSwipe?.currItem as EnteFile);
|
||||||
|
@ -240,7 +241,6 @@ function PhotoViewer(props: Iprops) {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
exifCopy.current = exif;
|
exifCopy.current = exif;
|
||||||
console.log(exif);
|
|
||||||
}, [exif]);
|
}, [exif]);
|
||||||
|
|
||||||
function updateFavButton(file: EnteFile) {
|
function updateFavButton(file: EnteFile) {
|
||||||
|
@ -294,7 +294,7 @@ function PhotoViewer(props: Iprops) {
|
||||||
if (callback || event === 'destroy') {
|
if (callback || event === 'destroy') {
|
||||||
photoSwipe.listen(event, function (...args) {
|
photoSwipe.listen(event, function (...args) {
|
||||||
if (callback) {
|
if (callback) {
|
||||||
args.unshift(this);
|
args.unshift(photoSwipe);
|
||||||
callback(...args);
|
callback(...args);
|
||||||
}
|
}
|
||||||
if (event === 'destroy') {
|
if (event === 'destroy') {
|
||||||
|
@ -308,6 +308,11 @@ function PhotoViewer(props: Iprops) {
|
||||||
});
|
});
|
||||||
photoSwipe.listen('beforeChange', () => {
|
photoSwipe.listen('beforeChange', () => {
|
||||||
const currItem = photoSwipe?.currItem as EnteFile;
|
const currItem = photoSwipe?.currItem as EnteFile;
|
||||||
|
updateFavButton(currItem);
|
||||||
|
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
|
||||||
|
setExif({ key: currItem.src, value: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!currItem ||
|
!currItem ||
|
||||||
!exifCopy?.current?.value === null ||
|
!exifCopy?.current?.value === null ||
|
||||||
|
@ -317,10 +322,13 @@ function PhotoViewer(props: Iprops) {
|
||||||
}
|
}
|
||||||
setExif({ key: currItem.src, value: undefined });
|
setExif({ key: currItem.src, value: undefined });
|
||||||
checkExifAvailable(currItem);
|
checkExifAvailable(currItem);
|
||||||
updateFavButton(currItem);
|
|
||||||
});
|
});
|
||||||
photoSwipe.listen('resize', () => {
|
photoSwipe.listen('resize', () => {
|
||||||
const currItem = photoSwipe?.currItem as EnteFile;
|
const currItem = photoSwipe?.currItem as EnteFile;
|
||||||
|
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
|
||||||
|
setExif({ key: currItem.src, value: null });
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
!currItem ||
|
!currItem ||
|
||||||
!exifCopy?.current?.value === null ||
|
!exifCopy?.current?.value === null ||
|
||||||
|
@ -416,16 +424,12 @@ function PhotoViewer(props: Iprops) {
|
||||||
|
|
||||||
const checkExifAvailable = async (file: EnteFile) => {
|
const checkExifAvailable = async (file: EnteFile) => {
|
||||||
try {
|
try {
|
||||||
console.log('checkExifAvailable', file.src);
|
|
||||||
if (exifExtractionInProgress.current === file.src) {
|
if (exifExtractionInProgress.current === file.src) {
|
||||||
console.log('already in process');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (file.isSourceLoaded) {
|
if (file.isSourceLoaded) {
|
||||||
console.log('starting processing');
|
|
||||||
exifExtractionInProgress.current = file.src;
|
exifExtractionInProgress.current = file.src;
|
||||||
console.log(file.originalImageURL);
|
|
||||||
const imageBlob = await (
|
const imageBlob = await (
|
||||||
await fetch(file.originalImageURL)
|
await fetch(file.originalImageURL)
|
||||||
).blob();
|
).blob();
|
||||||
|
@ -433,24 +437,21 @@ function PhotoViewer(props: Iprops) {
|
||||||
string,
|
string,
|
||||||
any
|
any
|
||||||
>;
|
>;
|
||||||
console.log({ exifData });
|
|
||||||
console.log(exifExtractionInProgress, file.src);
|
|
||||||
if (exifExtractionInProgress.current === file.src) {
|
if (exifExtractionInProgress.current === file.src) {
|
||||||
if (exifData) {
|
if (exifData) {
|
||||||
console.log('set extracted metadata');
|
|
||||||
setExif({ key: file.src, value: exifData });
|
setExif({ key: file.src, value: exifData });
|
||||||
} else {
|
} else {
|
||||||
console.log("doesn't have metadata");
|
|
||||||
setExif({ key: file.src, value: null });
|
setExif({ key: file.src, value: null });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
console.log('cleared exifExtractionInProgress');
|
|
||||||
exifExtractionInProgress.current = null;
|
exifExtractionInProgress.current = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -529,7 +530,7 @@ function PhotoViewer(props: Iprops) {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--close"
|
className="pswp__button pswp__button--close"
|
||||||
title={constants.CLOSE}
|
title={constants.CLOSE_OPTION}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{props.enableDownload && (
|
{props.enableDownload && (
|
||||||
|
|
|
@ -6,9 +6,13 @@ import { addLogLine, getDebugLogs } from 'utils/logging';
|
||||||
import SidebarButton from './Button';
|
import SidebarButton from './Button';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import ElectronService from 'services/electron/common';
|
import ElectronService from 'services/electron/common';
|
||||||
import { testUpload } from 'tests/upload.test';
|
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { isInternalUser } from 'utils/user';
|
import { isInternalUser } from 'utils/user';
|
||||||
|
import { testUpload } from '../../../tests/upload.test';
|
||||||
|
import {
|
||||||
|
testZipFileReading,
|
||||||
|
testZipWithRootFileReadingTest,
|
||||||
|
} from '../../../tests/zip-file-reading.test';
|
||||||
|
|
||||||
export default function DebugSection() {
|
export default function DebugSection() {
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
@ -63,9 +67,17 @@ export default function DebugSection() {
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
{isInternalUser() && (
|
{isInternalUser() && (
|
||||||
<SidebarButton onClick={testUpload}>
|
<>
|
||||||
{constants.RUN_TESTS}
|
<SidebarButton onClick={testUpload}>
|
||||||
</SidebarButton>
|
Test Upload
|
||||||
|
</SidebarButton>
|
||||||
|
<SidebarButton onClick={testZipFileReading}>
|
||||||
|
Test Zip file reading
|
||||||
|
</SidebarButton>
|
||||||
|
<SidebarButton onClick={testZipWithRootFileReadingTest}>
|
||||||
|
Zip with Root file Test
|
||||||
|
</SidebarButton>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,8 @@ import { GalleryContext } from 'pages/gallery';
|
||||||
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
||||||
import { CollectionSummaries } from 'types/collection';
|
import { CollectionSummaries } from 'types/collection';
|
||||||
import ShortcutButton from './ShortcutButton';
|
import ShortcutButton from './ShortcutButton';
|
||||||
import { ArchiveOutlined, DeleteOutline } from '@mui/icons-material';
|
import DeleteOutline from '@mui/icons-material/DeleteOutline';
|
||||||
|
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
closeSidebar: () => void;
|
closeSidebar: () => void;
|
||||||
collectionSummaries: CollectionSummaries;
|
collectionSummaries: CollectionSummaries;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({
|
||||||
<Box position={'relative'} width="100%">
|
<Box position={'relative'} width="100%">
|
||||||
<Progressbar
|
<Progressbar
|
||||||
sx={{ backgroundColor: 'transparent' }}
|
sx={{ backgroundColor: 'transparent' }}
|
||||||
value={(userUsage * 100) / totalStorage}
|
value={Math.min((userUsage * 100) / totalStorage, 100)}
|
||||||
/>
|
/>
|
||||||
<Progressbar
|
<Progressbar
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -28,7 +28,7 @@ export function FamilyUsageProgressBar({
|
||||||
},
|
},
|
||||||
width: '100%',
|
width: '100%',
|
||||||
}}
|
}}
|
||||||
value={(totalUsage * 100) / totalStorage}
|
value={Math.min((totalUsage * 100) / totalStorage, 100)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -13,7 +13,7 @@ interface Iprops {
|
||||||
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
|
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
|
||||||
return (
|
return (
|
||||||
<Box width="100%">
|
<Box width="100%">
|
||||||
<Progressbar value={(usage * 100) / storage} />
|
<Progressbar value={Math.min((usage * 100) / storage, 100)} />
|
||||||
<SpaceBetweenFlex
|
<SpaceBetweenFlex
|
||||||
sx={{
|
sx={{
|
||||||
marginTop: 1.5,
|
marginTop: 1.5,
|
||||||
|
|
|
@ -26,21 +26,24 @@ export default function StorageSection({ usage, storage }: Iprops) {
|
||||||
<Typography variant="body2" color={'text.secondary'}>
|
<Typography variant="body2" color={'text.secondary'}>
|
||||||
{constants.STORAGE}
|
{constants.STORAGE}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
<DefaultBox>
|
||||||
<Typography
|
<Typography
|
||||||
fontWeight={'bold'}
|
fontWeight={'bold'}
|
||||||
sx={{ fontSize: '24px', lineHeight: '30px' }}>
|
sx={{ fontSize: '24px', lineHeight: '30px' }}>
|
||||||
<DefaultBox>
|
|
||||||
{`${makeHumanReadableStorage(usage, 'round-up')} ${
|
{`${makeHumanReadableStorage(usage, 'round-up')} ${
|
||||||
constants.OF
|
constants.OF
|
||||||
} ${makeHumanReadableStorage(storage)} ${constants.USED}`}
|
} ${makeHumanReadableStorage(storage)} ${constants.USED}`}
|
||||||
</DefaultBox>
|
</Typography>
|
||||||
<MobileSmallBox>
|
</DefaultBox>
|
||||||
|
<MobileSmallBox>
|
||||||
|
<Typography
|
||||||
|
fontWeight={'bold'}
|
||||||
|
sx={{ fontSize: '24px', lineHeight: '30px' }}>
|
||||||
{`${convertBytesToGBs(usage)} / ${convertBytesToGBs(
|
{`${convertBytesToGBs(usage)} / ${convertBytesToGBs(
|
||||||
storage
|
storage
|
||||||
)} ${constants.GB} ${constants.USED}`}
|
)} ${constants.GB} ${constants.USED}`}
|
||||||
</MobileSmallBox>
|
</Typography>
|
||||||
</Typography>
|
</MobileSmallBox>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@ import { ToggleButton, ToggleButtonGroup } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
import DarkModeIcon from '@mui/icons-material/DarkMode';
|
||||||
import LightModeIcon from '@mui/icons-material/LightMode';
|
import LightModeIcon from '@mui/icons-material/LightMode';
|
||||||
import { THEMES } from 'types/theme';
|
import { THEME_COLOR } from 'constants/theme';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
theme: THEMES;
|
theme: THEME_COLOR;
|
||||||
setTheme: (theme: THEMES) => void;
|
setTheme: (theme: THEME_COLOR) => void;
|
||||||
}
|
}
|
||||||
export default function ThemeSwitcher({ theme, setTheme }: Iprops) {
|
export default function ThemeSwitcher({ theme, setTheme }: Iprops) {
|
||||||
const handleChange = (event, theme: THEMES) => {
|
const handleChange = (event, theme: THEME_COLOR) => {
|
||||||
if (theme !== null) {
|
if (theme !== null) {
|
||||||
setTheme(theme);
|
setTheme(theme);
|
||||||
}
|
}
|
||||||
|
@ -20,10 +20,10 @@ export default function ThemeSwitcher({ theme, setTheme }: Iprops) {
|
||||||
value={theme}
|
value={theme}
|
||||||
exclusive
|
exclusive
|
||||||
onChange={handleChange}>
|
onChange={handleChange}>
|
||||||
<ToggleButton value={THEMES.LIGHT}>
|
<ToggleButton value={THEME_COLOR.LIGHT}>
|
||||||
<LightModeIcon />
|
<LightModeIcon />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
<ToggleButton value={THEMES.DARK}>
|
<ToggleButton value={THEME_COLOR.DARK}>
|
||||||
<DarkModeIcon />
|
<DarkModeIcon />
|
||||||
</ToggleButton>
|
</ToggleButton>
|
||||||
</ToggleButtonGroup>
|
</ToggleButtonGroup>
|
||||||
|
|
|
@ -11,6 +11,10 @@ import isElectron from 'is-electron';
|
||||||
import WatchFolder from 'components/WatchFolder';
|
import WatchFolder from 'components/WatchFolder';
|
||||||
import { getDownloadAppMessage } from 'utils/ui';
|
import { getDownloadAppMessage } from 'utils/ui';
|
||||||
|
|
||||||
|
import ThemeSwitcher from './ThemeSwitcher';
|
||||||
|
import { SpaceBetweenFlex } from 'components/Container';
|
||||||
|
import { isInternalUser } from 'utils/user';
|
||||||
|
|
||||||
export default function UtilitySection({ closeSidebar }) {
|
export default function UtilitySection({ closeSidebar }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const {
|
const {
|
||||||
|
@ -18,6 +22,8 @@ export default function UtilitySection({ closeSidebar }) {
|
||||||
startLoading,
|
startLoading,
|
||||||
watchFolderView,
|
watchFolderView,
|
||||||
setWatchFolderView,
|
setWatchFolderView,
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
} = useContext(AppContext);
|
} = useContext(AppContext);
|
||||||
|
|
||||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||||
|
@ -70,6 +76,12 @@ export default function UtilitySection({ closeSidebar }) {
|
||||||
<SidebarButton onClick={openRecoveryKeyModal}>
|
<SidebarButton onClick={openRecoveryKeyModal}>
|
||||||
{constants.RECOVERY_KEY}
|
{constants.RECOVERY_KEY}
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
|
{isInternalUser() && (
|
||||||
|
<SpaceBetweenFlex sx={{ px: 1.5 }}>
|
||||||
|
{constants.CHOSE_THEME}
|
||||||
|
<ThemeSwitcher theme={theme} setTheme={setTheme} />
|
||||||
|
</SpaceBetweenFlex>
|
||||||
|
)}
|
||||||
<SidebarButton onClick={openTwoFactorModal}>
|
<SidebarButton onClick={openTwoFactorModal}>
|
||||||
{constants.TWO_FACTOR}
|
{constants.TWO_FACTOR}
|
||||||
</SidebarButton>
|
</SidebarButton>
|
||||||
|
@ -85,7 +97,6 @@ export default function UtilitySection({ closeSidebar }) {
|
||||||
{/* <SidebarButton onClick={openThumbnailCompressModal}>
|
{/* <SidebarButton onClick={openThumbnailCompressModal}>
|
||||||
{constants.COMPRESS_THUMBNAILS}
|
{constants.COMPRESS_THUMBNAILS}
|
||||||
</SidebarButton> */}
|
</SidebarButton> */}
|
||||||
|
|
||||||
<RecoveryKey
|
<RecoveryKey
|
||||||
show={recoverModalView}
|
show={recoverModalView}
|
||||||
onHide={closeRecoveryKeyModal}
|
onHide={closeRecoveryKeyModal}
|
||||||
|
@ -98,7 +109,6 @@ export default function UtilitySection({ closeSidebar }) {
|
||||||
setLoading={startLoading}
|
setLoading={startLoading}
|
||||||
/>
|
/>
|
||||||
<WatchFolder open={watchFolderView} onClose={closeWatchFolder} />
|
<WatchFolder open={watchFolderView} onClose={closeWatchFolder} />
|
||||||
|
|
||||||
{/* <FixLargeThumbnails
|
{/* <FixLargeThumbnails
|
||||||
isOpen={fixLargeThumbsView}
|
isOpen={fixLargeThumbsView}
|
||||||
hide={() => setFixLargeThumbsView(false)}
|
hide={() => setFixLargeThumbsView(false)}
|
||||||
|
|
|
@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) {
|
||||||
{ email, passphrase, confirm }: FormValues,
|
{ email, passphrase, confirm }: FormValues,
|
||||||
{ setFieldError }: FormikHelpers<FormValues>
|
{ setFieldError }: FormikHelpers<FormValues>
|
||||||
) => {
|
) => {
|
||||||
setLoading(true);
|
|
||||||
try {
|
try {
|
||||||
|
if (passphrase !== confirm) {
|
||||||
|
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
setData(LS_KEYS.USER, { email });
|
setData(LS_KEYS.USER, { email });
|
||||||
await sendOtt(email);
|
await sendOtt(email);
|
||||||
|
@ -60,30 +64,23 @@ export default function SignUp(props: SignUpProps) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
if (passphrase === confirm) {
|
const { keyAttributes, masterKey } =
|
||||||
const { keyAttributes, masterKey } =
|
await generateKeyAttributes(passphrase);
|
||||||
await generateKeyAttributes(passphrase);
|
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
||||||
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
await generateAndSaveIntermediateKeyAttributes(
|
||||||
await generateAndSaveIntermediateKeyAttributes(
|
passphrase,
|
||||||
passphrase,
|
keyAttributes,
|
||||||
keyAttributes,
|
masterKey
|
||||||
masterKey
|
|
||||||
);
|
|
||||||
|
|
||||||
await saveKeyInSessionStore(
|
|
||||||
SESSION_KEYS.ENCRYPTION_KEY,
|
|
||||||
masterKey
|
|
||||||
);
|
|
||||||
setJustSignedUp(true);
|
|
||||||
router.push(PAGES.VERIFY);
|
|
||||||
} else {
|
|
||||||
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
setFieldError(
|
|
||||||
'passphrase',
|
|
||||||
constants.PASSWORD_GENERATION_FAILED
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await saveKeyInSessionStore(
|
||||||
|
SESSION_KEYS.ENCRYPTION_KEY,
|
||||||
|
masterKey
|
||||||
|
);
|
||||||
|
setJustSignedUp(true);
|
||||||
|
router.push(PAGES.VERIFY);
|
||||||
|
} catch (e) {
|
||||||
|
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
@ -27,6 +27,8 @@ export interface SingleInputFormProps {
|
||||||
caption?: any;
|
caption?: any;
|
||||||
hiddenPostInput?: any;
|
hiddenPostInput?: any;
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
|
blockButton?: boolean;
|
||||||
|
hiddenLabel?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SingleInputForm(props: SingleInputFormProps) {
|
export default function SingleInputForm(props: SingleInputFormProps) {
|
||||||
|
@ -87,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
||||||
<form noValidate onSubmit={handleSubmit}>
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
{props.hiddenPreInput}
|
{props.hiddenPreInput}
|
||||||
<TextField
|
<TextField
|
||||||
|
hiddenLabel={props.hiddenLabel}
|
||||||
variant="filled"
|
variant="filled"
|
||||||
fullWidth
|
fullWidth
|
||||||
type={showPassword ? 'text' : props.fieldType}
|
type={showPassword ? 'text' : props.fieldType}
|
||||||
id={props.fieldType}
|
id={props.fieldType}
|
||||||
name={props.fieldType}
|
name={props.fieldType}
|
||||||
label={props.placeholder}
|
{...(props.hiddenLabel
|
||||||
|
? { placeholder: props.placeholder }
|
||||||
|
: { label: props.placeholder })}
|
||||||
value={values.inputValue}
|
value={values.inputValue}
|
||||||
onChange={handleChange('inputValue')}
|
onChange={handleChange('inputValue')}
|
||||||
error={Boolean(errors.inputValue)}
|
error={Boolean(errors.inputValue)}
|
||||||
|
@ -124,21 +129,35 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
||||||
{props.caption}
|
{props.caption}
|
||||||
</FormHelperText>
|
</FormHelperText>
|
||||||
{props.hiddenPostInput}
|
{props.hiddenPostInput}
|
||||||
<FlexWrapper justifyContent={'flex-end'}>
|
<FlexWrapper
|
||||||
|
justifyContent={'flex-end'}
|
||||||
|
flexWrap={
|
||||||
|
props.blockButton ? 'wrap-reverse' : 'nowrap'
|
||||||
|
}>
|
||||||
{props.secondaryButtonAction && (
|
{props.secondaryButtonAction && (
|
||||||
<Button
|
<Button
|
||||||
onClick={props.secondaryButtonAction}
|
onClick={props.secondaryButtonAction}
|
||||||
size="large"
|
size="large"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{
|
sx={{
|
||||||
'&&&': { mt: 2, mb: 4, mr: 1, ...buttonSx },
|
'&&&': {
|
||||||
|
mt: !props.blockButton ? 2 : 0.5,
|
||||||
|
mb: !props.blockButton ? 4 : 0,
|
||||||
|
mr: !props.blockButton ? 1 : 0,
|
||||||
|
...buttonSx,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
{...restSubmitButtonProps}>
|
{...restSubmitButtonProps}>
|
||||||
{constants.CANCEL}
|
{constants.CANCEL}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
sx={{ '&&&': { mt: 2, ...buttonSx } }}
|
sx={{
|
||||||
|
'&&&': {
|
||||||
|
mt: 2,
|
||||||
|
...buttonSx,
|
||||||
|
},
|
||||||
|
}}
|
||||||
buttonText={props.buttonText}
|
buttonText={props.buttonText}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
{...restSubmitButtonProps}
|
{...restSubmitButtonProps}
|
||||||
|
|
|
@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
|
||||||
disabled={disabled || loading || success}
|
disabled={disabled || loading || success}
|
||||||
sx={{
|
sx={{
|
||||||
my: 4,
|
my: 4,
|
||||||
'&.Mui-disabled': {
|
...(loading
|
||||||
backgroundColor: (theme) => theme.palette.accent.main,
|
? {
|
||||||
color: (theme) => theme.palette.text.primary,
|
'&.Mui-disabled': {
|
||||||
},
|
backgroundColor: (theme) =>
|
||||||
|
theme.palette.accent.main,
|
||||||
|
color: (theme) => theme.palette.text.primary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
...sx,
|
...sx,
|
||||||
}}
|
}}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { ArrowBack, Close } from '@mui/icons-material';
|
import Close from '@mui/icons-material/Close';
|
||||||
|
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||||
import { Box, IconButton, Typography } from '@mui/material';
|
import { Box, IconButton, Typography } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FlexWrapper } from './Container';
|
import { FlexWrapper } from './Container';
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
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 FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import uploadManager from 'services/upload/uploadManager';
|
import uploadManager from 'services/upload/uploadManager';
|
||||||
|
|
||||||
const Wrapper = styled('div')`
|
const Wrapper = styled('div')<{ $disableShrink: boolean }>`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -14,22 +14,35 @@ const Wrapper = styled('div')`
|
||||||
& .mobile-button {
|
& .mobile-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@media (max-width: 624px) {
|
${({ $disableShrink }) =>
|
||||||
|
!$disableShrink &&
|
||||||
|
`@media (max-width: 624px) {
|
||||||
& .mobile-button {
|
& .mobile-button {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
& .desktop-button {
|
& .desktop-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
openUploader: () => void;
|
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 (
|
return (
|
||||||
<Wrapper
|
<Wrapper
|
||||||
|
$disableShrink={disableShrink}
|
||||||
style={{
|
style={{
|
||||||
cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
|
cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
|
||||||
}}>
|
}}>
|
||||||
|
@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) {
|
||||||
onClick={openUploader}
|
onClick={openUploader}
|
||||||
disabled={!uploadManager.shouldAllowNewUpload()}
|
disabled={!uploadManager.shouldAllowNewUpload()}
|
||||||
className="desktop-button"
|
className="desktop-button"
|
||||||
color="secondary"
|
color={color ?? 'secondary'}
|
||||||
startIcon={<FileUploadOutlinedIcon />}>
|
startIcon={icon ?? <FileUploadOutlinedIcon />}>
|
||||||
{constants.UPLOAD}
|
{text ?? constants.UPLOAD}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { NotUploadSectionHeader } from './styledComponents';
|
||||||
import UploadProgressContext from 'contexts/uploadProgress';
|
import UploadProgressContext from 'contexts/uploadProgress';
|
||||||
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
||||||
import { APP_DOWNLOAD_URL } from 'utils/common';
|
import { APP_DOWNLOAD_URL } from 'utils/common';
|
||||||
|
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
||||||
|
|
||||||
export function UploadProgressDialog() {
|
export function UploadProgressDialog() {
|
||||||
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
||||||
|
@ -26,7 +27,8 @@ export function UploadProgressDialog() {
|
||||||
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
|
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
|
||||||
?.length > 0 ||
|
?.length > 0 ||
|
||||||
finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.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);
|
setHasUnUploadedFiles(true);
|
||||||
} else {
|
} else {
|
||||||
|
@ -84,6 +86,13 @@ export function UploadProgressDialog() {
|
||||||
uploadResult={UPLOAD_RESULT.FAILED}
|
uploadResult={UPLOAD_RESULT.FAILED}
|
||||||
sectionTitle={constants.FAILED_UPLOADS}
|
sectionTitle={constants.FAILED_UPLOADS}
|
||||||
/>
|
/>
|
||||||
|
<ResultSection
|
||||||
|
uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
|
||||||
|
sectionTitle={constants.SKIPPED_VIDEOS}
|
||||||
|
sectionInfo={constants.SKIPPED_VIDEOS_INFO(
|
||||||
|
ENTE_WEBSITE_LINK
|
||||||
|
)}
|
||||||
|
/>
|
||||||
<ResultSection
|
<ResultSection
|
||||||
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
|
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
|
||||||
sectionTitle={constants.SKIPPED_FILES}
|
sectionTitle={constants.SKIPPED_FILES}
|
||||||
|
|
|
@ -1,19 +1,48 @@
|
||||||
import React from 'react';
|
import React, { useContext, useEffect, useRef } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
|
import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
|
||||||
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
|
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
|
||||||
import GoogleIcon from '@mui/icons-material/Google';
|
import GoogleIcon from '@mui/icons-material/Google';
|
||||||
import { UploadTypeOption } from './option';
|
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 { 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({
|
export default function UploadTypeSelector({
|
||||||
onHide,
|
onClose,
|
||||||
show,
|
show,
|
||||||
uploadFiles,
|
uploadFiles,
|
||||||
uploadFolders,
|
uploadFolders,
|
||||||
uploadGoogleTakeoutZips,
|
uploadGoogleTakeoutZips,
|
||||||
}) {
|
hideZipUploadOption,
|
||||||
|
}: Iprops) {
|
||||||
|
const publicCollectionGalleryContext = useContext(
|
||||||
|
PublicCollectionGalleryContext
|
||||||
|
);
|
||||||
|
const directlyShowUploadFiles = useRef(isMobileOrTable());
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (
|
||||||
|
show &&
|
||||||
|
directlyShowUploadFiles.current &&
|
||||||
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
|
) {
|
||||||
|
uploadFiles();
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
}, [show]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={show}
|
open={show}
|
||||||
|
@ -24,9 +53,11 @@ export default function UploadTypeSelector({
|
||||||
[theme.breakpoints.down(360)]: { p: 0 },
|
[theme.breakpoints.down(360)]: { p: 0 },
|
||||||
}),
|
}),
|
||||||
}}
|
}}
|
||||||
onClose={onHide}>
|
onClose={dialogCloseHandler({ onClose })}>
|
||||||
<DialogTitleWithCloseButton onClose={onHide}>
|
<DialogTitleWithCloseButton onClose={onClose}>
|
||||||
{constants.UPLOAD}
|
{publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
|
? constants.SELECT_PHOTOS
|
||||||
|
: constants.UPLOAD}
|
||||||
</DialogTitleWithCloseButton>
|
</DialogTitleWithCloseButton>
|
||||||
<Box p={1.5} pt={0.5}>
|
<Box p={1.5} pt={0.5}>
|
||||||
<Stack spacing={0.5}>
|
<Stack spacing={0.5}>
|
||||||
|
@ -40,11 +71,13 @@ export default function UploadTypeSelector({
|
||||||
startIcon={<FolderUploadIcon />}>
|
startIcon={<FolderUploadIcon />}>
|
||||||
{constants.UPLOAD_DIRS}
|
{constants.UPLOAD_DIRS}
|
||||||
</UploadTypeOption>
|
</UploadTypeOption>
|
||||||
<UploadTypeOption
|
{!hideZipUploadOption && (
|
||||||
onClick={uploadGoogleTakeoutZips}
|
<UploadTypeOption
|
||||||
startIcon={<GoogleIcon />}>
|
onClick={uploadGoogleTakeoutZips}
|
||||||
{constants.UPLOAD_GOOGLE_TAKEOUT}
|
startIcon={<GoogleIcon />}>
|
||||||
</UploadTypeOption>
|
{constants.UPLOAD_GOOGLE_TAKEOUT}
|
||||||
|
</UploadTypeOption>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
<Typography p={1.5} pt={4} color="text.secondary">
|
<Typography p={1.5} pt={4} color="text.secondary">
|
||||||
{constants.DRAG_AND_DROP_HINT}
|
{constants.DRAG_AND_DROP_HINT}
|
||||||
|
|
|
@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import UploadManager from 'services/upload/uploadManager';
|
|
||||||
import uploadManager from 'services/upload/uploadManager';
|
import uploadManager from 'services/upload/uploadManager';
|
||||||
import ImportService from 'services/importService';
|
import ImportService from 'services/importService';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
|
@ -40,7 +39,10 @@ import {
|
||||||
PICKED_UPLOAD_TYPE,
|
PICKED_UPLOAD_TYPE,
|
||||||
} from 'constants/upload';
|
} from 'constants/upload';
|
||||||
import importService from 'services/importService';
|
import importService from 'services/importService';
|
||||||
import { getDownloadAppMessage } from 'utils/ui';
|
import {
|
||||||
|
getDownloadAppMessage,
|
||||||
|
getRootLevelFileWithFolderNotAllowMessage,
|
||||||
|
} from 'utils/ui';
|
||||||
import UploadTypeSelector from './UploadTypeSelector';
|
import UploadTypeSelector from './UploadTypeSelector';
|
||||||
import {
|
import {
|
||||||
filterOutSystemFiles,
|
filterOutSystemFiles,
|
||||||
|
@ -50,21 +52,28 @@ import {
|
||||||
import { getUserOwnedCollections } from 'utils/collection';
|
import { getUserOwnedCollections } from 'utils/collection';
|
||||||
import billingService from 'services/billingService';
|
import billingService from 'services/billingService';
|
||||||
import { addLogLine } from 'utils/logging';
|
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';
|
const FIRST_ALBUM_NAME = 'My First Album';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||||
closeCollectionSelector: () => void;
|
closeCollectionSelector?: () => void;
|
||||||
closeUploadTypeSelector: () => void;
|
closeUploadTypeSelector: () => void;
|
||||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
|
||||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
setCollectionNamerAttributes?: SetCollectionNamerAttributes;
|
||||||
setLoading: SetLoading;
|
setLoading: SetLoading;
|
||||||
setShouldDisableDropzone: (value: boolean) => void;
|
setShouldDisableDropzone: (value: boolean) => void;
|
||||||
showCollectionSelector: () => void;
|
showCollectionSelector?: () => void;
|
||||||
setFiles: SetFiles;
|
setFiles: SetFiles;
|
||||||
setCollections: SetCollections;
|
setCollections?: SetCollections;
|
||||||
isFirstUpload: boolean;
|
isFirstUpload?: boolean;
|
||||||
uploadTypeSelectorView: boolean;
|
uploadTypeSelectorView: boolean;
|
||||||
showSessionExpiredMessage: () => void;
|
showSessionExpiredMessage: () => void;
|
||||||
showUploadFilesDialog: () => void;
|
showUploadFilesDialog: () => void;
|
||||||
|
@ -72,9 +81,17 @@ interface Props {
|
||||||
webFolderSelectorFiles: File[];
|
webFolderSelectorFiles: File[];
|
||||||
webFileSelectorFiles: File[];
|
webFileSelectorFiles: File[];
|
||||||
dragAndDropFiles: File[];
|
dragAndDropFiles: File[];
|
||||||
|
zipUploadDisabled?: boolean;
|
||||||
|
uploadCollection?: Collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Uploader(props: Props) {
|
export default function Uploader(props: Props) {
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
const publicCollectionGalleryContext = useContext(
|
||||||
|
PublicCollectionGalleryContext
|
||||||
|
);
|
||||||
|
|
||||||
const [uploadProgressView, setUploadProgressView] = useState(false);
|
const [uploadProgressView, setUploadProgressView] = useState(false);
|
||||||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
||||||
UPLOAD_STAGES.START
|
UPLOAD_STAGES.START
|
||||||
|
@ -93,11 +110,13 @@ export default function Uploader(props: Props) {
|
||||||
const [hasLivePhotos, setHasLivePhotos] = useState(false);
|
const [hasLivePhotos, setHasLivePhotos] = useState(false);
|
||||||
|
|
||||||
const [choiceModalView, setChoiceModalView] = useState(false);
|
const [choiceModalView, setChoiceModalView] = useState(false);
|
||||||
|
const [userNameInputDialogView, setUserNameInputDialogView] =
|
||||||
|
useState(false);
|
||||||
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
|
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
|
||||||
DEFAULT_IMPORT_SUGGESTION
|
DEFAULT_IMPORT_SUGGESTION
|
||||||
);
|
);
|
||||||
const appContext = useContext(AppContext);
|
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
|
||||||
const galleryContext = useContext(GalleryContext);
|
const [webFiles, setWebFiles] = useState([]);
|
||||||
|
|
||||||
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
|
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
|
||||||
const isPendingDesktopUpload = useRef(false);
|
const isPendingDesktopUpload = useRef(false);
|
||||||
|
@ -106,20 +125,33 @@ export default function Uploader(props: Props) {
|
||||||
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
|
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
|
||||||
const zipPaths = useRef<string[]>(null);
|
const zipPaths = useRef<string[]>(null);
|
||||||
const currentUploadPromise = useRef<Promise<void>>(null);
|
const currentUploadPromise = useRef<Promise<void>>(null);
|
||||||
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
|
const uploadRunning = useRef(false);
|
||||||
const [webFiles, setWebFiles] = useState([]);
|
const uploaderNameRef = useRef<string>(null);
|
||||||
|
|
||||||
const closeUploadProgress = () => setUploadProgressView(false);
|
const closeUploadProgress = () => setUploadProgressView(false);
|
||||||
|
const showUserNameInputDialog = () => setUserNameInputDialogView(true);
|
||||||
|
|
||||||
const setCollectionName = (collectionName: string) => {
|
const setCollectionName = (collectionName: string) => {
|
||||||
isPendingDesktopUpload.current = true;
|
isPendingDesktopUpload.current = true;
|
||||||
pendingDesktopUploadCollectionName.current = collectionName;
|
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(() => {
|
useEffect(() => {
|
||||||
UploadManager.init(
|
uploadManager.init(
|
||||||
{
|
{
|
||||||
setPercentComplete,
|
setPercentComplete,
|
||||||
setUploadCounter,
|
setUploadCounter,
|
||||||
|
@ -129,12 +161,16 @@ export default function Uploader(props: Props) {
|
||||||
setUploadFilenames: setUploadFileNames,
|
setUploadFilenames: setUploadFileNames,
|
||||||
setHasLivePhotos,
|
setHasLivePhotos,
|
||||||
},
|
},
|
||||||
props.setFiles
|
props.setFiles,
|
||||||
|
publicCollectionGalleryContext
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
|
if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
|
||||||
ImportService.getPendingUploads().then(
|
ImportService.getPendingUploads().then(
|
||||||
({ files: electronFiles, collectionName, type }) => {
|
({ files: electronFiles, collectionName, type }) => {
|
||||||
|
addLogLine(
|
||||||
|
`found pending desktop upload, resuming uploads`
|
||||||
|
);
|
||||||
resumeDesktopUpload(type, electronFiles, collectionName);
|
resumeDesktopUpload(type, electronFiles, collectionName);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@ -145,7 +181,11 @@ export default function Uploader(props: Props) {
|
||||||
appContext.setIsFolderSyncRunning
|
appContext.setIsFolderSyncRunning
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [
|
||||||
|
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||||
|
publicCollectionGalleryContext.token,
|
||||||
|
publicCollectionGalleryContext.passwordToken,
|
||||||
|
]);
|
||||||
|
|
||||||
// this handles the change of selectorFiles changes on web when user selects
|
// this handles the change of selectorFiles changes on web when user selects
|
||||||
// files for upload through the opened file/folder selector or dragAndDrop them
|
// files for upload through the opened file/folder selector or dragAndDrop them
|
||||||
|
@ -160,13 +200,16 @@ export default function Uploader(props: Props) {
|
||||||
pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
|
pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
|
||||||
props.webFolderSelectorFiles?.length > 0
|
props.webFolderSelectorFiles?.length > 0
|
||||||
) {
|
) {
|
||||||
|
addLogLine(`received folder upload request`);
|
||||||
setWebFiles(props.webFolderSelectorFiles);
|
setWebFiles(props.webFolderSelectorFiles);
|
||||||
} else if (
|
} else if (
|
||||||
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
|
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
|
||||||
props.webFileSelectorFiles?.length > 0
|
props.webFileSelectorFiles?.length > 0
|
||||||
) {
|
) {
|
||||||
|
addLogLine(`received file upload request`);
|
||||||
setWebFiles(props.webFileSelectorFiles);
|
setWebFiles(props.webFileSelectorFiles);
|
||||||
} else if (props.dragAndDropFiles?.length > 0) {
|
} else if (props.dragAndDropFiles?.length > 0) {
|
||||||
|
addLogLine(`received drag and drop upload request`);
|
||||||
setWebFiles(props.dragAndDropFiles);
|
setWebFiles(props.dragAndDropFiles);
|
||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
|
@ -181,17 +224,37 @@ export default function Uploader(props: Props) {
|
||||||
webFiles?.length > 0 ||
|
webFiles?.length > 0 ||
|
||||||
appContext.sharedFiles?.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 (uploadRunning.current) {
|
||||||
if (watchFolderService.isUploadRunning()) {
|
if (watchFolderService.isUploadRunning()) {
|
||||||
|
addLogLine(
|
||||||
|
'watchFolder upload was running, pausing it to run user upload'
|
||||||
|
);
|
||||||
// pause watch folder service on user upload
|
// pause watch folder service on user upload
|
||||||
watchFolderService.pauseRunningSync();
|
watchFolderService.pauseRunningSync();
|
||||||
} else {
|
} else {
|
||||||
|
addLogLine(
|
||||||
|
'an upload is already running, rejecting new upload request'
|
||||||
|
);
|
||||||
// no-op
|
// no-op
|
||||||
// a user upload is already in progress
|
// a user upload is already in progress
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isCanvasBlocked()) {
|
if (isCanvasBlocked()) {
|
||||||
|
addLogLine('canvas blocked, blocking upload');
|
||||||
appContext.setDialogMessage({
|
appContext.setDialogMessage({
|
||||||
title: constants.CANVAS_BLOCKED_TITLE,
|
title: constants.CANVAS_BLOCKED_TITLE,
|
||||||
|
|
||||||
|
@ -236,7 +299,8 @@ export default function Uploader(props: Props) {
|
||||||
handleCollectionCreationAndUpload(
|
handleCollectionCreationAndUpload(
|
||||||
importSuggestion,
|
importSuggestion,
|
||||||
props.isFirstUpload,
|
props.isFirstUpload,
|
||||||
pickedUploadType.current
|
pickedUploadType.current,
|
||||||
|
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||||
);
|
);
|
||||||
pickedUploadType.current = null;
|
pickedUploadType.current = null;
|
||||||
props.setLoading(false);
|
props.setLoading(false);
|
||||||
|
@ -257,14 +321,20 @@ export default function Uploader(props: Props) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const preCollectionCreationAction = async () => {
|
const preCollectionCreationAction = async () => {
|
||||||
props.closeCollectionSelector();
|
props.closeCollectionSelector?.();
|
||||||
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
|
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
|
||||||
setUploadStage(UPLOAD_STAGES.START);
|
setUploadStage(UPLOAD_STAGES.START);
|
||||||
setUploadProgressView(true);
|
setUploadProgressView(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const uploadFilesToExistingCollection = async (collection: Collection) => {
|
const uploadFilesToExistingCollection = async (
|
||||||
|
collection: Collection,
|
||||||
|
uploaderName?: string
|
||||||
|
) => {
|
||||||
try {
|
try {
|
||||||
|
addLogLine(
|
||||||
|
`upload file to an existing collection - "${collection.name}"`
|
||||||
|
);
|
||||||
await preCollectionCreationAction();
|
await preCollectionCreationAction();
|
||||||
const filesWithCollectionToUpload: FileWithCollection[] =
|
const filesWithCollectionToUpload: FileWithCollection[] =
|
||||||
toUploadFiles.current.map((file, index) => ({
|
toUploadFiles.current.map((file, index) => ({
|
||||||
|
@ -272,10 +342,11 @@ export default function Uploader(props: Props) {
|
||||||
localID: index,
|
localID: index,
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
}));
|
}));
|
||||||
waitInQueueAndUploadFiles(filesWithCollectionToUpload, [
|
waitInQueueAndUploadFiles(
|
||||||
collection,
|
filesWithCollectionToUpload,
|
||||||
]);
|
[collection],
|
||||||
toUploadFiles.current = null;
|
uploaderName
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'Failed to upload files to existing collections');
|
logError(e, 'Failed to upload files to existing collections');
|
||||||
}
|
}
|
||||||
|
@ -286,6 +357,9 @@ export default function Uploader(props: Props) {
|
||||||
collectionName?: string
|
collectionName?: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
addLogLine(
|
||||||
|
`upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
|
||||||
|
);
|
||||||
await preCollectionCreationAction();
|
await preCollectionCreationAction();
|
||||||
let filesWithCollectionToUpload: FileWithCollection[] = [];
|
let filesWithCollectionToUpload: FileWithCollection[] = [];
|
||||||
const collections: Collection[] = [];
|
const collections: Collection[] = [];
|
||||||
|
@ -303,6 +377,9 @@ export default function Uploader(props: Props) {
|
||||||
toUploadFiles.current
|
toUploadFiles.current
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
addLogLine(
|
||||||
|
`upload collections - [${[...collectionNameToFilesMap.keys()]}]`
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const existingCollection = getUserOwnedCollections(
|
const existingCollection = getUserOwnedCollections(
|
||||||
await syncCollections()
|
await syncCollections()
|
||||||
|
@ -350,13 +427,18 @@ export default function Uploader(props: Props) {
|
||||||
|
|
||||||
const waitInQueueAndUploadFiles = (
|
const waitInQueueAndUploadFiles = (
|
||||||
filesWithCollectionToUploadIn: FileWithCollection[],
|
filesWithCollectionToUploadIn: FileWithCollection[],
|
||||||
collections: Collection[]
|
collections: Collection[],
|
||||||
|
uploaderName?: string
|
||||||
) => {
|
) => {
|
||||||
const currentPromise = currentUploadPromise.current;
|
const currentPromise = currentUploadPromise.current;
|
||||||
currentUploadPromise.current = waitAndRun(
|
currentUploadPromise.current = waitAndRun(
|
||||||
currentPromise,
|
currentPromise,
|
||||||
async () =>
|
async () =>
|
||||||
await uploadFiles(filesWithCollectionToUploadIn, collections)
|
await uploadFiles(
|
||||||
|
filesWithCollectionToUploadIn,
|
||||||
|
collections,
|
||||||
|
uploaderName
|
||||||
|
)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -374,9 +456,11 @@ export default function Uploader(props: Props) {
|
||||||
|
|
||||||
const uploadFiles = async (
|
const uploadFiles = async (
|
||||||
filesWithCollectionToUploadIn: FileWithCollection[],
|
filesWithCollectionToUploadIn: FileWithCollection[],
|
||||||
collections: Collection[]
|
collections: Collection[],
|
||||||
|
uploaderName?: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
addLogLine('uploadFiles called');
|
||||||
preUploadAction();
|
preUploadAction();
|
||||||
if (
|
if (
|
||||||
isElectron() &&
|
isElectron() &&
|
||||||
|
@ -401,7 +485,8 @@ export default function Uploader(props: Props) {
|
||||||
const shouldCloseUploadProgress =
|
const shouldCloseUploadProgress =
|
||||||
await uploadManager.queueFilesForUpload(
|
await uploadManager.queueFilesForUpload(
|
||||||
filesWithCollectionToUploadIn,
|
filesWithCollectionToUploadIn,
|
||||||
collections
|
collections,
|
||||||
|
uploaderName
|
||||||
);
|
);
|
||||||
if (shouldCloseUploadProgress) {
|
if (shouldCloseUploadProgress) {
|
||||||
closeUploadProgress();
|
closeUploadProgress();
|
||||||
|
@ -418,6 +503,7 @@ export default function Uploader(props: Props) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logError(err, 'failed to upload files');
|
||||||
showUserFacingError(err.message);
|
showUserFacingError(err.message);
|
||||||
closeUploadProgress();
|
closeUploadProgress();
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -430,13 +516,16 @@ export default function Uploader(props: Props) {
|
||||||
try {
|
try {
|
||||||
addLogLine('user retrying failed upload');
|
addLogLine('user retrying failed upload');
|
||||||
const filesWithCollections =
|
const filesWithCollections =
|
||||||
await uploadManager.getFailedFilesWithCollections();
|
uploadManager.getFailedFilesWithCollections();
|
||||||
|
const uploaderName = uploadManager.getUploaderName();
|
||||||
await preUploadAction();
|
await preUploadAction();
|
||||||
await uploadManager.queueFilesForUpload(
|
await uploadManager.queueFilesForUpload(
|
||||||
filesWithCollections.files,
|
filesWithCollections.files,
|
||||||
filesWithCollections.collections
|
filesWithCollections.collections,
|
||||||
|
uploaderName
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
logError(err, 'retry failed files failed');
|
||||||
showUserFacingError(err.message);
|
showUserFacingError(err.message);
|
||||||
closeUploadProgress();
|
closeUploadProgress();
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -478,6 +567,7 @@ export default function Uploader(props: Props) {
|
||||||
|
|
||||||
const uploadToSingleNewCollection = (collectionName: string) => {
|
const uploadToSingleNewCollection = (collectionName: string) => {
|
||||||
if (collectionName) {
|
if (collectionName) {
|
||||||
|
addLogLine(`upload to single collection - "${collectionName}"`);
|
||||||
uploadFilesToNewCollections(
|
uploadFilesToNewCollections(
|
||||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||||
collectionName
|
collectionName
|
||||||
|
@ -495,45 +585,75 @@ export default function Uploader(props: Props) {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCollectionCreationAndUpload = (
|
const handleCollectionCreationAndUpload = async (
|
||||||
importSuggestion: ImportSuggestion,
|
importSuggestion: ImportSuggestion,
|
||||||
isFirstUpload: boolean,
|
isFirstUpload: boolean,
|
||||||
pickedUploadType: PICKED_UPLOAD_TYPE
|
pickedUploadType: PICKED_UPLOAD_TYPE,
|
||||||
|
accessedThroughSharedURL?: boolean
|
||||||
) => {
|
) => {
|
||||||
if (isPendingDesktopUpload.current) {
|
try {
|
||||||
isPendingDesktopUpload.current = false;
|
if (accessedThroughSharedURL) {
|
||||||
if (pendingDesktopUploadCollectionName.current) {
|
addLogLine(
|
||||||
uploadToSingleNewCollection(
|
`uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}`
|
||||||
pendingDesktopUploadCollectionName.current
|
|
||||||
);
|
);
|
||||||
pendingDesktopUploadCollectionName.current = null;
|
const uploaderName = await getPublicCollectionUploaderName(
|
||||||
} else {
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
|
||||||
|
addLogLine('uploading zip files');
|
||||||
uploadFilesToNewCollections(
|
uploadFilesToNewCollections(
|
||||||
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
||||||
);
|
);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return;
|
if (isFirstUpload && !importSuggestion.rootFolderName) {
|
||||||
|
importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
|
||||||
|
}
|
||||||
|
let showNextModal = () => {};
|
||||||
|
if (importSuggestion.hasNestedFolders) {
|
||||||
|
addLogLine(`nested folders detected`);
|
||||||
|
showNextModal = () => setChoiceModalView(true);
|
||||||
|
} else {
|
||||||
|
showNextModal = () =>
|
||||||
|
uploadToSingleNewCollection(
|
||||||
|
importSuggestion.rootFolderName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
props.setCollectionSelectorAttributes({
|
||||||
|
callback: uploadFilesToExistingCollection,
|
||||||
|
onCancel: handleCollectionSelectorCancel,
|
||||||
|
showNextModal,
|
||||||
|
title: constants.UPLOAD_TO_COLLECTION,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'handleCollectionCreationAndUpload failed');
|
||||||
}
|
}
|
||||||
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
|
|
||||||
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isFirstUpload && !importSuggestion.rootFolderName) {
|
|
||||||
importSuggestion.rootFolderName = FIRST_ALBUM_NAME;
|
|
||||||
}
|
|
||||||
let showNextModal = () => {};
|
|
||||||
if (importSuggestion.hasNestedFolders) {
|
|
||||||
showNextModal = () => setChoiceModalView(true);
|
|
||||||
} else {
|
|
||||||
showNextModal = () =>
|
|
||||||
uploadToSingleNewCollection(importSuggestion.rootFolderName);
|
|
||||||
}
|
|
||||||
props.setCollectionSelectorAttributes({
|
|
||||||
callback: uploadFilesToExistingCollection,
|
|
||||||
showNextModal,
|
|
||||||
title: constants.UPLOAD_TO_COLLECTION,
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => {
|
const handleDesktopUpload = async (type: PICKED_UPLOAD_TYPE) => {
|
||||||
let files: ElectronFile[];
|
let files: ElectronFile[];
|
||||||
pickedUploadType.current = type;
|
pickedUploadType.current = type;
|
||||||
|
@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
|
||||||
zipPaths.current = response.zipPaths;
|
zipPaths.current = response.zipPaths;
|
||||||
}
|
}
|
||||||
if (files?.length > 0) {
|
if (files?.length > 0) {
|
||||||
|
addLogLine(
|
||||||
|
` desktop upload for type:${type} and fileCount: ${files?.length} requested`
|
||||||
|
);
|
||||||
setElectronFiles(files);
|
setElectronFiles(files);
|
||||||
props.closeUploadTypeSelector();
|
props.closeUploadTypeSelector();
|
||||||
}
|
}
|
||||||
|
@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
|
||||||
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
|
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
|
||||||
const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS);
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<UploadStrategyChoiceModal
|
<UploadStrategyChoiceModal
|
||||||
open={choiceModalView}
|
open={choiceModalView}
|
||||||
onClose={() => setChoiceModalView(false)}
|
onClose={handleChoiceModalClose}
|
||||||
uploadToSingleCollection={() =>
|
uploadToSingleCollection={handleUploadToSingleCollection}
|
||||||
uploadToSingleNewCollection(importSuggestion.rootFolderName)
|
uploadToMultipleCollection={handleUploadToMultipleCollections}
|
||||||
}
|
|
||||||
uploadToMultipleCollection={() =>
|
|
||||||
uploadFilesToNewCollections(
|
|
||||||
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<UploadTypeSelector
|
<UploadTypeSelector
|
||||||
show={props.uploadTypeSelectorView}
|
show={props.uploadTypeSelectorView}
|
||||||
onHide={props.closeUploadTypeSelector}
|
onClose={props.closeUploadTypeSelector}
|
||||||
uploadFiles={handleFileUpload}
|
uploadFiles={handleFileUpload}
|
||||||
uploadFolders={handleFolderUpload}
|
uploadFolders={handleFolderUpload}
|
||||||
uploadGoogleTakeoutZips={handleZipUpload}
|
uploadGoogleTakeoutZips={handleZipUpload}
|
||||||
|
hideZipUploadOption={props.zipUploadDisabled}
|
||||||
/>
|
/>
|
||||||
<UploadProgress
|
<UploadProgress
|
||||||
open={uploadProgressView}
|
open={uploadProgressView}
|
||||||
|
@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
|
||||||
finishedUploads={finishedUploads}
|
finishedUploads={finishedUploads}
|
||||||
cancelUploads={cancelUploads}
|
cancelUploads={cancelUploads}
|
||||||
/>
|
/>
|
||||||
|
<UserNameInputDialog
|
||||||
|
open={userNameInputDialogView}
|
||||||
|
onClose={handleUserNameInputDialogClose}
|
||||||
|
onNameSubmit={handlePublicUpload}
|
||||||
|
toUploadFilesCount={toUploadFiles.current?.length}
|
||||||
|
uploaderName={uploaderNameRef.current}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
43
src/components/UserNameInputDialog.tsx
Normal file
43
src/components/UserNameInputDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import SingleInputForm, {
|
import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
|
@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
|
||||||
|
|
||||||
import { Input } from '@mui/material';
|
import { Input } from '@mui/material';
|
||||||
import { KeyAttributes, User } from 'types/user';
|
import { KeyAttributes, User } from 'types/user';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
export interface VerifyMasterPasswordFormProps {
|
export interface VerifyMasterPasswordFormProps {
|
||||||
user: User;
|
user: User;
|
||||||
|
@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
|
||||||
setFieldError
|
setFieldError
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
let kek: string = null;
|
let kek: string = null;
|
||||||
try {
|
try {
|
||||||
kek = await cryptoWorker.deriveKey(
|
kek = await cryptoWorker.deriveKey(
|
||||||
|
@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
|
||||||
throw Error(CustomError.WEAK_DEVICE);
|
throw Error(CustomError.WEAK_DEVICE);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const key: string = await cryptoWorker.decryptB64(
|
const key = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.encryptedKey,
|
keyAttributes.encryptedKey,
|
||||||
keyAttributes.keyDecryptionNonce,
|
keyAttributes.keyDecryptionNonce,
|
||||||
kek
|
kek
|
||||||
|
|
14
src/components/icons/ente.tsx
Normal file
14
src/components/icons/ente.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import NavbarBase from 'components/Navbar/base';
|
import NavbarBase from 'components/Navbar/base';
|
||||||
import SidebarToggler from 'components/Navbar/SidebarToggler';
|
import SidebarToggler from 'components/Navbar/SidebarToggler';
|
||||||
import { getNonTrashedUniqueUserFiles } from 'utils/file';
|
|
||||||
import SearchBar from 'components/Search/SearchBar';
|
import SearchBar from 'components/Search/SearchBar';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
|
@ -36,7 +35,7 @@ export function GalleryNavbar({
|
||||||
isInSearchMode={isInSearchMode}
|
isInSearchMode={isInSearchMode}
|
||||||
setIsInSearchMode={setIsInSearchMode}
|
setIsInSearchMode={setIsInSearchMode}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
files={getNonTrashedUniqueUserFiles(files)}
|
files={files}
|
||||||
setActiveCollection={setActiveCollection}
|
setActiveCollection={setActiveCollection}
|
||||||
updateSearch={updateSearch}
|
updateSearch={updateSearch}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { Stack } from '@mui/material';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Subscription } from 'types/billing';
|
import { Subscription } from 'types/billing';
|
||||||
|
import { SetLoading } from 'types/gallery';
|
||||||
import {
|
import {
|
||||||
activateSubscription,
|
activateSubscription,
|
||||||
cancelSubscription,
|
cancelSubscription,
|
||||||
|
@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
subscription: Subscription;
|
subscription: Subscription;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
setLoading: (value: boolean) => void;
|
setLoading: SetLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ManageSubscription({
|
export function ManageSubscription({
|
||||||
|
|
|
@ -32,7 +32,7 @@ interface IProps {
|
||||||
showPlaceholder: boolean;
|
showPlaceholder: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Check = styled('input')<{ active: boolean }>`
|
const Check = styled('input')<{ $active: boolean }>`
|
||||||
appearance: none;
|
appearance: none;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
@ -85,7 +85,7 @@ const Check = styled('input')<{ active: boolean }>`
|
||||||
border-right: 2px solid #ddd;
|
border-right: 2px solid #ddd;
|
||||||
border-bottom: 2px solid #ddd;
|
border-bottom: 2px solid #ddd;
|
||||||
}
|
}
|
||||||
${(props) => props.active && 'opacity: 0.5 '};
|
${(props) => props.$active && 'opacity: 0.5 '};
|
||||||
&:checked {
|
&:checked {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
@ -104,15 +104,15 @@ export const HoverOverlay = styled('div')<{ checked: boolean }>`
|
||||||
'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'};
|
'background:linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0))'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const InSelectRangeOverLay = styled('div')<{ active: boolean }>`
|
export const InSelectRangeOverLay = styled('div')<{ $active: boolean }>`
|
||||||
opacity: ${(props) => (!props.active ? 0 : 1)};
|
opacity: ${(props) => (!props.$active ? 0 : 1)};
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 0;
|
top: 0;
|
||||||
outline: none;
|
outline: none;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
${(props) => props.active && 'background:rgba(81, 205, 124, 0.25)'};
|
${(props) => props.$active && 'background:rgba(81, 205, 124, 0.25)'};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const FileAndCollectionNameOverlay = styled('div')`
|
export const FileAndCollectionNameOverlay = styled('div')`
|
||||||
|
@ -304,7 +304,7 @@ export default function PreviewCard(props: IProps) {
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selected}
|
checked={selected}
|
||||||
onChange={handleSelect}
|
onChange={handleSelect}
|
||||||
active={isRangeSelectActive && isInsSelectRange}
|
$active={isRangeSelectActive && isInsSelectRange}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -313,7 +313,7 @@ export default function PreviewCard(props: IProps) {
|
||||||
<SelectedOverlay selected={selected} />
|
<SelectedOverlay selected={selected} />
|
||||||
<HoverOverlay checked={selected} />
|
<HoverOverlay checked={selected} />
|
||||||
<InSelectRangeOverLay
|
<InSelectRangeOverLay
|
||||||
active={isRangeSelectActive && isInsSelectRange}
|
$active={isRangeSelectActive && isInsSelectRange}
|
||||||
/>
|
/>
|
||||||
{isLivePhoto(file) && <LivePhotoIndicator />}
|
{isLivePhoto(file) && <LivePhotoIndicator />}
|
||||||
{deduplicateContext.isOnDeduplicatePage && (
|
{deduplicateContext.isOnDeduplicatePage && (
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, styled } from '@mui/material';
|
import { Button, styled } from '@mui/material';
|
||||||
import GetDeviceOS, { OS } from 'utils/common/deviceDetection';
|
import { getDeviceOS, OS } from 'utils/common/deviceDetection';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
|
||||||
export const NoStyleAnchor = styled('a')`
|
export const NoStyleAnchor = styled('a')`
|
||||||
|
@ -16,7 +16,7 @@ function GoToEnte() {
|
||||||
const [os, setOS] = useState<OS>(OS.UNKNOWN);
|
const [os, setOS] = useState<OS>(OS.UNKNOWN);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const os = GetDeviceOS();
|
const os = getDeviceOS();
|
||||||
setOS(os);
|
setOS(os);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,25 @@
|
||||||
import { CenteredFlex, FluidContainer } from 'components/Container';
|
import { EnteLinkLogo } from 'components/Navbar/EnteLinkLogo';
|
||||||
import { EnteLogo } from 'components/EnteLogo';
|
import { FluidContainer } from 'components/Container';
|
||||||
import NavbarBase from 'components/Navbar/base';
|
import NavbarBase from 'components/Navbar/base';
|
||||||
|
import UploadButton from 'components/Upload/UploadButton';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
import GoToEnte from './GoToEnte';
|
import GoToEnte from './GoToEnte';
|
||||||
|
|
||||||
export default function SharedAlbumNavbar() {
|
export default function SharedAlbumNavbar({ showUploadButton, openUploader }) {
|
||||||
return (
|
return (
|
||||||
<NavbarBase>
|
<NavbarBase>
|
||||||
<FluidContainer>
|
<FluidContainer>
|
||||||
<CenteredFlex>
|
<EnteLinkLogo />
|
||||||
<EnteLogo />
|
|
||||||
</CenteredFlex>
|
|
||||||
</FluidContainer>
|
</FluidContainer>
|
||||||
<GoToEnte />
|
{showUploadButton ? (
|
||||||
|
<UploadButton
|
||||||
|
openUploader={openUploader}
|
||||||
|
text={constants.ADD_PHOTOS}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GoToEnte />
|
||||||
|
)}
|
||||||
</NavbarBase>
|
</NavbarBase>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,13 +15,14 @@ export enum CollectionSummaryType {
|
||||||
archive = 'archive',
|
archive = 'archive',
|
||||||
trash = 'trash',
|
trash = 'trash',
|
||||||
all = 'all',
|
all = 'all',
|
||||||
shared = 'shared',
|
outgoingShare = 'outgoingShare',
|
||||||
|
incomingShare = 'incomingShare',
|
||||||
|
sharedOnlyViaLink = 'sharedOnlyViaLink',
|
||||||
archived = 'archived',
|
archived = 'archived',
|
||||||
}
|
}
|
||||||
export enum COLLECTION_SORT_BY {
|
export enum COLLECTION_SORT_BY {
|
||||||
NAME,
|
NAME,
|
||||||
CREATION_TIME_ASCENDING,
|
CREATION_TIME_ASCENDING,
|
||||||
CREATION_TIME_DESCENDING,
|
|
||||||
UPDATION_TIME_DESCENDING,
|
UPDATION_TIME_DESCENDING,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +35,9 @@ export const COLLECTION_SORT_ORDER = new Map([
|
||||||
[CollectionSummaryType.favorites, 1],
|
[CollectionSummaryType.favorites, 1],
|
||||||
[CollectionSummaryType.album, 2],
|
[CollectionSummaryType.album, 2],
|
||||||
[CollectionSummaryType.folder, 2],
|
[CollectionSummaryType.folder, 2],
|
||||||
[CollectionSummaryType.shared, 2],
|
[CollectionSummaryType.incomingShare, 2],
|
||||||
|
[CollectionSummaryType.outgoingShare, 2],
|
||||||
|
[CollectionSummaryType.sharedOnlyViaLink, 2],
|
||||||
[CollectionSummaryType.archived, 2],
|
[CollectionSummaryType.archived, 2],
|
||||||
[CollectionSummaryType.archive, 3],
|
[CollectionSummaryType.archive, 3],
|
||||||
[CollectionSummaryType.trash, 4],
|
[CollectionSummaryType.trash, 4],
|
||||||
|
@ -49,15 +52,15 @@ export const SYSTEM_COLLECTION_TYPES = new Set([
|
||||||
export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([
|
export const UPLOAD_NOT_ALLOWED_COLLECTION_TYPES = new Set([
|
||||||
CollectionSummaryType.all,
|
CollectionSummaryType.all,
|
||||||
CollectionSummaryType.archive,
|
CollectionSummaryType.archive,
|
||||||
CollectionSummaryType.shared,
|
CollectionSummaryType.incomingShare,
|
||||||
|
CollectionSummaryType.outgoingShare,
|
||||||
|
CollectionSummaryType.sharedOnlyViaLink,
|
||||||
CollectionSummaryType.trash,
|
CollectionSummaryType.trash,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
|
export const OPTIONS_NOT_HAVING_COLLECTION_TYPES = new Set([
|
||||||
CollectionSummaryType.all,
|
CollectionSummaryType.all,
|
||||||
CollectionSummaryType.archive,
|
CollectionSummaryType.archive,
|
||||||
CollectionSummaryType.shared,
|
|
||||||
CollectionSummaryType.favorites,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([
|
export const HIDE_FROM_COLLECTION_BAR_TYPES = new Set([
|
||||||
|
|
|
@ -2,7 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||||
|
|
||||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||||
export const MAX_CAPTION_SIZE = 280;
|
export const MAX_CAPTION_SIZE = 5000;
|
||||||
export const MAX_TRASH_BATCH_SIZE = 1000;
|
export const MAX_TRASH_BATCH_SIZE = 1000;
|
||||||
|
|
||||||
export const TYPE_HEIC = 'heic';
|
export const TYPE_HEIC = 'heic';
|
||||||
|
|
|
@ -15,7 +15,3 @@ export enum PAGES {
|
||||||
SHARED_ALBUMS = '/shared-albums',
|
SHARED_ALBUMS = '/shared-albums',
|
||||||
DEDUPLICATE = '/deduplicate',
|
DEDUPLICATE = '/deduplicate',
|
||||||
}
|
}
|
||||||
export const getAlbumSiteHost = () =>
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? 'albums.ente.io'
|
|
||||||
: `${window.location.hostname}:3002`;
|
|
||||||
|
|
4
src/constants/theme/index.ts
Normal file
4
src/constants/theme/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export enum THEME_COLOR {
|
||||||
|
LIGHT = 'light',
|
||||||
|
DARK = 'dark',
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ export const FORMAT_MISSED_BY_FILE_TYPE_LIB = [
|
||||||
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' },
|
{ fileType: FILE_TYPE.IMAGE, exactType: 'jpg', mimeType: 'image/jpeg' },
|
||||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
|
{ fileType: FILE_TYPE.VIDEO, exactType: 'webm', mimeType: 'video/webm' },
|
||||||
{ fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' },
|
{ fileType: FILE_TYPE.VIDEO, exactType: 'mod', mimeType: 'video/mpeg' },
|
||||||
|
{ fileType: FILE_TYPE.VIDEO, exactType: 'mp4', mimeType: 'video/mp4' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
// this is the chunk size of the un-encrypted file which is read and encrypted before uploading it as a single part.
|
||||||
|
@ -52,6 +53,7 @@ export enum UPLOAD_RESULT {
|
||||||
UPLOADED_WITH_STATIC_THUMBNAIL,
|
UPLOADED_WITH_STATIC_THUMBNAIL,
|
||||||
ADDED_SYMLINK,
|
ADDED_SYMLINK,
|
||||||
CANCELLED,
|
CANCELLED,
|
||||||
|
SKIPPED_VIDEOS,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PICKED_UPLOAD_TYPE {
|
export enum PICKED_UPLOAD_TYPE {
|
||||||
|
@ -76,6 +78,7 @@ export const USE_CF_PROXY = false;
|
||||||
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
|
export const DEFAULT_IMPORT_SUGGESTION: ImportSuggestion = {
|
||||||
rootFolderName: '',
|
rootFolderName: '',
|
||||||
hasNestedFolders: false,
|
hasNestedFolders: false,
|
||||||
|
hasRootLevelFileWithFolder: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const BLACK_THUMBNAIL_BASE64 =
|
export const BLACK_THUMBNAIL_BASE64 =
|
||||||
|
|
|
@ -16,6 +16,7 @@ import LoadingBar from 'react-top-loading-bar';
|
||||||
import DialogBox from 'components/DialogBox';
|
import DialogBox from 'components/DialogBox';
|
||||||
import { styled, ThemeProvider } from '@mui/material/styles';
|
import { styled, ThemeProvider } from '@mui/material/styles';
|
||||||
import darkThemeOptions from 'themes/darkThemeOptions';
|
import darkThemeOptions from 'themes/darkThemeOptions';
|
||||||
|
import lightThemeOptions from 'themes/lightThemeOptions';
|
||||||
import { CssBaseline, useMediaQuery } from '@mui/material';
|
import { CssBaseline, useMediaQuery } from '@mui/material';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
import * as types from 'styled-components/cssprop'; // need to css prop on styled component
|
import * as types from 'styled-components/cssprop'; // need to css prop on styled component
|
||||||
|
@ -44,6 +45,9 @@ import ArrowForward from '@mui/icons-material/ArrowForward';
|
||||||
import { AppUpdateInfo } from 'types/electron';
|
import { AppUpdateInfo } from 'types/electron';
|
||||||
import { getSentryUserID } from 'utils/user';
|
import { getSentryUserID } from 'utils/user';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
|
import { SetTheme } from 'types/theme';
|
||||||
|
import { useLocalState } from 'hooks/useLocalState';
|
||||||
|
import { THEME_COLOR } from 'constants/theme';
|
||||||
|
|
||||||
export const MessageContainer = styled('div')`
|
export const MessageContainer = styled('div')`
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
|
@ -77,6 +81,8 @@ type AppContextType = {
|
||||||
watchFolderFiles: FileList;
|
watchFolderFiles: FileList;
|
||||||
setWatchFolderFiles: (files: FileList) => void;
|
setWatchFolderFiles: (files: FileList) => void;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
|
theme: THEME_COLOR;
|
||||||
|
setTheme: SetTheme;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum FLASH_MESSAGE_TYPE {
|
export enum FLASH_MESSAGE_TYPE {
|
||||||
|
@ -119,6 +125,7 @@ export default function App({ Component, err }) {
|
||||||
const closeNotification = () => setNotificationView(false);
|
const closeNotification = () => setNotificationView(false);
|
||||||
const [notificationAttributes, setNotificationAttributes] =
|
const [notificationAttributes, setNotificationAttributes] =
|
||||||
useState<NotificationAttributes>(null);
|
useState<NotificationAttributes>(null);
|
||||||
|
const [theme, setTheme] = useLocalState(LS_KEYS.THEME, THEME_COLOR.DARK);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
HTTPService.getInterceptors().response.use(
|
HTTPService.getInterceptors().response.use(
|
||||||
|
@ -266,7 +273,12 @@ export default function App({ Component, err }) {
|
||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<ThemeProvider theme={darkThemeOptions}>
|
<ThemeProvider
|
||||||
|
theme={
|
||||||
|
theme === THEME_COLOR.DARK
|
||||||
|
? darkThemeOptions
|
||||||
|
: lightThemeOptions
|
||||||
|
}>
|
||||||
<CssBaseline enableColorScheme />
|
<CssBaseline enableColorScheme />
|
||||||
{showNavbar && <AppNavbar />}
|
{showNavbar && <AppNavbar />}
|
||||||
<MessageContainer>
|
<MessageContainer>
|
||||||
|
@ -325,6 +337,8 @@ export default function App({ Component, err }) {
|
||||||
setWatchFolderFiles,
|
setWatchFolderFiles,
|
||||||
isMobile,
|
isMobile,
|
||||||
setNotificationAttributes,
|
setNotificationAttributes,
|
||||||
|
theme,
|
||||||
|
setTheme,
|
||||||
}}>
|
}}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<VerticallyCentered>
|
<VerticallyCentered>
|
||||||
|
|
|
@ -2,10 +2,9 @@ import React, { useState, useEffect } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import CryptoWorker, {
|
import {
|
||||||
saveKeyInSessionStore,
|
saveKeyInSessionStore,
|
||||||
generateAndSaveIntermediateKeyAttributes,
|
generateAndSaveIntermediateKeyAttributes,
|
||||||
B64EncryptionResult,
|
|
||||||
} from 'utils/crypto';
|
} from 'utils/crypto';
|
||||||
import { getActualKey } from 'utils/common/key';
|
import { getActualKey } from 'utils/common/key';
|
||||||
import { setKeys } from 'services/userService';
|
import { setKeys } from 'services/userService';
|
||||||
|
@ -14,12 +13,13 @@ import SetPasswordForm, {
|
||||||
} from 'components/SetPasswordForm';
|
} from 'components/SetPasswordForm';
|
||||||
import { SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
import { PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import { KEK, UpdatedKey, User } from 'types/user';
|
import { KEK, KeyAttributes, UpdatedKey, User } from 'types/user';
|
||||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
import VerticallyCentered from 'components/Container';
|
import VerticallyCentered from 'components/Container';
|
||||||
import FormPaper from 'components/Form/FormPaper';
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
export default function ChangePassword() {
|
export default function ChangePassword() {
|
||||||
const [token, setToken] = useState<string>();
|
const [token, setToken] = useState<string>();
|
||||||
|
@ -40,10 +40,10 @@ export default function ChangePassword() {
|
||||||
passphrase,
|
passphrase,
|
||||||
setFieldError
|
setFieldError
|
||||||
) => {
|
) => {
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const key: string = await getActualKey();
|
const key = await getActualKey();
|
||||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
const keyAttributes: KeyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||||
let kek: KEK;
|
let kek: KEK;
|
||||||
try {
|
try {
|
||||||
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
kek = await cryptoWorker.deriveSensitiveKey(passphrase, kekSalt);
|
||||||
|
@ -51,8 +51,10 @@ export default function ChangePassword() {
|
||||||
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const encryptedKeyAttributes: B64EncryptionResult =
|
const encryptedKeyAttributes = await cryptoWorker.encryptToB64(
|
||||||
await cryptoWorker.encryptToB64(key, kek.key);
|
key,
|
||||||
|
kek.key
|
||||||
|
);
|
||||||
const updatedKey: UpdatedKey = {
|
const updatedKey: UpdatedKey = {
|
||||||
kekSalt,
|
kekSalt,
|
||||||
encryptedKey: encryptedKeyAttributes.encryptedData,
|
encryptedKey: encryptedKeyAttributes.encryptedData,
|
||||||
|
|
|
@ -34,7 +34,7 @@ import {
|
||||||
setIsFirstLogin,
|
setIsFirstLogin,
|
||||||
setJustSignedUp,
|
setJustSignedUp,
|
||||||
} from 'utils/storage';
|
} from 'utils/storage';
|
||||||
import { isTokenValid, logoutUser } from 'services/userService';
|
import { isTokenValid, logoutUser, validateKey } from 'services/userService';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
|
@ -42,7 +42,7 @@ import PhotoFrame from 'components/PhotoFrame';
|
||||||
import {
|
import {
|
||||||
changeFilesVisibility,
|
changeFilesVisibility,
|
||||||
downloadFiles,
|
downloadFiles,
|
||||||
getNonTrashedUniqueUserFiles,
|
getNonTrashedFiles,
|
||||||
getSelectedFiles,
|
getSelectedFiles,
|
||||||
mergeMetadata,
|
mergeMetadata,
|
||||||
sortFiles,
|
sortFiles,
|
||||||
|
@ -87,7 +87,7 @@ import FixCreationTime, {
|
||||||
} from 'components/FixCreationTime';
|
} from 'components/FixCreationTime';
|
||||||
import { Collection, CollectionSummaries } from 'types/collection';
|
import { Collection, CollectionSummaries } from 'types/collection';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { GalleryContextType, SelectedState } from 'types/gallery';
|
import { GalleryContextType, SelectedState, SetFiles } from 'types/gallery';
|
||||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||||
import Collections from 'components/Collections';
|
import Collections from 'components/Collections';
|
||||||
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
||||||
|
@ -124,12 +124,45 @@ export const GalleryContext = createContext<GalleryContextType>(
|
||||||
defaultGalleryContext
|
defaultGalleryContext
|
||||||
);
|
);
|
||||||
|
|
||||||
|
type FilesFn = EnteFile[] | ((files: EnteFile[]) => EnteFile[]);
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
const [collections, setCollections] = useState<Collection[]>(null);
|
const [collections, setCollections] = useState<Collection[]>(null);
|
||||||
|
const [files, setFilesOriginal] = useState<EnteFile[]>(null);
|
||||||
|
|
||||||
|
const filesUpdateInProgress = useRef(false);
|
||||||
|
const newerFilesFN = useRef<FilesFn>(null);
|
||||||
|
|
||||||
|
const setFilesOriginalWithReSyncIfRequired: SetFiles = (filesFn) => {
|
||||||
|
setFilesOriginal(filesFn);
|
||||||
|
filesUpdateInProgress.current = false;
|
||||||
|
if (newerFilesFN.current) {
|
||||||
|
const newerFiles = newerFilesFN.current;
|
||||||
|
setTimeout(() => setFiles(newerFiles), 0);
|
||||||
|
newerFilesFN.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setFiles: SetFiles = async (filesFn) => {
|
||||||
|
if (filesUpdateInProgress.current) {
|
||||||
|
newerFilesFN.current = filesFn;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
filesUpdateInProgress.current = true;
|
||||||
|
|
||||||
|
if (!files?.length || files.length < 5000) {
|
||||||
|
setFilesOriginalWithReSyncIfRequired(filesFn);
|
||||||
|
} else {
|
||||||
|
const waitTime = getData(LS_KEYS.WAIT_TIME) ?? 5000;
|
||||||
|
setTimeout(
|
||||||
|
() => setFilesOriginalWithReSyncIfRequired(filesFn),
|
||||||
|
waitTime
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const [files, setFiles] = useState<EnteFile[]>(null);
|
|
||||||
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
const [favItemIds, setFavItemIds] = useState<Set<number>>();
|
||||||
|
|
||||||
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
const [isFirstLoad, setIsFirstLoad] = useState(false);
|
||||||
|
@ -225,7 +258,12 @@ export default function Gallery() {
|
||||||
router.push(PAGES.ROOT);
|
router.push(PAGES.ROOT);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
preloadImage('/images/subscription-card-background');
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
const valid = await validateKey();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setActiveCollection(ALL_SECTION);
|
setActiveCollection(ALL_SECTION);
|
||||||
setIsFirstLoad(isFirstLogin());
|
setIsFirstLoad(isFirstLogin());
|
||||||
setIsFirstFetch(true);
|
setIsFirstFetch(true);
|
||||||
|
@ -245,7 +283,6 @@ export default function Gallery() {
|
||||||
setIsFirstLoad(false);
|
setIsFirstLoad(false);
|
||||||
setJustSignedUp(false);
|
setJustSignedUp(false);
|
||||||
setIsFirstFetch(false);
|
setIsFirstFetch(false);
|
||||||
preloadImage('/images/subscription-card-background');
|
|
||||||
};
|
};
|
||||||
main();
|
main();
|
||||||
}, []);
|
}, []);
|
||||||
|
@ -307,7 +344,7 @@ export default function Gallery() {
|
||||||
searchResultSummary={searchResultSummary}
|
searchResultSummary={searchResultSummary}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.HEADER,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [isInSearchMode, searchResultSummary]);
|
}, [isInSearchMode, searchResultSummary]);
|
||||||
|
@ -329,6 +366,7 @@ export default function Gallery() {
|
||||||
let files = await syncFiles(collections, setFiles);
|
let files = await syncFiles(collections, setFiles);
|
||||||
const trash = await syncTrash(collections, setFiles, files);
|
const trash = await syncTrash(collections, setFiles, files);
|
||||||
files = [...files, ...getTrashedFiles(trash)];
|
files = [...files, ...getTrashedFiles(trash)];
|
||||||
|
setFiles(sortFiles(files));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'syncWithRemote failed');
|
logError(e, 'syncWithRemote failed');
|
||||||
switch (e.message) {
|
switch (e.message) {
|
||||||
|
@ -511,7 +549,6 @@ export default function Gallery() {
|
||||||
if (newSearch?.collection) {
|
if (newSearch?.collection) {
|
||||||
setActiveCollection(newSearch?.collection);
|
setActiveCollection(newSearch?.collection);
|
||||||
} else {
|
} else {
|
||||||
setActiveCollection(ALL_SECTION);
|
|
||||||
setSearch(newSearch);
|
setSearch(newSearch);
|
||||||
}
|
}
|
||||||
if (!newSearch?.collection && !newSearch?.file) {
|
if (!newSearch?.collection && !newSearch?.file) {
|
||||||
|
@ -522,13 +559,6 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeCollectionSelector = (closeBtnClick?: boolean) => {
|
|
||||||
if (closeBtnClick === true) {
|
|
||||||
appContext.resetSharedFiles();
|
|
||||||
}
|
|
||||||
setCollectionSelectorView(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fixTimeHelper = async () => {
|
const fixTimeHelper = async () => {
|
||||||
const selectedFiles = getSelectedFiles(selected, files);
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
setFixCreationTimeAttributes({ files: selectedFiles });
|
setFixCreationTimeAttributes({ files: selectedFiles });
|
||||||
|
@ -552,6 +582,10 @@ export default function Gallery() {
|
||||||
setUploadTypeSelectorView(true);
|
setUploadTypeSelectorView(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const closeCollectionSelector = () => {
|
||||||
|
setCollectionSelectorView(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryContext.Provider
|
<GalleryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -611,7 +645,7 @@ export default function Gallery() {
|
||||||
openUploader={openUploader}
|
openUploader={openUploader}
|
||||||
isInSearchMode={isInSearchMode}
|
isInSearchMode={isInSearchMode}
|
||||||
collections={collections}
|
collections={collections}
|
||||||
files={getNonTrashedUniqueUserFiles(files)}
|
files={getNonTrashedFiles(files)}
|
||||||
setActiveCollection={setActiveCollection}
|
setActiveCollection={setActiveCollection}
|
||||||
updateSearch={updateSearch}
|
updateSearch={updateSearch}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -10,12 +10,13 @@ import SignUp from 'components/SignUp';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getAlbumSiteHost, PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import { EnteLogo } from 'components/EnteLogo';
|
import { EnteLogo } from 'components/EnteLogo';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import safeStorageService from 'services/electron/safeStorage';
|
import safeStorageService from 'services/electron/safeStorage';
|
||||||
import { saveKeyInSessionStore } from 'utils/crypto';
|
import { saveKeyInSessionStore } from 'utils/crypto';
|
||||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
|
import { getAlbumsURL } from 'utils/common/apiUtil';
|
||||||
|
|
||||||
const Container = styled('div')`
|
const Container = styled('div')`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -101,10 +102,10 @@ export default function LandingPage() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
appContext.showNavBar(false);
|
appContext.showNavBar(false);
|
||||||
const currentURL = new URL(window.location.href);
|
const currentURL = new URL(window.location.href);
|
||||||
const ALBUM_SITE_HOST = getAlbumSiteHost();
|
const albumsURL = new URL(getAlbumsURL());
|
||||||
currentURL.pathname = router.pathname;
|
currentURL.pathname = router.pathname;
|
||||||
if (
|
if (
|
||||||
currentURL.host === ALBUM_SITE_HOST &&
|
currentURL.host === albumsURL.host &&
|
||||||
currentURL.pathname !== PAGES.SHARED_ALBUMS
|
currentURL.pathname !== PAGES.SHARED_ALBUMS
|
||||||
) {
|
) {
|
||||||
handleAlbumsRedirect(currentURL);
|
handleAlbumsRedirect(currentURL);
|
||||||
|
|
|
@ -8,10 +8,7 @@ import {
|
||||||
} from 'utils/storage/localStorage';
|
} from 'utils/storage/localStorage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import CryptoWorker, {
|
import { decryptAndStoreToken, saveKeyInSessionStore } from 'utils/crypto';
|
||||||
decryptAndStoreToken,
|
|
||||||
saveKeyInSessionStore,
|
|
||||||
} from 'utils/crypto';
|
|
||||||
import SingleInputForm, {
|
import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
|
@ -24,6 +21,7 @@ import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
const bip39 = require('bip39');
|
const bip39 = require('bip39');
|
||||||
// mobile client library only supports english.
|
// mobile client library only supports english.
|
||||||
bip39.setDefaultWordlist('english');
|
bip39.setDefaultWordlist('english');
|
||||||
|
@ -72,8 +70,8 @@ export default function Recover() {
|
||||||
}
|
}
|
||||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
}
|
}
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const masterKey: string = await cryptoWorker.decryptB64(
|
const masterKey = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
keyAttributes.masterKeyEncryptedWithRecoveryKey,
|
||||||
keyAttributes.masterKeyDecryptionNonce,
|
keyAttributes.masterKeyDecryptionNonce,
|
||||||
await cryptoWorker.fromHex(recoveryKey)
|
await cryptoWorker.fromHex(recoveryKey)
|
||||||
|
|
|
@ -18,15 +18,11 @@ import { EnteFile } from 'types/file';
|
||||||
import { mergeMetadata, sortFiles } from 'utils/file';
|
import { mergeMetadata, sortFiles } from 'utils/file';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm';
|
import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm';
|
||||||
import {
|
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||||
defaultPublicCollectionGalleryContext,
|
|
||||||
PublicCollectionGalleryContext,
|
|
||||||
} from 'utils/publicCollectionGallery';
|
|
||||||
import { CustomError, parseSharingErrorCodes } from 'utils/error';
|
import { CustomError, parseSharingErrorCodes } from 'utils/error';
|
||||||
import VerticallyCentered from 'components/Container';
|
import VerticallyCentered, { CenteredFlex } from 'components/Container';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import { PAGES } from 'constants/pages';
|
import { PAGES } from 'constants/pages';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import SingleInputForm, {
|
import SingleInputForm, {
|
||||||
|
@ -41,6 +37,17 @@ import FormContainer from 'components/Form/FormContainer';
|
||||||
import FormPaper from 'components/Form/FormPaper';
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
|
import Uploader from 'components/Upload/Uploader';
|
||||||
|
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
|
import FullScreenDropZone from 'components/FullScreenDropZone';
|
||||||
|
import useFileInput from 'hooks/useFileInput';
|
||||||
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
import UploadSelectorInputs from 'components/UploadSelectorInputs';
|
||||||
|
import { logoutUser } from 'services/userService';
|
||||||
|
import UploadButton from 'components/Upload/UploadButton';
|
||||||
|
import bs58 from 'bs58';
|
||||||
|
import AddPhotoAlternateOutlined from '@mui/icons-material/AddPhotoAlternateOutlined';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const Loader = () => (
|
const Loader = () => (
|
||||||
<VerticallyCentered>
|
<VerticallyCentered>
|
||||||
|
@ -49,7 +56,6 @@ const Loader = () => (
|
||||||
</EnteSpinner>
|
</EnteSpinner>
|
||||||
</VerticallyCentered>
|
</VerticallyCentered>
|
||||||
);
|
);
|
||||||
const bs58 = require('bs58');
|
|
||||||
export default function PublicCollectionGallery() {
|
export default function PublicCollectionGallery() {
|
||||||
const token = useRef<string>(null);
|
const token = useRef<string>(null);
|
||||||
// passwordJWTToken refers to the jwt token which is used for album protected by password.
|
// passwordJWTToken refers to the jwt token which is used for album protected by password.
|
||||||
|
@ -70,6 +76,58 @@ export default function PublicCollectionGallery() {
|
||||||
const [photoListHeader, setPhotoListHeader] =
|
const [photoListHeader, setPhotoListHeader] =
|
||||||
useState<TimeStampListItem>(null);
|
useState<TimeStampListItem>(null);
|
||||||
|
|
||||||
|
const [photoListFooter, setPhotoListFooter] =
|
||||||
|
useState<TimeStampListItem>(null);
|
||||||
|
|
||||||
|
const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false);
|
||||||
|
const [blockingLoad, setBlockingLoad] = useState(false);
|
||||||
|
const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps: getDragAndDropRootProps,
|
||||||
|
getInputProps: getDragAndDropInputProps,
|
||||||
|
acceptedFiles: dragAndDropFiles,
|
||||||
|
} = useDropzone({
|
||||||
|
noClick: true,
|
||||||
|
noKeyboard: true,
|
||||||
|
disabled: shouldDisableDropzone,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
selectedFiles: webFileSelectorFiles,
|
||||||
|
open: openFileSelector,
|
||||||
|
getInputProps: getFileSelectorInputProps,
|
||||||
|
} = useFileInput({
|
||||||
|
directory: false,
|
||||||
|
});
|
||||||
|
const {
|
||||||
|
selectedFiles: webFolderSelectorFiles,
|
||||||
|
open: openFolderSelector,
|
||||||
|
getInputProps: getFolderSelectorInputProps,
|
||||||
|
} = useFileInput({
|
||||||
|
directory: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const openUploader = () => {
|
||||||
|
setUploadTypeSelectorView(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeUploadTypeSelectorView = () => {
|
||||||
|
setUploadTypeSelectorView(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showPublicLinkExpiredMessage = () =>
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: constants.LINK_EXPIRED,
|
||||||
|
content: constants.LINK_EXPIRED_MESSAGE,
|
||||||
|
|
||||||
|
nonClosable: true,
|
||||||
|
proceed: {
|
||||||
|
text: constants.LOGIN,
|
||||||
|
action: logoutUser,
|
||||||
|
variant: 'accent',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentURL = new URL(window.location.href);
|
const currentURL = new URL(window.location.href);
|
||||||
if (currentURL.pathname !== PAGES.ROOT) {
|
if (currentURL.pathname !== PAGES.ROOT) {
|
||||||
|
@ -91,7 +149,8 @@ export default function PublicCollectionGallery() {
|
||||||
}
|
}
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
|
||||||
url.current = window.location.href;
|
url.current = window.location.href;
|
||||||
const currentURL = new URL(url.current);
|
const currentURL = new URL(url.current);
|
||||||
const t = currentURL.searchParams.get('t');
|
const t = currentURL.searchParams.get('t');
|
||||||
|
@ -101,8 +160,8 @@ export default function PublicCollectionGallery() {
|
||||||
}
|
}
|
||||||
const dck =
|
const dck =
|
||||||
ck.length < 50
|
ck.length < 50
|
||||||
? await worker.toB64(bs58.decode(ck))
|
? await cryptoWorker.toB64(bs58.decode(ck))
|
||||||
: await worker.fromHex(ck);
|
: await cryptoWorker.fromHex(ck);
|
||||||
token.current = t;
|
token.current = t;
|
||||||
collectionKey.current = dck;
|
collectionKey.current = dck;
|
||||||
url.current = window.location.href;
|
url.current = window.location.href;
|
||||||
|
@ -111,6 +170,9 @@ export default function PublicCollectionGallery() {
|
||||||
);
|
);
|
||||||
if (localCollection) {
|
if (localCollection) {
|
||||||
setPublicCollection(localCollection);
|
setPublicCollection(localCollection);
|
||||||
|
const isPasswordProtected =
|
||||||
|
localCollection?.publicURLs?.[0]?.passwordEnabled;
|
||||||
|
setIsPasswordProtected(isPasswordProtected);
|
||||||
const collectionUID = getPublicCollectionUID(token.current);
|
const collectionUID = getPublicCollectionUID(token.current);
|
||||||
const localFiles = await getLocalPublicFiles(collectionUID);
|
const localFiles = await getLocalPublicFiles(collectionUID);
|
||||||
const localPublicFiles = sortFiles(
|
const localPublicFiles = sortFiles(
|
||||||
|
@ -140,15 +202,38 @@ export default function PublicCollectionGallery() {
|
||||||
/>
|
/>
|
||||||
</CollectionInfoBarWrapper>
|
</CollectionInfoBarWrapper>
|
||||||
),
|
),
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.HEADER,
|
||||||
height: 68,
|
height: 68,
|
||||||
});
|
});
|
||||||
}, [publicCollection, publicFiles]);
|
}, [publicCollection, publicFiles]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (publicCollection?.publicURLs?.[0]?.enableCollect) {
|
||||||
|
setPhotoListFooter({
|
||||||
|
item: (
|
||||||
|
<CenteredFlex sx={{ marginTop: '56px' }}>
|
||||||
|
<UploadButton
|
||||||
|
disableShrink
|
||||||
|
openUploader={openUploader}
|
||||||
|
text={constants.ADD_MORE_PHOTOS}
|
||||||
|
color="accent"
|
||||||
|
icon={<AddPhotoAlternateOutlined />}
|
||||||
|
/>
|
||||||
|
</CenteredFlex>
|
||||||
|
),
|
||||||
|
itemType: ITEM_TYPE.FOOTER,
|
||||||
|
height: 104,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setPhotoListFooter(null);
|
||||||
|
}
|
||||||
|
}, [publicCollection]);
|
||||||
|
|
||||||
const syncWithRemote = async () => {
|
const syncWithRemote = async () => {
|
||||||
const collectionUID = getPublicCollectionUID(token.current);
|
const collectionUID = getPublicCollectionUID(token.current);
|
||||||
try {
|
try {
|
||||||
appContext.startLoading();
|
appContext.startLoading();
|
||||||
|
setLoading(true);
|
||||||
const collection = await getPublicCollection(
|
const collection = await getPublicCollection(
|
||||||
token.current,
|
token.current,
|
||||||
collectionKey.current
|
collectionKey.current
|
||||||
|
@ -196,7 +281,7 @@ export default function PublicCollectionGallery() {
|
||||||
setErrorMessage(
|
setErrorMessage(
|
||||||
parsedError.message === CustomError.TOO_MANY_REQUESTS
|
parsedError.message === CustomError.TOO_MANY_REQUESTS
|
||||||
? constants.LINK_TOO_MANY_REQUESTS
|
? constants.LINK_TOO_MANY_REQUESTS
|
||||||
: constants.LINK_EXPIRED
|
: constants.LINK_EXPIRED_MESSAGE
|
||||||
);
|
);
|
||||||
// share has been disabled
|
// share has been disabled
|
||||||
// local cache should be cleared
|
// local cache should be cleared
|
||||||
|
@ -211,6 +296,7 @@ export default function PublicCollectionGallery() {
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
appContext.finishLoading();
|
appContext.finishLoading();
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -219,7 +305,7 @@ export default function PublicCollectionGallery() {
|
||||||
setFieldError
|
setFieldError
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
let hashedPassword: string = null;
|
let hashedPassword: string = null;
|
||||||
try {
|
try {
|
||||||
const publicUrl = publicCollection.publicURLs[0];
|
const publicUrl = publicCollection.publicURLs[0];
|
||||||
|
@ -297,31 +383,66 @@ export default function PublicCollectionGallery() {
|
||||||
return (
|
return (
|
||||||
<PublicCollectionGalleryContext.Provider
|
<PublicCollectionGalleryContext.Provider
|
||||||
value={{
|
value={{
|
||||||
...defaultPublicCollectionGalleryContext,
|
|
||||||
token: token.current,
|
token: token.current,
|
||||||
passwordToken: passwordJWTToken.current,
|
passwordToken: passwordJWTToken.current,
|
||||||
accessedThroughSharedURL: true,
|
accessedThroughSharedURL: true,
|
||||||
openReportForm,
|
openReportForm,
|
||||||
photoListHeader,
|
photoListHeader,
|
||||||
|
photoListFooter,
|
||||||
}}>
|
}}>
|
||||||
<SharedAlbumNavbar />
|
<FullScreenDropZone
|
||||||
<PhotoFrame
|
getDragAndDropRootProps={getDragAndDropRootProps}>
|
||||||
files={publicFiles}
|
<UploadSelectorInputs
|
||||||
syncWithRemote={syncWithRemote}
|
getDragAndDropInputProps={getDragAndDropInputProps}
|
||||||
setSelected={() => null}
|
getFileSelectorInputProps={getFileSelectorInputProps}
|
||||||
selected={{ count: 0, collectionID: null }}
|
getFolderSelectorInputProps={getFolderSelectorInputProps}
|
||||||
isFirstLoad={true}
|
/>
|
||||||
activeCollection={ALL_SECTION}
|
<SharedAlbumNavbar
|
||||||
isSharedCollection
|
showUploadButton={
|
||||||
enableDownload={
|
publicCollection?.publicURLs?.[0]?.enableCollect
|
||||||
publicCollection?.publicURLs?.[0]?.enableDownload ?? true
|
}
|
||||||
}
|
openUploader={openUploader}
|
||||||
/>
|
/>
|
||||||
<AbuseReportForm
|
<PhotoFrame
|
||||||
show={abuseReportFormView}
|
files={publicFiles}
|
||||||
close={closeReportForm}
|
syncWithRemote={syncWithRemote}
|
||||||
url={url.current}
|
setSelected={() => null}
|
||||||
/>
|
selected={{ count: 0, collectionID: null }}
|
||||||
|
isFirstLoad={true}
|
||||||
|
activeCollection={ALL_SECTION}
|
||||||
|
isSharedCollection
|
||||||
|
enableDownload={
|
||||||
|
publicCollection?.publicURLs?.[0]?.enableDownload ??
|
||||||
|
true
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<AbuseReportForm
|
||||||
|
show={abuseReportFormView}
|
||||||
|
close={closeReportForm}
|
||||||
|
url={url.current}
|
||||||
|
/>
|
||||||
|
{blockingLoad && (
|
||||||
|
<LoadingOverlay>
|
||||||
|
<EnteSpinner />
|
||||||
|
</LoadingOverlay>
|
||||||
|
)}
|
||||||
|
<Uploader
|
||||||
|
syncWithRemote={syncWithRemote}
|
||||||
|
uploadCollection={publicCollection}
|
||||||
|
setLoading={setBlockingLoad}
|
||||||
|
setShouldDisableDropzone={setShouldDisableDropzone}
|
||||||
|
setFiles={setPublicFiles}
|
||||||
|
webFileSelectorFiles={webFileSelectorFiles}
|
||||||
|
webFolderSelectorFiles={webFolderSelectorFiles}
|
||||||
|
dragAndDropFiles={dragAndDropFiles}
|
||||||
|
uploadTypeSelectorView={uploadTypeSelectorView}
|
||||||
|
closeUploadTypeSelector={closeUploadTypeSelectorView}
|
||||||
|
showUploadFilesDialog={openFileSelector}
|
||||||
|
showUploadDirsDialog={openFolderSelector}
|
||||||
|
showSessionExpiredMessage={showPublicLinkExpiredMessage}
|
||||||
|
zipUploadDisabled
|
||||||
|
/>
|
||||||
|
</FullScreenDropZone>
|
||||||
</PublicCollectionGalleryContext.Provider>
|
</PublicCollectionGalleryContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,6 @@ import React, { useContext, useEffect, useState } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
|
|
||||||
import SingleInputForm, {
|
import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
|
@ -15,6 +14,8 @@ import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
|
import { B64EncryptionResult } from 'types/crypto';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
const bip39 = require('bip39');
|
const bip39 = require('bip39');
|
||||||
// mobile client library only supports english.
|
// mobile client library only supports english.
|
||||||
bip39.setDefaultWordlist('english');
|
bip39.setDefaultWordlist('english');
|
||||||
|
@ -65,8 +66,8 @@ export default function Recover() {
|
||||||
}
|
}
|
||||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
}
|
}
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const twoFactorSecret: string = await cryptoWorker.decryptB64(
|
const twoFactorSecret = await cryptoWorker.decryptB64(
|
||||||
encryptedTwoFactorSecret.encryptedData,
|
encryptedTwoFactorSecret.encryptedData,
|
||||||
encryptedTwoFactorSecret.nonce,
|
encryptedTwoFactorSecret.nonce,
|
||||||
await cryptoWorker.fromHex(recoveryKey)
|
await cryptoWorker.fromHex(recoveryKey)
|
||||||
|
|
|
@ -3,9 +3,7 @@ import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
|
|
||||||
import { getActualKey, getToken } from 'utils/common/key';
|
import { getActualKey, getToken } from 'utils/common/key';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import { getPublicKey } from './userService';
|
import { getPublicKey } from './userService';
|
||||||
import { B64EncryptionResult } from 'utils/crypto';
|
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
@ -28,6 +26,9 @@ import {
|
||||||
CollectionSummaries,
|
CollectionSummaries,
|
||||||
CollectionSummary,
|
CollectionSummary,
|
||||||
CollectionFilesCount,
|
CollectionFilesCount,
|
||||||
|
EncryptedCollection,
|
||||||
|
CollectionMagicMetadata,
|
||||||
|
CollectionMagicMetadataProps,
|
||||||
} from 'types/collection';
|
} from 'types/collection';
|
||||||
import {
|
import {
|
||||||
COLLECTION_SORT_BY,
|
COLLECTION_SORT_BY,
|
||||||
|
@ -38,61 +39,77 @@ import {
|
||||||
ALL_SECTION,
|
ALL_SECTION,
|
||||||
CollectionSummaryType,
|
CollectionSummaryType,
|
||||||
} from 'constants/collection';
|
} from 'constants/collection';
|
||||||
import { UpdateMagicMetadataRequest } from 'types/magicMetadata';
|
import {
|
||||||
import { EncryptionResult } from 'types/upload';
|
NEW_COLLECTION_MAGIC_METADATA,
|
||||||
|
SUB_TYPE,
|
||||||
|
UpdateMagicMetadataRequest,
|
||||||
|
} from 'types/magicMetadata';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { IsArchived } from 'utils/magicMetadata';
|
import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { getNonHiddenCollections } from 'utils/collection';
|
import {
|
||||||
|
getNonHiddenCollections,
|
||||||
|
isQuickLinkCollection,
|
||||||
|
isSharedByMe,
|
||||||
|
isSharedWithMe,
|
||||||
|
isSharedOnlyViaLink,
|
||||||
|
} from 'utils/collection';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const COLLECTION_TABLE = 'collections';
|
const COLLECTION_TABLE = 'collections';
|
||||||
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
|
const COLLECTION_UPDATION_TIME = 'collection-updation-time';
|
||||||
|
|
||||||
const getCollectionWithSecrets = async (
|
const getCollectionWithSecrets = async (
|
||||||
collection: Collection,
|
collection: EncryptedCollection,
|
||||||
masterKey: string
|
masterKey: string
|
||||||
) => {
|
): Promise<Collection> => {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const userID = getData(LS_KEYS.USER).id;
|
const userID = getData(LS_KEYS.USER).id;
|
||||||
let decryptedKey: string;
|
let collectionKey: string;
|
||||||
if (collection.owner.id === userID) {
|
if (collection.owner.id === userID) {
|
||||||
decryptedKey = await worker.decryptB64(
|
collectionKey = await cryptoWorker.decryptB64(
|
||||||
collection.encryptedKey,
|
collection.encryptedKey,
|
||||||
collection.keyDecryptionNonce,
|
collection.keyDecryptionNonce,
|
||||||
masterKey
|
masterKey
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
|
||||||
const secretKey = await worker.decryptB64(
|
const secretKey = await cryptoWorker.decryptB64(
|
||||||
keyAttributes.encryptedSecretKey,
|
keyAttributes.encryptedSecretKey,
|
||||||
keyAttributes.secretKeyDecryptionNonce,
|
keyAttributes.secretKeyDecryptionNonce,
|
||||||
masterKey
|
masterKey
|
||||||
);
|
);
|
||||||
decryptedKey = await worker.boxSealOpen(
|
collectionKey = await cryptoWorker.boxSealOpen(
|
||||||
collection.encryptedKey,
|
collection.encryptedKey,
|
||||||
keyAttributes.publicKey,
|
keyAttributes.publicKey,
|
||||||
secretKey
|
secretKey
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
collection.name =
|
const collectionName =
|
||||||
collection.name ||
|
collection.name ||
|
||||||
(await worker.decryptToUTF8(
|
(await cryptoWorker.decryptToUTF8(
|
||||||
collection.encryptedName,
|
collection.encryptedName,
|
||||||
collection.nameDecryptionNonce,
|
collection.nameDecryptionNonce,
|
||||||
decryptedKey
|
collectionKey
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let collectionMagicMetadata: CollectionMagicMetadata;
|
||||||
if (collection.magicMetadata?.data) {
|
if (collection.magicMetadata?.data) {
|
||||||
collection.magicMetadata.data = await worker.decryptMetadata(
|
collectionMagicMetadata = {
|
||||||
collection.magicMetadata.data,
|
...collection.magicMetadata,
|
||||||
collection.magicMetadata.header,
|
data: await cryptoWorker.decryptMetadata(
|
||||||
decryptedKey
|
collection.magicMetadata.data,
|
||||||
);
|
collection.magicMetadata.header,
|
||||||
|
collectionKey
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
...collection,
|
...collection,
|
||||||
key: decryptedKey,
|
name: collectionName,
|
||||||
|
key: collectionKey,
|
||||||
|
magicMetadata: collectionMagicMetadata,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,27 +126,25 @@ const getCollections = async (
|
||||||
},
|
},
|
||||||
{ 'X-Auth-Token': token }
|
{ 'X-Auth-Token': token }
|
||||||
);
|
);
|
||||||
const promises: Promise<Collection>[] = resp.data.collections.map(
|
const decryptedCollections: Collection[] = await Promise.all(
|
||||||
async (collection: Collection) => {
|
resp.data.collections.map(
|
||||||
if (collection.isDeleted) {
|
async (collection: EncryptedCollection) => {
|
||||||
return collection;
|
if (collection.isDeleted) {
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await getCollectionWithSecrets(collection, key);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, `decryption failed for collection`, {
|
||||||
|
collectionID: collection.id,
|
||||||
|
});
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
let collectionWithSecrets = collection;
|
)
|
||||||
try {
|
|
||||||
collectionWithSecrets = await getCollectionWithSecrets(
|
|
||||||
collection,
|
|
||||||
key
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, `decryption failed for collection`, {
|
|
||||||
collectionID: collection.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return collectionWithSecrets;
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
// only allow deleted or collection with key, filtering out collection whose decryption failed
|
// only allow deleted or collection with key, filtering out collection whose decryption failed
|
||||||
const collections = (await Promise.all(promises)).filter(
|
const collections = decryptedCollections.filter(
|
||||||
(collection) => collection.isDeleted || collection.key
|
(collection) => collection.isDeleted || collection.key
|
||||||
);
|
);
|
||||||
return collections;
|
return collections;
|
||||||
|
@ -274,25 +289,15 @@ export const createCollection = async (
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const encryptionKey = await getActualKey();
|
const encryptionKey = await getActualKey();
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const collectionKey: string = await worker.generateEncryptionKey();
|
const collectionKey = await cryptoWorker.generateEncryptionKey();
|
||||||
const {
|
const { encryptedData: encryptedKey, nonce: keyDecryptionNonce } =
|
||||||
encryptedData: encryptedKey,
|
await cryptoWorker.encryptToB64(collectionKey, encryptionKey);
|
||||||
nonce: keyDecryptionNonce,
|
const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
|
||||||
}: B64EncryptionResult = await worker.encryptToB64(
|
await cryptoWorker.encryptUTF8(collectionName, collectionKey);
|
||||||
collectionKey,
|
const newCollection: EncryptedCollection = {
|
||||||
encryptionKey
|
|
||||||
);
|
|
||||||
const {
|
|
||||||
encryptedData: encryptedName,
|
|
||||||
nonce: nameDecryptionNonce,
|
|
||||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
|
||||||
collectionName,
|
|
||||||
collectionKey
|
|
||||||
);
|
|
||||||
const newCollection: Collection = {
|
|
||||||
id: null,
|
id: null,
|
||||||
owner: null,
|
owner: null,
|
||||||
encryptedKey,
|
encryptedKey,
|
||||||
|
@ -306,15 +311,12 @@ export const createCollection = async (
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
magicMetadata: null,
|
magicMetadata: null,
|
||||||
};
|
};
|
||||||
let createdCollection: Collection = await postCollection(
|
const createdCollection = await postCollection(newCollection, token);
|
||||||
newCollection,
|
const decryptedCreatedCollection = await getCollectionWithSecrets(
|
||||||
token
|
|
||||||
);
|
|
||||||
createdCollection = await getCollectionWithSecrets(
|
|
||||||
createdCollection,
|
createdCollection,
|
||||||
encryptionKey
|
encryptionKey
|
||||||
);
|
);
|
||||||
return createdCollection;
|
return decryptedCreatedCollection;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'create collection failed');
|
logError(e, 'create collection failed');
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -322,9 +324,9 @@ export const createCollection = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const postCollection = async (
|
const postCollection = async (
|
||||||
collectionData: Collection,
|
collectionData: EncryptedCollection,
|
||||||
token: string
|
token: string
|
||||||
): Promise<Collection> => {
|
): Promise<EncryptedCollection> => {
|
||||||
try {
|
try {
|
||||||
const response = await HTTPService.post(
|
const response = await HTTPService.post(
|
||||||
`${ENDPOINT}/collections`,
|
`${ENDPOINT}/collections`,
|
||||||
|
@ -457,19 +459,19 @@ const encryptWithNewCollectionKey = async (
|
||||||
files: EnteFile[]
|
files: EnteFile[]
|
||||||
): Promise<EncryptedFileKey[]> => {
|
): Promise<EncryptedFileKey[]> => {
|
||||||
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
|
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
const newEncryptedKey = await cryptoWorker.encryptToB64(
|
||||||
file.key,
|
file.key,
|
||||||
newCollection.key
|
newCollection.key
|
||||||
);
|
);
|
||||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
const encryptedKey = newEncryptedKey.encryptedData;
|
||||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
const keyDecryptionNonce = newEncryptedKey.nonce;
|
||||||
|
|
||||||
fileKeysEncryptedWithNewCollection.push({
|
fileKeysEncryptedWithNewCollection.push({
|
||||||
id: file.id,
|
id: file.id,
|
||||||
encryptedKey: file.encryptedKey,
|
encryptedKey,
|
||||||
keyDecryptionNonce: file.keyDecryptionNonce,
|
keyDecryptionNonce,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return fileKeysEncryptedWithNewCollection;
|
return fileKeysEncryptedWithNewCollection;
|
||||||
|
@ -513,26 +515,41 @@ export const deleteCollection = async (collectionID: number) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const leaveSharedAlbum = async (collectionID: number) => {
|
||||||
|
try {
|
||||||
|
const token = getToken();
|
||||||
|
|
||||||
|
await HTTPService.post(
|
||||||
|
`${ENDPOINT}/collections/leave/${collectionID}`,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
{ 'X-Auth-Token': token }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, constants.LEAVE_SHARED_ALBUM_FAILED);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const updateCollectionMagicMetadata = async (collection: Collection) => {
|
export const updateCollectionMagicMetadata = async (collection: Collection) => {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
|
||||||
const { file: encryptedMagicMetadata }: EncryptionResult =
|
const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata(
|
||||||
await worker.encryptMetadata(
|
collection.magicMetadata.data,
|
||||||
collection.magicMetadata.data,
|
collection.key
|
||||||
collection.key
|
);
|
||||||
);
|
|
||||||
|
|
||||||
const reqBody: UpdateMagicMetadataRequest = {
|
const reqBody: UpdateMagicMetadataRequest = {
|
||||||
id: collection.id,
|
id: collection.id,
|
||||||
magicMetadata: {
|
magicMetadata: {
|
||||||
version: collection.magicMetadata.version,
|
version: collection.magicMetadata.version,
|
||||||
count: collection.magicMetadata.count,
|
count: collection.magicMetadata.count,
|
||||||
data: encryptedMagicMetadata.encryptedData as unknown as string,
|
data: encryptedMagicMetadata.encryptedData,
|
||||||
header: encryptedMagicMetadata.decryptionHeader,
|
header: encryptedMagicMetadata.decryptionHeader,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -559,15 +576,14 @@ export const renameCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
newCollectionName: string
|
newCollectionName: string
|
||||||
) => {
|
) => {
|
||||||
|
if (isQuickLinkCollection(collection)) {
|
||||||
|
// Convert quick link collction to normal collection on rename
|
||||||
|
await updateCollectionSubType(collection, SUB_TYPE.DEFAULT);
|
||||||
|
}
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const {
|
const { encryptedData: encryptedName, nonce: nameDecryptionNonce } =
|
||||||
encryptedData: encryptedName,
|
await cryptoWorker.encryptUTF8(newCollectionName, collection.key);
|
||||||
nonce: nameDecryptionNonce,
|
|
||||||
}: B64EncryptionResult = await worker.encryptUTF8(
|
|
||||||
newCollectionName,
|
|
||||||
collection.key
|
|
||||||
);
|
|
||||||
const collectionRenameRequest = {
|
const collectionRenameRequest = {
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
encryptedName,
|
encryptedName,
|
||||||
|
@ -582,16 +598,34 @@ export const renameCollection = async (
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateCollectionSubType = async (
|
||||||
|
collection: Collection,
|
||||||
|
subType: SUB_TYPE
|
||||||
|
) => {
|
||||||
|
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||||
|
subType: subType,
|
||||||
|
};
|
||||||
|
const updatedCollection = {
|
||||||
|
...collection,
|
||||||
|
magicMetadata: await updateMagicMetadataProps(
|
||||||
|
collection.magicMetadata ?? NEW_COLLECTION_MAGIC_METADATA,
|
||||||
|
collection.key,
|
||||||
|
updatedMagicMetadataProps
|
||||||
|
),
|
||||||
|
} as Collection;
|
||||||
|
await updateCollectionMagicMetadata(updatedCollection);
|
||||||
|
};
|
||||||
|
|
||||||
export const shareCollection = async (
|
export const shareCollection = async (
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
withUserEmail: string
|
withUserEmail: string
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const publicKey: string = await getPublicKey(withUserEmail);
|
const publicKey: string = await getPublicKey(withUserEmail);
|
||||||
const encryptedKey: string = await worker.boxSeal(
|
const encryptedKey = await cryptoWorker.boxSeal(
|
||||||
collection.key,
|
collection.key,
|
||||||
publicKey
|
publicKey
|
||||||
);
|
);
|
||||||
|
@ -736,11 +770,6 @@ export function sortCollectionSummaries(
|
||||||
return collectionSummaries
|
return collectionSummaries
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
switch (sortBy) {
|
switch (sortBy) {
|
||||||
case COLLECTION_SORT_BY.CREATION_TIME_DESCENDING:
|
|
||||||
return compareCollectionsLatestFile(
|
|
||||||
b.latestFile,
|
|
||||||
a.latestFile
|
|
||||||
);
|
|
||||||
case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING:
|
case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING:
|
||||||
return (
|
return (
|
||||||
-1 *
|
-1 *
|
||||||
|
@ -798,12 +827,15 @@ export function getCollectionSummaries(
|
||||||
latestFile: collectionLatestFiles.get(collection.id),
|
latestFile: collectionLatestFiles.get(collection.id),
|
||||||
fileCount: collectionFilesCount.get(collection.id),
|
fileCount: collectionFilesCount.get(collection.id),
|
||||||
updationTime: collection.updationTime,
|
updationTime: collection.updationTime,
|
||||||
type:
|
type: isSharedWithMe(collection, user)
|
||||||
collection.owner.id !== user.id
|
? CollectionSummaryType.incomingShare
|
||||||
? CollectionSummaryType.shared
|
: isSharedByMe(collection)
|
||||||
: IsArchived(collection)
|
? CollectionSummaryType.outgoingShare
|
||||||
? CollectionSummaryType.archived
|
: isSharedOnlyViaLink(collection)
|
||||||
: CollectionSummaryType[collection.type],
|
? CollectionSummaryType.sharedOnlyViaLink
|
||||||
|
: IsArchived(collection)
|
||||||
|
? CollectionSummaryType.archived
|
||||||
|
: CollectionSummaryType[collection.type],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil';
|
import { getFileURL, getThumbnailURL } from 'utils/common/apiUtil';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import {
|
import {
|
||||||
generateStreamFromArrayBuffer,
|
generateStreamFromArrayBuffer,
|
||||||
getRenderableFileURL,
|
getRenderableFileURL,
|
||||||
|
@ -14,6 +13,8 @@ import { FILE_TYPE } from 'constants/file';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { openThumbnailCache } from './cacheService';
|
import { openThumbnailCache } from './cacheService';
|
||||||
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
|
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
|
||||||
const MAX_PARALLEL_DOWNLOADS = 10;
|
const MAX_PARALLEL_DOWNLOADS = 10;
|
||||||
|
|
||||||
|
@ -31,11 +32,17 @@ class DownloadManager {
|
||||||
|
|
||||||
public async getThumbnail(file: EnteFile) {
|
public async getThumbnail(file: EnteFile) {
|
||||||
try {
|
try {
|
||||||
|
addLogLine(`[${file.id}] [DownloadManager] getThumbnail called`);
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this.thumbnailObjectURLPromise.get(file.id)) {
|
if (this.thumbnailObjectURLPromise.has(file.id)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [DownloadManager] getThumbnail promise cache hit, returning existing promise`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
||||||
const downloadPromise = async () => {
|
const downloadPromise = async () => {
|
||||||
const thumbnailCache = await openThumbnailCache();
|
const thumbnailCache = await openThumbnailCache();
|
||||||
|
|
||||||
|
@ -43,8 +50,14 @@ class DownloadManager {
|
||||||
file.id.toString()
|
file.id.toString()
|
||||||
);
|
);
|
||||||
if (cacheResp) {
|
if (cacheResp) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [DownloadManager] in memory cache hit, using localCache files`
|
||||||
|
);
|
||||||
return URL.createObjectURL(await cacheResp.blob());
|
return URL.createObjectURL(await cacheResp.blob());
|
||||||
}
|
}
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [DownloadManager] in memory cache miss, DownloadManager getThumbnail download started`
|
||||||
|
);
|
||||||
const thumb =
|
const thumb =
|
||||||
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
|
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
|
||||||
() => this.downloadThumb(token, file)
|
() => this.downloadThumb(token, file)
|
||||||
|
@ -65,7 +78,7 @@ class DownloadManager {
|
||||||
return await this.thumbnailObjectURLPromise.get(file.id);
|
return await this.thumbnailObjectURLPromise.get(file.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.thumbnailObjectURLPromise.delete(file.id);
|
this.thumbnailObjectURLPromise.delete(file.id);
|
||||||
logError(e, 'get preview Failed');
|
logError(e, 'get DownloadManager preview Failed');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,10 +93,10 @@ class DownloadManager {
|
||||||
if (typeof resp.data === 'undefined') {
|
if (typeof resp.data === 'undefined') {
|
||||||
throw Error(CustomError.REQUEST_FAILED);
|
throw Error(CustomError.REQUEST_FAILED);
|
||||||
}
|
}
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const decrypted: Uint8Array = await worker.decryptThumbnail(
|
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||||
file.key
|
file.key
|
||||||
);
|
);
|
||||||
return decrypted;
|
return decrypted;
|
||||||
|
@ -93,6 +106,7 @@ class DownloadManager {
|
||||||
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
||||||
try {
|
try {
|
||||||
const getFilePromise = async () => {
|
const getFilePromise = async () => {
|
||||||
|
addLogLine(`[${file.id}] [DownloadManager] downloading file`);
|
||||||
const fileStream = await this.downloadFile(file);
|
const fileStream = await this.downloadFile(file);
|
||||||
const fileBlob = await new Response(fileStream).blob();
|
const fileBlob = await new Response(fileStream).blob();
|
||||||
if (forPreview) {
|
if (forPreview) {
|
||||||
|
@ -105,6 +119,11 @@ class DownloadManager {
|
||||||
return { converted: [fileURL], original: [fileURL] };
|
return { converted: [fileURL], original: [fileURL] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (this.fileObjectURLPromise.has(fileKey)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [DownloadManager] getFile promise cache hit, returning existing promise`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||||
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
||||||
}
|
}
|
||||||
|
@ -112,7 +131,7 @@ class DownloadManager {
|
||||||
return fileURLs;
|
return fileURLs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectURLPromise.delete(fileKey);
|
this.fileObjectURLPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'download manager Failed to get File');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -122,7 +141,8 @@ class DownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(file: EnteFile) {
|
async downloadFile(file: EnteFile) {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -140,9 +160,9 @@ class DownloadManager {
|
||||||
if (typeof resp.data === 'undefined') {
|
if (typeof resp.data === 'undefined') {
|
||||||
throw Error(CustomError.REQUEST_FAILED);
|
throw Error(CustomError.REQUEST_FAILED);
|
||||||
}
|
}
|
||||||
const decrypted: any = await worker.decryptFile(
|
const decrypted = await cryptoWorker.decryptFile(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.file.decryptionHeader),
|
await cryptoWorker.fromB64(file.file.decryptionHeader),
|
||||||
file.key
|
file.key
|
||||||
);
|
);
|
||||||
return generateStreamFromArrayBuffer(decrypted);
|
return generateStreamFromArrayBuffer(decrypted);
|
||||||
|
@ -155,12 +175,15 @@ class DownloadManager {
|
||||||
const reader = resp.body.getReader();
|
const reader = resp.body.getReader();
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const decryptionHeader = await worker.fromB64(
|
const decryptionHeader = await cryptoWorker.fromB64(
|
||||||
file.file.decryptionHeader
|
file.file.decryptionHeader
|
||||||
);
|
);
|
||||||
const fileKey = await worker.fromB64(file.key);
|
const fileKey = await cryptoWorker.fromB64(file.key);
|
||||||
const { pullState, decryptionChunkSize } =
|
const { pullState, decryptionChunkSize } =
|
||||||
await worker.initDecryption(decryptionHeader, fileKey);
|
await cryptoWorker.initDecryption(
|
||||||
|
decryptionHeader,
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
let data = new Uint8Array();
|
let data = new Uint8Array();
|
||||||
// The following function handles each data chunk
|
// The following function handles each data chunk
|
||||||
function push() {
|
function push() {
|
||||||
|
@ -179,7 +202,7 @@ class DownloadManager {
|
||||||
decryptionChunkSize
|
decryptionChunkSize
|
||||||
);
|
);
|
||||||
const { decryptedData } =
|
const { decryptedData } =
|
||||||
await worker.decryptChunk(
|
await cryptoWorker.decryptChunk(
|
||||||
fileData,
|
fileData,
|
||||||
pullState
|
pullState
|
||||||
);
|
);
|
||||||
|
@ -192,7 +215,10 @@ class DownloadManager {
|
||||||
} else {
|
} else {
|
||||||
if (data) {
|
if (data) {
|
||||||
const { decryptedData } =
|
const { decryptedData } =
|
||||||
await worker.decryptChunk(data, pullState);
|
await cryptoWorker.decryptChunk(
|
||||||
|
data,
|
||||||
|
pullState
|
||||||
|
);
|
||||||
controller.enqueue(decryptedData);
|
controller.enqueue(decryptedData);
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,11 @@ class ElectronService {
|
||||||
return this.electronAPIs.getAppVersion();
|
return this.electronAPIs.getAppVersion();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
logRendererProcessMemoryUsage(message: string) {
|
||||||
|
if (this.electronAPIs?.logRendererProcessMemoryUsage) {
|
||||||
|
return this.electronAPIs.logRendererProcessMemoryUsage(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new ElectronService();
|
export default new ElectronService();
|
||||||
|
|
|
@ -1,46 +0,0 @@
|
||||||
import { ElectronAPIs } from 'types/electron';
|
|
||||||
import { makeHumanReadableStorage } from 'utils/billing';
|
|
||||||
import { addLogLine } from 'utils/logging';
|
|
||||||
import { logError } from 'utils/sentry';
|
|
||||||
|
|
||||||
class ElectronHEICConverter {
|
|
||||||
private electronAPIs: ElectronAPIs;
|
|
||||||
private allElectronAPIExists: boolean;
|
|
||||||
constructor() {
|
|
||||||
this.electronAPIs = globalThis['ElectronAPIs'];
|
|
||||||
this.allElectronAPIExists = !!this.electronAPIs?.convertHEIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
apiExists() {
|
|
||||||
return this.allElectronAPIExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
async convert(fileBlob: Blob): Promise<Blob> {
|
|
||||||
try {
|
|
||||||
if (this.allElectronAPIExists) {
|
|
||||||
const startTime = Date.now();
|
|
||||||
const inputFileData = new Uint8Array(
|
|
||||||
await fileBlob.arrayBuffer()
|
|
||||||
);
|
|
||||||
const convertedFileData = await this.electronAPIs.convertHEIC(
|
|
||||||
inputFileData
|
|
||||||
);
|
|
||||||
addLogLine(
|
|
||||||
`originalFileSize:${makeHumanReadableStorage(
|
|
||||||
fileBlob?.size
|
|
||||||
)},convertedFileSize:${makeHumanReadableStorage(
|
|
||||||
convertedFileData?.length
|
|
||||||
)}, native heic conversion time: ${
|
|
||||||
Date.now() - startTime
|
|
||||||
}ms `
|
|
||||||
);
|
|
||||||
return new Blob([convertedFileData]);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'failed to convert heic natively');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default new ElectronHEICConverter();
|
|
77
src/services/electron/imageProcessor.ts
Normal file
77
src/services/electron/imageProcessor.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
import { ElectronAPIs } from 'types/electron';
|
||||||
|
import { ElectronFile } from 'types/upload';
|
||||||
|
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
|
class ElectronImageProcessorService {
|
||||||
|
private electronAPIs: ElectronAPIs;
|
||||||
|
constructor() {
|
||||||
|
this.electronAPIs = globalThis['ElectronAPIs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
convertAPIExists() {
|
||||||
|
return !!this.electronAPIs?.convertHEIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
generateImageThumbnailAPIExists() {
|
||||||
|
return !!this.electronAPIs?.generateImageThumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
async convertHEIC(fileBlob: Blob): Promise<Blob> {
|
||||||
|
try {
|
||||||
|
if (!this.electronAPIs?.convertHEIC) {
|
||||||
|
throw new Error('convertHEIC API not available');
|
||||||
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
|
const inputFileData = new Uint8Array(await fileBlob.arrayBuffer());
|
||||||
|
const convertedFileData = await this.electronAPIs.convertHEIC(
|
||||||
|
inputFileData
|
||||||
|
);
|
||||||
|
addLogLine(
|
||||||
|
`originalFileSize:${convertBytesToHumanReadable(
|
||||||
|
fileBlob?.size
|
||||||
|
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||||
|
convertedFileData?.length
|
||||||
|
)}, native heic conversion time: ${Date.now() - startTime}ms `
|
||||||
|
);
|
||||||
|
return new Blob([convertedFileData]);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to convert heic natively');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async generateImageThumbnail(
|
||||||
|
inputFile: File | ElectronFile,
|
||||||
|
maxDimension: number,
|
||||||
|
maxSize: number
|
||||||
|
): Promise<Uint8Array> {
|
||||||
|
try {
|
||||||
|
if (!this.electronAPIs?.generateImageThumbnail) {
|
||||||
|
throw new Error('generateImageThumbnail API not available');
|
||||||
|
}
|
||||||
|
const startTime = Date.now();
|
||||||
|
const thumb = await this.electronAPIs.generateImageThumbnail(
|
||||||
|
inputFile,
|
||||||
|
maxDimension,
|
||||||
|
maxSize
|
||||||
|
);
|
||||||
|
addLogLine(
|
||||||
|
`originalFileSize:${convertBytesToHumanReadable(
|
||||||
|
inputFile?.size
|
||||||
|
)},thumbFileSize:${convertBytesToHumanReadable(
|
||||||
|
thumb?.length
|
||||||
|
)}, native thumbnail generation time: ${
|
||||||
|
Date.now() - startTime
|
||||||
|
}ms `
|
||||||
|
);
|
||||||
|
return thumb;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to generate image thumbnail natively');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ElectronImageProcessorService();
|
|
@ -244,10 +244,7 @@ class ExportService {
|
||||||
file,
|
file,
|
||||||
RecordType.FAILED
|
RecordType.FAILED
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
`export failed for fileID:${file.id}, reason:`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
logError(
|
logError(
|
||||||
e,
|
e,
|
||||||
'download and save failed for file during export'
|
'download and save failed for file during export'
|
||||||
|
@ -486,16 +483,16 @@ class ExportService {
|
||||||
motionPhoto.videoNameTitle,
|
motionPhoto.videoNameTitle,
|
||||||
file.id
|
file.id
|
||||||
);
|
);
|
||||||
this.saveMediaFile(collectionPath, videoSaveName, videoStream);
|
await this.saveMediaFile(collectionPath, videoSaveName, videoStream);
|
||||||
await this.saveMetadataFile(collectionPath, videoSaveName, file);
|
await this.saveMetadataFile(collectionPath, videoSaveName, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveMediaFile(
|
private async saveMediaFile(
|
||||||
collectionFolderPath: string,
|
collectionFolderPath: string,
|
||||||
fileSaveName: string,
|
fileSaveName: string,
|
||||||
fileStream: ReadableStream<any>
|
fileStream: ReadableStream<any>
|
||||||
) {
|
) {
|
||||||
this.electronAPIs.saveStreamToDisk(
|
await this.electronAPIs.saveStreamToDisk(
|
||||||
getFileSavePath(collectionFolderPath, fileSaveName),
|
getFileSavePath(collectionFolderPath, fileSaveName),
|
||||||
fileStream
|
fileStream
|
||||||
);
|
);
|
||||||
|
@ -624,7 +621,6 @@ class ExportService {
|
||||||
oldFileSavePath,
|
oldFileSavePath,
|
||||||
newFileSavePath
|
newFileSavePath
|
||||||
);
|
);
|
||||||
console.log(oldFileMetadataSavePath, newFileMetadataSavePath);
|
|
||||||
await this.electronAPIs.checkExistsAndRename(
|
await this.electronAPIs.checkExistsAndRename(
|
||||||
oldFileMetadataSavePath,
|
oldFileMetadataSavePath,
|
||||||
newFileMetadataSavePath
|
newFileMetadataSavePath
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { ElectronFFmpeg } from 'services/electron/ffmpeg';
|
import { ElectronFFmpeg } from 'services/electron/ffmpeg';
|
||||||
import { ElectronFile } from 'types/upload';
|
import { ElectronFile } from 'types/upload';
|
||||||
import { FFmpegWorker } from 'utils/comlink';
|
import ComlinkFFmpegWorker from 'utils/comlink/ComlinkFFmpegWorker';
|
||||||
|
|
||||||
export interface IFFmpeg {
|
export interface IFFmpeg {
|
||||||
run: (
|
run: (
|
||||||
|
@ -13,16 +13,16 @@ export interface IFFmpeg {
|
||||||
|
|
||||||
class FFmpegFactory {
|
class FFmpegFactory {
|
||||||
private client: IFFmpeg;
|
private client: IFFmpeg;
|
||||||
|
|
||||||
async getFFmpegClient() {
|
async getFFmpegClient() {
|
||||||
if (!this.client) {
|
if (!this.client) {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
this.client = new ElectronFFmpeg();
|
this.client = new ElectronFFmpeg();
|
||||||
} else {
|
} else {
|
||||||
this.client = await new FFmpegWorker();
|
this.client = await ComlinkFFmpegWorker.getInstance();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.client;
|
return this.client;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FFmpegFactory();
|
export default new FFmpegFactory();
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
|
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import { EncryptionResult } from 'types/upload';
|
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
@ -12,14 +11,14 @@ import {
|
||||||
preservePhotoswipeProps,
|
preservePhotoswipeProps,
|
||||||
sortFiles,
|
sortFiles,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import { EnteFile, EncryptedEnteFile, TrashRequest } from 'types/file';
|
||||||
import { EnteFile, TrashRequest } from 'types/file';
|
|
||||||
import { SetFiles } from 'types/gallery';
|
import { SetFiles } from 'types/gallery';
|
||||||
import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
|
import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
|
||||||
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||||
import { addLogLine } from 'utils/logging';
|
import { addLogLine } from 'utils/logging';
|
||||||
import { isCollectionHidden } from 'utils/collection';
|
import { isCollectionHidden } from 'utils/collection';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const FILES_TABLE = 'files';
|
const FILES_TABLE = 'files';
|
||||||
|
@ -132,11 +131,12 @@ export const getFiles = async (
|
||||||
decryptedFiles = [
|
decryptedFiles = [
|
||||||
...decryptedFiles,
|
...decryptedFiles,
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
resp.data.diff.map(async (file: EnteFile) => {
|
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||||
if (!file.isDeleted) {
|
if (!file.isDeleted) {
|
||||||
file = await decryptFile(file, collection.key);
|
return await decryptFile(file, collection.key);
|
||||||
|
} else {
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
return file;
|
|
||||||
}) as Promise<EnteFile>[]
|
}) as Promise<EnteFile>[]
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
@ -250,16 +250,19 @@ export const updateFileMagicMetadata = async (files: EnteFile[]) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const { file: encryptedMagicMetadata }: EncryptionResult =
|
const { file: encryptedMagicMetadata } =
|
||||||
await worker.encryptMetadata(file.magicMetadata.data, file.key);
|
await cryptoWorker.encryptMetadata(
|
||||||
|
file.magicMetadata.data,
|
||||||
|
file.key
|
||||||
|
);
|
||||||
reqBody.metadataList.push({
|
reqBody.metadataList.push({
|
||||||
id: file.id,
|
id: file.id,
|
||||||
magicMetadata: {
|
magicMetadata: {
|
||||||
version: file.magicMetadata.version,
|
version: file.magicMetadata.version,
|
||||||
count: file.magicMetadata.count,
|
count: file.magicMetadata.count,
|
||||||
data: encryptedMagicMetadata.encryptedData as unknown as string,
|
data: encryptedMagicMetadata.encryptedData,
|
||||||
header: encryptedMagicMetadata.decryptionHeader,
|
header: encryptedMagicMetadata.decryptionHeader,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -284,16 +287,19 @@ export const updateFilePublicMagicMetadata = async (files: EnteFile[]) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
const reqBody: BulkUpdateMagicMetadataRequest = { metadataList: [] };
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const { file: encryptedPubMagicMetadata }: EncryptionResult =
|
const { file: encryptedPubMagicMetadata } =
|
||||||
await worker.encryptMetadata(file.pubMagicMetadata.data, file.key);
|
await cryptoWorker.encryptMetadata(
|
||||||
|
file.pubMagicMetadata.data,
|
||||||
|
file.key
|
||||||
|
);
|
||||||
reqBody.metadataList.push({
|
reqBody.metadataList.push({
|
||||||
id: file.id,
|
id: file.id,
|
||||||
magicMetadata: {
|
magicMetadata: {
|
||||||
version: file.pubMagicMetadata.version,
|
version: file.pubMagicMetadata.version,
|
||||||
count: file.pubMagicMetadata.count,
|
count: file.pubMagicMetadata.count,
|
||||||
data: encryptedPubMagicMetadata.encryptedData as unknown as string,
|
data: encryptedPubMagicMetadata.encryptedData,
|
||||||
header: encryptedPubMagicMetadata.decryptionHeader,
|
header: encryptedPubMagicMetadata.decryptionHeader,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import isElectron from 'is-electron';
|
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import WasmHEICConverterService from './wasmHeicConverter/wasmHEICConverterService';
|
import WasmHEICConverterService from './wasmHeicConverter/wasmHEICConverterService';
|
||||||
import ElectronHEICConvertor from 'services/electron/heicConvertor';
|
import ElectronImageProcessorService from 'services/electron/imageProcessor';
|
||||||
|
|
||||||
class HeicConversionService {
|
class HeicConversionService {
|
||||||
async convert(heicFileData: Blob): Promise<Blob> {
|
async convert(heicFileData: Blob): Promise<Blob> {
|
||||||
try {
|
try {
|
||||||
if (isElectron() && ElectronHEICConvertor.apiExists()) {
|
if (ElectronImageProcessorService.convertAPIExists()) {
|
||||||
try {
|
try {
|
||||||
return await ElectronHEICConvertor.convert(heicFileData);
|
return await ElectronImageProcessorService.convertHEIC(
|
||||||
|
heicFileData
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return await WasmHEICConverterService.convert(heicFileData);
|
return await WasmHEICConverterService.convert(heicFileData);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,14 +5,16 @@ import { getToken } from 'utils/common/key';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getEndpoint } from 'utils/common/apiUtil';
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import HTTPService from 'services/HTTPService';
|
import HTTPService from 'services/HTTPService';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import uploadHttpClient from 'services/upload/uploadHttpClient';
|
import uploadHttpClient from 'services/upload/uploadHttpClient';
|
||||||
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||||
import { getFileType } from 'services/typeDetectionService';
|
import { getFileType } from 'services/typeDetectionService';
|
||||||
import { getLocalTrash, getTrashedFiles } from './trashService';
|
import { getLocalTrash, getTrashedFiles } from './trashService';
|
||||||
import { EncryptionResult, UploadURL } from 'types/upload';
|
import { UploadURL } from 'types/upload';
|
||||||
import { fileAttribute } from 'types/file';
|
import { FileAttributes } from 'types/file';
|
||||||
import { USE_CF_PROXY } from 'constants/upload';
|
import { USE_CF_PROXY } from 'constants/upload';
|
||||||
|
import { Remote } from 'comlink';
|
||||||
|
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
||||||
|
@ -44,7 +46,7 @@ export async function replaceThumbnail(
|
||||||
let completedWithError = false;
|
let completedWithError = false;
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const files = await getLocalFiles();
|
const files = await getLocalFiles();
|
||||||
const trash = await getLocalTrash();
|
const trash = await getLocalTrash();
|
||||||
const trashFiles = getTrashedFiles(trash);
|
const trashFiles = getTrashedFiles(trash);
|
||||||
|
@ -83,7 +85,7 @@ export async function replaceThumbnail(
|
||||||
fileTypeInfo
|
fileTypeInfo
|
||||||
);
|
);
|
||||||
const newUploadedThumbnail = await uploadThumbnail(
|
const newUploadedThumbnail = await uploadThumbnail(
|
||||||
worker,
|
cryptoWorker,
|
||||||
file.key,
|
file.key,
|
||||||
newThumbnail,
|
newThumbnail,
|
||||||
uploadURLs.pop()
|
uploadURLs.pop()
|
||||||
|
@ -102,24 +104,26 @@ export async function replaceThumbnail(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadThumbnail(
|
export async function uploadThumbnail(
|
||||||
worker,
|
worker: Remote<DedicatedCryptoWorker>,
|
||||||
fileKey: string,
|
fileKey: string,
|
||||||
updatedThumbnail: Uint8Array,
|
updatedThumbnail: Uint8Array,
|
||||||
uploadURL: UploadURL
|
uploadURL: UploadURL
|
||||||
): Promise<fileAttribute> {
|
): Promise<FileAttributes> {
|
||||||
const { file: encryptedThumbnail }: EncryptionResult =
|
const { file: encryptedThumbnail } = await worker.encryptThumbnail(
|
||||||
await worker.encryptThumbnail(updatedThumbnail, fileKey);
|
updatedThumbnail,
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
let thumbnailObjectKey: string = null;
|
let thumbnailObjectKey: string = null;
|
||||||
if (USE_CF_PROXY) {
|
if (USE_CF_PROXY) {
|
||||||
thumbnailObjectKey = await uploadHttpClient.putFileV2(
|
thumbnailObjectKey = await uploadHttpClient.putFileV2(
|
||||||
uploadURL,
|
uploadURL,
|
||||||
encryptedThumbnail.encryptedData as Uint8Array,
|
encryptedThumbnail.encryptedData,
|
||||||
() => {}
|
() => {}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
thumbnailObjectKey = await uploadHttpClient.putFile(
|
thumbnailObjectKey = await uploadHttpClient.putFile(
|
||||||
uploadURL,
|
uploadURL,
|
||||||
encryptedThumbnail.encryptedData as Uint8Array,
|
encryptedThumbnail.encryptedData,
|
||||||
() => {}
|
() => {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -131,7 +135,7 @@ export async function uploadThumbnail(
|
||||||
|
|
||||||
export async function updateThumbnail(
|
export async function updateThumbnail(
|
||||||
fileID: number,
|
fileID: number,
|
||||||
newThumbnail: fileAttribute
|
newThumbnail: FileAttributes
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
|
@ -2,7 +2,6 @@ import {
|
||||||
getPublicCollectionFileURL,
|
getPublicCollectionFileURL,
|
||||||
getPublicCollectionThumbnailURL,
|
getPublicCollectionThumbnailURL,
|
||||||
} from 'utils/common/apiUtil';
|
} from 'utils/common/apiUtil';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import {
|
import {
|
||||||
generateStreamFromArrayBuffer,
|
generateStreamFromArrayBuffer,
|
||||||
getRenderableFileURL,
|
getRenderableFileURL,
|
||||||
|
@ -15,6 +14,8 @@ import { logError } from 'utils/sentry';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import QueueProcessor from './queueProcessor';
|
import QueueProcessor from './queueProcessor';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
|
||||||
class PublicCollectionDownloadManager {
|
class PublicCollectionDownloadManager {
|
||||||
private fileObjectURLPromise = new Map<
|
private fileObjectURLPromise = new Map<
|
||||||
|
@ -30,11 +31,17 @@ class PublicCollectionDownloadManager {
|
||||||
token: string,
|
token: string,
|
||||||
passwordToken: string
|
passwordToken: string
|
||||||
) {
|
) {
|
||||||
|
addLogLine(`[${file.id}] [PublicDownloadManger] getThumbnail called`);
|
||||||
try {
|
try {
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!this.thumbnailObjectURLPromise.get(file.id)) {
|
if (this.thumbnailObjectURLPromise.has(file.id)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [PublicDownloadManger] getThumbnail promise cache hit, returning existing promise`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.thumbnailObjectURLPromise.has(file.id)) {
|
||||||
const downloadPromise = async () => {
|
const downloadPromise = async () => {
|
||||||
const thumbnailCache = await (async () => {
|
const thumbnailCache = await (async () => {
|
||||||
try {
|
try {
|
||||||
|
@ -50,8 +57,14 @@ class PublicCollectionDownloadManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cacheResp) {
|
if (cacheResp) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [PublicDownloadManger] in memory cache hit, using localCache files`
|
||||||
|
);
|
||||||
return URL.createObjectURL(await cacheResp.blob());
|
return URL.createObjectURL(await cacheResp.blob());
|
||||||
}
|
}
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [PublicDownloadManger] in memory cache miss, getThumbnail download started`
|
||||||
|
);
|
||||||
const thumb =
|
const thumb =
|
||||||
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
|
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
|
||||||
() => this.downloadThumb(token, passwordToken, file)
|
() => this.downloadThumb(token, passwordToken, file)
|
||||||
|
@ -73,7 +86,7 @@ class PublicCollectionDownloadManager {
|
||||||
return await this.thumbnailObjectURLPromise.get(file.id);
|
return await this.thumbnailObjectURLPromise.get(file.id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.thumbnailObjectURLPromise.delete(file.id);
|
this.thumbnailObjectURLPromise.delete(file.id);
|
||||||
logError(e, 'get preview Failed');
|
logError(e, 'get publicDownloadManger preview Failed');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -97,10 +110,10 @@ class PublicCollectionDownloadManager {
|
||||||
if (typeof resp.data === 'undefined') {
|
if (typeof resp.data === 'undefined') {
|
||||||
throw Error(CustomError.REQUEST_FAILED);
|
throw Error(CustomError.REQUEST_FAILED);
|
||||||
}
|
}
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
const decrypted: Uint8Array = await worker.decryptThumbnail(
|
const decrypted = await cryptoWorker.decryptThumbnail(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.thumbnail.decryptionHeader),
|
await cryptoWorker.fromB64(file.thumbnail.decryptionHeader),
|
||||||
file.key
|
file.key
|
||||||
);
|
);
|
||||||
return decrypted;
|
return decrypted;
|
||||||
|
@ -115,6 +128,9 @@ class PublicCollectionDownloadManager {
|
||||||
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
const fileKey = forPreview ? `${file.id}_preview` : `${file.id}`;
|
||||||
try {
|
try {
|
||||||
const getFilePromise = async () => {
|
const getFilePromise = async () => {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [PublicDownloadManager] downloading file`
|
||||||
|
);
|
||||||
const fileStream = await this.downloadFile(
|
const fileStream = await this.downloadFile(
|
||||||
token,
|
token,
|
||||||
passwordToken,
|
passwordToken,
|
||||||
|
@ -131,6 +147,11 @@ class PublicCollectionDownloadManager {
|
||||||
return { converted: [fileURL], original: [fileURL] };
|
return { converted: [fileURL], original: [fileURL] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if (this.fileObjectURLPromise.has(fileKey)) {
|
||||||
|
addLogLine(
|
||||||
|
`[${file.id}] [PublicDownloadManager] getFile promise cache hit, returning existing promise`
|
||||||
|
);
|
||||||
|
}
|
||||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||||
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
this.fileObjectURLPromise.set(fileKey, getFilePromise());
|
||||||
}
|
}
|
||||||
|
@ -138,7 +159,7 @@ class PublicCollectionDownloadManager {
|
||||||
return fileURLs;
|
return fileURLs;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.fileObjectURLPromise.delete(fileKey);
|
this.fileObjectURLPromise.delete(fileKey);
|
||||||
logError(e, 'Failed to get File');
|
logError(e, 'public download manager Failed to get File');
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -148,7 +169,7 @@ class PublicCollectionDownloadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async downloadFile(token: string, passwordToken: string, file: EnteFile) {
|
async downloadFile(token: string, passwordToken: string, file: EnteFile) {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -170,9 +191,9 @@ class PublicCollectionDownloadManager {
|
||||||
if (typeof resp.data === 'undefined') {
|
if (typeof resp.data === 'undefined') {
|
||||||
throw Error(CustomError.REQUEST_FAILED);
|
throw Error(CustomError.REQUEST_FAILED);
|
||||||
}
|
}
|
||||||
const decrypted: any = await worker.decryptFile(
|
const decrypted = await cryptoWorker.decryptFile(
|
||||||
new Uint8Array(resp.data),
|
new Uint8Array(resp.data),
|
||||||
await worker.fromB64(file.file.decryptionHeader),
|
await cryptoWorker.fromB64(file.file.decryptionHeader),
|
||||||
file.key
|
file.key
|
||||||
);
|
);
|
||||||
return generateStreamFromArrayBuffer(decrypted);
|
return generateStreamFromArrayBuffer(decrypted);
|
||||||
|
@ -188,12 +209,15 @@ class PublicCollectionDownloadManager {
|
||||||
const reader = resp.body.getReader();
|
const reader = resp.body.getReader();
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const decryptionHeader = await worker.fromB64(
|
const decryptionHeader = await cryptoWorker.fromB64(
|
||||||
file.file.decryptionHeader
|
file.file.decryptionHeader
|
||||||
);
|
);
|
||||||
const fileKey = await worker.fromB64(file.key);
|
const fileKey = await cryptoWorker.fromB64(file.key);
|
||||||
const { pullState, decryptionChunkSize } =
|
const { pullState, decryptionChunkSize } =
|
||||||
await worker.initDecryption(decryptionHeader, fileKey);
|
await cryptoWorker.initDecryption(
|
||||||
|
decryptionHeader,
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
let data = new Uint8Array();
|
let data = new Uint8Array();
|
||||||
// The following function handles each data chunk
|
// The following function handles each data chunk
|
||||||
function push() {
|
function push() {
|
||||||
|
@ -212,7 +236,7 @@ class PublicCollectionDownloadManager {
|
||||||
decryptionChunkSize
|
decryptionChunkSize
|
||||||
);
|
);
|
||||||
const { decryptedData } =
|
const { decryptedData } =
|
||||||
await worker.decryptChunk(
|
await cryptoWorker.decryptChunk(
|
||||||
fileData,
|
fileData,
|
||||||
pullState
|
pullState
|
||||||
);
|
);
|
||||||
|
@ -225,7 +249,10 @@ class PublicCollectionDownloadManager {
|
||||||
} else {
|
} else {
|
||||||
if (data) {
|
if (data) {
|
||||||
const { decryptedData } =
|
const { decryptedData } =
|
||||||
await worker.decryptChunk(data, pullState);
|
await cryptoWorker.decryptChunk(
|
||||||
|
data,
|
||||||
|
pullState
|
||||||
|
);
|
||||||
controller.enqueue(decryptedData);
|
controller.enqueue(decryptedData);
|
||||||
data = null;
|
data = null;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { getEndpoint } from 'utils/common/apiUtil';
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection, EncryptedCollection } from 'types/collection';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||||
import { EnteFile } from 'types/file';
|
import { EncryptedEnteFile, EnteFile } from 'types/file';
|
||||||
import {
|
import {
|
||||||
AbuseReportDetails,
|
AbuseReportDetails,
|
||||||
AbuseReportRequest,
|
AbuseReportRequest,
|
||||||
LocalSavedPublicCollectionFiles,
|
LocalSavedPublicCollectionFiles,
|
||||||
} from 'types/publicCollection';
|
} from 'types/publicCollection';
|
||||||
import CryptoWorker from 'utils/crypto';
|
|
||||||
import { REPORT_REASON } from 'constants/publicCollection';
|
import { REPORT_REASON } from 'constants/publicCollection';
|
||||||
import { CustomError, parseSharingErrorCodes } from 'utils/error';
|
import { CustomError, parseSharingErrorCodes } from 'utils/error';
|
||||||
|
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
|
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
|
||||||
|
@ -20,12 +20,29 @@ const PUBLIC_COLLECTIONS_TABLE = 'public-collections';
|
||||||
|
|
||||||
export const getPublicCollectionUID = (token: string) => `${token}`;
|
export const getPublicCollectionUID = (token: string) => `${token}`;
|
||||||
|
|
||||||
const getPublicCollectionSyncTimeUID = (collectionUID: string) =>
|
const getPublicCollectionLastSyncTimeKey = (collectionUID: string) =>
|
||||||
`public-${collectionUID}-time`;
|
`public-${collectionUID}-time`;
|
||||||
|
|
||||||
const getPublicCollectionPasswordKey = (collectionUID: string) =>
|
const getPublicCollectionPasswordKey = (collectionUID: string) =>
|
||||||
`public-${collectionUID}-passkey`;
|
`public-${collectionUID}-passkey`;
|
||||||
|
|
||||||
|
const getPublicCollectionUploaderNameKey = (collectionUID: string) =>
|
||||||
|
`public-${collectionUID}-uploaderName`;
|
||||||
|
|
||||||
|
export const getPublicCollectionUploaderName = async (collectionUID: string) =>
|
||||||
|
await localForage.getItem<string>(
|
||||||
|
getPublicCollectionUploaderNameKey(collectionUID)
|
||||||
|
);
|
||||||
|
|
||||||
|
export const savePublicCollectionUploaderName = async (
|
||||||
|
collectionUID: string,
|
||||||
|
uploaderName: string
|
||||||
|
) =>
|
||||||
|
await localForage.setItem(
|
||||||
|
getPublicCollectionUploaderNameKey(collectionUID),
|
||||||
|
uploaderName
|
||||||
|
);
|
||||||
|
|
||||||
export const getLocalPublicFiles = async (collectionUID: string) => {
|
export const getLocalPublicFiles = async (collectionUID: string) => {
|
||||||
const localSavedPublicCollectionFiles =
|
const localSavedPublicCollectionFiles =
|
||||||
(
|
(
|
||||||
|
@ -129,15 +146,15 @@ const dedupeCollectionFiles = (
|
||||||
|
|
||||||
const getPublicCollectionLastSyncTime = async (collectionUID: string) =>
|
const getPublicCollectionLastSyncTime = async (collectionUID: string) =>
|
||||||
(await localForage.getItem<number>(
|
(await localForage.getItem<number>(
|
||||||
getPublicCollectionSyncTimeUID(collectionUID)
|
getPublicCollectionLastSyncTimeKey(collectionUID)
|
||||||
)) ?? 0;
|
)) ?? 0;
|
||||||
|
|
||||||
const setPublicCollectionLastSyncTime = async (
|
const savePublicCollectionLastSyncTime = async (
|
||||||
collectionUID: string,
|
collectionUID: string,
|
||||||
time: number
|
time: number
|
||||||
) =>
|
) =>
|
||||||
await localForage.setItem(
|
await localForage.setItem(
|
||||||
getPublicCollectionSyncTimeUID(collectionUID),
|
getPublicCollectionLastSyncTimeKey(collectionUID),
|
||||||
time
|
time
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -191,7 +208,7 @@ export const syncPublicFiles = async (
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
await savePublicCollectionFiles(collectionUID, files);
|
await savePublicCollectionFiles(collectionUID, files);
|
||||||
await setPublicCollectionLastSyncTime(
|
await savePublicCollectionLastSyncTime(
|
||||||
collectionUID,
|
collectionUID,
|
||||||
collection.updationTime
|
collection.updationTime
|
||||||
);
|
);
|
||||||
|
@ -200,7 +217,6 @@ export const syncPublicFiles = async (
|
||||||
const parsedError = parseSharingErrorCodes(e);
|
const parsedError = parseSharingErrorCodes(e);
|
||||||
logError(e, 'failed to sync shared collection files');
|
logError(e, 'failed to sync shared collection files');
|
||||||
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
|
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
|
||||||
console.log('invalid token or password');
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -243,11 +259,12 @@ const getPublicFiles = async (
|
||||||
decryptedFiles = [
|
decryptedFiles = [
|
||||||
...decryptedFiles,
|
...decryptedFiles,
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
resp.data.diff.map(async (file: EnteFile) => {
|
resp.data.diff.map(async (file: EncryptedEnteFile) => {
|
||||||
if (!file.isDeleted) {
|
if (!file.isDeleted) {
|
||||||
file = await decryptFile(file, collection.key);
|
return await decryptFile(file, collection.key);
|
||||||
|
} else {
|
||||||
|
return file;
|
||||||
}
|
}
|
||||||
return file;
|
|
||||||
}) as Promise<EnteFile>[]
|
}) as Promise<EnteFile>[]
|
||||||
)),
|
)),
|
||||||
];
|
];
|
||||||
|
@ -323,14 +340,14 @@ export const verifyPublicCollectionPassword = async (
|
||||||
};
|
};
|
||||||
|
|
||||||
const decryptCollectionName = async (
|
const decryptCollectionName = async (
|
||||||
collection: Collection,
|
collection: EncryptedCollection,
|
||||||
collectionKey: string
|
collectionKey: string
|
||||||
) => {
|
) => {
|
||||||
const worker = await new CryptoWorker();
|
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||||
|
|
||||||
return (collection.name =
|
return (collection.name =
|
||||||
collection.name ||
|
collection.name ||
|
||||||
(await worker.decryptToUTF8(
|
(await cryptoWorker.decryptToUTF8(
|
||||||
collection.encryptedName,
|
collection.encryptedName,
|
||||||
collection.nameDecryptionNonce,
|
collection.nameDecryptionNonce,
|
||||||
collectionKey
|
collectionKey
|
||||||
|
@ -379,7 +396,9 @@ export const removePublicCollectionWithFiles = async (
|
||||||
|
|
||||||
export const removePublicFiles = async (collectionUID: string) => {
|
export const removePublicFiles = async (collectionUID: string) => {
|
||||||
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
|
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
|
||||||
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID));
|
await localForage.removeItem(
|
||||||
|
getPublicCollectionLastSyncTimeKey(collectionUID)
|
||||||
|
);
|
||||||
|
|
||||||
const publicCollectionFiles =
|
const publicCollectionFiles =
|
||||||
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
(await localForage.getItem<LocalSavedPublicCollectionFiles[]>(
|
||||||
|
|
|
@ -17,6 +17,7 @@ import {
|
||||||
} from 'types/search';
|
} from 'types/search';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { getFormattedDate, isInsideBox, isSameDayAnyYear } from 'utils/search';
|
import { getFormattedDate, isInsideBox, isSameDayAnyYear } from 'utils/search';
|
||||||
|
import { getUniqueFiles } from 'utils/file';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
|
@ -43,8 +44,8 @@ export const getAutoCompleteSuggestions =
|
||||||
searchQuery: convertSuggestionToSearchQuery(suggestion),
|
searchQuery: convertSuggestionToSearchQuery(suggestion),
|
||||||
}))
|
}))
|
||||||
.map(({ suggestion, searchQuery }) => {
|
.map(({ suggestion, searchQuery }) => {
|
||||||
const resultFiles = files.filter((file) =>
|
const resultFiles = getUniqueFiles(
|
||||||
isSearchedFile(file, searchQuery)
|
files.filter((file) => isSearchedFile(file, searchQuery))
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
...suggestion,
|
...suggestion,
|
||||||
|
@ -114,7 +115,7 @@ function searchCollection(
|
||||||
}
|
}
|
||||||
|
|
||||||
function searchFiles(searchPhrase: string, files: EnteFile[]) {
|
function searchFiles(searchPhrase: string, files: EnteFile[]) {
|
||||||
return files
|
return getUniqueFiles(files)
|
||||||
.map((file) => ({
|
.map((file) => ({
|
||||||
title: file.metadata.title,
|
title: file.metadata.title,
|
||||||
id: file.id,
|
id: file.id,
|
||||||
|
|
|
@ -14,7 +14,7 @@ import { getCollection } from './collectionService';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
|
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { Trash, TrashItem } from 'types/trash';
|
import { EncryptedTrashItem, Trash } from 'types/trash';
|
||||||
|
|
||||||
const TRASH = 'file-trash';
|
const TRASH = 'file-trash';
|
||||||
const TRASH_TIME = 'trash-time';
|
const TRASH_TIME = 'trash-time';
|
||||||
|
@ -99,7 +99,8 @@ export const updateTrash = async (
|
||||||
'X-Auth-Token': token,
|
'X-Auth-Token': token,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
for (const trashItem of resp.data.diff as TrashItem[]) {
|
// #Perf: This can be optimized by running the decryption in parallel
|
||||||
|
for (const trashItem of resp.data.diff as EncryptedTrashItem[]) {
|
||||||
const collectionID = trashItem.file.collectionID;
|
const collectionID = trashItem.file.collectionID;
|
||||||
let collection = collections.get(collectionID);
|
let collection = collections.get(collectionID);
|
||||||
if (!collection) {
|
if (!collection) {
|
||||||
|
@ -110,19 +111,21 @@ export const updateTrash = async (
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
if (!trashItem.isDeleted && !trashItem.isRestored) {
|
if (!trashItem.isDeleted && !trashItem.isRestored) {
|
||||||
trashItem.file = await decryptFile(
|
const decryptedFile = await decryptFile(
|
||||||
trashItem.file,
|
trashItem.file,
|
||||||
collection.key
|
collection.key
|
||||||
);
|
);
|
||||||
|
updatedTrash.push({ ...trashItem, file: decryptedFile });
|
||||||
|
} else {
|
||||||
|
updatedTrash = updatedTrash.filter(
|
||||||
|
(item) => item.file.id !== trashItem.file.id
|
||||||
|
);
|
||||||
}
|
}
|
||||||
updatedTrash.push(trashItem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resp.data.diff.length) {
|
if (resp.data.diff.length) {
|
||||||
time = resp.data.diff.slice(-1)[0].updatedAt;
|
time = resp.data.diff.slice(-1)[0].updatedAt;
|
||||||
}
|
}
|
||||||
updatedTrash = removeDuplicates(updatedTrash);
|
|
||||||
updatedTrash = removeRestoredOrDeletedTrashItems(updatedTrash);
|
|
||||||
|
|
||||||
setFiles(
|
setFiles(
|
||||||
preservePhotoswipeProps(
|
preservePhotoswipeProps(
|
||||||
|
@ -145,28 +148,6 @@ export const updateTrash = async (
|
||||||
return currentTrash;
|
return currentTrash;
|
||||||
};
|
};
|
||||||
|
|
||||||
function removeDuplicates(trash: Trash) {
|
|
||||||
const latestVersionTrashItems = new Map<number, TrashItem>();
|
|
||||||
trash.forEach(({ file, updatedAt, ...rest }) => {
|
|
||||||
if (
|
|
||||||
!latestVersionTrashItems.has(file.id) ||
|
|
||||||
latestVersionTrashItems.get(file.id).updatedAt < updatedAt
|
|
||||||
) {
|
|
||||||
latestVersionTrashItems.set(file.id, { file, updatedAt, ...rest });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
trash = [];
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
for (const [_, trashedFile] of latestVersionTrashItems) {
|
|
||||||
trash.push(trashedFile);
|
|
||||||
}
|
|
||||||
return trash;
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeRestoredOrDeletedTrashItems(trash: Trash) {
|
|
||||||
return trash.filter((item) => !item.isDeleted && !item.isRestored);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTrashedFiles(trash: Trash) {
|
export function getTrashedFiles(trash: Trash) {
|
||||||
return mergeMetadata(
|
return mergeMetadata(
|
||||||
trash.map((trashedFile) => ({
|
trash.map((trashedFile) => ({
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { DataStream, EncryptionResult, isDataStream } from 'types/upload';
|
import { EncryptionResult } from 'types/crypto';
|
||||||
|
import { DataStream, isDataStream } from 'types/upload';
|
||||||
|
|
||||||
async function encryptFileStream(worker, fileData: DataStream) {
|
async function encryptFileStream(worker, fileData: DataStream) {
|
||||||
const { stream, chunkCount } = fileData;
|
const { stream, chunkCount } = fileData;
|
||||||
|
@ -33,7 +34,7 @@ async function encryptFileStream(worker, fileData: DataStream) {
|
||||||
export async function encryptFiledata(
|
export async function encryptFiledata(
|
||||||
worker,
|
worker,
|
||||||
filedata: Uint8Array | DataStream
|
filedata: Uint8Array | DataStream
|
||||||
): Promise<EncryptionResult> {
|
): Promise<EncryptionResult<Uint8Array | DataStream>> {
|
||||||
return isDataStream(filedata)
|
return isDataStream(filedata)
|
||||||
? await encryptFileStream(worker, filedata)
|
? await encryptFileStream(worker, filedata)
|
||||||
: await worker.encryptFile(filedata);
|
: await worker.encryptFile(filedata);
|
||||||
|
|
|
@ -3,9 +3,7 @@ import {
|
||||||
FileTypeInfo,
|
FileTypeInfo,
|
||||||
FileInMemory,
|
FileInMemory,
|
||||||
Metadata,
|
Metadata,
|
||||||
B64EncryptionResult,
|
|
||||||
EncryptedFile,
|
EncryptedFile,
|
||||||
EncryptionResult,
|
|
||||||
FileWithMetadata,
|
FileWithMetadata,
|
||||||
ParsedMetadataJSONMap,
|
ParsedMetadataJSONMap,
|
||||||
DataStream,
|
DataStream,
|
||||||
|
@ -22,6 +20,9 @@ import {
|
||||||
getUint8ArrayView,
|
getUint8ArrayView,
|
||||||
} from '../readerService';
|
} from '../readerService';
|
||||||
import { generateThumbnail } from './thumbnailService';
|
import { generateThumbnail } from './thumbnailService';
|
||||||
|
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||||
|
import { Remote } from 'comlink';
|
||||||
|
import { EncryptedMagicMetadata } from 'types/magicMetadata';
|
||||||
|
|
||||||
const EDITED_FILE_SUFFIX = '-edited';
|
const EDITED_FILE_SUFFIX = '-edited';
|
||||||
|
|
||||||
|
@ -68,10 +69,11 @@ export async function readFile(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function extractFileMetadata(
|
export async function extractFileMetadata(
|
||||||
|
worker,
|
||||||
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||||
rawFile: File | ElectronFile,
|
|
||||||
collectionID: number,
|
collectionID: number,
|
||||||
fileTypeInfo: FileTypeInfo
|
fileTypeInfo: FileTypeInfo,
|
||||||
|
rawFile: File | ElectronFile
|
||||||
) {
|
) {
|
||||||
const originalName = getFileOriginalName(rawFile);
|
const originalName = getFileOriginalName(rawFile);
|
||||||
const googleMetadata =
|
const googleMetadata =
|
||||||
|
@ -79,6 +81,7 @@ export async function extractFileMetadata(
|
||||||
getMetadataJSONMapKey(collectionID, originalName)
|
getMetadataJSONMapKey(collectionID, originalName)
|
||||||
) ?? {};
|
) ?? {};
|
||||||
const extractedMetadata: Metadata = await extractMetadata(
|
const extractedMetadata: Metadata = await extractMetadata(
|
||||||
|
worker,
|
||||||
rawFile,
|
rawFile,
|
||||||
fileTypeInfo
|
fileTypeInfo
|
||||||
);
|
);
|
||||||
|
@ -93,7 +96,7 @@ export async function extractFileMetadata(
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function encryptFile(
|
export async function encryptFile(
|
||||||
worker: any,
|
worker: Remote<DedicatedCryptoWorker>,
|
||||||
file: FileWithMetadata,
|
file: FileWithMetadata,
|
||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<EncryptedFile> {
|
): Promise<EncryptedFile> {
|
||||||
|
@ -103,21 +106,38 @@ export async function encryptFile(
|
||||||
file.filedata
|
file.filedata
|
||||||
);
|
);
|
||||||
|
|
||||||
const { file: encryptedThumbnail }: EncryptionResult =
|
const { file: encryptedThumbnail } = await worker.encryptThumbnail(
|
||||||
await worker.encryptThumbnail(file.thumbnail, fileKey);
|
file.thumbnail,
|
||||||
const { file: encryptedMetadata }: EncryptionResult =
|
fileKey
|
||||||
await worker.encryptMetadata(file.metadata, fileKey);
|
|
||||||
|
|
||||||
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
|
||||||
fileKey,
|
|
||||||
encryptionKey
|
|
||||||
);
|
);
|
||||||
|
const { file: encryptedMetadata } = await worker.encryptMetadata(
|
||||||
|
file.metadata,
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
|
|
||||||
|
let encryptedPubMagicMetadata: EncryptedMagicMetadata;
|
||||||
|
if (file.pubMagicMetadata) {
|
||||||
|
const { file: encryptedPubMagicMetadataData } =
|
||||||
|
await worker.encryptMetadata(
|
||||||
|
file.pubMagicMetadata.data,
|
||||||
|
fileKey
|
||||||
|
);
|
||||||
|
encryptedPubMagicMetadata = {
|
||||||
|
version: file.pubMagicMetadata.version,
|
||||||
|
count: file.pubMagicMetadata.count,
|
||||||
|
data: encryptedPubMagicMetadataData.encryptedData,
|
||||||
|
header: encryptedPubMagicMetadataData.decryptionHeader,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const encryptedKey = await worker.encryptToB64(fileKey, encryptionKey);
|
||||||
|
|
||||||
const result: EncryptedFile = {
|
const result: EncryptedFile = {
|
||||||
file: {
|
file: {
|
||||||
file: encryptedFiledata,
|
file: encryptedFiledata,
|
||||||
thumbnail: encryptedThumbnail,
|
thumbnail: encryptedThumbnail,
|
||||||
metadata: encryptedMetadata,
|
metadata: encryptedMetadata,
|
||||||
|
pubMagicMetadata: encryptedPubMagicMetadata,
|
||||||
localID: file.localID,
|
localID: file.localID,
|
||||||
},
|
},
|
||||||
fileKey: encryptedKey,
|
fileKey: encryptedKey,
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import { FILE_READER_CHUNK_SIZE } from 'constants/upload';
|
import { FILE_READER_CHUNK_SIZE } from 'constants/upload';
|
||||||
import { getFileStream, getElectronFileStream } from 'services/readerService';
|
import { getFileStream, getElectronFileStream } from 'services/readerService';
|
||||||
import { ElectronFile, DataStream } from 'types/upload';
|
import { ElectronFile, DataStream } from 'types/upload';
|
||||||
import CryptoWorker from 'utils/crypto';
|
import { CustomError } from 'utils/error';
|
||||||
|
import { addLogLine, getFileNameSize } from 'utils/logging';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
export async function getFileHash(file: File | ElectronFile) {
|
export async function getFileHash(worker, file: File | ElectronFile) {
|
||||||
try {
|
try {
|
||||||
|
addLogLine(`getFileHash called for ${getFileNameSize(file)}`);
|
||||||
let filedata: DataStream;
|
let filedata: DataStream;
|
||||||
if (file instanceof File) {
|
if (file instanceof File) {
|
||||||
filedata = getFileStream(file, FILE_READER_CHUNK_SIZE);
|
filedata = getFileStream(file, FILE_READER_CHUNK_SIZE);
|
||||||
|
@ -15,22 +17,29 @@ export async function getFileHash(file: File | ElectronFile) {
|
||||||
FILE_READER_CHUNK_SIZE
|
FILE_READER_CHUNK_SIZE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const cryptoWorker = await new CryptoWorker();
|
const hashState = await worker.initChunkHashing();
|
||||||
const hashState = await cryptoWorker.initChunkHashing();
|
|
||||||
|
|
||||||
const reader = filedata.stream.getReader();
|
const streamReader = filedata.stream.getReader();
|
||||||
// eslint-disable-next-line no-constant-condition
|
for (let i = 0; i < filedata.chunkCount; i++) {
|
||||||
while (true) {
|
const { done, value: chunk } = await streamReader.read();
|
||||||
const { done, value: chunk } = await reader.read();
|
|
||||||
if (done) {
|
if (done) {
|
||||||
break;
|
throw Error(CustomError.CHUNK_LESS_THAN_EXPECTED);
|
||||||
}
|
}
|
||||||
await cryptoWorker.hashFileChunk(hashState, Uint8Array.from(chunk));
|
await worker.hashFileChunk(hashState, Uint8Array.from(chunk));
|
||||||
}
|
}
|
||||||
const hash = await cryptoWorker.completeChunkHashing(hashState);
|
const { done } = await streamReader.read();
|
||||||
|
if (!done) {
|
||||||
|
throw Error(CustomError.CHUNK_MORE_THAN_EXPECTED);
|
||||||
|
}
|
||||||
|
const hash = await worker.completeChunkHashing(hashState);
|
||||||
|
addLogLine(
|
||||||
|
`file hashing completed successfully ${getFileNameSize(file)}`
|
||||||
|
);
|
||||||
return hash;
|
return hash;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'getFileHash failed');
|
logError(e, 'getFileHash failed');
|
||||||
throw e;
|
addLogLine(
|
||||||
|
`file hashing failed ${getFileNameSize(file)} ,${e.message} `
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,23 @@
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload';
|
import { LIVE_PHOTO_ASSET_SIZE_LIMIT } from 'constants/upload';
|
||||||
import { encodeMotionPhoto } from 'services/motionPhotoService';
|
import { encodeMotionPhoto } from 'services/motionPhotoService';
|
||||||
|
import { getFileType } from 'services/typeDetectionService';
|
||||||
import {
|
import {
|
||||||
ElectronFile,
|
ElectronFile,
|
||||||
FileTypeInfo,
|
FileTypeInfo,
|
||||||
FileWithCollection,
|
FileWithCollection,
|
||||||
LivePhotoAssets,
|
LivePhotoAssets,
|
||||||
Metadata,
|
ParsedMetadataJSONMap,
|
||||||
} from 'types/upload';
|
} from 'types/upload';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { isImageOrVideo, splitFilenameAndExtension } from 'utils/file';
|
import { getFileTypeFromExtensionForLivePhotoClustering } from 'utils/file/livePhoto';
|
||||||
|
import { splitFilenameAndExtension, isImageOrVideo } from 'utils/file';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getUint8ArrayView } from '../readerService';
|
import { getUint8ArrayView } from '../readerService';
|
||||||
|
import { extractFileMetadata } from './fileService';
|
||||||
|
import { getFileHash } from './hashService';
|
||||||
import { generateThumbnail } from './thumbnailService';
|
import { generateThumbnail } from './thumbnailService';
|
||||||
import uploadService from './uploadService';
|
import uploadCancelService from './uploadCancelService';
|
||||||
import UploadService from './uploadService';
|
|
||||||
|
|
||||||
interface LivePhotoIdentifier {
|
interface LivePhotoIdentifier {
|
||||||
collectionID: number;
|
collectionID: number;
|
||||||
|
@ -23,56 +26,58 @@ interface LivePhotoIdentifier {
|
||||||
size: number;
|
size: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Asset {
|
|
||||||
file: File | ElectronFile;
|
|
||||||
metadata: Metadata;
|
|
||||||
fileTypeInfo: FileTypeInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ENTE_LIVE_PHOTO_FORMAT = 'elp';
|
|
||||||
|
|
||||||
const UNDERSCORE_THREE = '_3';
|
const UNDERSCORE_THREE = '_3';
|
||||||
|
|
||||||
const UNDERSCORE = '_';
|
const UNDERSCORE = '_';
|
||||||
|
|
||||||
export function getLivePhotoFileType(
|
export async function getLivePhotoFileType(
|
||||||
imageFileTypeInfo: FileTypeInfo,
|
livePhotoAssets: LivePhotoAssets
|
||||||
videoTypeInfo: FileTypeInfo
|
): Promise<FileTypeInfo> {
|
||||||
): FileTypeInfo {
|
const imageFileTypeInfo = await getFileType(livePhotoAssets.image);
|
||||||
|
const videoFileTypeInfo = await getFileType(livePhotoAssets.video);
|
||||||
return {
|
return {
|
||||||
fileType: FILE_TYPE.LIVE_PHOTO,
|
fileType: FILE_TYPE.LIVE_PHOTO,
|
||||||
exactType: `${imageFileTypeInfo.exactType}+${videoTypeInfo.exactType}`,
|
exactType: `${imageFileTypeInfo.exactType}+${videoFileTypeInfo.exactType}`,
|
||||||
imageType: imageFileTypeInfo.exactType,
|
imageType: imageFileTypeInfo.exactType,
|
||||||
videoType: videoTypeInfo.exactType,
|
videoType: videoFileTypeInfo.exactType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLivePhotoMetadata(
|
export async function extractLivePhotoMetadata(
|
||||||
imageMetadata: Metadata,
|
worker,
|
||||||
videoMetadata: Metadata
|
parsedMetadataJSONMap: ParsedMetadataJSONMap,
|
||||||
|
collectionID: number,
|
||||||
|
fileTypeInfo: FileTypeInfo,
|
||||||
|
livePhotoAssets: LivePhotoAssets
|
||||||
) {
|
) {
|
||||||
|
const imageFileTypeInfo: FileTypeInfo = {
|
||||||
|
fileType: FILE_TYPE.IMAGE,
|
||||||
|
exactType: fileTypeInfo.imageType,
|
||||||
|
};
|
||||||
|
const imageMetadata = await extractFileMetadata(
|
||||||
|
worker,
|
||||||
|
parsedMetadataJSONMap,
|
||||||
|
collectionID,
|
||||||
|
imageFileTypeInfo,
|
||||||
|
livePhotoAssets.image
|
||||||
|
);
|
||||||
|
const videoHash = await getFileHash(worker, livePhotoAssets.video);
|
||||||
return {
|
return {
|
||||||
...imageMetadata,
|
...imageMetadata,
|
||||||
title: getLivePhotoName(imageMetadata.title),
|
title: getLivePhotoName(livePhotoAssets),
|
||||||
fileType: FILE_TYPE.LIVE_PHOTO,
|
fileType: FILE_TYPE.LIVE_PHOTO,
|
||||||
imageHash: imageMetadata.hash,
|
imageHash: imageMetadata.hash,
|
||||||
videoHash: videoMetadata.hash,
|
videoHash: videoHash,
|
||||||
hash: undefined,
|
hash: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLivePhotoFilePath(imageAsset: Asset): string {
|
|
||||||
return getLivePhotoName((imageAsset.file as any).path);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
|
export function getLivePhotoSize(livePhotoAssets: LivePhotoAssets) {
|
||||||
return livePhotoAssets.image.size + livePhotoAssets.video.size;
|
return livePhotoAssets.image.size + livePhotoAssets.video.size;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLivePhotoName(imageTitle: string) {
|
export function getLivePhotoName(livePhotoAssets: LivePhotoAssets) {
|
||||||
return `${
|
return livePhotoAssets.image.name;
|
||||||
splitFilenameAndExtension(imageTitle)[0]
|
|
||||||
}.${ENTE_LIVE_PHOTO_FORMAT}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readLivePhoto(
|
export async function readLivePhoto(
|
||||||
|
@ -103,7 +108,7 @@ export async function readLivePhoto(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
export async function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
||||||
try {
|
try {
|
||||||
const analysedMediaFiles: FileWithCollection[] = [];
|
const analysedMediaFiles: FileWithCollection[] = [];
|
||||||
mediaFiles
|
mediaFiles
|
||||||
|
@ -120,59 +125,48 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
||||||
);
|
);
|
||||||
let index = 0;
|
let index = 0;
|
||||||
while (index < mediaFiles.length - 1) {
|
while (index < mediaFiles.length - 1) {
|
||||||
|
if (uploadCancelService.isUploadCancelationRequested()) {
|
||||||
|
throw Error(CustomError.UPLOAD_CANCELLED);
|
||||||
|
}
|
||||||
const firstMediaFile = mediaFiles[index];
|
const firstMediaFile = mediaFiles[index];
|
||||||
const secondMediaFile = mediaFiles[index + 1];
|
const secondMediaFile = mediaFiles[index + 1];
|
||||||
const {
|
const firstFileType =
|
||||||
fileTypeInfo: firstFileTypeInfo,
|
getFileTypeFromExtensionForLivePhotoClustering(
|
||||||
metadata: firstFileMetadata,
|
firstMediaFile.file.name
|
||||||
} = UploadService.getFileMetadataAndFileTypeInfo(
|
);
|
||||||
firstMediaFile.localID
|
const secondFileType =
|
||||||
);
|
getFileTypeFromExtensionForLivePhotoClustering(
|
||||||
const {
|
secondMediaFile.file.name
|
||||||
fileTypeInfo: secondFileFileInfo,
|
);
|
||||||
metadata: secondFileMetadata,
|
|
||||||
} = UploadService.getFileMetadataAndFileTypeInfo(
|
|
||||||
secondMediaFile.localID
|
|
||||||
);
|
|
||||||
const firstFileIdentifier: LivePhotoIdentifier = {
|
const firstFileIdentifier: LivePhotoIdentifier = {
|
||||||
collectionID: firstMediaFile.collectionID,
|
collectionID: firstMediaFile.collectionID,
|
||||||
fileType: firstFileTypeInfo.fileType,
|
fileType: firstFileType,
|
||||||
name: firstMediaFile.file.name,
|
name: firstMediaFile.file.name,
|
||||||
size: firstMediaFile.file.size,
|
size: firstMediaFile.file.size,
|
||||||
};
|
};
|
||||||
const secondFileIdentifier: LivePhotoIdentifier = {
|
const secondFileIdentifier: LivePhotoIdentifier = {
|
||||||
collectionID: secondMediaFile.collectionID,
|
collectionID: secondMediaFile.collectionID,
|
||||||
fileType: secondFileFileInfo.fileType,
|
fileType: secondFileType,
|
||||||
name: secondMediaFile.file.name,
|
name: secondMediaFile.file.name,
|
||||||
size: secondMediaFile.file.size,
|
size: secondMediaFile.file.size,
|
||||||
};
|
};
|
||||||
const firstAsset = {
|
|
||||||
file: firstMediaFile.file,
|
|
||||||
metadata: firstFileMetadata,
|
|
||||||
fileTypeInfo: firstFileTypeInfo,
|
|
||||||
};
|
|
||||||
const secondAsset = {
|
|
||||||
file: secondMediaFile.file,
|
|
||||||
metadata: secondFileMetadata,
|
|
||||||
fileTypeInfo: secondFileFileInfo,
|
|
||||||
};
|
|
||||||
if (
|
if (
|
||||||
areFilesLivePhotoAssets(
|
areFilesLivePhotoAssets(
|
||||||
firstFileIdentifier,
|
firstFileIdentifier,
|
||||||
secondFileIdentifier
|
secondFileIdentifier
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
let imageAsset: Asset;
|
let imageFile: File | ElectronFile;
|
||||||
let videoAsset: Asset;
|
let videoFile: File | ElectronFile;
|
||||||
if (
|
if (
|
||||||
firstFileTypeInfo.fileType === FILE_TYPE.IMAGE &&
|
firstFileType === FILE_TYPE.IMAGE &&
|
||||||
secondFileFileInfo.fileType === FILE_TYPE.VIDEO
|
secondFileType === FILE_TYPE.VIDEO
|
||||||
) {
|
) {
|
||||||
imageAsset = firstAsset;
|
imageFile = firstMediaFile.file;
|
||||||
videoAsset = secondAsset;
|
videoFile = secondMediaFile.file;
|
||||||
} else {
|
} else {
|
||||||
videoAsset = firstAsset;
|
videoFile = firstMediaFile.file;
|
||||||
imageAsset = secondAsset;
|
imageFile = secondMediaFile.file;
|
||||||
}
|
}
|
||||||
const livePhotoLocalID = firstMediaFile.localID;
|
const livePhotoLocalID = firstMediaFile.localID;
|
||||||
analysedMediaFiles.push({
|
analysedMediaFiles.push({
|
||||||
|
@ -180,25 +174,10 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
||||||
collectionID: firstMediaFile.collectionID,
|
collectionID: firstMediaFile.collectionID,
|
||||||
isLivePhoto: true,
|
isLivePhoto: true,
|
||||||
livePhotoAssets: {
|
livePhotoAssets: {
|
||||||
image: imageAsset.file,
|
image: imageFile,
|
||||||
video: videoAsset.file,
|
video: videoFile,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const livePhotoFileTypeInfo: FileTypeInfo =
|
|
||||||
getLivePhotoFileType(
|
|
||||||
imageAsset.fileTypeInfo,
|
|
||||||
videoAsset.fileTypeInfo
|
|
||||||
);
|
|
||||||
const livePhotoMetadata: Metadata = getLivePhotoMetadata(
|
|
||||||
imageAsset.metadata,
|
|
||||||
videoAsset.metadata
|
|
||||||
);
|
|
||||||
const livePhotoPath = getLivePhotoFilePath(imageAsset);
|
|
||||||
uploadService.setFileMetadataAndFileTypeInfo(livePhotoLocalID, {
|
|
||||||
fileTypeInfo: { ...livePhotoFileTypeInfo },
|
|
||||||
metadata: { ...livePhotoMetadata },
|
|
||||||
filePath: livePhotoPath,
|
|
||||||
});
|
|
||||||
index += 2;
|
index += 2;
|
||||||
} else {
|
} else {
|
||||||
analysedMediaFiles.push({
|
analysedMediaFiles.push({
|
||||||
|
@ -216,8 +195,12 @@ export function clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
||||||
}
|
}
|
||||||
return analysedMediaFiles;
|
return analysedMediaFiles;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'failed to cluster live photo');
|
if (e.message === CustomError.UPLOAD_CANCELLED) {
|
||||||
throw e;
|
throw e;
|
||||||
|
} else {
|
||||||
|
logError(e, 'failed to cluster live photo');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
src/services/upload/magicMetadataService.ts
Normal file
17
src/services/upload/magicMetadataService.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import {
|
||||||
|
NEW_FILE_MAGIC_METADATA,
|
||||||
|
FilePublicMagicMetadataProps,
|
||||||
|
FilePublicMagicMetadata,
|
||||||
|
} from 'types/magicMetadata';
|
||||||
|
import { updateMagicMetadataProps } from 'utils/magicMetadata';
|
||||||
|
|
||||||
|
export async function constructPublicMagicMetadata(
|
||||||
|
publicMagicMetadataProps: FilePublicMagicMetadataProps
|
||||||
|
): Promise<FilePublicMagicMetadata> {
|
||||||
|
const pubMagicMetadata = await updateMagicMetadataProps(
|
||||||
|
NEW_FILE_MAGIC_METADATA,
|
||||||
|
null,
|
||||||
|
publicMagicMetadataProps
|
||||||
|
);
|
||||||
|
return pubMagicMetadata;
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ const NULL_PARSED_METADATA_JSON: ParsedMetadataJSON = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function extractMetadata(
|
export async function extractMetadata(
|
||||||
|
worker,
|
||||||
receivedFile: File | ElectronFile,
|
receivedFile: File | ElectronFile,
|
||||||
fileTypeInfo: FileTypeInfo
|
fileTypeInfo: FileTypeInfo
|
||||||
) {
|
) {
|
||||||
|
@ -39,7 +40,7 @@ export async function extractMetadata(
|
||||||
} else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
|
} else if (fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
|
||||||
extractedMetadata = await getVideoMetadata(receivedFile);
|
extractedMetadata = await getVideoMetadata(receivedFile);
|
||||||
}
|
}
|
||||||
const fileHash = await getFileHash(receivedFile);
|
const fileHash = await getFileHash(worker, receivedFile);
|
||||||
|
|
||||||
const metadata: Metadata = {
|
const metadata: Metadata = {
|
||||||
title: receivedFile.name,
|
title: receivedFile.name,
|
||||||
|
|
116
src/services/upload/publicUploadHttpClient.ts
Normal file
116
src/services/upload/publicUploadHttpClient.ts
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import HTTPService from 'services/HTTPService';
|
||||||
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import { EnteFile } from 'types/file';
|
||||||
|
import { CustomError, handleUploadError } from 'utils/error';
|
||||||
|
import { UploadFile, UploadURL, MultipartUploadURLs } from 'types/upload';
|
||||||
|
import { retryHTTPCall } from 'utils/upload/uploadRetrier';
|
||||||
|
|
||||||
|
const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
|
const MAX_URL_REQUESTS = 50;
|
||||||
|
|
||||||
|
class PublicUploadHttpClient {
|
||||||
|
private uploadURLFetchInProgress = null;
|
||||||
|
|
||||||
|
async uploadFile(
|
||||||
|
uploadFile: UploadFile,
|
||||||
|
token: string,
|
||||||
|
passwordToken: string
|
||||||
|
): Promise<EnteFile> {
|
||||||
|
try {
|
||||||
|
if (!token) {
|
||||||
|
throw Error(CustomError.TOKEN_MISSING);
|
||||||
|
}
|
||||||
|
const response = await retryHTTPCall(
|
||||||
|
() =>
|
||||||
|
HTTPService.post(
|
||||||
|
`${ENDPOINT}/public-collection/file`,
|
||||||
|
uploadFile,
|
||||||
|
null,
|
||||||
|
{
|
||||||
|
'X-Auth-Access-Token': token,
|
||||||
|
...(passwordToken && {
|
||||||
|
'X-Auth-Access-Token-JWT': passwordToken,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
handleUploadError
|
||||||
|
);
|
||||||
|
return response.data;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'upload public File Failed');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchUploadURLs(
|
||||||
|
count: number,
|
||||||
|
urlStore: UploadURL[],
|
||||||
|
token: string,
|
||||||
|
passwordToken: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
if (!this.uploadURLFetchInProgress) {
|
||||||
|
try {
|
||||||
|
if (!token) {
|
||||||
|
throw Error(CustomError.TOKEN_MISSING);
|
||||||
|
}
|
||||||
|
this.uploadURLFetchInProgress = HTTPService.get(
|
||||||
|
`${ENDPOINT}/public-collection/upload-urls`,
|
||||||
|
{
|
||||||
|
count: Math.min(MAX_URL_REQUESTS, count * 2),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'X-Auth-Access-Token': token,
|
||||||
|
...(passwordToken && {
|
||||||
|
'X-Auth-Access-Token-JWT': passwordToken,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const response = await this.uploadURLFetchInProgress;
|
||||||
|
for (const url of response.data['urls']) {
|
||||||
|
urlStore.push(url);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
this.uploadURLFetchInProgress = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.uploadURLFetchInProgress;
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'fetch public upload-url failed ');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fetchMultipartUploadURLs(
|
||||||
|
count: number,
|
||||||
|
token: string,
|
||||||
|
passwordToken: string
|
||||||
|
): Promise<MultipartUploadURLs> {
|
||||||
|
try {
|
||||||
|
if (!token) {
|
||||||
|
throw Error(CustomError.TOKEN_MISSING);
|
||||||
|
}
|
||||||
|
const response = await HTTPService.get(
|
||||||
|
`${ENDPOINT}/public-collection/multipart-upload-urls`,
|
||||||
|
{
|
||||||
|
count,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'X-Auth-Access-Token': token,
|
||||||
|
...(passwordToken && {
|
||||||
|
'X-Auth-Access-Token-JWT': passwordToken,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data['urls'];
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'fetch public multipart-upload-url failed');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new PublicUploadHttpClient();
|
|
@ -3,6 +3,7 @@ import { CustomError, errorWithContext } from 'utils/error';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload';
|
import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload';
|
||||||
import * as FFmpegService from 'services/ffmpeg/ffmpegService';
|
import * as FFmpegService from 'services/ffmpeg/ffmpegService';
|
||||||
|
import ElectronImageProcessorService from 'services/electron/imageProcessor';
|
||||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||||
import { isExactTypeHEIC } from 'utils/file';
|
import { isExactTypeHEIC } from 'utils/file';
|
||||||
import { ElectronFile, FileTypeInfo } from 'types/upload';
|
import { ElectronFile, FileTypeInfo } from 'types/upload';
|
||||||
|
@ -30,42 +31,24 @@ export async function generateThumbnail(
|
||||||
try {
|
try {
|
||||||
addLogLine(`generating thumbnail for ${getFileNameSize(file)}`);
|
addLogLine(`generating thumbnail for ${getFileNameSize(file)}`);
|
||||||
let hasStaticThumbnail = false;
|
let hasStaticThumbnail = false;
|
||||||
let canvas = document.createElement('canvas');
|
|
||||||
let thumbnail: Uint8Array;
|
let thumbnail: Uint8Array;
|
||||||
try {
|
try {
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||||
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
|
thumbnail = await generateImageThumbnail(file, fileTypeInfo);
|
||||||
canvas = await generateImageThumbnail(file, isHEIC);
|
|
||||||
} else {
|
} else {
|
||||||
try {
|
thumbnail = await generateVideoThumbnail(file, fileTypeInfo);
|
||||||
addLogLine(
|
}
|
||||||
`ffmpeg generateThumbnail called for ${getFileNameSize(
|
if (thumbnail.length > 1.5 * MAX_THUMBNAIL_SIZE) {
|
||||||
file
|
logError(
|
||||||
)}`
|
Error('thumbnail_too_large'),
|
||||||
);
|
'thumbnail greater than max limit',
|
||||||
|
{
|
||||||
const thumbFile =
|
thumbnailSize: convertBytesToHumanReadable(
|
||||||
await FFmpegService.generateVideoThumbnail(file);
|
thumbnail.length
|
||||||
addLogLine(
|
),
|
||||||
`ffmpeg thumbnail successfully generated ${getFileNameSize(
|
}
|
||||||
file
|
);
|
||||||
)}`
|
|
||||||
);
|
|
||||||
canvas = await generateImageThumbnail(thumbFile, false);
|
|
||||||
} catch (e) {
|
|
||||||
addLogLine(
|
|
||||||
`ffmpeg thumbnail generated failed ${getFileNameSize(
|
|
||||||
file
|
|
||||||
)} error: ${e.message}`
|
|
||||||
);
|
|
||||||
logError(e, 'failed to generate thumbnail using ffmpeg', {
|
|
||||||
fileFormat: fileTypeInfo.exactType,
|
|
||||||
});
|
|
||||||
canvas = await generateVideoThumbnail(file);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
const thumbnailBlob = await thumbnailCanvasToBlob(canvas);
|
|
||||||
thumbnail = await getUint8ArrayView(thumbnailBlob);
|
|
||||||
if (thumbnail.length === 0) {
|
if (thumbnail.length === 0) {
|
||||||
throw Error('EMPTY THUMBNAIL');
|
throw Error('EMPTY THUMBNAIL');
|
||||||
}
|
}
|
||||||
|
@ -93,16 +76,42 @@ export async function generateThumbnail(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateImageThumbnail(
|
async function generateImageThumbnail(
|
||||||
file: File | ElectronFile,
|
file: File | ElectronFile,
|
||||||
isHEIC: boolean
|
fileTypeInfo: FileTypeInfo
|
||||||
|
) {
|
||||||
|
if (ElectronImageProcessorService.generateImageThumbnailAPIExists()) {
|
||||||
|
try {
|
||||||
|
return await ElectronImageProcessorService.generateImageThumbnail(
|
||||||
|
file,
|
||||||
|
MAX_THUMBNAIL_DIMENSION,
|
||||||
|
MAX_THUMBNAIL_SIZE
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError(
|
||||||
|
e,
|
||||||
|
'Error generating thumbnail using electron image processor',
|
||||||
|
{
|
||||||
|
fileFormat: fileTypeInfo.exactType,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return await generateImageThumbnailUsingCanvas(file, fileTypeInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateImageThumbnailUsingCanvas(
|
||||||
|
file: File | ElectronFile,
|
||||||
|
fileTypeInfo: FileTypeInfo
|
||||||
) {
|
) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const canvasCTX = canvas.getContext('2d');
|
const canvasCTX = canvas.getContext('2d');
|
||||||
|
|
||||||
let imageURL = null;
|
let imageURL = null;
|
||||||
let timeout = null;
|
let timeout = null;
|
||||||
|
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
|
||||||
if (isHEIC) {
|
if (isHEIC) {
|
||||||
addLogLine(`HEICConverter called for ${getFileNameSize(file)}`);
|
addLogLine(`HEICConverter called for ${getFileNameSize(file)}`);
|
||||||
const convertedBlob = await HeicConversionService.convert(
|
const convertedBlob = await HeicConversionService.convert(
|
||||||
|
@ -151,10 +160,42 @@ export async function generateImageThumbnail(
|
||||||
WAIT_TIME_THUMBNAIL_GENERATION
|
WAIT_TIME_THUMBNAIL_GENERATION
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return canvas;
|
const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas);
|
||||||
|
return await getUint8ArrayView(thumbnailBlob);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateVideoThumbnail(file: File | ElectronFile) {
|
async function generateVideoThumbnail(
|
||||||
|
file: File | ElectronFile,
|
||||||
|
fileTypeInfo: FileTypeInfo
|
||||||
|
) {
|
||||||
|
let thumbnail: Uint8Array;
|
||||||
|
try {
|
||||||
|
addLogLine(
|
||||||
|
`ffmpeg generateThumbnail called for ${getFileNameSize(file)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const thumbnail = await FFmpegService.generateVideoThumbnail(file);
|
||||||
|
addLogLine(
|
||||||
|
`ffmpeg thumbnail successfully generated ${getFileNameSize(file)}`
|
||||||
|
);
|
||||||
|
return getUint8ArrayView(thumbnail);
|
||||||
|
} catch (e) {
|
||||||
|
addLogLine(
|
||||||
|
`ffmpeg thumbnail generated failed ${getFileNameSize(
|
||||||
|
file
|
||||||
|
)} error: ${e.message}`
|
||||||
|
);
|
||||||
|
logError(e, 'failed to generate thumbnail using ffmpeg', {
|
||||||
|
fileFormat: fileTypeInfo.exactType,
|
||||||
|
});
|
||||||
|
thumbnail = await generateVideoThumbnailUsingCanvas(file);
|
||||||
|
}
|
||||||
|
return thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateVideoThumbnailUsingCanvas(
|
||||||
|
file: File | ElectronFile
|
||||||
|
) {
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
const canvasCTX = canvas.getContext('2d');
|
const canvasCTX = canvas.getContext('2d');
|
||||||
|
|
||||||
|
@ -205,10 +246,11 @@ export async function generateVideoThumbnail(file: File | ElectronFile) {
|
||||||
WAIT_TIME_THUMBNAIL_GENERATION
|
WAIT_TIME_THUMBNAIL_GENERATION
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
return canvas;
|
const thumbnailBlob = await getCompressedThumbnailBlobFromCanvas(canvas);
|
||||||
|
return await getUint8ArrayView(thumbnailBlob);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
|
async function getCompressedThumbnailBlobFromCanvas(canvas: HTMLCanvasElement) {
|
||||||
let thumbnailBlob: Blob = null;
|
let thumbnailBlob: Blob = null;
|
||||||
let prevSize = Number.MAX_SAFE_INTEGER;
|
let prevSize = Number.MAX_SAFE_INTEGER;
|
||||||
let quality = MAX_QUALITY;
|
let quality = MAX_QUALITY;
|
||||||
|
@ -234,13 +276,6 @@ async function thumbnailCanvasToBlob(canvas: HTMLCanvasElement) {
|
||||||
percentageSizeDiff(thumbnailBlob.size, prevSize) >=
|
percentageSizeDiff(thumbnailBlob.size, prevSize) >=
|
||||||
MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF
|
MIN_COMPRESSION_PERCENTAGE_SIZE_DIFF
|
||||||
);
|
);
|
||||||
if (thumbnailBlob.size > MAX_THUMBNAIL_SIZE) {
|
|
||||||
logError(
|
|
||||||
Error('thumbnail_too_large'),
|
|
||||||
'thumbnail greater than max limit',
|
|
||||||
{ thumbnailSize: convertBytesToHumanReadable(thumbnailBlob.size) }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return thumbnailBlob;
|
return thumbnailBlob;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { getLocalFiles } from '../fileService';
|
import { getLocalFiles } from '../fileService';
|
||||||
import { SetFiles } from 'types/gallery';
|
import { SetFiles } from 'types/gallery';
|
||||||
import { getDedicatedCryptoWorker } from 'utils/crypto';
|
|
||||||
import {
|
import {
|
||||||
sortFiles,
|
sortFiles,
|
||||||
preservePhotoswipeProps,
|
preservePhotoswipeProps,
|
||||||
|
@ -18,23 +17,15 @@ import UIService from './uiService';
|
||||||
import UploadService from './uploadService';
|
import UploadService from './uploadService';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import { EnteFile } from 'types/file';
|
import { EncryptedEnteFile, EnteFile } from 'types/file';
|
||||||
import {
|
import {
|
||||||
ElectronFile,
|
|
||||||
FileWithCollection,
|
FileWithCollection,
|
||||||
Metadata,
|
|
||||||
MetadataAndFileTypeInfo,
|
|
||||||
MetadataAndFileTypeInfoMap,
|
|
||||||
ParsedMetadataJSON,
|
ParsedMetadataJSON,
|
||||||
ParsedMetadataJSONMap,
|
ParsedMetadataJSONMap,
|
||||||
|
PublicUploadProps,
|
||||||
} from 'types/upload';
|
} from 'types/upload';
|
||||||
import {
|
|
||||||
UPLOAD_RESULT,
|
import { UPLOAD_RESULT, UPLOAD_STAGES } from 'constants/upload';
|
||||||
MAX_FILE_SIZE_SUPPORTED,
|
|
||||||
UPLOAD_STAGES,
|
|
||||||
} from 'constants/upload';
|
|
||||||
import { ComlinkWorker } from 'utils/comlink';
|
|
||||||
import { FILE_TYPE } from 'constants/file';
|
|
||||||
import uiService from './uiService';
|
import uiService from './uiService';
|
||||||
import { addLogLine, getFileNameSize } from 'utils/logging';
|
import { addLogLine, getFileNameSize } from 'utils/logging';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
|
@ -42,14 +33,23 @@ import ImportService from 'services/importService';
|
||||||
import watchFolderService from 'services/watchFolder/watchFolderService';
|
import watchFolderService from 'services/watchFolder/watchFolderService';
|
||||||
import { ProgressUpdater } from 'types/upload/ui';
|
import { ProgressUpdater } from 'types/upload/ui';
|
||||||
import uploadCancelService from './uploadCancelService';
|
import uploadCancelService from './uploadCancelService';
|
||||||
|
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||||
|
import { ComlinkWorker } from 'utils/comlink/comlinkWorker';
|
||||||
|
import { Remote } from 'comlink';
|
||||||
|
import {
|
||||||
|
getLocalPublicFiles,
|
||||||
|
getPublicCollectionUID,
|
||||||
|
} from 'services/publicCollectionService';
|
||||||
|
import { getDedicatedCryptoWorker } from 'utils/comlink/ComlinkCryptoWorker';
|
||||||
|
|
||||||
const MAX_CONCURRENT_UPLOADS = 4;
|
const MAX_CONCURRENT_UPLOADS = 4;
|
||||||
const FILE_UPLOAD_COMPLETED = 100;
|
const FILE_UPLOAD_COMPLETED = 100;
|
||||||
|
|
||||||
class UploadManager {
|
class UploadManager {
|
||||||
private cryptoWorkers = new Array<ComlinkWorker>(MAX_CONCURRENT_UPLOADS);
|
private cryptoWorkers = new Array<
|
||||||
|
ComlinkWorker<typeof DedicatedCryptoWorker>
|
||||||
|
>(MAX_CONCURRENT_UPLOADS);
|
||||||
private parsedMetadataJSONMap: ParsedMetadataJSONMap;
|
private parsedMetadataJSONMap: ParsedMetadataJSONMap;
|
||||||
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap;
|
|
||||||
private filesToBeUploaded: FileWithCollection[];
|
private filesToBeUploaded: FileWithCollection[];
|
||||||
private remainingFiles: FileWithCollection[] = [];
|
private remainingFiles: FileWithCollection[] = [];
|
||||||
private failedFiles: FileWithCollection[];
|
private failedFiles: FileWithCollection[];
|
||||||
|
@ -58,12 +58,18 @@ class UploadManager {
|
||||||
private setFiles: SetFiles;
|
private setFiles: SetFiles;
|
||||||
private collections: Map<number, Collection>;
|
private collections: Map<number, Collection>;
|
||||||
private uploadInProgress: boolean;
|
private uploadInProgress: boolean;
|
||||||
|
private publicUploadProps: PublicUploadProps;
|
||||||
|
private uploaderName: string;
|
||||||
|
|
||||||
public async init(progressUpdater: ProgressUpdater, setFiles: SetFiles) {
|
public async init(
|
||||||
UIService.init(progressUpdater);
|
progressUpdater: ProgressUpdater,
|
||||||
this.setFiles = setFiles;
|
setFiles: SetFiles,
|
||||||
|
publicCollectProps: PublicUploadProps
|
||||||
|
) {
|
||||||
UIService.init(progressUpdater);
|
UIService.init(progressUpdater);
|
||||||
|
UploadService.init(publicCollectProps);
|
||||||
this.setFiles = setFiles;
|
this.setFiles = setFiles;
|
||||||
|
this.publicUploadProps = publicCollectProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isUploadRunning() {
|
public isUploadRunning() {
|
||||||
|
@ -75,10 +81,8 @@ class UploadManager {
|
||||||
this.remainingFiles = [];
|
this.remainingFiles = [];
|
||||||
this.failedFiles = [];
|
this.failedFiles = [];
|
||||||
this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
|
this.parsedMetadataJSONMap = new Map<string, ParsedMetadataJSON>();
|
||||||
this.metadataAndFileTypeInfoMap = new Map<
|
|
||||||
number,
|
this.uploaderName = null;
|
||||||
MetadataAndFileTypeInfo
|
|
||||||
>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
prepareForNewUpload() {
|
prepareForNewUpload() {
|
||||||
|
@ -89,10 +93,17 @@ class UploadManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateExistingFilesAndCollections(collections: Collection[]) {
|
async updateExistingFilesAndCollections(collections: Collection[]) {
|
||||||
this.existingFiles = await getLocalFiles();
|
if (this.publicUploadProps.accessedThroughSharedURL) {
|
||||||
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles(
|
this.existingFiles = await getLocalPublicFiles(
|
||||||
this.existingFiles
|
getPublicCollectionUID(this.publicUploadProps.token)
|
||||||
);
|
);
|
||||||
|
this.userOwnedNonTrashedExistingFiles = this.existingFiles;
|
||||||
|
} else {
|
||||||
|
this.existingFiles = await getLocalFiles();
|
||||||
|
this.userOwnedNonTrashedExistingFiles = getUserOwnedNonTrashedFiles(
|
||||||
|
this.existingFiles
|
||||||
|
);
|
||||||
|
}
|
||||||
this.collections = new Map(
|
this.collections = new Map(
|
||||||
collections.map((collection) => [collection.id, collection])
|
collections.map((collection) => [collection.id, collection])
|
||||||
);
|
);
|
||||||
|
@ -100,7 +111,8 @@ class UploadManager {
|
||||||
|
|
||||||
public async queueFilesForUpload(
|
public async queueFilesForUpload(
|
||||||
filesWithCollectionToUploadIn: FileWithCollection[],
|
filesWithCollectionToUploadIn: FileWithCollection[],
|
||||||
collections: Collection[]
|
collections: Collection[],
|
||||||
|
uploaderName?: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
if (this.uploadInProgress) {
|
if (this.uploadInProgress) {
|
||||||
|
@ -108,6 +120,7 @@ class UploadManager {
|
||||||
}
|
}
|
||||||
this.uploadInProgress = true;
|
this.uploadInProgress = true;
|
||||||
await this.updateExistingFilesAndCollections(collections);
|
await this.updateExistingFilesAndCollections(collections);
|
||||||
|
this.uploaderName = uploaderName;
|
||||||
addLogLine(
|
addLogLine(
|
||||||
`received ${filesWithCollectionToUploadIn.length} files to upload`
|
`received ${filesWithCollectionToUploadIn.length} files to upload`
|
||||||
);
|
);
|
||||||
|
@ -134,50 +147,18 @@ class UploadManager {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (mediaFiles.length) {
|
if (mediaFiles.length) {
|
||||||
UIService.setUploadStage(UPLOAD_STAGES.EXTRACTING_METADATA);
|
|
||||||
await this.extractMetadataFromFiles(mediaFiles);
|
|
||||||
|
|
||||||
UploadService.setMetadataAndFileTypeInfoMap(
|
|
||||||
this.metadataAndFileTypeInfoMap
|
|
||||||
);
|
|
||||||
|
|
||||||
// filter out files whose metadata detection failed or those that have been skipped because the files are too large,
|
|
||||||
// as they will be rejected during upload and are not valid upload files which we need to clustering
|
|
||||||
const rejectedFileLocalIDs = new Set(
|
|
||||||
[...this.metadataAndFileTypeInfoMap.entries()].map(
|
|
||||||
([localID, metadataAndFileTypeInfo]) => {
|
|
||||||
if (
|
|
||||||
!metadataAndFileTypeInfo.metadata ||
|
|
||||||
!metadataAndFileTypeInfo.fileTypeInfo
|
|
||||||
) {
|
|
||||||
return localID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const rejectedFiles = [];
|
|
||||||
const filesWithMetadata = [];
|
|
||||||
mediaFiles.forEach((m) => {
|
|
||||||
if (rejectedFileLocalIDs.has(m.localID)) {
|
|
||||||
rejectedFiles.push(m);
|
|
||||||
} else {
|
|
||||||
filesWithMetadata.push(m);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
addLogLine(`clusterLivePhotoFiles started`);
|
addLogLine(`clusterLivePhotoFiles started`);
|
||||||
|
|
||||||
const analysedMediaFiles =
|
const analysedMediaFiles =
|
||||||
UploadService.clusterLivePhotoFiles(filesWithMetadata);
|
await UploadService.clusterLivePhotoFiles(mediaFiles);
|
||||||
|
|
||||||
addLogLine(`clusterLivePhotoFiles ended`);
|
addLogLine(`clusterLivePhotoFiles ended`);
|
||||||
const allFiles = [...rejectedFiles, ...analysedMediaFiles];
|
|
||||||
addLogLine(
|
addLogLine(
|
||||||
`got live photos: ${mediaFiles.length !== allFiles.length}`
|
`got live photos: ${
|
||||||
|
mediaFiles.length !== analysedMediaFiles.length
|
||||||
|
}`
|
||||||
);
|
);
|
||||||
uiService.setFilenames(
|
uiService.setFilenames(
|
||||||
new Map<number, string>(
|
new Map<number, string>(
|
||||||
allFiles.map((mediaFile) => [
|
analysedMediaFiles.map((mediaFile) => [
|
||||||
mediaFile.localID,
|
mediaFile.localID,
|
||||||
UploadService.getAssetName(mediaFile),
|
UploadService.getAssetName(mediaFile),
|
||||||
])
|
])
|
||||||
|
@ -185,10 +166,10 @@ class UploadManager {
|
||||||
);
|
);
|
||||||
|
|
||||||
UIService.setHasLivePhoto(
|
UIService.setHasLivePhoto(
|
||||||
mediaFiles.length !== allFiles.length
|
mediaFiles.length !== analysedMediaFiles.length
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.uploadMediaFiles(allFiles);
|
await this.uploadMediaFiles(analysedMediaFiles);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === CustomError.UPLOAD_CANCELLED) {
|
if (e.message === CustomError.UPLOAD_CANCELLED) {
|
||||||
|
@ -203,7 +184,7 @@ class UploadManager {
|
||||||
UIService.setUploadStage(UPLOAD_STAGES.FINISH);
|
UIService.setUploadStage(UPLOAD_STAGES.FINISH);
|
||||||
UIService.setPercentComplete(FILE_UPLOAD_COMPLETED);
|
UIService.setPercentComplete(FILE_UPLOAD_COMPLETED);
|
||||||
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
|
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
|
||||||
this.cryptoWorkers[i]?.worker.terminate();
|
this.cryptoWorkers[i]?.terminate();
|
||||||
}
|
}
|
||||||
this.uploadInProgress = false;
|
this.uploadInProgress = false;
|
||||||
}
|
}
|
||||||
|
@ -273,99 +254,6 @@ class UploadManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async extractMetadataFromFiles(mediaFiles: FileWithCollection[]) {
|
|
||||||
try {
|
|
||||||
addLogLine(`extractMetadataFromFiles executed`);
|
|
||||||
UIService.reset(mediaFiles.length);
|
|
||||||
for (const { file, localID, collectionID } of mediaFiles) {
|
|
||||||
UIService.setFileProgress(localID, 0);
|
|
||||||
if (uploadCancelService.isUploadCancelationRequested()) {
|
|
||||||
throw Error(CustomError.UPLOAD_CANCELLED);
|
|
||||||
}
|
|
||||||
let fileTypeInfo = null;
|
|
||||||
let metadata = null;
|
|
||||||
let filePath = null;
|
|
||||||
try {
|
|
||||||
addLogLine(
|
|
||||||
`metadata extraction started ${getFileNameSize(file)} `
|
|
||||||
);
|
|
||||||
const result = await this.extractFileTypeAndMetadata(
|
|
||||||
file,
|
|
||||||
collectionID
|
|
||||||
);
|
|
||||||
fileTypeInfo = result.fileTypeInfo;
|
|
||||||
metadata = result.metadata;
|
|
||||||
filePath = result.filePath;
|
|
||||||
addLogLine(
|
|
||||||
`metadata extraction successful${getFileNameSize(
|
|
||||||
file
|
|
||||||
)} `
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === CustomError.UPLOAD_CANCELLED) {
|
|
||||||
throw e;
|
|
||||||
} else {
|
|
||||||
// and don't break for subsequent files just log and move on
|
|
||||||
logError(e, 'extractFileTypeAndMetadata failed');
|
|
||||||
addLogLine(
|
|
||||||
`metadata extraction failed ${getFileNameSize(
|
|
||||||
file
|
|
||||||
)} error: ${e.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.metadataAndFileTypeInfoMap.set(localID, {
|
|
||||||
fileTypeInfo: fileTypeInfo && { ...fileTypeInfo },
|
|
||||||
metadata: metadata && { ...metadata },
|
|
||||||
filePath: filePath,
|
|
||||||
});
|
|
||||||
UIService.removeFromInProgressList(localID);
|
|
||||||
UIService.increaseFileUploaded();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message !== CustomError.UPLOAD_CANCELLED) {
|
|
||||||
logError(e, 'error extracting metadata');
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async extractFileTypeAndMetadata(
|
|
||||||
file: File | ElectronFile,
|
|
||||||
collectionID: number
|
|
||||||
) {
|
|
||||||
if (file.size >= MAX_FILE_SIZE_SUPPORTED) {
|
|
||||||
addLogLine(
|
|
||||||
`${getFileNameSize(file)} rejected because of large size`
|
|
||||||
);
|
|
||||||
|
|
||||||
return { fileTypeInfo: null, metadata: null };
|
|
||||||
}
|
|
||||||
const fileTypeInfo = await UploadService.getFileType(file);
|
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
|
||||||
addLogLine(
|
|
||||||
`${getFileNameSize(
|
|
||||||
file
|
|
||||||
)} rejected because of unknown file format`
|
|
||||||
);
|
|
||||||
return { fileTypeInfo, metadata: null };
|
|
||||||
}
|
|
||||||
addLogLine(` extracting ${getFileNameSize(file)} metadata`);
|
|
||||||
let metadata: Metadata;
|
|
||||||
try {
|
|
||||||
metadata = await UploadService.extractFileMetadata(
|
|
||||||
file,
|
|
||||||
collectionID,
|
|
||||||
fileTypeInfo
|
|
||||||
);
|
|
||||||
const filePath = (file as any).path as string;
|
|
||||||
return { fileTypeInfo, metadata, filePath };
|
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'failed to extract file metadata');
|
|
||||||
return { fileTypeInfo, metadata: null, filePath: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async uploadMediaFiles(mediaFiles: FileWithCollection[]) {
|
private async uploadMediaFiles(mediaFiles: FileWithCollection[]) {
|
||||||
addLogLine(`uploadMediaFiles called`);
|
addLogLine(`uploadMediaFiles called`);
|
||||||
this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles];
|
this.filesToBeUploaded = [...this.filesToBeUploaded, ...mediaFiles];
|
||||||
|
@ -386,21 +274,14 @@ class UploadManager {
|
||||||
i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0;
|
i < MAX_CONCURRENT_UPLOADS && this.filesToBeUploaded.length > 0;
|
||||||
i++
|
i++
|
||||||
) {
|
) {
|
||||||
const cryptoWorker = getDedicatedCryptoWorker();
|
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
|
||||||
if (!cryptoWorker) {
|
const worker = await new this.cryptoWorkers[i].remote();
|
||||||
throw Error(CustomError.FAILED_TO_LOAD_WEB_WORKER);
|
uploadProcesses.push(this.uploadNextFileInQueue(worker));
|
||||||
}
|
|
||||||
this.cryptoWorkers[i] = cryptoWorker;
|
|
||||||
uploadProcesses.push(
|
|
||||||
this.uploadNextFileInQueue(
|
|
||||||
await new this.cryptoWorkers[i].comlink()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
await Promise.all(uploadProcesses);
|
await Promise.all(uploadProcesses);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async uploadNextFileInQueue(worker: any) {
|
private async uploadNextFileInQueue(worker: Remote<DedicatedCryptoWorker>) {
|
||||||
while (this.filesToBeUploaded.length > 0) {
|
while (this.filesToBeUploaded.length > 0) {
|
||||||
if (uploadCancelService.isUploadCancelationRequested()) {
|
if (uploadCancelService.isUploadCancelationRequested()) {
|
||||||
throw Error(CustomError.UPLOAD_CANCELLED);
|
throw Error(CustomError.UPLOAD_CANCELLED);
|
||||||
|
@ -412,7 +293,9 @@ class UploadManager {
|
||||||
const { fileUploadResult, uploadedFile } = await uploader(
|
const { fileUploadResult, uploadedFile } = await uploader(
|
||||||
worker,
|
worker,
|
||||||
this.userOwnedNonTrashedExistingFiles,
|
this.userOwnedNonTrashedExistingFiles,
|
||||||
fileWithCollection
|
fileWithCollection,
|
||||||
|
this.uploaderName,
|
||||||
|
this.publicUploadProps?.accessedThroughSharedURL
|
||||||
);
|
);
|
||||||
|
|
||||||
const finalUploadResult = await this.postUploadTask(
|
const finalUploadResult = await this.postUploadTask(
|
||||||
|
@ -431,7 +314,7 @@ class UploadManager {
|
||||||
|
|
||||||
async postUploadTask(
|
async postUploadTask(
|
||||||
fileUploadResult: UPLOAD_RESULT,
|
fileUploadResult: UPLOAD_RESULT,
|
||||||
uploadedFile: EnteFile | null,
|
uploadedFile: EncryptedEnteFile | EnteFile | null,
|
||||||
fileWithCollection: FileWithCollection
|
fileWithCollection: FileWithCollection
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
|
@ -446,22 +329,23 @@ class UploadManager {
|
||||||
this.failedFiles.push(fileWithCollection);
|
this.failedFiles.push(fileWithCollection);
|
||||||
break;
|
break;
|
||||||
case UPLOAD_RESULT.ALREADY_UPLOADED:
|
case UPLOAD_RESULT.ALREADY_UPLOADED:
|
||||||
decryptedFile = uploadedFile;
|
decryptedFile = uploadedFile as EnteFile;
|
||||||
break;
|
break;
|
||||||
case UPLOAD_RESULT.ADDED_SYMLINK:
|
case UPLOAD_RESULT.ADDED_SYMLINK:
|
||||||
decryptedFile = uploadedFile;
|
decryptedFile = uploadedFile as EnteFile;
|
||||||
fileUploadResult = UPLOAD_RESULT.UPLOADED;
|
fileUploadResult = UPLOAD_RESULT.UPLOADED;
|
||||||
break;
|
break;
|
||||||
case UPLOAD_RESULT.UPLOADED:
|
case UPLOAD_RESULT.UPLOADED:
|
||||||
case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL:
|
case UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL:
|
||||||
decryptedFile = await decryptFile(
|
decryptedFile = await decryptFile(
|
||||||
uploadedFile,
|
uploadedFile as EncryptedEnteFile,
|
||||||
fileWithCollection.collection.key
|
fileWithCollection.collection.key
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
case UPLOAD_RESULT.UNSUPPORTED:
|
case UPLOAD_RESULT.UNSUPPORTED:
|
||||||
case UPLOAD_RESULT.TOO_LARGE:
|
case UPLOAD_RESULT.TOO_LARGE:
|
||||||
case UPLOAD_RESULT.CANCELLED:
|
case UPLOAD_RESULT.CANCELLED:
|
||||||
|
case UPLOAD_RESULT.SKIPPED_VIDEOS:
|
||||||
// no-op
|
// no-op
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -479,7 +363,7 @@ class UploadManager {
|
||||||
await this.watchFolderCallback(
|
await this.watchFolderCallback(
|
||||||
fileUploadResult,
|
fileUploadResult,
|
||||||
fileWithCollection,
|
fileWithCollection,
|
||||||
uploadedFile
|
uploadedFile as EncryptedEnteFile
|
||||||
);
|
);
|
||||||
return fileUploadResult;
|
return fileUploadResult;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -491,7 +375,7 @@ class UploadManager {
|
||||||
private async watchFolderCallback(
|
private async watchFolderCallback(
|
||||||
fileUploadResult: UPLOAD_RESULT,
|
fileUploadResult: UPLOAD_RESULT,
|
||||||
fileWithCollection: FileWithCollection,
|
fileWithCollection: FileWithCollection,
|
||||||
uploadedFile: EnteFile
|
uploadedFile: EncryptedEnteFile
|
||||||
) {
|
) {
|
||||||
if (isElectron()) {
|
if (isElectron()) {
|
||||||
await watchFolderService.onFileUpload(
|
await watchFolderService.onFileUpload(
|
||||||
|
@ -508,13 +392,17 @@ class UploadManager {
|
||||||
uploadCancelService.requestUploadCancelation();
|
uploadCancelService.requestUploadCancelation();
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFailedFilesWithCollections() {
|
getFailedFilesWithCollections() {
|
||||||
return {
|
return {
|
||||||
files: this.failedFiles,
|
files: this.failedFiles,
|
||||||
collections: [...this.collections.values()],
|
collections: [...this.collections.values()],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUploaderName() {
|
||||||
|
return this.uploaderName;
|
||||||
|
}
|
||||||
|
|
||||||
private updateExistingFiles(decryptedFile: EnteFile) {
|
private updateExistingFiles(decryptedFile: EnteFile) {
|
||||||
if (!decryptedFile) {
|
if (!decryptedFile) {
|
||||||
throw Error("decrypted file can't be undefined");
|
throw Error("decrypted file can't be undefined");
|
||||||
|
|
|
@ -5,26 +5,25 @@ import { extractFileMetadata, getFilename } from './fileService';
|
||||||
import { getFileType } from '../typeDetectionService';
|
import { getFileType } from '../typeDetectionService';
|
||||||
import { CustomError, handleUploadError } from 'utils/error';
|
import { CustomError, handleUploadError } from 'utils/error';
|
||||||
import {
|
import {
|
||||||
B64EncryptionResult,
|
|
||||||
BackupedFile,
|
BackupedFile,
|
||||||
ElectronFile,
|
|
||||||
EncryptedFile,
|
EncryptedFile,
|
||||||
FileTypeInfo,
|
FileTypeInfo,
|
||||||
FileWithCollection,
|
FileWithCollection,
|
||||||
FileWithMetadata,
|
FileWithMetadata,
|
||||||
isDataStream,
|
isDataStream,
|
||||||
Metadata,
|
Metadata,
|
||||||
MetadataAndFileTypeInfo,
|
|
||||||
MetadataAndFileTypeInfoMap,
|
|
||||||
ParsedMetadataJSON,
|
ParsedMetadataJSON,
|
||||||
ParsedMetadataJSONMap,
|
ParsedMetadataJSONMap,
|
||||||
ProcessedFile,
|
ProcessedFile,
|
||||||
|
PublicUploadProps,
|
||||||
UploadAsset,
|
UploadAsset,
|
||||||
UploadFile,
|
UploadFile,
|
||||||
UploadURL,
|
UploadURL,
|
||||||
} from 'types/upload';
|
} from 'types/upload';
|
||||||
import {
|
import {
|
||||||
clusterLivePhotoFiles,
|
clusterLivePhotoFiles,
|
||||||
|
extractLivePhotoMetadata,
|
||||||
|
getLivePhotoFileType,
|
||||||
getLivePhotoName,
|
getLivePhotoName,
|
||||||
getLivePhotoSize,
|
getLivePhotoSize,
|
||||||
readLivePhoto,
|
readLivePhoto,
|
||||||
|
@ -33,6 +32,12 @@ import { encryptFile, getFileSize, readFile } from './fileService';
|
||||||
import { uploadStreamUsingMultipart } from './multiPartUploadService';
|
import { uploadStreamUsingMultipart } from './multiPartUploadService';
|
||||||
import UIService from './uiService';
|
import UIService from './uiService';
|
||||||
import { USE_CF_PROXY } from 'constants/upload';
|
import { USE_CF_PROXY } from 'constants/upload';
|
||||||
|
import { Remote } from 'comlink';
|
||||||
|
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||||
|
import publicUploadHttpClient from './publicUploadHttpClient';
|
||||||
|
import { constructPublicMagicMetadata } from './magicMetadataService';
|
||||||
|
import { FilePublicMagicMetadataProps } from 'types/magicMetadata';
|
||||||
|
import { B64EncryptionResult } from 'types/crypto';
|
||||||
|
|
||||||
class UploadService {
|
class UploadService {
|
||||||
private uploadURLs: UploadURL[] = [];
|
private uploadURLs: UploadURL[] = [];
|
||||||
|
@ -40,26 +45,32 @@ class UploadService {
|
||||||
string,
|
string,
|
||||||
ParsedMetadataJSON
|
ParsedMetadataJSON
|
||||||
>();
|
>();
|
||||||
private metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap = new Map<
|
|
||||||
number,
|
private uploaderName: string;
|
||||||
MetadataAndFileTypeInfo
|
|
||||||
>();
|
|
||||||
|
|
||||||
private pendingUploadCount: number = 0;
|
private pendingUploadCount: number = 0;
|
||||||
|
|
||||||
|
private publicUploadProps: PublicUploadProps = undefined;
|
||||||
|
|
||||||
|
init(publicUploadProps: PublicUploadProps) {
|
||||||
|
this.publicUploadProps = publicUploadProps;
|
||||||
|
}
|
||||||
|
|
||||||
async setFileCount(fileCount: number) {
|
async setFileCount(fileCount: number) {
|
||||||
this.pendingUploadCount = fileCount;
|
this.pendingUploadCount = fileCount;
|
||||||
await this.preFetchUploadURLs();
|
this.preFetchUploadURLs();
|
||||||
}
|
}
|
||||||
|
|
||||||
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
|
setParsedMetadataJSONMap(parsedMetadataJSONMap: ParsedMetadataJSONMap) {
|
||||||
this.parsedMetadataJSONMap = parsedMetadataJSONMap;
|
this.parsedMetadataJSONMap = parsedMetadataJSONMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
setMetadataAndFileTypeInfoMap(
|
setUploaderName(uploaderName: string) {
|
||||||
metadataAndFileTypeInfoMap: MetadataAndFileTypeInfoMap
|
this.uploaderName = uploaderName;
|
||||||
) {
|
}
|
||||||
this.metadataAndFileTypeInfoMap = metadataAndFileTypeInfoMap;
|
|
||||||
|
getUploaderName() {
|
||||||
|
return this.uploaderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
reducePendingUploadCount() {
|
reducePendingUploadCount() {
|
||||||
|
@ -72,14 +83,16 @@ class UploadService {
|
||||||
: getFileSize(file);
|
: getFileSize(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAssetName({ isLivePhoto, file, livePhotoAssets }: FileWithCollection) {
|
getAssetName({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
|
||||||
return isLivePhoto
|
return isLivePhoto
|
||||||
? getLivePhotoName(livePhotoAssets.image.name)
|
? getLivePhotoName(livePhotoAssets)
|
||||||
: getFilename(file);
|
: getFilename(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getFileType(file: File | ElectronFile) {
|
getAssetFileType({ isLivePhoto, file, livePhotoAssets }: UploadAsset) {
|
||||||
return getFileType(file);
|
return isLivePhoto
|
||||||
|
? getLivePhotoFileType(livePhotoAssets)
|
||||||
|
: getFileType(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async readAsset(
|
async readAsset(
|
||||||
|
@ -91,39 +104,41 @@ class UploadService {
|
||||||
: await readFile(fileTypeInfo, file);
|
: await readFile(fileTypeInfo, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractFileMetadata(
|
async extractAssetMetadata(
|
||||||
file: File | ElectronFile,
|
worker,
|
||||||
|
{ isLivePhoto, file, livePhotoAssets }: UploadAsset,
|
||||||
collectionID: number,
|
collectionID: number,
|
||||||
fileTypeInfo: FileTypeInfo
|
fileTypeInfo: FileTypeInfo
|
||||||
): Promise<Metadata> {
|
): Promise<Metadata> {
|
||||||
return extractFileMetadata(
|
return isLivePhoto
|
||||||
this.parsedMetadataJSONMap,
|
? extractLivePhotoMetadata(
|
||||||
file,
|
worker,
|
||||||
collectionID,
|
this.parsedMetadataJSONMap,
|
||||||
fileTypeInfo
|
collectionID,
|
||||||
);
|
fileTypeInfo,
|
||||||
}
|
livePhotoAssets
|
||||||
|
)
|
||||||
getFileMetadataAndFileTypeInfo(localID: number) {
|
: await extractFileMetadata(
|
||||||
return this.metadataAndFileTypeInfoMap.get(localID);
|
worker,
|
||||||
}
|
this.parsedMetadataJSONMap,
|
||||||
|
collectionID,
|
||||||
setFileMetadataAndFileTypeInfo(
|
fileTypeInfo,
|
||||||
localID: number,
|
file
|
||||||
metadataAndFileTypeInfo: MetadataAndFileTypeInfo
|
);
|
||||||
) {
|
|
||||||
return this.metadataAndFileTypeInfoMap.set(
|
|
||||||
localID,
|
|
||||||
metadataAndFileTypeInfo
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
clusterLivePhotoFiles(mediaFiles: FileWithCollection[]) {
|
||||||
return clusterLivePhotoFiles(mediaFiles);
|
return clusterLivePhotoFiles(mediaFiles);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constructPublicMagicMetadata(
|
||||||
|
publicMagicMetadataProps: FilePublicMagicMetadataProps
|
||||||
|
) {
|
||||||
|
return constructPublicMagicMetadata(publicMagicMetadataProps);
|
||||||
|
}
|
||||||
|
|
||||||
async encryptAsset(
|
async encryptAsset(
|
||||||
worker: any,
|
worker: Remote<DedicatedCryptoWorker>,
|
||||||
file: FileWithMetadata,
|
file: FileWithMetadata,
|
||||||
encryptionKey: string
|
encryptionKey: string
|
||||||
): Promise<EncryptedFile> {
|
): Promise<EncryptedFile> {
|
||||||
|
@ -146,13 +161,13 @@ class UploadService {
|
||||||
if (USE_CF_PROXY) {
|
if (USE_CF_PROXY) {
|
||||||
fileObjectKey = await UploadHttpClient.putFileV2(
|
fileObjectKey = await UploadHttpClient.putFileV2(
|
||||||
fileUploadURL,
|
fileUploadURL,
|
||||||
file.file.encryptedData,
|
file.file.encryptedData as Uint8Array,
|
||||||
progressTracker
|
progressTracker
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
fileObjectKey = await UploadHttpClient.putFile(
|
fileObjectKey = await UploadHttpClient.putFile(
|
||||||
fileUploadURL,
|
fileUploadURL,
|
||||||
file.file.encryptedData,
|
file.file.encryptedData as Uint8Array,
|
||||||
progressTracker
|
progressTracker
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -162,13 +177,13 @@ class UploadService {
|
||||||
if (USE_CF_PROXY) {
|
if (USE_CF_PROXY) {
|
||||||
thumbnailObjectKey = await UploadHttpClient.putFileV2(
|
thumbnailObjectKey = await UploadHttpClient.putFileV2(
|
||||||
thumbnailUploadURL,
|
thumbnailUploadURL,
|
||||||
file.thumbnail.encryptedData as Uint8Array,
|
file.thumbnail.encryptedData,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
thumbnailObjectKey = await UploadHttpClient.putFile(
|
thumbnailObjectKey = await UploadHttpClient.putFile(
|
||||||
thumbnailUploadURL,
|
thumbnailUploadURL,
|
||||||
file.thumbnail.encryptedData as Uint8Array,
|
file.thumbnail.encryptedData,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -183,6 +198,7 @@ class UploadService {
|
||||||
objectKey: thumbnailObjectKey,
|
objectKey: thumbnailObjectKey,
|
||||||
},
|
},
|
||||||
metadata: file.metadata,
|
metadata: file.metadata,
|
||||||
|
pubMagicMetadata: file.pubMagicMetadata,
|
||||||
};
|
};
|
||||||
return backupedFile;
|
return backupedFile;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -225,11 +241,32 @@ class UploadService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadFile(uploadFile: UploadFile) {
|
||||||
|
if (this.publicUploadProps.accessedThroughSharedURL) {
|
||||||
|
return publicUploadHttpClient.uploadFile(
|
||||||
|
uploadFile,
|
||||||
|
this.publicUploadProps.token,
|
||||||
|
this.publicUploadProps.passwordToken
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return UploadHttpClient.uploadFile(uploadFile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async fetchUploadURLs() {
|
private async fetchUploadURLs() {
|
||||||
await UploadHttpClient.fetchUploadURLs(
|
if (this.publicUploadProps.accessedThroughSharedURL) {
|
||||||
this.pendingUploadCount,
|
await publicUploadHttpClient.fetchUploadURLs(
|
||||||
this.uploadURLs
|
this.pendingUploadCount,
|
||||||
);
|
this.uploadURLs,
|
||||||
|
this.publicUploadProps.token,
|
||||||
|
this.publicUploadProps.passwordToken
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await UploadHttpClient.fetchUploadURLs(
|
||||||
|
this.pendingUploadCount,
|
||||||
|
this.uploadURLs
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,17 +2,26 @@ import { EnteFile } from 'types/file';
|
||||||
import { handleUploadError, CustomError } from 'utils/error';
|
import { handleUploadError, CustomError } from 'utils/error';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { findMatchingExistingFiles } from 'utils/upload';
|
import { findMatchingExistingFiles } from 'utils/upload';
|
||||||
import UploadHttpClient from './uploadHttpClient';
|
|
||||||
import UIService from './uiService';
|
import UIService from './uiService';
|
||||||
import UploadService from './uploadService';
|
import UploadService from './uploadService';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { UPLOAD_RESULT, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload';
|
import { UPLOAD_RESULT, MAX_FILE_SIZE_SUPPORTED } from 'constants/upload';
|
||||||
import { FileWithCollection, BackupedFile, UploadFile } from 'types/upload';
|
import {
|
||||||
|
FileWithCollection,
|
||||||
|
BackupedFile,
|
||||||
|
UploadFile,
|
||||||
|
FileWithMetadata,
|
||||||
|
FileTypeInfo,
|
||||||
|
} from 'types/upload';
|
||||||
import { addLocalLog, addLogLine } from 'utils/logging';
|
import { addLocalLog, addLogLine } from 'utils/logging';
|
||||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||||
import { sleep } from 'utils/common';
|
import { sleep } from 'utils/common';
|
||||||
import { addToCollection } from 'services/collectionService';
|
import { addToCollection } from 'services/collectionService';
|
||||||
import uploadCancelService from './uploadCancelService';
|
import uploadCancelService from './uploadCancelService';
|
||||||
|
import { Remote } from 'comlink';
|
||||||
|
import { DedicatedCryptoWorker } from 'worker/crypto.worker';
|
||||||
|
import uploadService from './uploadService';
|
||||||
|
import { FilePublicMagicMetadata } from 'types/magicMetadata';
|
||||||
|
|
||||||
interface UploadResponse {
|
interface UploadResponse {
|
||||||
fileUploadResult: UPLOAD_RESULT;
|
fileUploadResult: UPLOAD_RESULT;
|
||||||
|
@ -20,9 +29,11 @@ interface UploadResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function uploader(
|
export default async function uploader(
|
||||||
worker: any,
|
worker: Remote<DedicatedCryptoWorker>,
|
||||||
existingFiles: EnteFile[],
|
existingFiles: EnteFile[],
|
||||||
fileWithCollection: FileWithCollection
|
fileWithCollection: FileWithCollection,
|
||||||
|
uploaderName: string,
|
||||||
|
skipVideos: boolean
|
||||||
): Promise<UploadResponse> {
|
): Promise<UploadResponse> {
|
||||||
const { collection, localID, ...uploadAsset } = fileWithCollection;
|
const { collection, localID, ...uploadAsset } = fileWithCollection;
|
||||||
const fileNameSize = `${UploadService.getAssetName(
|
const fileNameSize = `${UploadService.getAssetName(
|
||||||
|
@ -32,20 +43,33 @@ export default async function uploader(
|
||||||
addLogLine(`uploader called for ${fileNameSize}`);
|
addLogLine(`uploader called for ${fileNameSize}`);
|
||||||
UIService.setFileProgress(localID, 0);
|
UIService.setFileProgress(localID, 0);
|
||||||
await sleep(0);
|
await sleep(0);
|
||||||
const { fileTypeInfo, metadata } =
|
let fileTypeInfo: FileTypeInfo;
|
||||||
UploadService.getFileMetadataAndFileTypeInfo(localID);
|
|
||||||
try {
|
try {
|
||||||
const fileSize = UploadService.getAssetSize(uploadAsset);
|
const fileSize = UploadService.getAssetSize(uploadAsset);
|
||||||
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
|
if (fileSize >= MAX_FILE_SIZE_SUPPORTED) {
|
||||||
return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE };
|
return { fileUploadResult: UPLOAD_RESULT.TOO_LARGE };
|
||||||
}
|
}
|
||||||
|
addLogLine(`getting filetype for ${fileNameSize}`);
|
||||||
|
fileTypeInfo = await UploadService.getAssetFileType(uploadAsset);
|
||||||
|
addLogLine(`got filetype for ${fileNameSize}`);
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
|
||||||
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
|
||||||
}
|
}
|
||||||
if (!metadata) {
|
if (skipVideos && fileTypeInfo.fileType === FILE_TYPE.VIDEO) {
|
||||||
throw Error(CustomError.NO_METADATA);
|
addLogLine(
|
||||||
|
`skipped video upload for public upload ${fileNameSize}`
|
||||||
|
);
|
||||||
|
return { fileUploadResult: UPLOAD_RESULT.SKIPPED_VIDEOS };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addLogLine(`extracting metadata ${fileNameSize}`);
|
||||||
|
const metadata = await UploadService.extractAssetMetadata(
|
||||||
|
worker,
|
||||||
|
uploadAsset,
|
||||||
|
collection.id,
|
||||||
|
fileTypeInfo
|
||||||
|
);
|
||||||
|
|
||||||
const matchingExistingFiles = findMatchingExistingFiles(
|
const matchingExistingFiles = findMatchingExistingFiles(
|
||||||
existingFiles,
|
existingFiles,
|
||||||
metadata
|
metadata
|
||||||
|
@ -100,11 +124,18 @@ export default async function uploader(
|
||||||
if (file.hasStaticThumbnail) {
|
if (file.hasStaticThumbnail) {
|
||||||
metadata.hasStaticThumbnail = true;
|
metadata.hasStaticThumbnail = true;
|
||||||
}
|
}
|
||||||
const fileWithMetadata = {
|
let pubMagicMetadata: FilePublicMagicMetadata;
|
||||||
|
if (uploaderName) {
|
||||||
|
pubMagicMetadata = await uploadService.constructPublicMagicMetadata(
|
||||||
|
{ uploaderName }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const fileWithMetadata: FileWithMetadata = {
|
||||||
localID,
|
localID,
|
||||||
filedata: file.filedata,
|
filedata: file.filedata,
|
||||||
thumbnail: file.thumbnail,
|
thumbnail: file.thumbnail,
|
||||||
metadata,
|
metadata,
|
||||||
|
pubMagicMetadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (uploadCancelService.isUploadCancelationRequested()) {
|
if (uploadCancelService.isUploadCancelationRequested()) {
|
||||||
|
@ -132,7 +163,7 @@ export default async function uploader(
|
||||||
encryptedFile.fileKey
|
encryptedFile.fileKey
|
||||||
);
|
);
|
||||||
|
|
||||||
const uploadedFile = await UploadHttpClient.uploadFile(uploadFile);
|
const uploadedFile = await UploadService.uploadFile(uploadFile);
|
||||||
|
|
||||||
UIService.increaseFileUploaded();
|
UIService.increaseFileUploaded();
|
||||||
addLogLine(`${fileNameSize} successfully uploaded`);
|
addLogLine(`${fileNameSize} successfully uploaded`);
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { clearData, getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { B64EncryptionResult } from 'utils/crypto';
|
import { getRecoveryKey } from 'utils/crypto';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import {
|
import {
|
||||||
KeyAttributes,
|
KeyAttributes,
|
||||||
|
@ -23,6 +23,7 @@ import { ServerErrorCodes } from 'utils/error';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import safeStorageService from './electron/safeStorage';
|
import safeStorageService from './electron/safeStorage';
|
||||||
import { deleteThumbnailCache } from './cacheService';
|
import { deleteThumbnailCache } from './cacheService';
|
||||||
|
import { B64EncryptionResult } from 'types/crypto';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
|
@ -364,3 +365,13 @@ export const deleteAccount = async (challenge: string) => {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const validateKey = async () => {
|
||||||
|
try {
|
||||||
|
await getRecoveryKey();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
await logoutUser();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
|
@ -56,13 +56,15 @@ export class WasmFFmpeg {
|
||||||
let tempOutputFilePath: string;
|
let tempOutputFilePath: string;
|
||||||
try {
|
try {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
tempInputFilePath = `${generateTempName(10)}- ${inputFile.name}`;
|
const extension = getFileExtension(inputFile.name);
|
||||||
|
const tempNameSuffix = extension ? `input.${extension}` : 'input';
|
||||||
|
tempInputFilePath = `${generateTempName(10, tempNameSuffix)}`;
|
||||||
this.ffmpeg.FS(
|
this.ffmpeg.FS(
|
||||||
'writeFile',
|
'writeFile',
|
||||||
tempInputFilePath,
|
tempInputFilePath,
|
||||||
await getUint8ArrayView(inputFile)
|
await getUint8ArrayView(inputFile)
|
||||||
);
|
);
|
||||||
tempOutputFilePath = `${generateTempName(10)}-${outputFileName}`;
|
tempOutputFilePath = `${generateTempName(10, outputFileName)}`;
|
||||||
|
|
||||||
cmd = cmd.map((cmdPart) => {
|
cmd = cmd.map((cmdPart) => {
|
||||||
if (cmdPart === FFMPEG_PLACEHOLDER) {
|
if (cmdPart === FFMPEG_PLACEHOLDER) {
|
||||||
|
@ -95,3 +97,11 @@ export class WasmFFmpeg {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getFileExtension(filename: string) {
|
||||||
|
const lastDotPosition = filename.lastIndexOf('.');
|
||||||
|
if (lastDotPosition === -1) return null;
|
||||||
|
else {
|
||||||
|
return filename.slice(lastDotPosition + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import QueueProcessor from 'services/queueProcessor';
|
import QueueProcessor from 'services/queueProcessor';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { createNewConvertWorker } from 'utils/heicConverter';
|
|
||||||
import { retryAsyncFunction } from 'utils/network';
|
import { retryAsyncFunction } from 'utils/network';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { addLogLine } from 'utils/logging';
|
import { addLogLine } from 'utils/logging';
|
||||||
import { makeHumanReadableStorage } from 'utils/billing';
|
import { DedicatedConvertWorker } from 'worker/convert.worker';
|
||||||
|
import { ComlinkWorker } from 'utils/comlink/comlinkWorker';
|
||||||
|
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||||
|
import { getDedicatedConvertWorker } from 'utils/comlink/ComlinkConvertWorker';
|
||||||
|
|
||||||
const WORKER_POOL_SIZE = 2;
|
const WORKER_POOL_SIZE = 2;
|
||||||
const MAX_CONVERSION_IN_PARALLEL = 1;
|
const MAX_CONVERSION_IN_PARALLEL = 1;
|
||||||
|
@ -17,7 +19,7 @@ class HEICConverter {
|
||||||
private convertProcessor = new QueueProcessor<Blob>(
|
private convertProcessor = new QueueProcessor<Blob>(
|
||||||
MAX_CONVERSION_IN_PARALLEL
|
MAX_CONVERSION_IN_PARALLEL
|
||||||
);
|
);
|
||||||
private workerPool: { comlink: any; worker: Worker }[];
|
private workerPool: ComlinkWorker<typeof DedicatedConvertWorker>[] = [];
|
||||||
private ready: Promise<void>;
|
private ready: Promise<void>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
@ -26,14 +28,15 @@ class HEICConverter {
|
||||||
private async init() {
|
private async init() {
|
||||||
this.workerPool = [];
|
this.workerPool = [];
|
||||||
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
|
for (let i = 0; i < WORKER_POOL_SIZE; i++) {
|
||||||
this.workerPool.push(await createNewConvertWorker());
|
this.workerPool.push(getDedicatedConvertWorker());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async convert(fileBlob: Blob): Promise<Blob> {
|
async convert(fileBlob: Blob): Promise<Blob> {
|
||||||
await this.ready;
|
await this.ready;
|
||||||
const response = this.convertProcessor.queueUpRequest(() =>
|
const response = this.convertProcessor.queueUpRequest(() =>
|
||||||
retryAsyncFunction<Blob>(async () => {
|
retryAsyncFunction<Blob>(async () => {
|
||||||
const { comlink, worker } = this.workerPool.shift();
|
const convertWorker = this.workerPool.shift();
|
||||||
|
const worker = await new convertWorker.remote();
|
||||||
try {
|
try {
|
||||||
const convertedHEIC = await new Promise<Blob>(
|
const convertedHEIC = await new Promise<Blob>(
|
||||||
(resolve, reject) => {
|
(resolve, reject) => {
|
||||||
|
@ -43,15 +46,15 @@ class HEICConverter {
|
||||||
reject(Error('wait time exceeded'));
|
reject(Error('wait time exceeded'));
|
||||||
}, WAIT_TIME_IN_MICROSECONDS);
|
}, WAIT_TIME_IN_MICROSECONDS);
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const convertedHEIC: Blob =
|
const convertedHEIC =
|
||||||
await comlink.convertHEIC(
|
await worker.convertHEIC(
|
||||||
fileBlob,
|
fileBlob,
|
||||||
CONVERT_FORMAT
|
CONVERT_FORMAT
|
||||||
);
|
);
|
||||||
addLogLine(
|
addLogLine(
|
||||||
`originalFileSize:${makeHumanReadableStorage(
|
`originalFileSize:${convertBytesToHumanReadable(
|
||||||
fileBlob?.size
|
fileBlob?.size
|
||||||
)},convertedFileSize:${makeHumanReadableStorage(
|
)},convertedFileSize:${convertBytesToHumanReadable(
|
||||||
convertedHEIC?.size
|
convertedHEIC?.size
|
||||||
)}, heic conversion time: ${
|
)}, heic conversion time: ${
|
||||||
Date.now() - startTime
|
Date.now() - startTime
|
||||||
|
@ -71,10 +74,10 @@ class HEICConverter {
|
||||||
Error(`converted heic fileSize is Zero`),
|
Error(`converted heic fileSize is Zero`),
|
||||||
'converted heic fileSize is Zero',
|
'converted heic fileSize is Zero',
|
||||||
{
|
{
|
||||||
originalFileSize: makeHumanReadableStorage(
|
originalFileSize: convertBytesToHumanReadable(
|
||||||
fileBlob?.size ?? 0
|
fileBlob?.size ?? 0
|
||||||
),
|
),
|
||||||
convertedFileSize: makeHumanReadableStorage(
|
convertedFileSize: convertBytesToHumanReadable(
|
||||||
convertedHEIC?.size ?? 0
|
convertedHEIC?.size ?? 0
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -86,12 +89,12 @@ class HEICConverter {
|
||||||
BREATH_TIME_IN_MICROSECONDS
|
BREATH_TIME_IN_MICROSECONDS
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.workerPool.push({ comlink, worker });
|
this.workerPool.push(convertWorker);
|
||||||
return convertedHEIC;
|
return convertedHEIC;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'heic conversion failed');
|
logError(e, 'heic conversion failed');
|
||||||
worker.terminate();
|
convertWorker.terminate();
|
||||||
this.workerPool.push(await createNewConvertWorker());
|
this.workerPool.push(getDedicatedConvertWorker());
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS)
|
}, WAIT_TIME_BEFORE_NEXT_ATTEMPT_IN_MICROSECONDS)
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import { EnteFile } from 'types/file';
|
import { EncryptedEnteFile } from 'types/file';
|
||||||
import { ElectronFile, FileWithCollection } from 'types/upload';
|
import { ElectronFile, FileWithCollection } from 'types/upload';
|
||||||
import { runningInBrowser } from 'utils/common';
|
import { runningInBrowser } from 'utils/common';
|
||||||
import { removeFromCollection } from '../collectionService';
|
import { removeFromCollection } from '../collectionService';
|
||||||
|
@ -33,7 +33,7 @@ class watchFolderService {
|
||||||
private trashingDirQueue: string[] = [];
|
private trashingDirQueue: string[] = [];
|
||||||
private isEventRunning: boolean = false;
|
private isEventRunning: boolean = false;
|
||||||
private uploadRunning: boolean = false;
|
private uploadRunning: boolean = false;
|
||||||
private filePathToUploadedFileIDMap = new Map<string, EnteFile>();
|
private filePathToUploadedFileIDMap = new Map<string, EncryptedEnteFile>();
|
||||||
private unUploadableFilePaths = new Set<string>();
|
private unUploadableFilePaths = new Set<string>();
|
||||||
private isPaused = false;
|
private isPaused = false;
|
||||||
private setElectronFiles: (files: ElectronFile[]) => void;
|
private setElectronFiles: (files: ElectronFile[]) => void;
|
||||||
|
@ -295,10 +295,10 @@ class watchFolderService {
|
||||||
async onFileUpload(
|
async onFileUpload(
|
||||||
fileUploadResult: UPLOAD_RESULT,
|
fileUploadResult: UPLOAD_RESULT,
|
||||||
fileWithCollection: FileWithCollection,
|
fileWithCollection: FileWithCollection,
|
||||||
file: EnteFile
|
file: EncryptedEnteFile
|
||||||
) {
|
) {
|
||||||
addLocalLog(() => `onFileUpload called`);
|
addLocalLog(() => `onFileUpload called`);
|
||||||
if (!this.isUploadRunning) {
|
if (!this.isUploadRunning()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue