Merge branch 'main' into ml-alpha
This commit is contained in:
parent
cbdae5b1e8
commit
3b468cb154
221 changed files with 7271 additions and 5523 deletions
13
.babelrc
13
.babelrc
|
@ -1,13 +0,0 @@
|
|||
{
|
||||
"presets": ["next/babel"],
|
||||
"plugins": [
|
||||
[
|
||||
"styled-components",
|
||||
{
|
||||
"ssr": true,
|
||||
"displayName": true,
|
||||
"preprocess": false
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
thirdparty
|
|
@ -1,60 +1,65 @@
|
|||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true,
|
||||
"node": true
|
||||
"parserOptions": {
|
||||
"project": ["./tsconfig.json"]
|
||||
},
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"next/core-web-vitals",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"google",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||
"prettier"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"react",
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
|
||||
"rules": {
|
||||
"indent":"off",
|
||||
"indent": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"react/no-unescaped-entities": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error"
|
||||
],
|
||||
"@typescript-eslint/no-unused-vars": ["error"],
|
||||
"require-jsdoc": "off",
|
||||
"valid-jsdoc": "off",
|
||||
"max-len": "off",
|
||||
"new-cap": "off",
|
||||
"no-invalid-this": "off",
|
||||
"eqeqeq": "error",
|
||||
"object-curly-spacing": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"object-curly-spacing": ["error", "always"],
|
||||
"space-before-function-paren": "off",
|
||||
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
|
||||
},
|
||||
"settings": {
|
||||
"react": {
|
||||
"version": "detect"
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"JSX": "readonly",
|
||||
"NodeJS": "readonly",
|
||||
"ReadableStreamDefaultController": "readonly"
|
||||
"operator-linebreak": [
|
||||
"error",
|
||||
"after",
|
||||
{ "overrides": { "?": "before", ":": "before" } }
|
||||
],
|
||||
"import/no-anonymous-default-export": [
|
||||
"error",
|
||||
{
|
||||
"allowNew": true
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||
"@typescript-eslint/no-unsafe-return": "off",
|
||||
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||
"@typescript-eslint/no-inferrable-types": "off",
|
||||
"@typescript-eslint/restrict-template-expressions": "off",
|
||||
"@typescript-eslint/ban-types": "off",
|
||||
"@typescript-eslint/no-floating-promises": "off",
|
||||
"@typescript-eslint/no-unsafe-call": "off",
|
||||
"@typescript-eslint/require-await": "off",
|
||||
"@typescript-eslint/restrict-plus-operands": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"@typescript-eslint/no-empty-interface": "off",
|
||||
"@typescript-eslint/no-misused-promises": "off",
|
||||
"@typescript-eslint/no-empty-function": "off",
|
||||
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off",
|
||||
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||
"react-hooks/rules-of-hooks": "off",
|
||||
"react-hooks/exhaustive-deps": "off",
|
||||
"@next/next/no-img-element": "off",
|
||||
"@typescript-eslint/no-unsafe-argument": "off",
|
||||
"jsx-a11y/alt-text": "off"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npx lint-staged
|
||||
yarn lint-staged
|
||||
|
|
13
.lintstagedrc.js
Normal file
13
.lintstagedrc.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
const path = require('path');
|
||||
|
||||
const buildEslintCommand = (filenames) =>
|
||||
`next lint --fix --file ${filenames
|
||||
.map((f) => path.relative(process.cwd(), f))
|
||||
.join(' --file ')}`;
|
||||
|
||||
const buildPrettierCommand = (filenames) =>
|
||||
`yarn prettier --write --ignore-unknown ${filenames.join(' ')}`;
|
||||
|
||||
module.exports = {
|
||||
'*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand],
|
||||
};
|
1
.yarnrc
Normal file
1
.yarnrc
Normal file
|
@ -0,0 +1 @@
|
|||
network-timeout 500000
|
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.
|
||||
|
||||
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/>
|
||||
|
||||

|
||||
|
@ -30,7 +37,7 @@ The deployed application is accessible @ [web.ente.io](https://web.ente.io).
|
|||
|
||||
## 🧑💻 Building from source
|
||||
|
||||
1. Clone this repository with `git clone git@github.com:ente-io/bada-frame.git`
|
||||
1. Clone this repository with `git clone https://github.com/ente-io/photos-web.git`
|
||||
2. Pull in all submodules with `git submodule update --init --recursive`
|
||||
3. Install dependencies with `yarn install`
|
||||
4. Finally, run the development server with `yarn dev`
|
||||
|
@ -55,7 +62,8 @@ We maintain a public roadmap, that's driven by our community @ [roadmap.ente.io]
|
|||
|
||||
If you like this project, please consider upgrading to a paid subscription.
|
||||
|
||||
If you would like to motivate us to keep building, you can do so by [starring](https://github.com/ente-io/bada-frame/stargazers) this project.
|
||||
If you would like to motivate us to keep building, you can do so by
|
||||
[starring](https://github.com/ente-io/photos-web/stargazers) this project.
|
||||
|
||||
<br/>
|
||||
|
||||
|
@ -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)
|
||||
|
|
|
@ -26,7 +26,7 @@ module.exports = {
|
|||
'style-src': "'self' 'unsafe-inline'",
|
||||
'font-src ': "'self'; script-src 'self' 'unsafe-eval' blob:",
|
||||
'connect-src':
|
||||
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ",
|
||||
"'self' https://*.ente.io http://localhost:8080 data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/",
|
||||
'base-uri ': "'self'",
|
||||
// to allow worker
|
||||
'child-src': "'self' blob:",
|
||||
|
@ -37,11 +37,6 @@ module.exports = {
|
|||
'report-to': ' https://csp-reporter.ente.io/local',
|
||||
},
|
||||
|
||||
WORKBOX_CONFIG: {
|
||||
swSrc: 'src/serviceWorker.js',
|
||||
exclude: [/manifest\.json$/i],
|
||||
},
|
||||
|
||||
ALL_ROUTES: '/(.*)',
|
||||
|
||||
buildCSPHeader: (directives) => ({
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
enabled: process.env.ANALYZE === 'true',
|
||||
});
|
||||
const withWorkbox = require('@ente-io/next-with-workbox');
|
||||
|
||||
const { withSentryConfig } = require('@sentry/nextjs');
|
||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||
|
@ -19,7 +18,6 @@ const {
|
|||
COOP_COEP_HEADERS,
|
||||
WEB_SECURITY_HEADERS,
|
||||
CSP_DIRECTIVES,
|
||||
WORKBOX_CONFIG,
|
||||
ALL_ROUTES,
|
||||
getIsSentryEnabled,
|
||||
} = require('./configUtil');
|
||||
|
@ -30,37 +28,39 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
|
|||
|
||||
module.exports = (phase) =>
|
||||
withSentryConfig(
|
||||
withWorkbox(
|
||||
withBundleAnalyzer(
|
||||
withTM({
|
||||
env: {
|
||||
SENTRY_RELEASE: GIT_SHA,
|
||||
NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA,
|
||||
withBundleAnalyzer(
|
||||
withTM({
|
||||
compiler: {
|
||||
styledComponents: {
|
||||
ssr: true,
|
||||
displayName: true,
|
||||
},
|
||||
workbox: WORKBOX_CONFIG,
|
||||
},
|
||||
env: {
|
||||
SENTRY_RELEASE: GIT_SHA,
|
||||
},
|
||||
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
// Apply these headers to all routes in your application....
|
||||
source: ALL_ROUTES,
|
||||
headers: convertToNextHeaderFormat({
|
||||
...COOP_COEP_HEADERS,
|
||||
...WEB_SECURITY_HEADERS,
|
||||
...buildCSPHeader(CSP_DIRECTIVES),
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
})
|
||||
)
|
||||
headers() {
|
||||
return [
|
||||
{
|
||||
// Apply these headers to all routes in your application....
|
||||
source: ALL_ROUTES,
|
||||
headers: convertToNextHeaderFormat({
|
||||
...COOP_COEP_HEADERS,
|
||||
...WEB_SECURITY_HEADERS,
|
||||
...buildCSPHeader(CSP_DIRECTIVES),
|
||||
}),
|
||||
},
|
||||
];
|
||||
},
|
||||
// https://dev.to/marcinwosinek/how-to-add-resolve-fallback-to-webpack-5-in-nextjs-10-i6j
|
||||
webpack: (config, { isServer }) => {
|
||||
if (!isServer) {
|
||||
config.resolve.fallback.fs = false;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
})
|
||||
),
|
||||
{
|
||||
release: GIT_SHA,
|
||||
|
|
37
package.json
37
package.json
|
@ -5,8 +5,7 @@
|
|||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"albums": "next dev -p 3002",
|
||||
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
|
||||
"prebuild": "yarn lint",
|
||||
"lint": "next lint",
|
||||
"build": "next build",
|
||||
"postbuild": "next export",
|
||||
"build-analyze": "ANALYZE=true next build",
|
||||
|
@ -15,7 +14,6 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@date-io/date-fns": "^2.14.0",
|
||||
"@ente-io/next-with-workbox": "^1.0.3",
|
||||
"@mui/icons-material": "^5.6.2",
|
||||
"@mui/material": "^5.6.2",
|
||||
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
|
||||
|
@ -53,16 +51,16 @@
|
|||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"ml-matrix": "^6.8.2",
|
||||
"next": "^12.1.0",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"next": "^13.0.6",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"p-queue": "^7.1.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
"piexifjs": "^1.0.6",
|
||||
"react": "^17.0.2",
|
||||
"react": "^18.2.0",
|
||||
"react-bootstrap": "^1.3.0",
|
||||
"react-d3-tree": "^3.1.1",
|
||||
"react-datepicker": "^4.3.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-otp-input": "^2.3.1",
|
||||
"react-select": "^4.3.1",
|
||||
|
@ -70,6 +68,7 @@
|
|||
"react-top-loading-bar": "^2.0.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.6",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"similarity-transformation": "^0.0.1",
|
||||
"styled-components": "^5.3.5",
|
||||
"tesseract.js": "file:./thirdparty/tesseract",
|
||||
|
@ -85,6 +84,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^9.5.3",
|
||||
"@types/bs58": "^4.0.1",
|
||||
"@types/debounce-promise": "^3.1.3",
|
||||
"@types/libsodium-wrappers": "^0.7.8",
|
||||
"@types/node": "^14.6.4",
|
||||
|
@ -98,17 +98,10 @@
|
|||
"@types/styled-components": "^5.1.25",
|
||||
"@types/wicg-file-system-access": "^2020.9.5",
|
||||
"@types/yup": "^0.29.7",
|
||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
||||
"@typescript-eslint/parser": "^4.25.0",
|
||||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-google": "^0.14.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.23.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||
"eslint": "^8.28.0",
|
||||
"eslint-config-next": "^13.0.6",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"husky": "^7.0.1",
|
||||
"lint-staged": "^11.1.2",
|
||||
"prettier": "2.3.2",
|
||||
|
@ -117,13 +110,7 @@
|
|||
"standard": {
|
||||
"parser": "babel-eslint"
|
||||
},
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write --ignore-unknown"
|
||||
]
|
||||
},
|
||||
"resolutions": {
|
||||
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,5 +8,5 @@
|
|||
X-Frame-Options: deny
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Referrer-Policy: same-origin
|
||||
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
|
||||
Content-Security-Policy-Report-Only: default-src 'self'; img-src 'self' blob: data:; media-src 'self' blob:; style-src 'self' 'unsafe-inline'; font-src 'self'; script-src 'self' 'unsafe-eval' blob:; manifest-src 'self'; child-src 'self' blob:; object-src 'none'; connect-src 'self' https://*.ente.io data: blob: https://ente-prod-eu.s3.eu-central-003.backblazeb2.com https://ente-prod-v3.s3.eu-central-2.wasabisys.com/ ; base-uri 'self'; frame-ancestors 'none'; form-action 'none'; report-uri https://csp-reporter.ente.io; report-to https://csp-reporter.ente.io;
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV();
|
|||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.setUser({ id: getSentryUserID() });
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: IS_ENABLED,
|
||||
|
@ -39,3 +38,9 @@ Sentry.init({
|
|||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
getIsSentryEnabled,
|
||||
} from 'constants/sentry';
|
||||
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
|
@ -18,3 +20,9 @@ Sentry.init({
|
|||
release: SENTRY_RELEASE,
|
||||
autoSessionTracking: false,
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
const ENV_DEVELOPMENT = 'development';
|
||||
|
||||
module.exports.getIsSentryEnabled = () => {
|
||||
if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) {
|
||||
return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes';
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
|
||||
return false;
|
||||
} else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
|
||||
return false;
|
||||
} else {
|
||||
if (process.env.NEXT_PUBLIC_SENTRY_ENV) {
|
||||
return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
|
|||
|
||||
export const Badge = styled(Paper)(({ theme }) => ({
|
||||
padding: '2px 4px',
|
||||
backgroundColor: theme.palette.glass.main,
|
||||
color: theme.palette.glass.contrastText,
|
||||
backgroundColor: theme.palette.backdrop.main,
|
||||
backdropFilter: `blur(${theme.palette.blur.muted})`,
|
||||
color: theme.palette.primary.contrastText,
|
||||
textTransform: 'uppercase',
|
||||
...(theme.typography.mini as CSSProperties),
|
||||
}));
|
||||
|
|
10
src/components/Chip.tsx
Normal file
10
src/components/Chip.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { Box, styled } from '@mui/material';
|
||||
import { CSSProperties } from 'react';
|
||||
|
||||
export const Chip = styled(Box)(({ theme }) => ({
|
||||
...(theme.typography.body2 as CSSProperties),
|
||||
padding: '8px 12px',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: theme.palette.fill.dark,
|
||||
fontWeight: 'bold',
|
||||
}));
|
|
@ -1,20 +1,43 @@
|
|||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CopyButtonWrapper } from './styledComponents';
|
||||
import DoneIcon from '@mui/icons-material/Done';
|
||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||
import { Tooltip } from '@mui/material';
|
||||
import {
|
||||
IconButton,
|
||||
IconButtonProps,
|
||||
SvgIconProps,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
|
||||
export default function CopyButton({ code, copied, copyToClipboardHelper }) {
|
||||
export default function CopyButton({
|
||||
code,
|
||||
color,
|
||||
size,
|
||||
}: {
|
||||
code: string;
|
||||
color?: IconButtonProps['color'];
|
||||
size?: SvgIconProps['fontSize'];
|
||||
}) {
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboardHelper = (text: string) => () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
};
|
||||
return (
|
||||
<Tooltip arrow open={copied} title={constants.COPIED}>
|
||||
<CopyButtonWrapper onClick={copyToClipboardHelper(code)}>
|
||||
<Tooltip
|
||||
arrow
|
||||
open={copied}
|
||||
title={constants.COPIED}
|
||||
PopperProps={{ sx: { zIndex: 2000 } }}>
|
||||
<IconButton onClick={copyToClipboardHelper(code)} color={color}>
|
||||
{copied ? (
|
||||
<DoneIcon fontSize="small" />
|
||||
<DoneIcon fontSize={size ?? 'small'} />
|
||||
) : (
|
||||
<ContentCopyIcon fontSize="small" />
|
||||
<ContentCopyIcon fontSize={size ?? 'small'} />
|
||||
)}
|
||||
</CopyButtonWrapper>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FreeFlowText } from '../Container';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import EnteSpinner from '../EnteSpinner';
|
||||
import { Wrapper, CodeWrapper } from './styledComponents';
|
||||
import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
|
||||
import CopyButton from './CopyButton';
|
||||
import { BoxProps } from '@mui/material';
|
||||
|
||||
|
@ -15,14 +15,6 @@ export default function CodeBlock({
|
|||
wordBreak,
|
||||
...props
|
||||
}: BoxProps<'div', Iprops>) {
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
|
||||
const copyToClipboardHelper = (text: string) => () => {
|
||||
navigator.clipboard.writeText(text);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1000);
|
||||
};
|
||||
|
||||
if (!code) {
|
||||
return (
|
||||
<Wrapper>
|
||||
|
@ -37,11 +29,9 @@ export default function CodeBlock({
|
|||
{code}
|
||||
</FreeFlowText>
|
||||
</CodeWrapper>
|
||||
<CopyButton
|
||||
code={code}
|
||||
copied={copied}
|
||||
copyToClipboardHelper={copyToClipboardHelper}
|
||||
/>
|
||||
<CopyButtonWrapper>
|
||||
<CopyButton code={code} />
|
||||
</CopyButtonWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,9 +12,6 @@ export default function CollectionSortOptions(props: CollectionSortProps) {
|
|||
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
||||
{constants.SORT_BY_NAME}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
|
||||
{constants.SORT_BY_CREATION_TIME_DESCENDING}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
|
||||
{constants.SORT_BY_CREATION_TIME_ASCENDING}
|
||||
</SortByOption>
|
||||
|
|
|
@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents';
|
|||
import { shouldShowOptions } from 'utils/collection';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
|
||||
interface Iprops {
|
||||
activeCollection: Collection;
|
||||
|
@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({
|
|||
return <Favorite />;
|
||||
case CollectionSummaryType.archived:
|
||||
case CollectionSummaryType.archive:
|
||||
return <VisibilityOff />;
|
||||
return <ArchiveOutlined />;
|
||||
case CollectionSummaryType.trash:
|
||||
return <Delete />;
|
||||
default:
|
||||
|
|
|
@ -11,7 +11,8 @@ import TruncateText from 'components/TruncateText';
|
|||
import { Box } from '@mui/material';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
import PeopleIcon from '@mui/icons-material/People';
|
||||
|
||||
interface Iprops {
|
||||
active: boolean;
|
||||
|
@ -50,8 +51,9 @@ function CollectionCardIcon({ collectionType }) {
|
|||
<CollectionBarTileIcon>
|
||||
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
||||
{collectionType === CollectionSummaryType.archived && (
|
||||
<VisibilityOff />
|
||||
<ArchiveOutlined />
|
||||
)}
|
||||
{collectionType === CollectionSummaryType.shared && <PeopleIcon />}
|
||||
</CollectionBarTileIcon>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,11 +4,11 @@ import React from 'react';
|
|||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CollectionActions } from '.';
|
||||
import Unarchive from '@mui/icons-material/Unarchive';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
|
||||
interface Iprops {
|
||||
IsArchived: boolean;
|
||||
|
@ -53,13 +53,13 @@ export function AlbumCollectionOption({
|
|||
onClick={handleCollectionAction(
|
||||
CollectionActions.UNARCHIVE
|
||||
)}
|
||||
startIcon={<VisibilityOnOutlinedIcon />}>
|
||||
startIcon={<Unarchive />}>
|
||||
{constants.UNARCHIVE}
|
||||
</OverflowMenuOption>
|
||||
) : (
|
||||
<OverflowMenuOption
|
||||
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
||||
startIcon={<VisibilityOffOutlinedIcon />}>
|
||||
startIcon={<ArchiveOutlined />}>
|
||||
{constants.ARCHIVE}
|
||||
</OverflowMenuOption>
|
||||
)}
|
||||
|
|
|
@ -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,6 +17,7 @@ import { AppContext } from 'pages/_app';
|
|||
import OverflowMenu from 'components/OverflowMenu/menu';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import { TrashCollectionOption } from './TrashCollectionOption';
|
||||
import { SharedCollectionOption } from './SharedCollectionOption';
|
||||
import MoreHoriz from '@mui/icons-material/MoreHoriz';
|
||||
|
||||
interface CollectionOptionsProps {
|
||||
|
@ -39,6 +40,8 @@ export enum CollectionActions {
|
|||
SHOW_SHARE_DIALOG,
|
||||
CONFIRM_EMPTY_TRASH,
|
||||
EMPTY_TRASH,
|
||||
CONFIRM_LEAVE_SHARED_ALBUM,
|
||||
LEAVE_SHARED_ALBUM,
|
||||
}
|
||||
|
||||
const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||
|
@ -93,6 +96,12 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
case CollectionActions.EMPTY_TRASH:
|
||||
callback = emptyTrash;
|
||||
break;
|
||||
case CollectionActions.CONFIRM_LEAVE_SHARED_ALBUM:
|
||||
callback = confirmLeaveSharedAlbum;
|
||||
break;
|
||||
case CollectionActions.LEAVE_SHARED_ALBUM:
|
||||
callback = leaveSharedAlbum;
|
||||
break;
|
||||
default:
|
||||
logError(
|
||||
Error('invalid collection action '),
|
||||
|
@ -130,6 +139,11 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
redirectToAll();
|
||||
};
|
||||
|
||||
const leaveSharedAlbum = async () => {
|
||||
await CollectionAPI.leaveSharedAlbum(activeCollection.id);
|
||||
redirectToAll();
|
||||
};
|
||||
|
||||
const archiveCollection = () => {
|
||||
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
|
||||
};
|
||||
|
@ -200,6 +214,23 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
close: { text: constants.CANCEL },
|
||||
});
|
||||
|
||||
const confirmLeaveSharedAlbum = () => {
|
||||
setDialogMessage({
|
||||
title: constants.LEAVE_SHARED_ALBUM_TITLE,
|
||||
content: constants.LEAVE_SHARED_ALBUM_MESSAGE,
|
||||
proceed: {
|
||||
text: constants.LEAVE_SHARED_ALBUM,
|
||||
action: handleCollectionAction(
|
||||
CollectionActions.LEAVE_SHARED_ALBUM
|
||||
),
|
||||
variant: 'danger',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<OverflowMenu
|
||||
ariaControls={'collection-options'}
|
||||
|
@ -213,6 +244,10 @@ const CollectionOptions = (props: CollectionOptionsProps) => {
|
|||
<TrashCollectionOption
|
||||
handleCollectionAction={handleCollectionAction}
|
||||
/>
|
||||
) : collectionSummaryType === CollectionSummaryType.shared ? (
|
||||
<SharedCollectionOption
|
||||
handleCollectionAction={handleCollectionAction}
|
||||
/>
|
||||
) : (
|
||||
<AlbumCollectionOption
|
||||
IsArchived={IsArchived(activeCollection)}
|
||||
|
|
|
@ -14,11 +14,12 @@ export interface CollectionSelectorAttributes {
|
|||
showNextModal: () => void;
|
||||
title: string;
|
||||
fromCollection?: number;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
onClose: (closeBtnClick?: boolean) => void;
|
||||
onClose: () => void;
|
||||
attributes: CollectionSelectorAttributes;
|
||||
collections: Collection[];
|
||||
collectionSummaries: CollectionSummaries;
|
||||
|
@ -61,15 +62,18 @@ function CollectionSelector({
|
|||
props.onClose();
|
||||
};
|
||||
|
||||
const onCloseButtonClick = () => props.onClose(true);
|
||||
const onUserTriggeredClose = () => {
|
||||
attributes.onCancel?.();
|
||||
props.onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<AllCollectionDialog
|
||||
onClose={props.onClose}
|
||||
onClose={onUserTriggeredClose}
|
||||
open={props.open}
|
||||
position="center"
|
||||
fullScreen={appContext.isMobile}>
|
||||
<DialogTitleWithCloseButton onClose={onCloseButtonClick}>
|
||||
<DialogTitleWithCloseButton onClose={onUserTriggeredClose}>
|
||||
{attributes.title}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
|
|||
import React, { useContext } from 'react';
|
||||
import { shareCollection } from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CollectionShareSharees } from './sharees';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
|
@ -8,7 +7,7 @@ import {
|
|||
deleteShareableURL,
|
||||
} from 'services/collectionService';
|
||||
import { Collection, PublicURL } from 'types/collection';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from './switch';
|
||||
interface Iprops {
|
||||
|
@ -60,7 +59,7 @@ export default function PublicShareControl({
|
|||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: disablePublicSharing,
|
||||
variant: ButtonVariant.danger,
|
||||
variant: 'danger',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -2,15 +2,22 @@ import { Box, Typography } from '@mui/material';
|
|||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { DropdownStyle } from 'styles/dropdown';
|
||||
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||
import { getDeviceLimitOptions } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
||||
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
collection: Collection;
|
||||
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ManageDeviceLimit({
|
||||
publicShareProp,
|
||||
collection,
|
||||
updatePublicShareURLHelper,
|
||||
}) {
|
||||
}: Iprops) {
|
||||
const updateDeviceLimit = async (newLimit: number) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
|
|
|
@ -1,14 +1,21 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from '../switch';
|
||||
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
collection: Collection;
|
||||
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ManageDownloadAccess({
|
||||
publicShareProp,
|
||||
updatePublicShareURLHelper,
|
||||
collection,
|
||||
}) {
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const handleFileDownloadSetting = () => {
|
||||
|
@ -34,7 +41,7 @@ export function ManageDownloadAccess({
|
|||
collectionID: collection.id,
|
||||
enableDownload: false,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
variant: 'danger',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
@ -42,7 +49,7 @@ export function ManageDownloadAccess({
|
|||
<Box>
|
||||
<Typography mb={0.5}>{constants.FILE_DOWNLOAD}</Typography>
|
||||
<PublicShareSwitch
|
||||
checked={publicShareProp?.enableDownload ?? false}
|
||||
checked={publicShareProp?.enableDownload ?? true}
|
||||
onChange={handleFileDownloadSetting}
|
||||
/>
|
||||
</Box>
|
||||
|
|
|
@ -1,33 +1,37 @@
|
|||
import { ManageLinkPassword } from './linkPassword';
|
||||
import { ManageDeviceLimit } from './deviceLimit';
|
||||
import { ManageLinkExpiry } from './linkExpiry';
|
||||
import { PublicLinkSetPassword } from '../setPassword';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { updateShareableURL } from 'services/collectionService';
|
||||
import { UpdatePublicURL } from 'types/collection';
|
||||
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||
import { sleep } from 'utils/common';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
ManageSectionLabel,
|
||||
ManageSectionOptions,
|
||||
} from '../../styledComponents';
|
||||
import { ManageDownloadAccess } from './downloadAccess';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
import { SetPublicShareProp } from 'types/publicCollection';
|
||||
import { ManagePublicCollect } from './publicCollect';
|
||||
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
collection: Collection;
|
||||
setPublicShareProp: SetPublicShareProp;
|
||||
}
|
||||
|
||||
export default function PublicShareManage({
|
||||
publicShareProp,
|
||||
collection,
|
||||
setPublicShareProp,
|
||||
}) {
|
||||
}: Iprops) {
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
|
||||
const [changePasswordView, setChangePasswordView] = useState(false);
|
||||
const [sharableLinkError, setSharableLinkError] = useState(null);
|
||||
|
||||
const closeConfigurePassword = () => setChangePasswordView(false);
|
||||
|
||||
const updatePublicShareURLHelper = async (req: UpdatePublicURL) => {
|
||||
try {
|
||||
galleryContext.setBlockingLoad(true);
|
||||
|
@ -73,6 +77,13 @@ export default function PublicShareManage({
|
|||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManagePublicCollect
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
updatePublicShareURLHelper
|
||||
}
|
||||
/>
|
||||
<ManageDownloadAccess
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
|
@ -81,7 +92,6 @@ export default function PublicShareManage({
|
|||
}
|
||||
/>
|
||||
<ManageLinkPassword
|
||||
setChangePasswordView={setChangePasswordView}
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={
|
||||
|
@ -102,14 +112,6 @@ export default function PublicShareManage({
|
|||
)}
|
||||
</ManageSectionOptions>
|
||||
</details>
|
||||
<PublicLinkSetPassword
|
||||
open={changePasswordView}
|
||||
onClose={closeConfigurePassword}
|
||||
collection={collection}
|
||||
publicShareProp={publicShareProp}
|
||||
updatePublicShareURLHelper={updatePublicShareURLHelper}
|
||||
setChangePasswordView={setChangePasswordView}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,16 +2,23 @@ import { Box, Typography } from '@mui/material';
|
|||
import React from 'react';
|
||||
import Select from 'react-select';
|
||||
import { linkExpiryStyle } from 'styles/linkExpiry';
|
||||
import { PublicURL, Collection, UpdatePublicURL } from 'types/collection';
|
||||
import { shareExpiryOptions } from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { dateStringWithMMH } from 'utils/time';
|
||||
import { formatDateTime } from 'utils/time/format';
|
||||
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
||||
|
||||
interface Iprops {
|
||||
publicShareProp: PublicURL;
|
||||
collection: Collection;
|
||||
updatePublicShareURLHelper: (req: UpdatePublicURL) => Promise<void>;
|
||||
}
|
||||
|
||||
export function ManageLinkExpiry({
|
||||
publicShareProp,
|
||||
collection,
|
||||
updatePublicShareURLHelper,
|
||||
}) {
|
||||
}: Iprops) {
|
||||
const updateDeviceExpiry = async (optionFn) => {
|
||||
return updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
|
@ -31,7 +38,7 @@ export function ManageLinkExpiry({
|
|||
}}
|
||||
placeholder={
|
||||
publicShareProp?.validTill
|
||||
? dateStringWithMMH(publicShareProp?.validTill)
|
||||
? formatDateTime(publicShareProp?.validTill / 1000)
|
||||
: 'never'
|
||||
}
|
||||
onChange={(e) => {
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import { Box, Typography } from '@mui/material';
|
||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from '../switch';
|
||||
export function ManageLinkPassword({
|
||||
collection,
|
||||
publicShareProp,
|
||||
updatePublicShareURLHelper,
|
||||
setChangePasswordView,
|
||||
}) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const handlePasswordChangeSetting = async () => {
|
||||
if (publicShareProp.passwordEnabled) {
|
||||
await confirmDisablePublicUrlPassword();
|
||||
} else {
|
||||
setChangePasswordView(true);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmDisablePublicUrlPassword = async () => {
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DISABLE_PASSWORD,
|
||||
content: constants.DISABLE_PASSWORD_MESSAGE,
|
||||
close: { text: constants.CANCEL },
|
||||
proceed: {
|
||||
text: constants.DISABLE,
|
||||
action: () =>
|
||||
updatePublicShareURLHelper({
|
||||
collectionID: collection.id,
|
||||
disablePassword: true,
|
||||
}),
|
||||
variant: ButtonVariant.danger,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography mb={0.5}> {constants.LINK_PASSWORD_LOCK}</Typography>
|
||||
<PublicShareSwitch
|
||||
checked={!!publicShareProp?.passwordEnabled}
|
||||
onChange={handlePasswordChangeSetting}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
} from 'components/SingleInputForm';
|
||||
import React from 'react';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
export function PublicLinkSetPassword({
|
||||
|
@ -28,8 +28,8 @@ export function PublicLinkSetPassword({
|
|||
};
|
||||
|
||||
const enablePublicUrlPassword = async (password: string) => {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const kekSalt: string = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
const kekSalt = await cryptoWorker.generateSaltToDeriveKey();
|
||||
const kek = await cryptoWorker.deriveInteractiveKey(password, kekSalt);
|
||||
|
||||
return updatePublicShareURLHelper({
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -96,7 +96,7 @@ export default function Collections(props: Iprops) {
|
|||
itemType: ITEM_TYPE.OTHER,
|
||||
height: 68,
|
||||
});
|
||||
}, [collectionSummaries, activeCollectionID]);
|
||||
}, [collectionSummaries, activeCollectionID, isInSearchMode]);
|
||||
|
||||
if (shouldBeHidden) {
|
||||
return <></>;
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
import React from 'react';
|
||||
import { styled } from '@mui/material';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { IconWithMessage } from './IconWithMessage';
|
||||
|
||||
const Wrapper = styled('button')`
|
||||
border: none;
|
||||
background-color: #ff6666;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
bottom: 30px;
|
||||
right: 30px;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
`;
|
||||
export default function DeleteBtn(props) {
|
||||
return (
|
||||
<IconWithMessage message={constants.EMPTY_TRASH}>
|
||||
<Wrapper onClick={props.onClick}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
|
||||
</svg>
|
||||
</Wrapper>
|
||||
</IconWithMessage>
|
||||
);
|
||||
}
|
||||
|
||||
DeleteBtn.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
18
src/components/DialogBox/DialogIcon.tsx
Normal file
18
src/components/DialogBox/DialogIcon.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
className="DialogIcon"
|
||||
sx={{
|
||||
svg: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
},
|
||||
color: 'stroke.secondary',
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -5,6 +5,12 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
|||
padding: theme.spacing(1, 1.5),
|
||||
maxWidth: '346px',
|
||||
},
|
||||
|
||||
'& .DialogIcon': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
|
||||
'& .MuiDialogTitle-root': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
|
@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
|||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
|
||||
'.DialogIcon + .MuiDialogTitle-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
||||
'.MuiDialogTitle-root + .MuiDialogContent-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, {
|
|||
} from './TitleWithCloseButton';
|
||||
import DialogBoxBase from './base';
|
||||
import { DialogBoxAttributes } from 'types/dialogBox';
|
||||
import DialogIcon from './DialogIcon';
|
||||
|
||||
type IProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
||||
|
@ -48,6 +49,7 @@ export default function DialogBox({
|
|||
maxWidth={size}
|
||||
onClose={handleClose}
|
||||
{...props}>
|
||||
{attributes.icon && <DialogIcon icon={attributes.icon} />}
|
||||
{attributes.title && (
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={
|
||||
|
|
|
@ -4,7 +4,6 @@ import {
|
|||
MIN_EDITED_CREATION_TIME,
|
||||
MAX_EDITED_CREATION_TIME,
|
||||
} from 'constants/file';
|
||||
import { TextField } from '@mui/material';
|
||||
import {
|
||||
LocalizationProvider,
|
||||
MobileDateTimePicker,
|
||||
|
@ -60,14 +59,7 @@ const EnteDateTimePicker = ({
|
|||
},
|
||||
},
|
||||
}}
|
||||
renderInput={(params) => (
|
||||
<TextField
|
||||
{...params}
|
||||
hiddenLabel
|
||||
margin="none"
|
||||
variant="standard"
|
||||
/>
|
||||
)}
|
||||
renderInput={() => <></>}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
);
|
||||
|
|
11
src/components/EnteDrawer.tsx
Normal file
11
src/components/EnteDrawer.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Drawer } from '@mui/material';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const EnteDrawer = styled(Drawer)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '375px',
|
||||
width: '100%',
|
||||
scrollbarWidth: 'thin',
|
||||
padding: theme.spacing(1),
|
||||
},
|
||||
}));
|
|
@ -1,8 +1,8 @@
|
|||
import { Button, DialogActions, DialogContent, Stack } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { ExportStats } from 'types/export';
|
||||
import { formatDateTime } from 'utils/time';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { formatDateTime } from 'utils/time/format';
|
||||
import { FlexWrapper, Label, Value } from './Container';
|
||||
import { ComfySpan } from './ExportInProgress';
|
||||
|
||||
|
|
|
@ -106,7 +106,6 @@ export default function FixCreationTime(props: Props) {
|
|||
<div
|
||||
style={{
|
||||
marginBottom: '10px',
|
||||
padding: '0 5%',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
...(fixState === FIX_STATE.RUNNING
|
||||
|
|
|
@ -23,7 +23,6 @@ const Option = ({
|
|||
color: value !== Number(selected) ? '#aaa' : '#fff',
|
||||
}}>
|
||||
<Form.Check.Input
|
||||
style={{ marginTop: '6px' }}
|
||||
id={value.toString()}
|
||||
type="radio"
|
||||
value={value}
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import React from 'react';
|
||||
|
||||
interface IconWithMessageProps {
|
||||
children?: any;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const IconWithMessage = (props: IconWithMessageProps) => (
|
||||
<OverlayTrigger
|
||||
placement="bottom"
|
||||
overlay={
|
||||
<Tooltip id="on-hover-info" style={{ zIndex: 1002 }}>
|
||||
{props.message}
|
||||
</Tooltip>
|
||||
}>
|
||||
{props.children}
|
||||
</OverlayTrigger>
|
||||
);
|
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>
|
||||
);
|
||||
}
|
|
@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
};
|
||||
|
||||
const handleClick = () => {
|
||||
attributes.action?.callback();
|
||||
attributes.onClick();
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
|
@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
anchorOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'bottom',
|
||||
}}>
|
||||
}}
|
||||
sx={{ backgroundColor: '#000', width: '320px' }}>
|
||||
<Paper
|
||||
component={Button}
|
||||
color={attributes.variant}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
textAlign: 'left',
|
||||
width: '320px',
|
||||
flex: '1',
|
||||
padding: (theme) => theme.spacing(1.5, 2),
|
||||
}}>
|
||||
<Stack
|
||||
|
@ -55,34 +56,38 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
spacing={2}
|
||||
direction="row"
|
||||
alignItems={'center'}>
|
||||
<Box>
|
||||
{attributes?.icon ?? <InfoIcon fontSize="large" />}
|
||||
<Box sx={{ svg: { fontSize: '36px' } }}>
|
||||
{attributes.startIcon ?? <InfoIcon />}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="rgba(255, 255, 255, 0.7)"
|
||||
mb={0.5}>
|
||||
{attributes.message}{' '}
|
||||
</Typography>
|
||||
{attributes?.action && (
|
||||
<Typography
|
||||
mb={0.5}
|
||||
variant="button"
|
||||
fontWeight={'bold'}>
|
||||
{attributes?.action.text}
|
||||
|
||||
<Stack
|
||||
direction={'column'}
|
||||
spacing={0.5}
|
||||
flex={1}
|
||||
textAlign="left">
|
||||
{attributes.subtext && (
|
||||
<Typography variant="body2">
|
||||
{attributes.subtext}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{attributes.message && (
|
||||
<Typography variant="button">
|
||||
{attributes.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{attributes.endIcon ? (
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
}}>
|
||||
onClick={attributes.onClick}
|
||||
sx={{ fontSize: '36px' }}>
|
||||
{attributes?.endIcon}
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Snackbar>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
import { GalleryContext } from 'pages/gallery';
|
||||
import PreviewCard from './pages/gallery/PreviewCard';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { styled } from '@mui/material';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import PhotoSwipe from 'components/PhotoSwipe';
|
||||
import PhotoViewer from 'components/PhotoViewer';
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
import { isSharedFile } from 'utils/file';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { SetFiles, SelectedState } from 'types/gallery';
|
||||
import { SelectedState } from 'types/gallery';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
|
@ -30,6 +30,8 @@ import { logError } from 'utils/sentry';
|
|||
import { CustomError } from 'utils/error';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { useMemo } from 'react';
|
||||
import { Collection } from 'types/collection';
|
||||
|
||||
const Container = styled('div')`
|
||||
display: block;
|
||||
|
@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
|
|||
|
||||
interface Props {
|
||||
files: EnteFile[];
|
||||
setFiles: SetFiles;
|
||||
collections?: Collection[];
|
||||
syncWithRemote: () => Promise<void>;
|
||||
favItemIds?: Set<number>;
|
||||
archivedCollections?: Set<number>;
|
||||
|
@ -60,7 +62,8 @@ interface Props {
|
|||
openUploader?;
|
||||
isInSearchMode?: boolean;
|
||||
search?: Search;
|
||||
deleted?: number[];
|
||||
deletedFileIds?: Set<number>;
|
||||
setDeletedFileIds?: (value: Set<number>) => void;
|
||||
activeCollection: number;
|
||||
isSharedCollection?: boolean;
|
||||
enableDownload?: boolean;
|
||||
|
@ -69,13 +72,15 @@ interface Props {
|
|||
}
|
||||
|
||||
type SourceURL = {
|
||||
imageURL?: string;
|
||||
videoURL?: string;
|
||||
originalImageURL?: string;
|
||||
originalVideoURL?: string;
|
||||
convertedImageURL?: string;
|
||||
convertedVideoURL?: string;
|
||||
};
|
||||
|
||||
const PhotoFrame = ({
|
||||
files,
|
||||
setFiles,
|
||||
collections,
|
||||
syncWithRemote,
|
||||
favItemIds,
|
||||
archivedCollections,
|
||||
|
@ -86,7 +91,8 @@ const PhotoFrame = ({
|
|||
isInSearchMode,
|
||||
search,
|
||||
resetSearch,
|
||||
deleted,
|
||||
deletedFileIds,
|
||||
setDeletedFileIds,
|
||||
activeCollection,
|
||||
isSharedCollection,
|
||||
enableDownload,
|
||||
|
@ -104,75 +110,26 @@ const PhotoFrame = ({
|
|||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
const filteredDataRef = useRef<EnteFile[]>([]);
|
||||
const filteredData = filteredDataRef?.current ?? [];
|
||||
const router = useRouter();
|
||||
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(true);
|
||||
}
|
||||
};
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
router.events.on('hashChangeComplete', (url: string) => {
|
||||
const start = url.indexOf('#');
|
||||
const hash = url.slice(start !== -1 ? start : url.length);
|
||||
const shouldPhotoSwipeBeOpened = hash.endsWith(
|
||||
PHOTOSWIPE_HASH_SUFFIX
|
||||
);
|
||||
if (shouldPhotoSwipeBeOpened) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNaN(search?.file)) {
|
||||
const filteredDataIdx = filteredData.findIndex((file) => {
|
||||
return file.id === search.file;
|
||||
});
|
||||
if (!isNaN(filteredDataIdx)) {
|
||||
onThumbnailClick(filteredDataIdx)();
|
||||
}
|
||||
resetSearch();
|
||||
}
|
||||
}, [search, filteredData]);
|
||||
|
||||
const resetFetching = () => {
|
||||
setFetching({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
setRangeStart(null);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
const filteredData = useMemo(() => {
|
||||
const idSet = new Set();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
filteredDataRef.current = files
|
||||
|
||||
return files
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (deleted?.includes(item.id)) {
|
||||
if (
|
||||
deletedFileIds?.has(item.id) &&
|
||||
activeCollection !== TRASH_SECTION
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
|
@ -236,7 +193,8 @@ const PhotoFrame = ({
|
|||
activeCollection === ALL_SECTION ||
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === TRASH_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
activeCollection === item.collectionID ||
|
||||
isInSearchMode
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
|
@ -245,7 +203,37 @@ const PhotoFrame = ({
|
|||
}
|
||||
return false;
|
||||
});
|
||||
}, [files, deleted, search, activeCollection]);
|
||||
}, [
|
||||
files,
|
||||
deletedFileIds,
|
||||
search?.date,
|
||||
search?.location,
|
||||
activeCollection,
|
||||
]);
|
||||
|
||||
const fileToCollectionsMap = useMemo(() => {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
files.forEach((file) => {
|
||||
if (!fileToCollectionsMap.get(file.id)) {
|
||||
fileToCollectionsMap.set(file.id, []);
|
||||
}
|
||||
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||
});
|
||||
return fileToCollectionsMap;
|
||||
}, [files]);
|
||||
|
||||
const collectionNameMap = useMemo(() => {
|
||||
if (collections) {
|
||||
return new Map<number, string>(
|
||||
collections.map((collection) => [
|
||||
collection.id,
|
||||
collection.name,
|
||||
])
|
||||
);
|
||||
} else {
|
||||
return new Map();
|
||||
}
|
||||
}, [collections]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentURL = new URL(window.location.href);
|
||||
|
@ -262,6 +250,59 @@ const PhotoFrame = ({
|
|||
}
|
||||
}, [open]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(true);
|
||||
}
|
||||
};
|
||||
const handleKeyUp = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
setIsShiftKeyPressed(false);
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
router.events.on('hashChangeComplete', (url: string) => {
|
||||
const start = url.indexOf('#');
|
||||
const hash = url.slice(start !== -1 ? start : url.length);
|
||||
const shouldPhotoSwipeBeOpened = hash.endsWith(
|
||||
PHOTOSWIPE_HASH_SUFFIX
|
||||
);
|
||||
if (shouldPhotoSwipeBeOpened) {
|
||||
setOpen(true);
|
||||
} else {
|
||||
setOpen(false);
|
||||
}
|
||||
});
|
||||
return () => {
|
||||
document.addEventListener('keydown', handleKeyDown, false);
|
||||
document.addEventListener('keyup', handleKeyUp, false);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isNaN(search?.file)) {
|
||||
const filteredDataIdx = filteredData.findIndex((file) => {
|
||||
return file.id === search.file;
|
||||
});
|
||||
if (!isNaN(filteredDataIdx)) {
|
||||
onThumbnailClick(filteredDataIdx)();
|
||||
}
|
||||
resetSearch();
|
||||
}
|
||||
}, [search, filteredData]);
|
||||
|
||||
const resetFetching = () => {
|
||||
setFetching({});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
setRangeStart(null);
|
||||
}
|
||||
}, [selected]);
|
||||
|
||||
const getFileIndexFromID = (files: EnteFile[], id: number) => {
|
||||
const index = files.findIndex((file) => file.id === id);
|
||||
if (index === -1) {
|
||||
|
@ -272,12 +313,10 @@ const PhotoFrame = ({
|
|||
|
||||
const updateURL = (id: number) => (url: string) => {
|
||||
const updateFile = (file: EnteFile) => {
|
||||
file = {
|
||||
...file,
|
||||
msrc: url,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
file.msrc = url;
|
||||
file.w = window.innerWidth;
|
||||
file.h = window.innerHeight;
|
||||
|
||||
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
|
||||
file.html = `
|
||||
<div class="pswp-item-container">
|
||||
|
@ -307,29 +346,30 @@ const PhotoFrame = ({
|
|||
}
|
||||
return file;
|
||||
};
|
||||
setFiles((files) => {
|
||||
const index = getFileIndexFromID(files, id);
|
||||
files[index] = updateFile(files[index]);
|
||||
return files;
|
||||
});
|
||||
const index = getFileIndexFromID(files, id);
|
||||
return updateFile(files[index]);
|
||||
};
|
||||
|
||||
const updateSrcURL = async (id: number, srcURL: SourceURL) => {
|
||||
const { videoURL, imageURL } = srcURL;
|
||||
const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
|
||||
const {
|
||||
originalImageURL,
|
||||
convertedImageURL,
|
||||
originalVideoURL,
|
||||
convertedVideoURL,
|
||||
} = srcURL;
|
||||
const isPlayable =
|
||||
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
|
||||
const updateFile = (file: EnteFile) => {
|
||||
file = {
|
||||
...file,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
};
|
||||
file.w = window.innerWidth;
|
||||
file.h = window.innerHeight;
|
||||
file.isSourceLoaded = true;
|
||||
file.originalImageURL = originalImageURL;
|
||||
file.originalVideoURL = originalVideoURL;
|
||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
if (isPlayable) {
|
||||
file.html = `
|
||||
<video controls onContextMenu="return false;">
|
||||
<source src="${videoURL}" />
|
||||
<source src="${convertedVideoURL}" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
`;
|
||||
|
@ -339,7 +379,7 @@ const PhotoFrame = ({
|
|||
<img src="${file.msrc}" onContextMenu="return false;"/>
|
||||
<div class="download-banner" >
|
||||
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
||||
<a class="btn btn-outline-success" href=${videoURL} download="${file.metadata.title}"">Download</a>
|
||||
<a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
@ -348,9 +388,9 @@ const PhotoFrame = ({
|
|||
if (isPlayable) {
|
||||
file.html = `
|
||||
<div class = 'pswp-item-container'>
|
||||
<img id = "live-photo-image-${file.id}" src="${imageURL}" onContextMenu="return false;"/>
|
||||
<img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
|
||||
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
|
||||
<source src="${videoURL}" />
|
||||
<source src="${convertedVideoURL}" />
|
||||
Your browser does not support the video tag.
|
||||
</video>
|
||||
</div>
|
||||
|
@ -367,15 +407,10 @@ const PhotoFrame = ({
|
|||
`;
|
||||
}
|
||||
} else {
|
||||
file.src = imageURL;
|
||||
file.src = convertedImageURL;
|
||||
}
|
||||
return file;
|
||||
};
|
||||
setFiles((files) => {
|
||||
const index = getFileIndexFromID(files, id);
|
||||
files[index] = updateFile(files[index]);
|
||||
return files;
|
||||
});
|
||||
setIsSourceLoaded(true);
|
||||
const index = getFileIndexFromID(files, id);
|
||||
return updateFile(files[index]);
|
||||
|
@ -441,7 +476,11 @@ const PhotoFrame = ({
|
|||
handleSelect(filteredData[index].id, index)(!checked);
|
||||
}
|
||||
};
|
||||
const getThumbnail = (files: EnteFile[], index: number) =>
|
||||
const getThumbnail = (
|
||||
files: EnteFile[],
|
||||
index: number,
|
||||
isScrolling: boolean
|
||||
) =>
|
||||
files[index] ? (
|
||||
<PreviewCard
|
||||
key={`tile-${files[index].id}-selected-${
|
||||
|
@ -465,6 +504,7 @@ const PhotoFrame = ({
|
|||
(index >= currentHover && index <= rangeStart)
|
||||
}
|
||||
activeCollection={activeCollection}
|
||||
showPlaceholder={isScrolling}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
|
@ -499,6 +539,9 @@ const PhotoFrame = ({
|
|||
item.msrc = newFile.msrc;
|
||||
item.html = newFile.html;
|
||||
item.src = newFile.src;
|
||||
item.isSourceLoaded = newFile.isSourceLoaded;
|
||||
item.originalImageURL = newFile.originalImageURL;
|
||||
item.originalVideoURL = newFile.originalVideoURL;
|
||||
item.w = newFile.w;
|
||||
item.h = newFile.h;
|
||||
|
||||
|
@ -521,10 +564,13 @@ const PhotoFrame = ({
|
|||
if (!fetching[item.id]) {
|
||||
try {
|
||||
fetching[item.id] = true;
|
||||
let urls: string[];
|
||||
let urls: { original: string[]; converted: string[] };
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
const mergedURL = galleryContext.files.get(item.id);
|
||||
urls = mergedURL.split(',');
|
||||
urls = {
|
||||
original: mergedURL.original.split(','),
|
||||
converted: mergedURL.converted.split(','),
|
||||
};
|
||||
} else {
|
||||
appContext.startLoading();
|
||||
if (
|
||||
|
@ -540,26 +586,40 @@ const PhotoFrame = ({
|
|||
urls = await DownloadManager.getFile(item, true);
|
||||
}
|
||||
appContext.finishLoading();
|
||||
const mergedURL = urls.join(',');
|
||||
const mergedURL = {
|
||||
original: urls.original.join(','),
|
||||
converted: urls.converted.join(','),
|
||||
};
|
||||
galleryContext.files.set(item.id, mergedURL);
|
||||
}
|
||||
let imageURL;
|
||||
let videoURL;
|
||||
let originalImageURL;
|
||||
let originalVideoURL;
|
||||
let convertedImageURL;
|
||||
let convertedVideoURL;
|
||||
|
||||
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||
[imageURL, videoURL] = urls;
|
||||
[originalImageURL, originalVideoURL] = urls.original;
|
||||
[convertedImageURL, convertedVideoURL] = urls.converted;
|
||||
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||
[videoURL] = urls;
|
||||
[originalVideoURL] = urls.original;
|
||||
[convertedVideoURL] = urls.converted;
|
||||
} else {
|
||||
[imageURL] = urls;
|
||||
[originalImageURL] = urls.original;
|
||||
[convertedImageURL] = urls.converted;
|
||||
}
|
||||
setIsSourceLoaded(false);
|
||||
const newFile = await updateSrcURL(item.id, {
|
||||
imageURL,
|
||||
videoURL,
|
||||
originalImageURL,
|
||||
originalVideoURL,
|
||||
convertedImageURL,
|
||||
convertedVideoURL,
|
||||
});
|
||||
item.msrc = newFile.msrc;
|
||||
item.html = newFile.html;
|
||||
item.src = newFile.src;
|
||||
item.isSourceLoaded = newFile.isSourceLoaded;
|
||||
item.originalImageURL = newFile.originalImageURL;
|
||||
item.originalVideoURL = newFile.originalVideoURL;
|
||||
item.w = newFile.w;
|
||||
item.h = newFile.h;
|
||||
try {
|
||||
|
@ -606,17 +666,21 @@ const PhotoFrame = ({
|
|||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<PhotoSwipe
|
||||
<PhotoViewer
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
currentIndex={currentIndex}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
favItemIds={favItemIds}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
isSharedCollection={isSharedCollection}
|
||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||
enableDownload={enableDownload}
|
||||
isSourceLoaded={isSourceLoaded}
|
||||
fileToCollectionsMap={fileToCollectionsMap}
|
||||
collectionNameMap={collectionNameMap}
|
||||
/>
|
||||
</Container>
|
||||
)}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useRef, useEffect, useContext } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import { Box, styled } from '@mui/material';
|
||||
import { Box, Link, styled } from '@mui/material';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
|
@ -15,17 +15,17 @@ import {
|
|||
import constants from 'utils/strings/constants';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
||||
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
|
||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { FlexWrapper } from './Container';
|
||||
import { Typography } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { SpecialPadding } from 'styles/SpecialPadding';
|
||||
import { formatDate } from 'utils/time/format';
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
const NO_OF_PAGES = 2;
|
||||
const FOOTER_HEIGHT = 90;
|
||||
const ALBUM_FOOTER_HEIGHT = 75;
|
||||
|
||||
export enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
|
@ -129,7 +129,6 @@ const SizeAndCountContainer = styled(DateContainer)`
|
|||
`;
|
||||
|
||||
const FooterContainer = styled(ListItemContainer)`
|
||||
font-size: 14px;
|
||||
margin-bottom: 0.75rem;
|
||||
@media (max-width: 540px) {
|
||||
font-size: 12px;
|
||||
|
@ -142,6 +141,13 @@ const FooterContainer = styled(ListItemContainer)`
|
|||
margin-top: calc(2rem + 20px);
|
||||
`;
|
||||
|
||||
const AlbumFooterContainer = styled(ListItemContainer)`
|
||||
margin-top: 48px;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const NothingContainer = styled(ListItemContainer)`
|
||||
color: #979797;
|
||||
text-align: center;
|
||||
|
@ -153,7 +159,11 @@ interface Props {
|
|||
width: number;
|
||||
filteredData: EnteFile[];
|
||||
showAppDownloadBanner: boolean;
|
||||
getThumbnail: (files: EnteFile[], index: number) => JSX.Element;
|
||||
getThumbnail: (
|
||||
files: EnteFile[],
|
||||
index: number,
|
||||
isScrolling?: boolean
|
||||
) => JSX.Element;
|
||||
activeCollection: number;
|
||||
resetFetching: () => void;
|
||||
}
|
||||
|
@ -223,11 +233,18 @@ export function PhotoList({
|
|||
if (timeStampList.length === 1) {
|
||||
timeStampList.push(getEmptyListItem());
|
||||
}
|
||||
timeStampList.push(getVacuumItem(timeStampList));
|
||||
if (publicCollectionGalleryContext.photoListFooter) {
|
||||
timeStampList.push(
|
||||
getPhotoListFooter(
|
||||
publicCollectionGalleryContext.photoListFooter
|
||||
)
|
||||
);
|
||||
}
|
||||
if (
|
||||
showAppDownloadBanner ||
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
) {
|
||||
timeStampList.push(getVacuumItem(timeStampList));
|
||||
if (publicCollectionGalleryContext.accessedThroughSharedURL) {
|
||||
timeStampList.push(getAlbumsFooter());
|
||||
} else {
|
||||
|
@ -244,6 +261,11 @@ export function PhotoList({
|
|||
filteredData,
|
||||
showAppDownloadBanner,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
galleryContext.photoListHeader,
|
||||
publicCollectionGalleryContext.photoListFooter,
|
||||
publicCollectionGalleryContext.photoListHeader,
|
||||
deduplicateContext.isOnDeduplicatePage,
|
||||
deduplicateContext.fileSizeMap,
|
||||
]);
|
||||
|
||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||
|
@ -288,32 +310,27 @@ export function PhotoList({
|
|||
|
||||
const groupByTime = (timeStampList: TimeStampListItem[]) => {
|
||||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
|
||||
let currentDate;
|
||||
filteredData.forEach((item, index) => {
|
||||
if (
|
||||
!currentDate ||
|
||||
!isSameDay(
|
||||
new Date(item.metadata.creationTime / 1000),
|
||||
new Date(currentDate)
|
||||
)
|
||||
) {
|
||||
currentDate = item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: isSameDay(new Date(currentDate), new Date())
|
||||
? 'Today'
|
||||
? constants.TODAY
|
||||
: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(Date.now() - A_DAY)
|
||||
)
|
||||
? 'Yesterday'
|
||||
: dateTimeFormat.format(currentDate),
|
||||
? constants.YESTERDAY
|
||||
: formatDate(currentDate),
|
||||
id: currentDate.toString(),
|
||||
});
|
||||
timeStampList.push({
|
||||
|
@ -336,10 +353,13 @@ export function PhotoList({
|
|||
});
|
||||
};
|
||||
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate();
|
||||
const isSameDay = (first, second) => {
|
||||
return (
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
const getPhotoListHeader = (photoListHeader) => {
|
||||
return {
|
||||
|
@ -352,6 +372,17 @@ export function PhotoList({
|
|||
};
|
||||
};
|
||||
|
||||
const getPhotoListFooter = (photoListFooter) => {
|
||||
return {
|
||||
...photoListFooter,
|
||||
item: (
|
||||
<ListItemContainer span={columns}>
|
||||
{photoListFooter.item}
|
||||
</ListItemContainer>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const getEmptyListItem = () => {
|
||||
return {
|
||||
itemType: ITEM_TYPE.OTHER,
|
||||
|
@ -365,12 +396,17 @@ export function PhotoList({
|
|||
};
|
||||
};
|
||||
const getVacuumItem = (timeStampList) => {
|
||||
const footerHeight =
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
? ALBUM_FOOTER_HEIGHT +
|
||||
(publicCollectionGalleryContext.photoListFooter?.height ?? 0)
|
||||
: FOOTER_HEIGHT;
|
||||
const photoFrameHeight = (() => {
|
||||
let sum = 0;
|
||||
const getCurrentItemSize = getItemSize(timeStampList);
|
||||
for (let i = 0; i < timeStampList.length; i++) {
|
||||
sum += getCurrentItemSize(i);
|
||||
if (height - sum <= FOOTER_HEIGHT) {
|
||||
if (height - sum <= footerHeight) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -379,7 +415,7 @@ export function PhotoList({
|
|||
return {
|
||||
itemType: ITEM_TYPE.OTHER,
|
||||
item: <></>,
|
||||
height: Math.max(height - photoFrameHeight - FOOTER_HEIGHT, 0),
|
||||
height: Math.max(height - photoFrameHeight - footerHeight, 0),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -389,7 +425,9 @@ export function PhotoList({
|
|||
height: FOOTER_HEIGHT,
|
||||
item: (
|
||||
<FooterContainer span={columns}>
|
||||
<Typography>{constants.INSTALL_MOBILE_APP()}</Typography>
|
||||
<Typography variant="body2">
|
||||
{constants.INSTALL_MOBILE_APP()}
|
||||
</Typography>
|
||||
</FooterContainer>
|
||||
),
|
||||
};
|
||||
|
@ -398,22 +436,16 @@ export function PhotoList({
|
|||
const getAlbumsFooter = () => {
|
||||
return {
|
||||
itemType: ITEM_TYPE.OTHER,
|
||||
height: FOOTER_HEIGHT,
|
||||
height: ALBUM_FOOTER_HEIGHT,
|
||||
item: (
|
||||
<FooterContainer span={columns}>
|
||||
<p>
|
||||
{constants.PRESERVED_BY}{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
style={{
|
||||
color: getVariantColor(ButtonVariant.success),
|
||||
}}
|
||||
href={ENTE_WEBSITE_LINK}
|
||||
rel="noreferrer">
|
||||
<AlbumFooterContainer span={columns}>
|
||||
<Typography variant="body2">
|
||||
{constants.SHARED_USING}{' '}
|
||||
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
|
||||
{constants.ENTE_IO}
|
||||
</a>
|
||||
</p>
|
||||
</FooterContainer>
|
||||
</Link>
|
||||
</Typography>
|
||||
</AlbumFooterContainer>
|
||||
),
|
||||
};
|
||||
};
|
||||
|
@ -453,9 +485,10 @@ export function PhotoList({
|
|||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
});
|
||||
newList[newIndex + 1].items = newList[
|
||||
newIndex + 1
|
||||
].items.concat(items[index + 1].items);
|
||||
newList[newIndex + 1].items = [
|
||||
...newList[newIndex + 1].items,
|
||||
...items[index + 1].items,
|
||||
];
|
||||
index += 2;
|
||||
} else {
|
||||
// Adding items would exceed the number of columns.
|
||||
|
@ -512,10 +545,6 @@ export function PhotoList({
|
|||
}
|
||||
};
|
||||
|
||||
const extraRowsToRender = Math.ceil(
|
||||
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
|
||||
);
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.FILE:
|
||||
|
@ -527,7 +556,10 @@ export function PhotoList({
|
|||
}
|
||||
};
|
||||
|
||||
const renderListItem = (listItem: TimeStampListItem) => {
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem,
|
||||
isScrolling: boolean
|
||||
) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return listItem.dates ? (
|
||||
|
@ -553,7 +585,8 @@ export function PhotoList({
|
|||
const ret = listItem.items.map((item, idx) =>
|
||||
getThumbnail(
|
||||
filteredDataCopy,
|
||||
listItem.itemStartIndex + idx
|
||||
listItem.itemStartIndex + idx,
|
||||
isScrolling
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
|
@ -584,14 +617,15 @@ export function PhotoList({
|
|||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={extraRowsToRender}>
|
||||
{({ index, style }) => (
|
||||
overscanCount={0}
|
||||
useIsScrolling>
|
||||
{({ index, style, isScrolling }) => (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
shrinkRatio={shrinkRatio}
|
||||
groups={timeStampList[index].groups}>
|
||||
{renderListItem(timeStampList[index])}
|
||||
{renderListItem(timeStampList[index], isScrolling)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
)}
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
import { RenderInfoItem } from './RenderInfoItem';
|
||||
import { LegendContainer } from '../styledComponents/LegendContainer';
|
||||
import { Pre } from '../styledComponents/Pre';
|
||||
import {
|
||||
Checkbox,
|
||||
FormControlLabel,
|
||||
FormGroup,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export function ExifData(props: { exif: any }) {
|
||||
const { exif } = props;
|
||||
const [showAll, setShowAll] = useState(false);
|
||||
|
||||
const changeHandler = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShowAll(e.target.checked);
|
||||
};
|
||||
|
||||
const renderAllValues = () => <Pre>{exif.raw}</Pre>;
|
||||
|
||||
const renderSelectedValues = () => (
|
||||
<>
|
||||
{exif?.Make &&
|
||||
exif?.Model &&
|
||||
RenderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
|
||||
{exif?.ImageWidth &&
|
||||
exif?.ImageHeight &&
|
||||
RenderInfoItem(
|
||||
constants.IMAGE_SIZE,
|
||||
`${exif.ImageWidth} x ${exif.ImageHeight}`
|
||||
)}
|
||||
{exif?.Flash && RenderInfoItem(constants.FLASH, exif.Flash)}
|
||||
{exif?.FocalLength &&
|
||||
RenderInfoItem(
|
||||
constants.FOCAL_LENGTH,
|
||||
exif.FocalLength.toString()
|
||||
)}
|
||||
{exif?.ApertureValue &&
|
||||
RenderInfoItem(
|
||||
constants.APERTURE,
|
||||
exif.ApertureValue.toString()
|
||||
)}
|
||||
{exif?.ISOSpeedRatings &&
|
||||
RenderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<LegendContainer>
|
||||
<Typography variant="subtitle" mb={1}>
|
||||
{constants.EXIF}
|
||||
</Typography>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
size="small"
|
||||
onChange={changeHandler}
|
||||
color="accent"
|
||||
/>
|
||||
}
|
||||
label={constants.SHOW_ALL}
|
||||
/>
|
||||
</FormGroup>
|
||||
</LegendContainer>
|
||||
{showAll ? renderAllValues() : renderSelectedValues()}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Col, Form, FormControl } from 'react-bootstrap';
|
||||
import { FlexWrapper, Value } from 'components/Container';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { MAX_EDITED_FILE_NAME_LENGTH } from 'constants/file';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export interface formValues {
|
||||
filename: string;
|
||||
}
|
||||
|
||||
export const FileNameEditForm = ({
|
||||
filename,
|
||||
saveEdits,
|
||||
discardEdits,
|
||||
extension,
|
||||
}) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.filename);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Formik<formValues>
|
||||
initialValues={{ filename }}
|
||||
validationSchema={Yup.object().shape({
|
||||
filename: Yup.string()
|
||||
.required(constants.REQUIRED)
|
||||
.max(
|
||||
MAX_EDITED_FILE_NAME_LENGTH,
|
||||
constants.FILE_NAME_CHARACTER_LIMIT
|
||||
),
|
||||
})}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({ values, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Row>
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={extension ? 8 : 9}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder={constants.FILE_NAME}
|
||||
value={values.filename}
|
||||
onChange={handleChange('filename')}
|
||||
isInvalid={Boolean(errors.filename)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback
|
||||
type="invalid"
|
||||
style={{ textAlign: 'center' }}>
|
||||
{errors.filename}
|
||||
</FormControl.Feedback>
|
||||
</Form.Group>
|
||||
{extension && (
|
||||
<Form.Group
|
||||
bsPrefix="ente-form-group"
|
||||
as={Col}
|
||||
xs={1}
|
||||
controlId="formHorizontalFileName">
|
||||
<FlexWrapper style={{ padding: '5px' }}>
|
||||
{`.${extension}`}
|
||||
</FlexWrapper>
|
||||
</Form.Group>
|
||||
)}
|
||||
<Form.Group bsPrefix="ente-form-group" as={Col} xs={3}>
|
||||
<Value width={'16.67%'}>
|
||||
<IconButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<TickIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={discardEdits}
|
||||
disabled={loading}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
</Form.Group>
|
||||
</Form.Row>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
);
|
||||
};
|
|
@ -1,98 +0,0 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
changeFileName,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { FreeFlowText, Label, Row, Value } from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FileNameEditForm } from './FileNameEditForm';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
export function RenderFileName({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const originalTitle = file?.metadata.title;
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const [originalFileName, extension] =
|
||||
splitFilenameAndExtension(originalTitle);
|
||||
const [filename, setFilename] = useState(originalFileName);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async (newFilename: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (filename === newFilename) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
setFilename(newFilename);
|
||||
const newTitle = getFileTitle(newFilename, extension);
|
||||
let updatedFile = await changeFileName(file, newTitle);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update file name');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.FILE_NAME}</Label>
|
||||
{!isInEditMode ? (
|
||||
<>
|
||||
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
|
||||
<FreeFlowText>
|
||||
{getFileTitle(filename, extension)}
|
||||
</FreeFlowText>
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width="10%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px',
|
||||
}}>
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<FileNameEditForm
|
||||
extension={extension}
|
||||
filename={filename}
|
||||
saveEdits={saveEdits}
|
||||
discardEdits={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,175 +0,0 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { formatDateTime } from 'utils/time';
|
||||
import { RenderFileName } from './RenderFileName';
|
||||
import { ExifData } from './ExifData';
|
||||
import { RenderCreationTime } from './RenderCreationTime';
|
||||
import { RenderInfoItem } from './RenderInfoItem';
|
||||
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
|
||||
import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { Location, Metadata } from 'types/upload';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import { getEXIFLocation } from 'services/upload/exifService';
|
||||
import {
|
||||
PhotoPeopleList,
|
||||
UnidentifiedFaces,
|
||||
} from 'components/MachineLearning/PeopleList';
|
||||
|
||||
import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
|
||||
import { WordList } from 'components/MachineLearning/WordList';
|
||||
import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
|
||||
|
||||
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
|
||||
zIndex: 1501,
|
||||
'& .MuiDialog-container': {
|
||||
alignItems: 'flex-start',
|
||||
},
|
||||
'& .MuiDialog-paper': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const Legend = styled('span')`
|
||||
font-size: 20px;
|
||||
color: #ddd;
|
||||
display: inline;
|
||||
`;
|
||||
|
||||
interface Iprops {
|
||||
shouldDisableEdits: boolean;
|
||||
showInfo: boolean;
|
||||
handleCloseInfo: () => void;
|
||||
items: any[];
|
||||
photoSwipe: Photoswipe<Photoswipe.Options>;
|
||||
metadata: Metadata;
|
||||
exif: any;
|
||||
scheduleUpdate: () => void;
|
||||
}
|
||||
|
||||
export function FileInfo({
|
||||
shouldDisableEdits,
|
||||
showInfo,
|
||||
handleCloseInfo,
|
||||
items,
|
||||
photoSwipe,
|
||||
metadata,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const [location, setLocation] = useState<Location>(null);
|
||||
const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!location && metadata) {
|
||||
if (metadata.longitude || metadata.longitude === 0) {
|
||||
setLocation({
|
||||
latitude: metadata.latitude,
|
||||
longitude: metadata.longitude,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [metadata]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!location && exif) {
|
||||
const exifLocation = getEXIFLocation(exif);
|
||||
if (exifLocation.latitude || exifLocation.latitude === 0) {
|
||||
setLocation(exifLocation);
|
||||
}
|
||||
}
|
||||
}, [exif]);
|
||||
|
||||
return (
|
||||
<FileInfoDialog
|
||||
open={showInfo}
|
||||
onClose={handleCloseInfo}
|
||||
fullScreen={appContext.isMobile}>
|
||||
<DialogTitleWithCloseButton onClose={handleCloseInfo}>
|
||||
{constants.INFO}
|
||||
</DialogTitleWithCloseButton>
|
||||
<DialogContent>
|
||||
<Typography variant="subtitle" mb={1}>
|
||||
{constants.METADATA}
|
||||
</Typography>
|
||||
|
||||
{RenderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
{metadata?.title && (
|
||||
<RenderFileName
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.creationTime && (
|
||||
<RenderCreationTime
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
)}
|
||||
{metadata?.modificationTime &&
|
||||
RenderInfoItem(
|
||||
constants.UPDATED_ON,
|
||||
formatDateTime(metadata.modificationTime / 1000)
|
||||
)}
|
||||
{location &&
|
||||
RenderInfoItem(
|
||||
constants.LOCATION,
|
||||
<Link
|
||||
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{constants.SHOW_MAP}
|
||||
</Link>
|
||||
)}
|
||||
{appContext.mlSearchEnabled && (
|
||||
<>
|
||||
<div>
|
||||
<Legend>{constants.PEOPLE}</Legend>
|
||||
</div>
|
||||
<PhotoPeopleList
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
<div>
|
||||
<Legend>{constants.UNIDENTIFIED_FACES}</Legend>
|
||||
</div>
|
||||
<UnidentifiedFaces
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
<div>
|
||||
<Legend>{constants.OBJECTS}</Legend>
|
||||
<ObjectLabelList
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Legend>{constants.TEXT}</Legend>
|
||||
<WordList
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
</div>
|
||||
<MLServiceFileInfoButton
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
setUpdateMLDataIndex={setUpdateMLDataIndex}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{exif && (
|
||||
<>
|
||||
<ExifData exif={exif} />
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
</FileInfoDialog>
|
||||
);
|
||||
}
|
175
src/components/PhotoSwipe/infoDialog.tsx
Normal file
175
src/components/PhotoSwipe/infoDialog.tsx
Normal file
|
@ -0,0 +1,175 @@
|
|||
export {}; // import React, { useContext, useEffect, useState } from 'react';
|
||||
// import constants from 'utils/strings/constants';
|
||||
// import { formatDateTime } from 'utils/time';
|
||||
// import { RenderFileName } from './RenderFileName';
|
||||
// import { ExifData } from './ExifData';
|
||||
// import { RenderCreationTime } from './RenderCreationTime';
|
||||
// import { RenderInfoItem } from './RenderInfoItem';
|
||||
// import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
|
||||
// import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
|
||||
// import { AppContext } from 'pages/_app';
|
||||
// import { Location, Metadata } from 'types/upload';
|
||||
// import Photoswipe from 'photoswipe';
|
||||
// import { getEXIFLocation } from 'services/upload/exifService';
|
||||
// import {
|
||||
// PhotoPeopleList,
|
||||
// UnidentifiedFaces,
|
||||
// } from 'components/MachineLearning/PeopleList';
|
||||
|
||||
// import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
|
||||
// import { WordList } from 'components/MachineLearning/WordList';
|
||||
// import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
|
||||
|
||||
// const FileInfoDialog = styled(Dialog)(({ theme }) => ({
|
||||
// zIndex: 1501,
|
||||
// '& .MuiDialog-container': {
|
||||
// alignItems: 'flex-start',
|
||||
// },
|
||||
// '& .MuiDialog-paper': {
|
||||
// padding: theme.spacing(2),
|
||||
// },
|
||||
// }));
|
||||
|
||||
// const Legend = styled('span')`
|
||||
// font-size: 20px;
|
||||
// color: #ddd;
|
||||
// display: inline;
|
||||
// `;
|
||||
|
||||
// interface Iprops {
|
||||
// shouldDisableEdits: boolean;
|
||||
// showInfo: boolean;
|
||||
// handleCloseInfo: () => void;
|
||||
// items: any[];
|
||||
// photoSwipe: Photoswipe<Photoswipe.Options>;
|
||||
// metadata: Metadata;
|
||||
// exif: any;
|
||||
// scheduleUpdate: () => void;
|
||||
// }
|
||||
|
||||
// export function FileInfo({
|
||||
// shouldDisableEdits,
|
||||
// showInfo,
|
||||
// handleCloseInfo,
|
||||
// items,
|
||||
// photoSwipe,
|
||||
// metadata,
|
||||
// exif,
|
||||
// scheduleUpdate,
|
||||
// }: Iprops) {
|
||||
// const appContext = useContext(AppContext);
|
||||
// const [location, setLocation] = useState<Location>(null);
|
||||
// const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!location && metadata) {
|
||||
// if (metadata.longitude || metadata.longitude === 0) {
|
||||
// setLocation({
|
||||
// latitude: metadata.latitude,
|
||||
// longitude: metadata.longitude,
|
||||
// });
|
||||
// }
|
||||
// }
|
||||
// }, [metadata]);
|
||||
|
||||
// useEffect(() => {
|
||||
// if (!location && exif) {
|
||||
// const exifLocation = getEXIFLocation(exif);
|
||||
// if (exifLocation.latitude || exifLocation.latitude === 0) {
|
||||
// setLocation(exifLocation);
|
||||
// }
|
||||
// }
|
||||
// }, [exif]);
|
||||
|
||||
// return (
|
||||
// <FileInfoDialog
|
||||
// open={showInfo}
|
||||
// onClose={handleCloseInfo}
|
||||
// fullScreen={appContext.isMobile}>
|
||||
// <DialogTitleWithCloseButton onClose={handleCloseInfo}>
|
||||
// {constants.INFO}
|
||||
// </DialogTitleWithCloseButton>
|
||||
// <DialogContent>
|
||||
// <Typography variant="subtitle" mb={1}>
|
||||
// {constants.METADATA}
|
||||
// </Typography>
|
||||
|
||||
// {RenderInfoItem(
|
||||
// constants.FILE_ID,
|
||||
// items[photoSwipe?.getCurrentIndex()]?.id
|
||||
// )}
|
||||
// {metadata?.title && (
|
||||
// <RenderFileName
|
||||
// shouldDisableEdits={shouldDisableEdits}
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// scheduleUpdate={scheduleUpdate}
|
||||
// />
|
||||
// )}
|
||||
// {metadata?.creationTime && (
|
||||
// <RenderCreationTime
|
||||
// shouldDisableEdits={shouldDisableEdits}
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// scheduleUpdate={scheduleUpdate}
|
||||
// />
|
||||
// )}
|
||||
// {metadata?.modificationTime &&
|
||||
// RenderInfoItem(
|
||||
// constants.UPDATED_ON,
|
||||
// formatDateTime(metadata.modificationTime / 1000)
|
||||
// )}
|
||||
// {location &&
|
||||
// RenderInfoItem(
|
||||
// constants.LOCATION,
|
||||
// <Link
|
||||
// href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
||||
// target="_blank"
|
||||
// rel="noopener noreferrer">
|
||||
// {constants.SHOW_MAP}
|
||||
// </Link>
|
||||
// )}
|
||||
// {appContext.mlSearchEnabled && (
|
||||
// <>
|
||||
// <div>
|
||||
// <Legend>{constants.PEOPLE}</Legend>
|
||||
// </div>
|
||||
// <PhotoPeopleList
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// updateMLDataIndex={updateMLDataIndex}
|
||||
// />
|
||||
// <div>
|
||||
// <Legend>{constants.UNIDENTIFIED_FACES}</Legend>
|
||||
// </div>
|
||||
// <UnidentifiedFaces
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// updateMLDataIndex={updateMLDataIndex}
|
||||
// />
|
||||
// <div>
|
||||
// <Legend>{constants.OBJECTS}</Legend>
|
||||
// <ObjectLabelList
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// updateMLDataIndex={updateMLDataIndex}
|
||||
// />
|
||||
// </div>
|
||||
// <div>
|
||||
// <Legend>{constants.TEXT}</Legend>
|
||||
// <WordList
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// updateMLDataIndex={updateMLDataIndex}
|
||||
// />
|
||||
// </div>
|
||||
// <MLServiceFileInfoButton
|
||||
// file={items[photoSwipe?.getCurrentIndex()]}
|
||||
// updateMLDataIndex={updateMLDataIndex}
|
||||
// setUpdateMLDataIndex={setUpdateMLDataIndex}
|
||||
// />
|
||||
// </>
|
||||
// )}
|
||||
// {exif && (
|
||||
// <>
|
||||
// <ExifData exif={exif} />
|
||||
// </>
|
||||
// )}
|
||||
// </DialogContent>
|
||||
// </FileInfoDialog>
|
||||
// );
|
||||
// }
|
92
src/components/PhotoViewer/FileInfo/ExifData.tsx
Normal file
92
src/components/PhotoViewer/FileInfo/ExifData.tsx
Normal file
|
@ -0,0 +1,92 @@
|
|||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
import { Stack, styled, Typography } from '@mui/material';
|
||||
import { FileInfoSidebar } from '.';
|
||||
import Titlebar from 'components/Titlebar';
|
||||
import { Box } from '@mui/system';
|
||||
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||
import { formatDateFull } from 'utils/time/format';
|
||||
|
||||
const ExifItem = styled(Box)`
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
`;
|
||||
|
||||
function parseExifValue(value: any) {
|
||||
switch (typeof value) {
|
||||
case 'string':
|
||||
case 'number':
|
||||
return value;
|
||||
default:
|
||||
if (value instanceof Date) {
|
||||
return formatDateFull(value);
|
||||
}
|
||||
try {
|
||||
return JSON.stringify(Array.from(value));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
export function ExifData(props: {
|
||||
exif: any;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
filename: string;
|
||||
onInfoClose: () => void;
|
||||
}) {
|
||||
const { exif, open, onClose, filename, onInfoClose } = props;
|
||||
|
||||
if (!exif) {
|
||||
return <></>;
|
||||
}
|
||||
const handleRootClose = () => {
|
||||
onClose();
|
||||
onInfoClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<FileInfoSidebar open={open} onClose={onClose}>
|
||||
<Titlebar
|
||||
onClose={onClose}
|
||||
title={constants.EXIF}
|
||||
caption={filename}
|
||||
onRootClose={handleRootClose}
|
||||
actionButton={
|
||||
<CopyButton
|
||||
code={JSON.stringify(exif)}
|
||||
color={'secondary'}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Stack py={3} px={1} spacing={2}>
|
||||
{[...Object.entries(exif)].map(([key, value]) =>
|
||||
value ? (
|
||||
<ExifItem key={key}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={'text.secondary'}>
|
||||
{key}
|
||||
</Typography>
|
||||
<Typography
|
||||
sx={{
|
||||
width: '100%',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
}}>
|
||||
{parseExifValue(value)}
|
||||
</Typography>
|
||||
</ExifItem>
|
||||
) : (
|
||||
<></>
|
||||
)
|
||||
)}
|
||||
</Stack>
|
||||
</FileInfoSidebar>
|
||||
);
|
||||
}
|
47
src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx
Normal file
47
src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
import React from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { DialogContent, DialogTitle } from '@mui/material';
|
||||
import DialogBoxBase from 'components/DialogBox/base';
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from 'components/SingleInputForm';
|
||||
|
||||
export const FileNameEditDialog = ({
|
||||
isInEditMode,
|
||||
closeEditMode,
|
||||
filename,
|
||||
extension,
|
||||
saveEdits,
|
||||
}) => {
|
||||
const onSubmit: SingleInputFormProps['callback'] = async (
|
||||
filename,
|
||||
setFieldError
|
||||
) => {
|
||||
try {
|
||||
await saveEdits(filename);
|
||||
closeEditMode();
|
||||
} catch (e) {
|
||||
setFieldError(constants.UNKNOWN_ERROR);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DialogBoxBase
|
||||
open={isInEditMode}
|
||||
onClose={closeEditMode}
|
||||
sx={{ zIndex: 1600 }}>
|
||||
<DialogTitle>{constants.RENAME_FILE}</DialogTitle>
|
||||
<DialogContent>
|
||||
<SingleInputForm
|
||||
initialValue={filename}
|
||||
callback={onSubmit}
|
||||
placeholder={constants.ENTER_FILE_NAME}
|
||||
buttonText={constants.RENAME}
|
||||
fieldType="text"
|
||||
caption={extension}
|
||||
secondaryButtonAction={closeEditMode}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
</DialogContent>
|
||||
</DialogBoxBase>
|
||||
);
|
||||
};
|
61
src/components/PhotoViewer/FileInfo/InfoItem.tsx
Normal file
61
src/components/PhotoViewer/FileInfo/InfoItem.tsx
Normal file
|
@ -0,0 +1,61 @@
|
|||
import Edit from '@mui/icons-material/Edit';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import React from 'react';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
|
||||
interface Iprops {
|
||||
icon: JSX.Element;
|
||||
title?: string;
|
||||
caption?: string | JSX.Element;
|
||||
openEditor?: any;
|
||||
loading?: boolean;
|
||||
hideEditOption?: any;
|
||||
customEndButton?: any;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export default function InfoItem({
|
||||
icon,
|
||||
title,
|
||||
caption,
|
||||
openEditor,
|
||||
loading,
|
||||
hideEditOption,
|
||||
customEndButton,
|
||||
children,
|
||||
}: Iprops): JSX.Element {
|
||||
return (
|
||||
<FlexWrapper justifyContent="space-between">
|
||||
<Box display={'flex'} alignItems="flex-start" gap={0.5} pr={1}>
|
||||
<IconButton
|
||||
color="secondary"
|
||||
sx={{ '&&': { cursor: 'default', m: 0.5 } }}
|
||||
disableRipple>
|
||||
{icon}
|
||||
</IconButton>
|
||||
<Box py={0.5}>
|
||||
{children ? (
|
||||
children
|
||||
) : (
|
||||
<>
|
||||
<Typography sx={{ wordBreak: 'break-all' }}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{caption}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{customEndButton
|
||||
? customEndButton
|
||||
: !hideEditOption && (
|
||||
<IconButton onClick={openEditor} color="secondary">
|
||||
{!loading ? <Edit /> : <SmallLoadingSpinner />}
|
||||
</IconButton>
|
||||
)}
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
126
src/components/PhotoViewer/FileInfo/RenderCaption.tsx
Normal file
126
src/components/PhotoViewer/FileInfo/RenderCaption.tsx
Normal file
|
@ -0,0 +1,126 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { changeCaption, updateExistingFilePubMetadata } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { Box, IconButton, TextField } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import { MAX_CAPTION_SIZE } from 'constants/file';
|
||||
import { Formik } from 'formik';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
import * as Yup from 'yup';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
import Done from '@mui/icons-material/Done';
|
||||
|
||||
interface formValues {
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export function RenderCaption({
|
||||
file,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
}) {
|
||||
const [caption, setCaption] = useState(
|
||||
file?.pubMagicMetadata?.data.caption
|
||||
);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const saveEdits = async (newCaption: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (caption === newCaption) {
|
||||
return;
|
||||
}
|
||||
setCaption(newCaption);
|
||||
|
||||
let updatedFile = await changeCaption(file, newCaption);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
file.title = file.pubMagicMetadata.data.caption;
|
||||
refreshPhotoswipe();
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update caption');
|
||||
}
|
||||
};
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.caption);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Box p={1}>
|
||||
<Formik<formValues>
|
||||
initialValues={{ caption }}
|
||||
validationSchema={Yup.object().shape({
|
||||
caption: Yup.string().max(
|
||||
MAX_CAPTION_SIZE,
|
||||
constants.CAPTION_CHARACTER_LIMIT
|
||||
),
|
||||
})}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({
|
||||
values,
|
||||
errors,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
resetForm,
|
||||
}) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
hiddenLabel
|
||||
fullWidth
|
||||
id="caption"
|
||||
name="caption"
|
||||
type="text"
|
||||
multiline
|
||||
placeholder={constants.CAPTION_PLACEHOLDER}
|
||||
value={values.caption}
|
||||
onChange={handleChange('caption')}
|
||||
error={Boolean(errors.caption)}
|
||||
helperText={errors.caption}
|
||||
disabled={loading}
|
||||
/>
|
||||
{values.caption !== caption && (
|
||||
<FlexWrapper justifyContent={'flex-end'}>
|
||||
<IconButton type="submit" disabled={loading}>
|
||||
{loading ? (
|
||||
<SmallLoadingSpinner />
|
||||
) : (
|
||||
<Done />
|
||||
)}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
resetForm({
|
||||
values: { caption: caption ?? '' },
|
||||
touched: { caption: false },
|
||||
})
|
||||
}
|
||||
disabled={loading}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</FlexWrapper>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,18 +1,16 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||
import {
|
||||
changeFileCreationTime,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import { formatDateTime } from 'utils/time';
|
||||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import { Label, Row, Value } from 'components/Container';
|
||||
import { formatDate, formatTime } from 'utils/time/format';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||
import { IconButton } from '@mui/material';
|
||||
import InfoItem from './InfoItem';
|
||||
|
||||
export function RenderCreationTime({
|
||||
shouldDisableEdits,
|
||||
|
@ -59,39 +57,24 @@ export function RenderCreationTime({
|
|||
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.CREATION_TIME}</Label>
|
||||
<Value
|
||||
width={
|
||||
!shouldDisableEdits ? !isInEditMode && '60%' : '70%'
|
||||
}>
|
||||
{isInEditMode ? (
|
||||
<EnteDateTimePicker
|
||||
initialValue={originalCreationTime}
|
||||
disabled={loading}
|
||||
onSubmit={saveEdits}
|
||||
onClose={closeEditMode}
|
||||
/>
|
||||
) : (
|
||||
formatDateTime(originalCreationTime)
|
||||
)}
|
||||
</Value>
|
||||
{!shouldDisableEdits && !isInEditMode && (
|
||||
<Value
|
||||
width={'10%'}
|
||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||
{loading ? (
|
||||
<IconButton>
|
||||
<SmallLoadingSpinner />
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</Value>
|
||||
<FlexWrapper>
|
||||
<InfoItem
|
||||
icon={<CalendarTodayIcon />}
|
||||
title={formatDate(originalCreationTime)}
|
||||
caption={formatTime(originalCreationTime)}
|
||||
openEditor={openEditMode}
|
||||
loading={loading}
|
||||
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||
/>
|
||||
{isInEditMode && (
|
||||
<EnteDateTimePicker
|
||||
initialValue={originalCreationTime}
|
||||
disabled={loading}
|
||||
onSubmit={saveEdits}
|
||||
onClose={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</FlexWrapper>
|
||||
</>
|
||||
);
|
||||
}
|
122
src/components/PhotoViewer/FileInfo/RenderFileName.tsx
Normal file
122
src/components/PhotoViewer/FileInfo/RenderFileName.tsx
Normal file
|
@ -0,0 +1,122 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
changeFileName,
|
||||
splitFilenameAndExtension,
|
||||
updateExistingFilePubMetadata,
|
||||
} from 'utils/file';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import InfoItem from './InfoItem';
|
||||
import { makeHumanReadableStorage } from 'utils/billing';
|
||||
import Box from '@mui/material/Box';
|
||||
import { FileNameEditDialog } from './FileNameEditDialog';
|
||||
import VideocamOutlined from '@mui/icons-material/VideocamOutlined';
|
||||
import PhotoOutlined from '@mui/icons-material/PhotoOutlined';
|
||||
|
||||
const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
const getCaption = (file: EnteFile, parsedExifData) => {
|
||||
const megaPixels = parsedExifData?.['megaPixels'];
|
||||
const resolution = parsedExifData?.['resolution'];
|
||||
const fileSize = file.info?.fileSize;
|
||||
|
||||
const captionParts = [];
|
||||
if (megaPixels) {
|
||||
captionParts.push(megaPixels);
|
||||
}
|
||||
if (resolution) {
|
||||
captionParts.push(resolution);
|
||||
}
|
||||
if (fileSize) {
|
||||
captionParts.push(makeHumanReadableStorage(fileSize));
|
||||
}
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
{captionParts.map((caption) => (
|
||||
<Box key={caption}> {caption}</Box>
|
||||
))}
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export function RenderFileName({
|
||||
parsedExifData,
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
}) {
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
const [filename, setFilename] = useState<string>();
|
||||
const [extension, setExtension] = useState<string>();
|
||||
|
||||
useEffect(() => {
|
||||
const [filename, extension] = splitFilenameAndExtension(
|
||||
file.metadata.title
|
||||
);
|
||||
setFilename(filename);
|
||||
setExtension(extension);
|
||||
}, []);
|
||||
|
||||
const saveEdits = async (newFilename: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (filename === newFilename) {
|
||||
closeEditMode();
|
||||
return;
|
||||
}
|
||||
setFilename(newFilename);
|
||||
const newTitle = getFileTitle(newFilename, extension);
|
||||
let updatedFile = await changeFileName(file, newTitle);
|
||||
updatedFile = (
|
||||
await updateFilePublicMagicMetadata([updatedFile])
|
||||
)[0];
|
||||
updateExistingFilePubMetadata(file, updatedFile);
|
||||
scheduleUpdate();
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'failed to update file name');
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InfoItem
|
||||
icon={
|
||||
file.metadata.fileType === FILE_TYPE.VIDEO ? (
|
||||
<VideocamOutlined />
|
||||
) : (
|
||||
<PhotoOutlined />
|
||||
)
|
||||
}
|
||||
title={getFileTitle(filename, extension)}
|
||||
caption={getCaption(file, parsedExifData)}
|
||||
openEditor={openEditMode}
|
||||
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||
/>
|
||||
<FileNameEditDialog
|
||||
isInEditMode={isInEditMode}
|
||||
closeEditMode={closeEditMode}
|
||||
filename={filename}
|
||||
extension={extension}
|
||||
saveEdits={saveEdits}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
329
src/components/PhotoViewer/FileInfo/index.tsx
Normal file
329
src/components/PhotoViewer/FileInfo/index.tsx
Normal file
|
@ -0,0 +1,329 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { RenderFileName } from './RenderFileName';
|
||||
import { RenderCreationTime } from './RenderCreationTime';
|
||||
import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
|
||||
import { Location } from 'types/upload';
|
||||
import { getEXIFLocation } from 'services/upload/exifService';
|
||||
import { RenderCaption } from './RenderCaption';
|
||||
|
||||
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||
import { formatDate, formatTime } from 'utils/time/format';
|
||||
import Titlebar from 'components/Titlebar';
|
||||
import InfoItem from './InfoItem';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { Chip } from 'components/Chip';
|
||||
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||
import { ExifData } from './ExifData';
|
||||
import { EnteDrawer } from 'components/EnteDrawer';
|
||||
import CameraOutlined from '@mui/icons-material/CameraOutlined';
|
||||
import LocationOnOutlined from '@mui/icons-material/LocationOnOutlined';
|
||||
import TextSnippetOutlined from '@mui/icons-material/TextSnippetOutlined';
|
||||
import FolderOutlined from '@mui/icons-material/FolderOutlined';
|
||||
import BackupOutlined from '@mui/icons-material/BackupOutlined';
|
||||
|
||||
import {
|
||||
PhotoPeopleList,
|
||||
UnidentifiedFaces,
|
||||
} from 'components/MachineLearning/PeopleList';
|
||||
|
||||
import { ObjectLabelList } from 'components/MachineLearning/ObjectList';
|
||||
import { WordList } from 'components/MachineLearning/WordList';
|
||||
import MLServiceFileInfoButton from 'components/MachineLearning/MLServiceFileInfoButton';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { Legend } from '../styledComponents/Legend';
|
||||
|
||||
export const FileInfoSidebar = styled((props: DialogProps) => (
|
||||
<EnteDrawer {...props} anchor="right" />
|
||||
))({
|
||||
zIndex: 1501,
|
||||
'& .MuiPaper-root': {
|
||||
padding: 8,
|
||||
},
|
||||
});
|
||||
|
||||
interface Iprops {
|
||||
shouldDisableEdits: boolean;
|
||||
showInfo: boolean;
|
||||
handleCloseInfo: () => void;
|
||||
file: EnteFile;
|
||||
exif: any;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
fileToCollectionsMap: Map<number, number[]>;
|
||||
collectionNameMap: Map<number, string>;
|
||||
isTrashCollection: boolean;
|
||||
}
|
||||
|
||||
function BasicDeviceCamera({
|
||||
parsedExifData,
|
||||
}: {
|
||||
parsedExifData: Record<string, any>;
|
||||
}) {
|
||||
return (
|
||||
<FlexWrapper gap={1}>
|
||||
<Box>{parsedExifData['fNumber']}</Box>
|
||||
<Box>{parsedExifData['exposureTime']}</Box>
|
||||
<Box>{parsedExifData['ISO']}</Box>
|
||||
</FlexWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
function getOpenStreetMapLink(location: {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
}) {
|
||||
return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
|
||||
}
|
||||
|
||||
export function FileInfo({
|
||||
shouldDisableEdits,
|
||||
showInfo,
|
||||
handleCloseInfo,
|
||||
file,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
fileToCollectionsMap,
|
||||
collectionNameMap,
|
||||
isTrashCollection,
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const [location, setLocation] = useState<Location>(null);
|
||||
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
|
||||
const [showExif, setShowExif] = useState(false);
|
||||
const [updateMLDataIndex, setUpdateMLDataIndex] = useState(0);
|
||||
|
||||
const openExif = () => setShowExif(true);
|
||||
const closeExif = () => setShowExif(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!location && file && file.metadata) {
|
||||
if (file.metadata.longitude || file.metadata.longitude === 0) {
|
||||
setLocation({
|
||||
latitude: file.metadata.latitude,
|
||||
longitude: file.metadata.longitude,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [file]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!location && exif) {
|
||||
const exifLocation = getEXIFLocation(exif);
|
||||
if (exifLocation.latitude || exifLocation.latitude === 0) {
|
||||
setLocation(exifLocation);
|
||||
}
|
||||
}
|
||||
}, [exif]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!exif) {
|
||||
setParsedExifData({});
|
||||
return;
|
||||
}
|
||||
const parsedExifData = {};
|
||||
if (exif['fNumber']) {
|
||||
parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`;
|
||||
} else if (exif['ApertureValue'] && exif['FocalLength']) {
|
||||
parsedExifData['fNumber'] = `f/${Math.ceil(
|
||||
exif['FocalLength'] / exif['ApertureValue']
|
||||
)}`;
|
||||
}
|
||||
const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth'];
|
||||
const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight'];
|
||||
if (imageWidth && imageHeight) {
|
||||
parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`;
|
||||
const megaPixels = Math.round((imageWidth * imageHeight) / 1000000);
|
||||
if (megaPixels) {
|
||||
parsedExifData['megaPixels'] = `${Math.round(
|
||||
(imageWidth * imageHeight) / 1000000
|
||||
)}MP`;
|
||||
}
|
||||
}
|
||||
if (exif['Make'] && exif['Model']) {
|
||||
parsedExifData[
|
||||
'takenOnDevice'
|
||||
] = `${exif['Make']} ${exif['Model']}`;
|
||||
}
|
||||
if (exif['ExposureTime']) {
|
||||
parsedExifData['exposureTime'] = `1/${
|
||||
1 / parseFloat(exif['ExposureTime'])
|
||||
}`;
|
||||
}
|
||||
if (exif['ISO']) {
|
||||
parsedExifData['ISO'] = `ISO${exif['ISO']}`;
|
||||
}
|
||||
setParsedExifData(parsedExifData);
|
||||
}, [exif]);
|
||||
|
||||
if (!file) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
|
||||
<Titlebar
|
||||
onClose={handleCloseInfo}
|
||||
title={constants.INFO}
|
||||
backIsClose
|
||||
/>
|
||||
<Stack pt={1} pb={3} spacing={'20px'}>
|
||||
<RenderCaption
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={file}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
/>
|
||||
|
||||
<RenderCreationTime
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={file}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
|
||||
<RenderFileName
|
||||
parsedExifData={parsedExifData}
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={file}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
/>
|
||||
{parsedExifData && parsedExifData['takenOnDevice'] && (
|
||||
<InfoItem
|
||||
icon={<CameraOutlined />}
|
||||
title={parsedExifData['takenOnDevice']}
|
||||
caption={
|
||||
<BasicDeviceCamera
|
||||
parsedExifData={parsedExifData}
|
||||
/>
|
||||
}
|
||||
hideEditOption
|
||||
/>
|
||||
)}
|
||||
|
||||
{location && (
|
||||
<InfoItem
|
||||
icon={<LocationOnOutlined />}
|
||||
title={constants.LOCATION}
|
||||
caption={
|
||||
<Link
|
||||
href={getOpenStreetMapLink({
|
||||
latitude: file.metadata.latitude,
|
||||
longitude: file.metadata.longitude,
|
||||
})}
|
||||
target="_blank"
|
||||
sx={{ fontWeight: 'bold' }}>
|
||||
{constants.SHOW_ON_MAP}
|
||||
</Link>
|
||||
}
|
||||
customEndButton={
|
||||
<CopyButton
|
||||
code={getOpenStreetMapLink({
|
||||
latitude: file.metadata.latitude,
|
||||
longitude: file.metadata.longitude,
|
||||
})}
|
||||
color="secondary"
|
||||
size="medium"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<InfoItem
|
||||
icon={<TextSnippetOutlined />}
|
||||
title={constants.DETAILS}
|
||||
caption={
|
||||
typeof exif === 'undefined' ? (
|
||||
<EnteSpinner size={11.33} />
|
||||
) : exif !== null ? (
|
||||
<LinkButton
|
||||
onClick={openExif}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
color: 'text.secondary',
|
||||
fontWeight: 'bold',
|
||||
}}>
|
||||
{constants.VIEW_EXIF}
|
||||
</LinkButton>
|
||||
) : (
|
||||
constants.NO_EXIF
|
||||
)
|
||||
}
|
||||
hideEditOption
|
||||
/>
|
||||
<InfoItem
|
||||
icon={<BackupOutlined />}
|
||||
title={formatDate(file.metadata.modificationTime / 1000)}
|
||||
caption={formatTime(file.metadata.modificationTime / 1000)}
|
||||
hideEditOption
|
||||
/>
|
||||
{!isTrashCollection && (
|
||||
<InfoItem icon={<FolderOutlined />} hideEditOption>
|
||||
<Box
|
||||
display={'flex'}
|
||||
gap={1}
|
||||
flexWrap="wrap"
|
||||
justifyContent={'flex-start'}
|
||||
alignItems={'flex-start'}>
|
||||
{fileToCollectionsMap
|
||||
.get(file.id)
|
||||
?.filter((collectionID) =>
|
||||
collectionNameMap.has(collectionID)
|
||||
)
|
||||
?.map((collectionID) => (
|
||||
<Chip key={collectionID}>
|
||||
{collectionNameMap.get(collectionID)}
|
||||
</Chip>
|
||||
))}
|
||||
</Box>
|
||||
</InfoItem>
|
||||
)}
|
||||
{appContext.mlSearchEnabled && (
|
||||
<>
|
||||
<div>
|
||||
<Legend>{constants.PEOPLE}</Legend>
|
||||
</div>
|
||||
<PhotoPeopleList
|
||||
file={file}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
<div>
|
||||
<Legend>{constants.UNIDENTIFIED_FACES}</Legend>
|
||||
</div>
|
||||
<UnidentifiedFaces
|
||||
file={file}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
<div>
|
||||
<Legend>{constants.OBJECTS}</Legend>
|
||||
<ObjectLabelList
|
||||
file={file}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Legend>{constants.TEXT}</Legend>
|
||||
<WordList
|
||||
file={file}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
/>
|
||||
</div>
|
||||
<MLServiceFileInfoButton
|
||||
file={file}
|
||||
updateMLDataIndex={updateMLDataIndex}
|
||||
setUpdateMLDataIndex={setUpdateMLDataIndex}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
<ExifData
|
||||
exif={exif}
|
||||
open={showExif}
|
||||
onClose={closeExif}
|
||||
onInfoClose={handleCloseInfo}
|
||||
filename={file.metadata.title}
|
||||
/>
|
||||
</FileInfoSidebar>
|
||||
);
|
||||
}
|
|
@ -9,26 +9,54 @@ import {
|
|||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import exifr from 'exifr';
|
||||
import events from './events';
|
||||
import { downloadFile } from 'utils/file';
|
||||
import { prettyPrintExif } from 'utils/exif';
|
||||
import {
|
||||
downloadFile,
|
||||
copyFileToClipboard,
|
||||
getFileExtension,
|
||||
} from 'utils/file';
|
||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||
import { logError } from 'utils/sentry';
|
||||
|
||||
import { FILE_TYPE } from 'constants/file';
|
||||
import { sleep } from 'utils/common';
|
||||
import { isClipboardItemPresent } from 'utils/common';
|
||||
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { FileInfo } from './InfoDialog';
|
||||
import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe';
|
||||
import { FileInfo } from './FileInfo';
|
||||
import {
|
||||
defaultLivePhotoDefaultOptions,
|
||||
photoSwipeV4Events,
|
||||
} from 'constants/photoViewer';
|
||||
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import InfoIcon from '@mui/icons-material/InfoOutlined';
|
||||
import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
|
||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
|
||||
import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import { trashFiles } from 'services/fileService';
|
||||
import { getTrashFileMessage } from 'utils/ui';
|
||||
import { styled } from '@mui/material';
|
||||
import { addLocalLog } from 'utils/logging';
|
||||
import ContentCopy from '@mui/icons-material/ContentCopy';
|
||||
import ChevronLeft from '@mui/icons-material/ChevronLeft';
|
||||
|
||||
interface PhotoswipeFullscreenAPI {
|
||||
enter: () => void;
|
||||
exit: () => void;
|
||||
isFullscreen: () => boolean;
|
||||
}
|
||||
|
||||
const CaptionContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
wordBreak: 'break-word',
|
||||
textAlign: 'right',
|
||||
maxWidth: '375px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '17px',
|
||||
backgroundColor: theme.palette.backdrop.light,
|
||||
backdropFilter: `blur(${theme.palette.blur.base})`,
|
||||
}));
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
items: any[];
|
||||
|
@ -38,13 +66,17 @@ interface Iprops {
|
|||
id?: string;
|
||||
className?: string;
|
||||
favItemIds: Set<number>;
|
||||
deletedFileIds: Set<number>;
|
||||
setDeletedFileIds?: (value: Set<number>) => void;
|
||||
isSharedCollection: boolean;
|
||||
isTrashCollection: boolean;
|
||||
enableDownload: boolean;
|
||||
isSourceLoaded: boolean;
|
||||
fileToCollectionsMap: Map<number, number[]>;
|
||||
collectionNameMap: Map<number, string>;
|
||||
}
|
||||
|
||||
function PhotoSwipe(props: Iprops) {
|
||||
function PhotoViewer(props: Iprops) {
|
||||
const pswpElement = useRef<HTMLDivElement>();
|
||||
const [photoSwipe, setPhotoSwipe] =
|
||||
useState<Photoswipe<Photoswipe.Options>>();
|
||||
|
@ -52,8 +84,9 @@ function PhotoSwipe(props: Iprops) {
|
|||
const { isOpen, items, isSourceLoaded } = props;
|
||||
const [isFav, setIsFav] = useState(false);
|
||||
const [showInfo, setShowInfo] = useState(false);
|
||||
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
|
||||
const [exif, setExif] = useState<any>(null);
|
||||
const [exif, setExif] =
|
||||
useState<{ key: string; value: Record<string, any> }>();
|
||||
const exifCopy = useRef(null);
|
||||
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
||||
defaultLivePhotoDefaultOptions
|
||||
);
|
||||
|
@ -63,6 +96,9 @@ function PhotoSwipe(props: Iprops) {
|
|||
);
|
||||
const appContext = useContext(AppContext);
|
||||
|
||||
const exifExtractionInProgress = useRef<string>(null);
|
||||
const [shouldShowCopyOption] = useState(isClipboardItemPresent());
|
||||
|
||||
useEffect(() => {
|
||||
if (!pswpElement) return;
|
||||
if (isOpen) {
|
||||
|
@ -76,6 +112,57 @@ function PhotoSwipe(props: Iprops) {
|
|||
};
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!photoSwipe) return;
|
||||
function handleCopyEvent() {
|
||||
copyToClipboardHelper(photoSwipe.currItem as EnteFile);
|
||||
}
|
||||
|
||||
function handleKeyUp(event: KeyboardEvent) {
|
||||
if (!isOpen || showInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
addLocalLog(() => 'Event: ' + event.key);
|
||||
|
||||
switch (event.key) {
|
||||
case 'i':
|
||||
case 'I':
|
||||
setShowInfo(true);
|
||||
break;
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
confirmTrashFile(photoSwipe?.currItem as EnteFile);
|
||||
break;
|
||||
case 'd':
|
||||
case 'D':
|
||||
downloadFileHelper(photoSwipe?.currItem as EnteFile);
|
||||
break;
|
||||
case 'f':
|
||||
case 'F':
|
||||
toggleFullscreen(photoSwipe);
|
||||
break;
|
||||
case 'l':
|
||||
case 'L':
|
||||
onFavClick(photoSwipe?.currItem as EnteFile);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keyup', handleKeyUp);
|
||||
if (shouldShowCopyOption) {
|
||||
window.addEventListener('copy', handleCopyEvent);
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('keyup', handleKeyUp);
|
||||
if (shouldShowCopyOption) {
|
||||
window.removeEventListener('copy', handleCopyEvent);
|
||||
}
|
||||
};
|
||||
}, [isOpen, photoSwipe, showInfo]);
|
||||
|
||||
useEffect(() => {
|
||||
updateItems(items);
|
||||
}, [items]);
|
||||
|
@ -152,8 +239,12 @@ function PhotoSwipe(props: Iprops) {
|
|||
}
|
||||
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
||||
|
||||
function updateFavButton() {
|
||||
setIsFav(isInFav(this?.currItem));
|
||||
useEffect(() => {
|
||||
exifCopy.current = exif;
|
||||
}, [exif]);
|
||||
|
||||
function updateFavButton(file: EnteFile) {
|
||||
setIsFav(isInFav(file));
|
||||
}
|
||||
|
||||
const openPhotoSwipe = () => {
|
||||
|
@ -198,12 +289,12 @@ function PhotoSwipe(props: Iprops) {
|
|||
items,
|
||||
options
|
||||
);
|
||||
events.forEach((event) => {
|
||||
photoSwipeV4Events.forEach((event) => {
|
||||
const callback = props[event];
|
||||
if (callback || event === 'destroy') {
|
||||
photoSwipe.listen(event, function (...args) {
|
||||
if (callback) {
|
||||
args.unshift(this);
|
||||
args.unshift(photoSwipe);
|
||||
callback(...args);
|
||||
}
|
||||
if (event === 'destroy') {
|
||||
|
@ -215,11 +306,39 @@ function PhotoSwipe(props: Iprops) {
|
|||
});
|
||||
}
|
||||
});
|
||||
photoSwipe.listen('beforeChange', function () {
|
||||
updateInfo.call(this);
|
||||
updateFavButton.call(this);
|
||||
photoSwipe.listen('beforeChange', () => {
|
||||
const currItem = photoSwipe?.currItem as EnteFile;
|
||||
updateFavButton(currItem);
|
||||
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
|
||||
setExif({ key: currItem.src, value: null });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!currItem ||
|
||||
!exifCopy?.current?.value === null ||
|
||||
exifCopy?.current?.key === currItem.src
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setExif({ key: currItem.src, value: undefined });
|
||||
checkExifAvailable(currItem);
|
||||
});
|
||||
photoSwipe.listen('resize', () => {
|
||||
const currItem = photoSwipe?.currItem as EnteFile;
|
||||
if (currItem.metadata.fileType !== FILE_TYPE.IMAGE) {
|
||||
setExif({ key: currItem.src, value: null });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!currItem ||
|
||||
!exifCopy?.current?.value === null ||
|
||||
exifCopy?.current?.key === currItem.src
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setExif({ key: currItem.src, value: undefined });
|
||||
checkExifAvailable(currItem);
|
||||
});
|
||||
photoSwipe.listen('resize', checkExifAvailable);
|
||||
photoSwipe.init();
|
||||
needUpdate.current = false;
|
||||
setPhotoSwipe(photoSwipe);
|
||||
|
@ -240,7 +359,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
}
|
||||
handleCloseInfo();
|
||||
};
|
||||
const isInFav = (file) => {
|
||||
const isInFav = (file: EnteFile) => {
|
||||
const { favItemIds } = props;
|
||||
if (favItemIds && file) {
|
||||
return favItemIds.has(file.id);
|
||||
|
@ -248,7 +367,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
return false;
|
||||
};
|
||||
|
||||
const onFavClick = async (file) => {
|
||||
const onFavClick = async (file: EnteFile) => {
|
||||
const { favItemIds } = props;
|
||||
if (!isInFav(file)) {
|
||||
favItemIds.add(file.id);
|
||||
|
@ -262,46 +381,80 @@ function PhotoSwipe(props: Iprops) {
|
|||
needUpdate.current = true;
|
||||
};
|
||||
|
||||
const trashFile = async (file: EnteFile) => {
|
||||
const { deletedFileIds, setDeletedFileIds } = props;
|
||||
deletedFileIds.add(file.id);
|
||||
setDeletedFileIds(new Set(deletedFileIds));
|
||||
await trashFiles([file]);
|
||||
needUpdate.current = true;
|
||||
};
|
||||
|
||||
const confirmTrashFile = (file: EnteFile) => {
|
||||
if (props.isSharedCollection || props.isTrashCollection) {
|
||||
return;
|
||||
}
|
||||
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||
};
|
||||
|
||||
const updateItems = (items = []) => {
|
||||
if (photoSwipe) {
|
||||
if (items.length === 0) {
|
||||
photoSwipe.close();
|
||||
}
|
||||
photoSwipe.items.length = 0;
|
||||
items.forEach((item) => {
|
||||
photoSwipe.items.push(item);
|
||||
});
|
||||
photoSwipe.invalidateCurrItems();
|
||||
// photoSwipe.updateSize(true);
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
|
||||
photoSwipe.goTo(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
await sleep(100);
|
||||
const refreshPhotoswipe = () => {
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async (file: EnteFile) => {
|
||||
try {
|
||||
const img: HTMLImageElement = document.querySelector(
|
||||
'.pswp__img:not(.pswp__img--placeholder)'
|
||||
);
|
||||
if (img) {
|
||||
const exifData = await exifr.parse(img);
|
||||
if (!exifData) {
|
||||
return;
|
||||
if (exifExtractionInProgress.current === file.src) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (file.isSourceLoaded) {
|
||||
exifExtractionInProgress.current = file.src;
|
||||
const imageBlob = await (
|
||||
await fetch(file.originalImageURL)
|
||||
).blob();
|
||||
const exifData = (await exifr.parse(imageBlob)) as Record<
|
||||
string,
|
||||
any
|
||||
>;
|
||||
if (exifExtractionInProgress.current === file.src) {
|
||||
if (exifData) {
|
||||
setExif({ key: file.src, value: exifData });
|
||||
} else {
|
||||
setExif({ key: file.src, value: null });
|
||||
}
|
||||
}
|
||||
}
|
||||
exifData.raw = prettyPrintExif(exifData);
|
||||
setExif(exifData);
|
||||
} finally {
|
||||
exifExtractionInProgress.current = null;
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'exifr parsing failed');
|
||||
setExif({ key: file.src, value: null });
|
||||
const fileExtension = getFileExtension(file.metadata.title);
|
||||
logError(e, 'exifr parsing failed', { extension: fileExtension });
|
||||
}
|
||||
};
|
||||
|
||||
function updateInfo() {
|
||||
const file: EnteFile = this?.currItem;
|
||||
if (file?.metadata) {
|
||||
setMetaData(file.metadata);
|
||||
setExif(null);
|
||||
checkExifAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
const handleCloseInfo = () => {
|
||||
setShowInfo(false);
|
||||
};
|
||||
|
@ -310,15 +463,37 @@ function PhotoSwipe(props: Iprops) {
|
|||
};
|
||||
|
||||
const downloadFileHelper = async (file) => {
|
||||
appContext.startLoading();
|
||||
await downloadFile(
|
||||
file,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken
|
||||
);
|
||||
if (props.enableDownload) {
|
||||
appContext.startLoading();
|
||||
await downloadFile(
|
||||
file,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken
|
||||
);
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
appContext.finishLoading();
|
||||
const copyToClipboardHelper = async (file: EnteFile) => {
|
||||
if (props.enableDownload && shouldShowCopyOption) {
|
||||
appContext.startLoading();
|
||||
await copyFileToClipboard(file.src);
|
||||
appContext.finishLoading();
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = (photoSwipe) => {
|
||||
const fullScreenApi: PhotoswipeFullscreenAPI =
|
||||
photoSwipe?.ui?.getFullscreenAPI();
|
||||
if (!fullScreenApi) {
|
||||
return;
|
||||
}
|
||||
if (fullScreenApi.isFullscreen()) {
|
||||
fullScreenApi.exit();
|
||||
} else {
|
||||
fullScreenApi.enter();
|
||||
}
|
||||
};
|
||||
const scheduleUpdate = () => (needUpdate.current = true);
|
||||
const { id } = props;
|
||||
|
@ -355,33 +530,74 @@ function PhotoSwipe(props: Iprops) {
|
|||
|
||||
<button
|
||||
className="pswp__button pswp__button--close"
|
||||
title={constants.CLOSE}
|
||||
title={constants.CLOSE_OPTION}
|
||||
/>
|
||||
|
||||
{props.enableDownload && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.DOWNLOAD}
|
||||
title={constants.DOWNLOAD_OPTION}
|
||||
onClick={() =>
|
||||
downloadFileHelper(photoSwipe.currItem)
|
||||
}>
|
||||
<DownloadIcon fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title={constants.TOGGLE_FULLSCREEN}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--zoom"
|
||||
title={constants.ZOOM_IN_OUT}
|
||||
/>
|
||||
{props.enableDownload && shouldShowCopyOption && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.COPY_OPTION}
|
||||
onClick={() =>
|
||||
copyToClipboardHelper(
|
||||
photoSwipe.currItem as EnteFile
|
||||
)
|
||||
}>
|
||||
<ContentCopy fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!props.isSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.DELETE_OPTION}
|
||||
onClick={() => {
|
||||
onFavClick(photoSwipe?.currItem);
|
||||
confirmTrashFile(
|
||||
photoSwipe?.currItem as EnteFile
|
||||
);
|
||||
}}>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="pswp__button pswp__button--zoom"
|
||||
title={constants.ZOOM_IN_OUT}
|
||||
/>
|
||||
<button
|
||||
className="pswp__button pswp__button--fs"
|
||||
title={constants.TOGGLE_FULLSCREEN}
|
||||
/>
|
||||
|
||||
{!props.isSharedCollection && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.INFO_OPTION}
|
||||
onClick={handleOpenInfo}>
|
||||
<InfoIcon fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
{!props.isSharedCollection &&
|
||||
!props.isTrashCollection && (
|
||||
<button
|
||||
title={
|
||||
isFav
|
||||
? constants.UNFAVORITE_OPTION
|
||||
: constants.FAVORITE_OPTION
|
||||
}
|
||||
className="pswp__button pswp__button--custom"
|
||||
onClick={() => {
|
||||
onFavClick(
|
||||
photoSwipe?.currItem as EnteFile
|
||||
);
|
||||
}}>
|
||||
{isFav ? (
|
||||
<FavoriteIcon fontSize="small" />
|
||||
|
@ -390,14 +606,7 @@ function PhotoSwipe(props: Iprops) {
|
|||
)}
|
||||
</button>
|
||||
)}
|
||||
{!props.isSharedCollection && (
|
||||
<button
|
||||
className="pswp__button pswp__button--custom"
|
||||
title={constants.INFO}
|
||||
onClick={handleOpenInfo}>
|
||||
<InfoIcon fontSize="small" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="pswp__preloader">
|
||||
<div className="pswp__preloader__icn">
|
||||
<div className="pswp__preloader__cut">
|
||||
|
@ -411,36 +620,34 @@ function PhotoSwipe(props: Iprops) {
|
|||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title={constants.PREVIOUS}
|
||||
onClick={photoSwipe?.prev}>
|
||||
<ChevronRight
|
||||
sx={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
title={constants.PREVIOUS}>
|
||||
<ChevronLeft sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title={constants.NEXT}
|
||||
onClick={photoSwipe?.next}>
|
||||
<ChevronRight />
|
||||
title={constants.NEXT}>
|
||||
<ChevronRight sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<div className="pswp__caption">
|
||||
<div />
|
||||
<div className="pswp__caption pswp-custom-caption-container">
|
||||
<CaptionContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileInfo
|
||||
isTrashCollection={props.isTrashCollection}
|
||||
shouldDisableEdits={props.isSharedCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
items={items}
|
||||
photoSwipe={photoSwipe}
|
||||
metadata={metadata}
|
||||
exif={exif}
|
||||
file={photoSwipe?.currItem as EnteFile}
|
||||
exif={exif?.value}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||
collectionNameMap={props.collectionNameMap}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default PhotoSwipe;
|
||||
export default PhotoViewer;
|
|
@ -1,6 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import { PeopleList } from 'components/MachineLearning/PeopleList';
|
||||
import { Legend } from 'components/PhotoSwipe/styledComponents/Legend';
|
||||
import { IndexStatus } from 'types/machineLearning/ui';
|
||||
import { SuggestionType, Suggestion } from 'types/search';
|
||||
import { components } from 'react-select';
|
||||
|
@ -18,6 +17,12 @@ const LegendRow = styled(Row)`
|
|||
margin-bottom: 0px;
|
||||
`;
|
||||
|
||||
const Legend = styled('span')`
|
||||
font-size: 20px;
|
||||
color: #ddd;
|
||||
display: inline;
|
||||
`;
|
||||
|
||||
const Caption = styled('span')`
|
||||
font-size: 12px;
|
||||
display: inline;
|
||||
|
|
|
@ -44,7 +44,9 @@ export default function SearchInput(props: Iprops) {
|
|||
};
|
||||
const [defaultOptions, setDefaultOptions] = useState([]);
|
||||
|
||||
useEffect(() => search(value), [value]);
|
||||
useEffect(() => {
|
||||
search(value);
|
||||
}, [value]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshDefaultOptions();
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import { downloadAsFile } from 'utils/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { addLogLine, getDebugLogs } from 'utils/logging';
|
||||
import SidebarButton from './Button';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { User } from 'types/user';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
export default function DebugLogs() {
|
||||
const appContext = useContext(AppContext);
|
||||
const confirmLogDownload = () =>
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DOWNLOAD_LOGS,
|
||||
content: constants.DOWNLOAD_LOGS_MESSAGE(),
|
||||
proceed: {
|
||||
text: constants.DOWNLOAD,
|
||||
variant: 'accent',
|
||||
action: downloadDebugLogs,
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
|
||||
const downloadDebugLogs = () => {
|
||||
addLogLine(
|
||||
'latest commit id :' + process.env.NEXT_PUBLIC_LATEST_COMMIT_HASH
|
||||
);
|
||||
addLogLine(`user sentry id ${getSentryUserID()}`);
|
||||
addLogLine(`ente userID ${(getData(LS_KEYS.USER) as User)?.id}`);
|
||||
addLogLine('exporting logs');
|
||||
const logs = getDebugLogs();
|
||||
const logString = logs.join('\n');
|
||||
downloadAsFile(`debug_logs_${Date.now()}.txt`, logString);
|
||||
};
|
||||
|
||||
return (
|
||||
<SidebarButton
|
||||
onClick={confirmLogDownload}
|
||||
typographyVariant="caption"
|
||||
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
|
||||
{constants.DOWNLOAD_UPLOAD_LOGS}
|
||||
</SidebarButton>
|
||||
);
|
||||
}
|
84
src/components/Sidebar/DebugSection.tsx
Normal file
84
src/components/Sidebar/DebugSection.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { downloadAsFile } from 'utils/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { addLogLine, getDebugLogs } from 'utils/logging';
|
||||
import SidebarButton from './Button';
|
||||
import isElectron from 'is-electron';
|
||||
import ElectronService from 'services/electron/common';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { isInternalUser } from 'utils/user';
|
||||
import { testUpload } from '../../../tests/upload.test';
|
||||
import {
|
||||
testZipFileReading,
|
||||
testZipWithRootFileReadingTest,
|
||||
} from '../../../tests/zip-file-reading.test';
|
||||
|
||||
export default function DebugSection() {
|
||||
const appContext = useContext(AppContext);
|
||||
const [appVersion, setAppVersion] = useState<string>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (isElectron()) {
|
||||
const appVersion = await ElectronService.getAppVersion();
|
||||
setAppVersion(appVersion);
|
||||
}
|
||||
};
|
||||
main();
|
||||
});
|
||||
|
||||
const confirmLogDownload = () =>
|
||||
appContext.setDialogMessage({
|
||||
title: constants.DOWNLOAD_LOGS,
|
||||
content: constants.DOWNLOAD_LOGS_MESSAGE(),
|
||||
proceed: {
|
||||
text: constants.DOWNLOAD,
|
||||
variant: 'accent',
|
||||
action: downloadDebugLogs,
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
|
||||
const downloadDebugLogs = () => {
|
||||
addLogLine('exporting logs');
|
||||
if (isElectron()) {
|
||||
ElectronService.openLogDirectory();
|
||||
} else {
|
||||
const logs = getDebugLogs();
|
||||
|
||||
downloadAsFile(`debug_logs_${Date.now()}.txt`, logs);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarButton
|
||||
onClick={confirmLogDownload}
|
||||
typographyVariant="caption"
|
||||
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
|
||||
{constants.DOWNLOAD_UPLOAD_LOGS}
|
||||
</SidebarButton>
|
||||
{appVersion && (
|
||||
<Typography p={1.5} color="text.secondary" variant="caption">
|
||||
{appVersion}
|
||||
</Typography>
|
||||
)}
|
||||
{isInternalUser() && (
|
||||
<>
|
||||
<SidebarButton onClick={testUpload}>
|
||||
Test Upload
|
||||
</SidebarButton>
|
||||
<SidebarButton onClick={testZipFileReading}>
|
||||
Test Zip file reading
|
||||
</SidebarButton>
|
||||
<SidebarButton onClick={testZipWithRootFileReadingTest}>
|
||||
Zip with Root file Test
|
||||
</SidebarButton>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app';
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { getDownloadAppMessage } from 'utils/ui';
|
||||
import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
|
||||
import { openLink } from 'utils/common';
|
||||
|
||||
export default function HelpSection() {
|
||||
const [exportModalView, setExportModalView] = useState(false);
|
||||
|
@ -20,8 +21,7 @@ export default function HelpSection() {
|
|||
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
|
||||
getToken()
|
||||
)}`;
|
||||
const win = window.open(feedbackURL, '_blank');
|
||||
win.focus();
|
||||
openLink(feedbackURL, true);
|
||||
}
|
||||
|
||||
function exportFiles() {
|
||||
|
|
|
@ -2,10 +2,10 @@ import React, { useContext } from 'react';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import { CollectionSummaries } from 'types/collection';
|
||||
import ShortcutButton from './ShortcutButton';
|
||||
import DeleteOutline from '@mui/icons-material/DeleteOutline';
|
||||
import ArchiveOutlined from '@mui/icons-material/ArchiveOutlined';
|
||||
interface Iprops {
|
||||
closeSidebar: () => void;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
|
@ -30,13 +30,13 @@ export default function ShortcutSection({
|
|||
return (
|
||||
<>
|
||||
<ShortcutButton
|
||||
startIcon={<DeleteIcon />}
|
||||
startIcon={<DeleteOutline />}
|
||||
label={constants.TRASH}
|
||||
count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
|
||||
onClick={openTrashSection}
|
||||
/>
|
||||
<ShortcutButton
|
||||
startIcon={<VisibilityOffIcon />}
|
||||
startIcon={<ArchiveOutlined />}
|
||||
label={constants.ARCHIVE_SECTION_NAME}
|
||||
count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
|
||||
onClick={openArchiveSection}
|
||||
|
|
|
@ -16,7 +16,7 @@ export function FamilyUsageProgressBar({
|
|||
<Box position={'relative'} width="100%">
|
||||
<Progressbar
|
||||
sx={{ backgroundColor: 'transparent' }}
|
||||
value={(userUsage * 100) / totalStorage}
|
||||
value={Math.min((userUsage * 100) / totalStorage, 100)}
|
||||
/>
|
||||
<Progressbar
|
||||
sx={{
|
||||
|
@ -28,7 +28,7 @@ export function FamilyUsageProgressBar({
|
|||
},
|
||||
width: '100%',
|
||||
}}
|
||||
value={(totalUsage * 100) / totalStorage}
|
||||
value={Math.min((totalUsage * 100) / totalStorage, 100)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ interface Iprops {
|
|||
export function IndividualUsageSection({ usage, storage, fileCount }: Iprops) {
|
||||
return (
|
||||
<Box width="100%">
|
||||
<Progressbar value={(usage * 100) / storage} />
|
||||
<Progressbar value={Math.min((usage * 100) / storage, 100)} />
|
||||
<SpaceBetweenFlex
|
||||
sx={{
|
||||
marginTop: 1.5,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { GalleryContext } from 'pages/gallery';
|
||||
import React, { useContext, useMemo } from 'react';
|
||||
import React, { MouseEventHandler, useContext, useMemo } from 'react';
|
||||
import {
|
||||
hasPaidSubscription,
|
||||
isFamilyAdmin,
|
||||
|
@ -43,19 +43,23 @@ export default function SubscriptionStatus({
|
|||
}, [userDetails]);
|
||||
|
||||
const handleClick = useMemo(() => {
|
||||
if (userDetails) {
|
||||
if (isSubscriptionActive(userDetails.subscription)) {
|
||||
if (hasExceededStorageQuota(userDetails)) {
|
||||
return showPlanSelectorModal;
|
||||
}
|
||||
} else {
|
||||
if (hasStripeSubscription(userDetails.subscription)) {
|
||||
return billingService.redirectToCustomerPortal;
|
||||
const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
|
||||
e.stopPropagation();
|
||||
if (userDetails) {
|
||||
if (isSubscriptionActive(userDetails.subscription)) {
|
||||
if (hasExceededStorageQuota(userDetails)) {
|
||||
showPlanSelectorModal();
|
||||
}
|
||||
} else {
|
||||
return showPlanSelectorModal;
|
||||
if (hasStripeSubscription(userDetails.subscription)) {
|
||||
billingService.redirectToCustomerPortal();
|
||||
} else {
|
||||
showPlanSelectorModal();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return eventHandler;
|
||||
}, [userDetails]);
|
||||
|
||||
if (!hasAMessage) {
|
||||
|
@ -80,13 +84,9 @@ export default function SubscriptionStatus({
|
|||
)
|
||||
: hasExceededStorageQuota(userDetails) &&
|
||||
constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO(
|
||||
showPlanSelectorModal
|
||||
handleClick
|
||||
)
|
||||
: constants.SUBSCRIPTION_EXPIRED_MESSAGE(
|
||||
hasStripeSubscription(userDetails.subscription)
|
||||
? billingService.redirectToCustomerPortal
|
||||
: showPlanSelectorModal
|
||||
)}
|
||||
: constants.SUBSCRIPTION_EXPIRED_MESSAGE(handleClick)}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
|
|
@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
|
|||
import UtilitySection from './UtilitySection';
|
||||
import HelpSection from './HelpSection';
|
||||
import ExitSection from './ExitSection';
|
||||
import DebugLogs from './DebugLogs';
|
||||
import DebugSection from './DebugSection';
|
||||
import { DrawerSidebar } from './styledComponents';
|
||||
import HeaderSection from './Header';
|
||||
import { CollectionSummaries } from 'types/collection';
|
||||
|
@ -37,7 +37,7 @@ export default function Sidebar({
|
|||
<Divider />
|
||||
<ExitSection />
|
||||
<Divider />
|
||||
<DebugLogs />
|
||||
<DebugSection />
|
||||
</Stack>
|
||||
</DrawerSidebar>
|
||||
);
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import { Drawer, styled } from '@mui/material';
|
||||
import { styled } from '@mui/material';
|
||||
import CircleIcon from '@mui/icons-material/Circle';
|
||||
import { EnteDrawer } from 'components/EnteDrawer';
|
||||
|
||||
export const DrawerSidebar = styled(Drawer)(({ theme }) => ({
|
||||
export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '375px',
|
||||
width: '100%',
|
||||
scrollbarWidth: 'thin',
|
||||
padding: theme.spacing(1.5),
|
||||
},
|
||||
}));
|
||||
|
|
|
@ -47,8 +47,12 @@ export default function SignUp(props: SignUpProps) {
|
|||
{ email, passphrase, confirm }: FormValues,
|
||||
{ setFieldError }: FormikHelpers<FormValues>
|
||||
) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
if (passphrase !== confirm) {
|
||||
setFieldError('confirm', constants.PASSPHRASE_MATCH_ERROR);
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
setData(LS_KEYS.USER, { email });
|
||||
await sendOtt(email);
|
||||
|
@ -60,30 +64,23 @@ export default function SignUp(props: SignUpProps) {
|
|||
throw e;
|
||||
}
|
||||
try {
|
||||
if (passphrase === confirm) {
|
||||
const { keyAttributes, masterKey } =
|
||||
await generateKeyAttributes(passphrase);
|
||||
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
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
|
||||
const { keyAttributes, masterKey } =
|
||||
await generateKeyAttributes(passphrase);
|
||||
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
|
||||
await generateAndSaveIntermediateKeyAttributes(
|
||||
passphrase,
|
||||
keyAttributes,
|
||||
masterKey
|
||||
);
|
||||
|
||||
await saveKeyInSessionStore(
|
||||
SESSION_KEYS.ENCRYPTION_KEY,
|
||||
masterKey
|
||||
);
|
||||
setJustSignedUp(true);
|
||||
router.push(PAGES.VERIFY);
|
||||
} catch (e) {
|
||||
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
|
||||
throw e;
|
||||
}
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton';
|
|||
import TextField from '@mui/material/TextField';
|
||||
import ShowHidePassword from './Form/ShowHidePassword';
|
||||
import { FlexWrapper } from './Container';
|
||||
import { Button } from '@mui/material';
|
||||
import { Button, FormHelperText } from '@mui/material';
|
||||
|
||||
interface formValues {
|
||||
inputValue: string;
|
||||
|
@ -24,8 +24,11 @@ export interface SingleInputFormProps {
|
|||
secondaryButtonAction?: () => void;
|
||||
disableAutoFocus?: boolean;
|
||||
hiddenPreInput?: any;
|
||||
caption?: any;
|
||||
hiddenPostInput?: any;
|
||||
autoComplete?: string;
|
||||
blockButton?: boolean;
|
||||
hiddenLabel?: boolean;
|
||||
}
|
||||
|
||||
export default function SingleInputForm(props: SingleInputFormProps) {
|
||||
|
@ -86,12 +89,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
|||
<form noValidate onSubmit={handleSubmit}>
|
||||
{props.hiddenPreInput}
|
||||
<TextField
|
||||
hiddenLabel={props.hiddenLabel}
|
||||
variant="filled"
|
||||
fullWidth
|
||||
type={showPassword ? 'text' : props.fieldType}
|
||||
id={props.fieldType}
|
||||
name={props.fieldType}
|
||||
label={props.placeholder}
|
||||
{...(props.hiddenLabel
|
||||
? { placeholder: props.placeholder }
|
||||
: { label: props.placeholder })}
|
||||
value={values.inputValue}
|
||||
onChange={handleChange('inputValue')}
|
||||
error={Boolean(errors.inputValue)}
|
||||
|
@ -113,20 +119,45 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
|||
),
|
||||
}}
|
||||
/>
|
||||
<FormHelperText
|
||||
sx={{
|
||||
position: 'relative',
|
||||
top: errors.inputValue ? '-22px' : '0',
|
||||
float: 'right',
|
||||
padding: '0 8px',
|
||||
}}>
|
||||
{props.caption}
|
||||
</FormHelperText>
|
||||
{props.hiddenPostInput}
|
||||
<FlexWrapper justifyContent={'flex-end'}>
|
||||
<FlexWrapper
|
||||
justifyContent={'flex-end'}
|
||||
flexWrap={
|
||||
props.blockButton ? 'wrap-reverse' : 'nowrap'
|
||||
}>
|
||||
{props.secondaryButtonAction && (
|
||||
<Button
|
||||
onClick={props.secondaryButtonAction}
|
||||
size="large"
|
||||
color="secondary"
|
||||
sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }}
|
||||
sx={{
|
||||
'&&&': {
|
||||
mt: !props.blockButton ? 2 : 0.5,
|
||||
mb: !props.blockButton ? 4 : 0,
|
||||
mr: !props.blockButton ? 1 : 0,
|
||||
...buttonSx,
|
||||
},
|
||||
}}
|
||||
{...restSubmitButtonProps}>
|
||||
{constants.CANCEL}
|
||||
</Button>
|
||||
)}
|
||||
<SubmitButton
|
||||
sx={{ mt: 2, ...buttonSx }}
|
||||
sx={{
|
||||
'&&&': {
|
||||
mt: 2,
|
||||
...buttonSx,
|
||||
},
|
||||
}}
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
{...restSubmitButtonProps}
|
||||
|
|
|
@ -26,10 +26,15 @@ const SubmitButton: FC<ButtonProps<'button', SubmitButtonProps>> = ({
|
|||
disabled={disabled || loading || success}
|
||||
sx={{
|
||||
my: 4,
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: (theme) => theme.palette.accent.main,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
},
|
||||
...(loading
|
||||
? {
|
||||
'&.Mui-disabled': {
|
||||
backgroundColor: (theme) =>
|
||||
theme.palette.accent.main,
|
||||
color: (theme) => theme.palette.text.primary,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...sx,
|
||||
}}
|
||||
{...props}>
|
||||
|
|
57
src/components/Titlebar.tsx
Normal file
57
src/components/Titlebar.tsx
Normal file
|
@ -0,0 +1,57 @@
|
|||
import Close from '@mui/icons-material/Close';
|
||||
import ArrowBack from '@mui/icons-material/ArrowBack';
|
||||
import { Box, IconButton, Typography } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { FlexWrapper } from './Container';
|
||||
|
||||
interface Iprops {
|
||||
title: string;
|
||||
caption?: string;
|
||||
onClose: () => void;
|
||||
backIsClose?: boolean;
|
||||
onRootClose?: () => void;
|
||||
actionButton?: JSX.Element;
|
||||
}
|
||||
|
||||
export default function Titlebar({
|
||||
title,
|
||||
caption,
|
||||
onClose,
|
||||
backIsClose,
|
||||
actionButton,
|
||||
onRootClose,
|
||||
}: Iprops): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<FlexWrapper
|
||||
height={48}
|
||||
alignItems={'center'}
|
||||
justifyContent="space-between">
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
color={backIsClose ? 'secondary' : 'primary'}>
|
||||
{backIsClose ? <Close /> : <ArrowBack />}
|
||||
</IconButton>
|
||||
<Box display={'flex'} gap="4px">
|
||||
{actionButton && actionButton}
|
||||
{!backIsClose && (
|
||||
<IconButton onClick={onRootClose} color={'secondary'}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
</Box>
|
||||
</FlexWrapper>
|
||||
<Box py={0.5} px={2}>
|
||||
<Typography variant="h3" fontWeight={'bold'}>
|
||||
{title}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="text.secondary"
|
||||
sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
|
||||
{caption}
|
||||
</Typography>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
import React from 'react';
|
||||
import { IconButton, styled } from '@mui/material';
|
||||
import { ButtonProps, IconButton, styled } from '@mui/material';
|
||||
import FileUploadOutlinedIcon from '@mui/icons-material/FileUploadOutlined';
|
||||
import { Button } from '@mui/material';
|
||||
import constants from 'utils/strings/constants';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
|
||||
const Wrapper = styled('div')`
|
||||
const Wrapper = styled('div')<{ $disableShrink: boolean }>`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
@ -14,22 +14,35 @@ const Wrapper = styled('div')`
|
|||
& .mobile-button {
|
||||
display: none;
|
||||
}
|
||||
@media (max-width: 624px) {
|
||||
${({ $disableShrink }) =>
|
||||
!$disableShrink &&
|
||||
`@media (max-width: 624px) {
|
||||
& .mobile-button {
|
||||
display: block;
|
||||
}
|
||||
& .desktop-button {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}`}
|
||||
`;
|
||||
|
||||
interface Iprops {
|
||||
openUploader: () => void;
|
||||
text?: string;
|
||||
color?: ButtonProps['color'];
|
||||
disableShrink?: boolean;
|
||||
icon?: JSX.Element;
|
||||
}
|
||||
function UploadButton({ openUploader }: Iprops) {
|
||||
function UploadButton({
|
||||
openUploader,
|
||||
text,
|
||||
color,
|
||||
disableShrink,
|
||||
icon,
|
||||
}: Iprops) {
|
||||
return (
|
||||
<Wrapper
|
||||
$disableShrink={disableShrink}
|
||||
style={{
|
||||
cursor: !uploadManager.shouldAllowNewUpload() && 'not-allowed',
|
||||
}}>
|
||||
|
@ -37,9 +50,9 @@ function UploadButton({ openUploader }: Iprops) {
|
|||
onClick={openUploader}
|
||||
disabled={!uploadManager.shouldAllowNewUpload()}
|
||||
className="desktop-button"
|
||||
color="secondary"
|
||||
startIcon={<FileUploadOutlinedIcon />}>
|
||||
{constants.UPLOAD}
|
||||
color={color ?? 'secondary'}
|
||||
startIcon={icon ?? <FileUploadOutlinedIcon />}>
|
||||
{text ?? constants.UPLOAD}
|
||||
</Button>
|
||||
|
||||
<IconButton
|
||||
|
|
|
@ -7,9 +7,10 @@ import { UploadProgressHeader } from './header';
|
|||
import { InProgressSection } from './inProgressSection';
|
||||
import { ResultSection } from './resultSection';
|
||||
import { NotUploadSectionHeader } from './styledComponents';
|
||||
import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
|
||||
import UploadProgressContext from 'contexts/uploadProgress';
|
||||
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
||||
import { APP_DOWNLOAD_URL } from 'utils/common';
|
||||
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
||||
|
||||
export function UploadProgressDialog() {
|
||||
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
||||
|
@ -26,7 +27,8 @@ export function UploadProgressDialog() {
|
|||
finishedUploads.get(UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE)
|
||||
?.length > 0 ||
|
||||
finishedUploads.get(UPLOAD_RESULT.TOO_LARGE)?.length > 0 ||
|
||||
finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0
|
||||
finishedUploads.get(UPLOAD_RESULT.UNSUPPORTED)?.length > 0 ||
|
||||
finishedUploads.get(UPLOAD_RESULT.SKIPPED_VIDEOS)?.length > 0
|
||||
) {
|
||||
setHasUnUploadedFiles(true);
|
||||
} else {
|
||||
|
@ -40,70 +42,85 @@ export function UploadProgressDialog() {
|
|||
<Dialog maxWidth="xs" open={open} onClose={handleClose}>
|
||||
<UploadProgressHeader />
|
||||
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||
uploadStage === UPLOAD_STAGES.FINISH) && (
|
||||
uploadStage === UPLOAD_STAGES.FINISH ||
|
||||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
|
||||
<DialogContent sx={{ '&&&': { px: 0 } }}>
|
||||
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
||||
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
|
||||
<InProgressSection />
|
||||
)}
|
||||
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||
uploadStage === UPLOAD_STAGES.FINISH) && (
|
||||
<>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.UPLOADED}
|
||||
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={
|
||||
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
|
||||
}
|
||||
sectionTitle={
|
||||
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
|
||||
}
|
||||
sectionInfo={
|
||||
constants.THUMBNAIL_GENERATION_FAILED_INFO
|
||||
}
|
||||
/>
|
||||
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.UPLOADED}
|
||||
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={
|
||||
UPLOAD_RESULT.UPLOADED_WITH_STATIC_THUMBNAIL
|
||||
}
|
||||
sectionTitle={
|
||||
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
|
||||
}
|
||||
sectionInfo={constants.THUMBNAIL_GENERATION_FAILED_INFO}
|
||||
/>
|
||||
{uploadStage === UPLOAD_STAGES.FINISH &&
|
||||
hasUnUploadedFiles && (
|
||||
<NotUploadSectionHeader>
|
||||
{constants.FILE_NOT_UPLOADED_LIST}
|
||||
</NotUploadSectionHeader>
|
||||
)}
|
||||
|
||||
{uploadStage === UPLOAD_STAGES.FINISH &&
|
||||
hasUnUploadedFiles && (
|
||||
<NotUploadSectionHeader>
|
||||
{constants.FILE_NOT_UPLOADED_LIST}
|
||||
</NotUploadSectionHeader>
|
||||
)}
|
||||
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.BLOCKED}
|
||||
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||
sectionInfo={constants.ETAGS_BLOCKED(
|
||||
getOSSpecificDesktopAppDownloadLink()
|
||||
)}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.FAILED}
|
||||
sectionTitle={constants.FAILED_UPLOADS}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
|
||||
sectionTitle={constants.SKIPPED_FILES}
|
||||
sectionInfo={constants.SKIPPED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={
|
||||
UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE
|
||||
}
|
||||
sectionTitle={
|
||||
constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS
|
||||
}
|
||||
sectionInfo={
|
||||
constants.LARGER_THAN_AVAILABLE_STORAGE_INFO
|
||||
}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.UNSUPPORTED}
|
||||
sectionTitle={constants.UNSUPPORTED_FILES}
|
||||
sectionInfo={constants.UNSUPPORTED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.TOO_LARGE}
|
||||
sectionTitle={constants.TOO_LARGE_UPLOADS}
|
||||
sectionInfo={constants.TOO_LARGE_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.BLOCKED}
|
||||
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||
sectionInfo={constants.ETAGS_BLOCKED(
|
||||
APP_DOWNLOAD_URL
|
||||
)}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.FAILED}
|
||||
sectionTitle={constants.FAILED_UPLOADS}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.SKIPPED_VIDEOS}
|
||||
sectionTitle={constants.SKIPPED_VIDEOS}
|
||||
sectionInfo={constants.SKIPPED_VIDEOS_INFO(
|
||||
ENTE_WEBSITE_LINK
|
||||
)}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.ALREADY_UPLOADED}
|
||||
sectionTitle={constants.SKIPPED_FILES}
|
||||
sectionInfo={constants.SKIPPED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={
|
||||
UPLOAD_RESULT.LARGER_THAN_AVAILABLE_STORAGE
|
||||
}
|
||||
sectionTitle={
|
||||
constants.LARGER_THAN_AVAILABLE_STORAGE_UPLOADS
|
||||
}
|
||||
sectionInfo={
|
||||
constants.LARGER_THAN_AVAILABLE_STORAGE_INFO
|
||||
}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.UNSUPPORTED}
|
||||
sectionTitle={constants.UNSUPPORTED_FILES}
|
||||
sectionInfo={constants.UNSUPPORTED_INFO}
|
||||
/>
|
||||
<ResultSection
|
||||
uploadResult={UPLOAD_RESULT.TOO_LARGE}
|
||||
sectionTitle={constants.TOO_LARGE_UPLOADS}
|
||||
sectionInfo={constants.TOO_LARGE_INFO}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</DialogContent>
|
||||
)}
|
||||
{uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}
|
||||
|
|
|
@ -10,17 +10,19 @@ import {
|
|||
} from './section';
|
||||
import UploadProgressContext from 'contexts/uploadProgress';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { UPLOAD_STAGES } from 'constants/upload';
|
||||
|
||||
export const InProgressSection = () => {
|
||||
const { inProgressUploads, hasLivePhotos, uploadFileNames } = useContext(
|
||||
UploadProgressContext
|
||||
);
|
||||
const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } =
|
||||
useContext(UploadProgressContext);
|
||||
const fileList = inProgressUploads ?? [];
|
||||
|
||||
return (
|
||||
<UploadProgressSection defaultExpanded>
|
||||
<UploadProgressSection>
|
||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||
{constants.INPROGRESS_UPLOADS}
|
||||
{uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||
? constants.INPROGRESS_METADATA_EXTRACTION
|
||||
: constants.INPROGRESS_UPLOADS}
|
||||
</UploadProgressSectionTitle>
|
||||
<UploadProgressSectionContent>
|
||||
{hasLivePhotos && (
|
||||
|
@ -30,8 +32,13 @@ export const InProgressSection = () => {
|
|||
fileList={fileList.map(({ localFileID, progress }) => (
|
||||
<InProgressItemContainer key={localFileID}>
|
||||
<span>{uploadFileNames.get(localFileID)}</span>
|
||||
<span className="separator">{`-`}</span>
|
||||
<span>{`${progress}%`}</span>
|
||||
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="separator">{`-`}</span>
|
||||
<span>{`${progress}%`}</span>
|
||||
</>
|
||||
)}
|
||||
</InProgressItemContainer>
|
||||
))}
|
||||
/>
|
||||
|
|
|
@ -20,6 +20,8 @@ function UploadProgressSubtitleText() {
|
|||
return (
|
||||
<Typography color="text.secondary">
|
||||
{uploadStage === UPLOAD_STAGES.UPLOADING
|
||||
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
|
||||
: uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
|
||||
: constants.UPLOAD_STAGE_MESSAGE[uploadStage]}
|
||||
</Typography>
|
||||
|
|
|
@ -1,19 +1,48 @@
|
|||
import React from 'react';
|
||||
import React, { useContext, useEffect, useRef } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { default as FileUploadIcon } from '@mui/icons-material/ImageOutlined';
|
||||
import { default as FolderUploadIcon } from '@mui/icons-material/PermMediaOutlined';
|
||||
import GoogleIcon from '@mui/icons-material/Google';
|
||||
import { UploadTypeOption } from './option';
|
||||
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
|
||||
import DialogTitleWithCloseButton, {
|
||||
dialogCloseHandler,
|
||||
} from 'components/DialogBox/TitleWithCloseButton';
|
||||
import { Box, Dialog, Stack, Typography } from '@mui/material';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { isMobileOrTable } from 'utils/common/deviceDetection';
|
||||
|
||||
interface Iprops {
|
||||
onClose: () => void;
|
||||
show: boolean;
|
||||
uploadFiles: () => void;
|
||||
uploadFolders: () => void;
|
||||
uploadGoogleTakeoutZips: () => void;
|
||||
hideZipUploadOption?: boolean;
|
||||
}
|
||||
export default function UploadTypeSelector({
|
||||
onHide,
|
||||
onClose,
|
||||
show,
|
||||
uploadFiles,
|
||||
uploadFolders,
|
||||
uploadGoogleTakeoutZips,
|
||||
}) {
|
||||
hideZipUploadOption,
|
||||
}: Iprops) {
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
const directlyShowUploadFiles = useRef(isMobileOrTable());
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
show &&
|
||||
directlyShowUploadFiles.current &&
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
) {
|
||||
uploadFiles();
|
||||
onClose();
|
||||
}
|
||||
}, [show]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={show}
|
||||
|
@ -24,9 +53,11 @@ export default function UploadTypeSelector({
|
|||
[theme.breakpoints.down(360)]: { p: 0 },
|
||||
}),
|
||||
}}
|
||||
onClose={onHide}>
|
||||
<DialogTitleWithCloseButton onClose={onHide}>
|
||||
{constants.UPLOAD}
|
||||
onClose={dialogCloseHandler({ onClose })}>
|
||||
<DialogTitleWithCloseButton onClose={onClose}>
|
||||
{publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
? constants.SELECT_PHOTOS
|
||||
: constants.UPLOAD}
|
||||
</DialogTitleWithCloseButton>
|
||||
<Box p={1.5} pt={0.5}>
|
||||
<Stack spacing={0.5}>
|
||||
|
@ -40,11 +71,13 @@ export default function UploadTypeSelector({
|
|||
startIcon={<FolderUploadIcon />}>
|
||||
{constants.UPLOAD_DIRS}
|
||||
</UploadTypeOption>
|
||||
<UploadTypeOption
|
||||
onClick={uploadGoogleTakeoutZips}
|
||||
startIcon={<GoogleIcon />}>
|
||||
{constants.UPLOAD_GOOGLE_TAKEOUT}
|
||||
</UploadTypeOption>
|
||||
{!hideZipUploadOption && (
|
||||
<UploadTypeOption
|
||||
onClick={uploadGoogleTakeoutZips}
|
||||
startIcon={<GoogleIcon />}>
|
||||
{constants.UPLOAD_GOOGLE_TAKEOUT}
|
||||
</UploadTypeOption>
|
||||
)}
|
||||
</Stack>
|
||||
<Typography p={1.5} pt={4} color="text.secondary">
|
||||
{constants.DRAG_AND_DROP_HINT}
|
||||
|
|
|
@ -10,7 +10,6 @@ import { SetCollections, SetCollectionSelectorAttributes } from 'types/gallery';
|
|||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { logError } from 'utils/sentry';
|
||||
import UploadManager from 'services/upload/uploadManager';
|
||||
import uploadManager from 'services/upload/uploadManager';
|
||||
import ImportService from 'services/importService';
|
||||
import isElectron from 'is-electron';
|
||||
|
@ -40,7 +39,10 @@ import {
|
|||
PICKED_UPLOAD_TYPE,
|
||||
} from 'constants/upload';
|
||||
import importService from 'services/importService';
|
||||
import { getDownloadAppMessage } from 'utils/ui';
|
||||
import {
|
||||
getDownloadAppMessage,
|
||||
getRootLevelFileWithFolderNotAllowMessage,
|
||||
} from 'utils/ui';
|
||||
import UploadTypeSelector from './UploadTypeSelector';
|
||||
import {
|
||||
filterOutSystemFiles,
|
||||
|
@ -49,21 +51,29 @@ import {
|
|||
} from 'utils/upload';
|
||||
import { getUserOwnedCollections } from 'utils/collection';
|
||||
import billingService from 'services/billingService';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import UserNameInputDialog from 'components/UserNameInputDialog';
|
||||
import {
|
||||
getPublicCollectionUID,
|
||||
getPublicCollectionUploaderName,
|
||||
savePublicCollectionUploaderName,
|
||||
} from 'services/publicCollectionService';
|
||||
|
||||
const FIRST_ALBUM_NAME = 'My First Album';
|
||||
|
||||
interface Props {
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
closeCollectionSelector: () => void;
|
||||
closeCollectionSelector?: () => void;
|
||||
closeUploadTypeSelector: () => void;
|
||||
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
setCollectionSelectorAttributes?: SetCollectionSelectorAttributes;
|
||||
setCollectionNamerAttributes?: SetCollectionNamerAttributes;
|
||||
setLoading: SetLoading;
|
||||
setShouldDisableDropzone: (value: boolean) => void;
|
||||
showCollectionSelector: () => void;
|
||||
showCollectionSelector?: () => void;
|
||||
setFiles: SetFiles;
|
||||
setCollections: SetCollections;
|
||||
isFirstUpload: boolean;
|
||||
setCollections?: SetCollections;
|
||||
isFirstUpload?: boolean;
|
||||
uploadTypeSelectorView: boolean;
|
||||
showSessionExpiredMessage: () => void;
|
||||
showUploadFilesDialog: () => void;
|
||||
|
@ -71,9 +81,17 @@ interface Props {
|
|||
webFolderSelectorFiles: File[];
|
||||
webFileSelectorFiles: File[];
|
||||
dragAndDropFiles: File[];
|
||||
zipUploadDisabled?: boolean;
|
||||
uploadCollection?: Collection;
|
||||
}
|
||||
|
||||
export default function Uploader(props: Props) {
|
||||
const appContext = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
|
||||
const [uploadProgressView, setUploadProgressView] = useState(false);
|
||||
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
|
||||
UPLOAD_STAGES.START
|
||||
|
@ -92,11 +110,13 @@ export default function Uploader(props: Props) {
|
|||
const [hasLivePhotos, setHasLivePhotos] = useState(false);
|
||||
|
||||
const [choiceModalView, setChoiceModalView] = useState(false);
|
||||
const [userNameInputDialogView, setUserNameInputDialogView] =
|
||||
useState(false);
|
||||
const [importSuggestion, setImportSuggestion] = useState<ImportSuggestion>(
|
||||
DEFAULT_IMPORT_SUGGESTION
|
||||
);
|
||||
const appContext = useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
|
||||
const [webFiles, setWebFiles] = useState([]);
|
||||
|
||||
const toUploadFiles = useRef<File[] | ElectronFile[]>(null);
|
||||
const isPendingDesktopUpload = useRef(false);
|
||||
|
@ -105,20 +125,33 @@ export default function Uploader(props: Props) {
|
|||
const pickedUploadType = useRef<PICKED_UPLOAD_TYPE>(null);
|
||||
const zipPaths = useRef<string[]>(null);
|
||||
const currentUploadPromise = useRef<Promise<void>>(null);
|
||||
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(null);
|
||||
const [webFiles, setWebFiles] = useState([]);
|
||||
const uploadRunning = useRef(false);
|
||||
const uploaderNameRef = useRef<string>(null);
|
||||
|
||||
const closeUploadProgress = () => setUploadProgressView(false);
|
||||
const showUserNameInputDialog = () => setUserNameInputDialogView(true);
|
||||
|
||||
const setCollectionName = (collectionName: string) => {
|
||||
isPendingDesktopUpload.current = true;
|
||||
pendingDesktopUploadCollectionName.current = collectionName;
|
||||
};
|
||||
|
||||
const uploadRunning = useRef(false);
|
||||
const handleChoiceModalClose = () => {
|
||||
setChoiceModalView(false);
|
||||
uploadRunning.current = false;
|
||||
};
|
||||
const handleCollectionSelectorCancel = () => {
|
||||
uploadRunning.current = false;
|
||||
appContext.resetSharedFiles();
|
||||
};
|
||||
|
||||
const handleUserNameInputDialogClose = () => {
|
||||
setUserNameInputDialogView(false);
|
||||
uploadRunning.current = false;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
UploadManager.init(
|
||||
uploadManager.init(
|
||||
{
|
||||
setPercentComplete,
|
||||
setUploadCounter,
|
||||
|
@ -128,12 +161,16 @@ export default function Uploader(props: Props) {
|
|||
setUploadFilenames: setUploadFileNames,
|
||||
setHasLivePhotos,
|
||||
},
|
||||
props.setFiles
|
||||
props.setFiles,
|
||||
publicCollectionGalleryContext
|
||||
);
|
||||
|
||||
if (isElectron() && ImportService.checkAllElectronAPIsExists()) {
|
||||
ImportService.getPendingUploads().then(
|
||||
({ files: electronFiles, collectionName, type }) => {
|
||||
addLogLine(
|
||||
`found pending desktop upload, resuming uploads`
|
||||
);
|
||||
resumeDesktopUpload(type, electronFiles, collectionName);
|
||||
}
|
||||
);
|
||||
|
@ -144,7 +181,11 @@ export default function Uploader(props: Props) {
|
|||
appContext.setIsFolderSyncRunning
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
}, [
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
publicCollectionGalleryContext.token,
|
||||
publicCollectionGalleryContext.passwordToken,
|
||||
]);
|
||||
|
||||
// this handles the change of selectorFiles changes on web when user selects
|
||||
// files for upload through the opened file/folder selector or dragAndDrop them
|
||||
|
@ -159,13 +200,16 @@ export default function Uploader(props: Props) {
|
|||
pickedUploadType.current === PICKED_UPLOAD_TYPE.FOLDERS &&
|
||||
props.webFolderSelectorFiles?.length > 0
|
||||
) {
|
||||
addLogLine(`received folder upload request`);
|
||||
setWebFiles(props.webFolderSelectorFiles);
|
||||
} else if (
|
||||
pickedUploadType.current === PICKED_UPLOAD_TYPE.FILES &&
|
||||
props.webFileSelectorFiles?.length > 0
|
||||
) {
|
||||
addLogLine(`received file upload request`);
|
||||
setWebFiles(props.webFileSelectorFiles);
|
||||
} else if (props.dragAndDropFiles?.length > 0) {
|
||||
addLogLine(`received drag and drop upload request`);
|
||||
setWebFiles(props.dragAndDropFiles);
|
||||
}
|
||||
}, [
|
||||
|
@ -180,17 +224,37 @@ export default function Uploader(props: Props) {
|
|||
webFiles?.length > 0 ||
|
||||
appContext.sharedFiles?.length > 0
|
||||
) {
|
||||
addLogLine(
|
||||
`upload request type:${
|
||||
electronFiles?.length > 0
|
||||
? 'electronFiles'
|
||||
: webFiles?.length > 0
|
||||
? 'webFiles'
|
||||
: 'sharedFiles'
|
||||
} count ${
|
||||
electronFiles?.length ??
|
||||
webFiles?.length ??
|
||||
appContext?.sharedFiles.length
|
||||
}`
|
||||
);
|
||||
if (uploadRunning.current) {
|
||||
if (watchFolderService.isUploadRunning()) {
|
||||
addLogLine(
|
||||
'watchFolder upload was running, pausing it to run user upload'
|
||||
);
|
||||
// pause watch folder service on user upload
|
||||
watchFolderService.pauseRunningSync();
|
||||
} else {
|
||||
addLogLine(
|
||||
'an upload is already running, rejecting new upload request'
|
||||
);
|
||||
// no-op
|
||||
// a user upload is already in progress
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (isCanvasBlocked()) {
|
||||
addLogLine('canvas blocked, blocking upload');
|
||||
appContext.setDialogMessage({
|
||||
title: constants.CANVAS_BLOCKED_TITLE,
|
||||
|
||||
|
@ -235,7 +299,8 @@ export default function Uploader(props: Props) {
|
|||
handleCollectionCreationAndUpload(
|
||||
importSuggestion,
|
||||
props.isFirstUpload,
|
||||
pickedUploadType.current
|
||||
pickedUploadType.current,
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
);
|
||||
pickedUploadType.current = null;
|
||||
props.setLoading(false);
|
||||
|
@ -256,14 +321,20 @@ export default function Uploader(props: Props) {
|
|||
};
|
||||
|
||||
const preCollectionCreationAction = async () => {
|
||||
props.closeCollectionSelector();
|
||||
props.closeCollectionSelector?.();
|
||||
props.setShouldDisableDropzone(!uploadManager.shouldAllowNewUpload());
|
||||
setUploadStage(UPLOAD_STAGES.START);
|
||||
setUploadProgressView(true);
|
||||
};
|
||||
|
||||
const uploadFilesToExistingCollection = async (collection: Collection) => {
|
||||
const uploadFilesToExistingCollection = async (
|
||||
collection: Collection,
|
||||
uploaderName?: string
|
||||
) => {
|
||||
try {
|
||||
addLogLine(
|
||||
`upload file to an existing collection - "${collection.name}"`
|
||||
);
|
||||
await preCollectionCreationAction();
|
||||
const filesWithCollectionToUpload: FileWithCollection[] =
|
||||
toUploadFiles.current.map((file, index) => ({
|
||||
|
@ -271,10 +342,11 @@ export default function Uploader(props: Props) {
|
|||
localID: index,
|
||||
collectionID: collection.id,
|
||||
}));
|
||||
waitInQueueAndUploadFiles(filesWithCollectionToUpload, [
|
||||
collection,
|
||||
]);
|
||||
toUploadFiles.current = null;
|
||||
waitInQueueAndUploadFiles(
|
||||
filesWithCollectionToUpload,
|
||||
[collection],
|
||||
uploaderName
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'Failed to upload files to existing collections');
|
||||
}
|
||||
|
@ -285,8 +357,11 @@ export default function Uploader(props: Props) {
|
|||
collectionName?: string
|
||||
) => {
|
||||
try {
|
||||
addLogLine(
|
||||
`upload file to an new collections strategy:${strategy} ,collectionName:${collectionName}`
|
||||
);
|
||||
await preCollectionCreationAction();
|
||||
const filesWithCollectionToUpload: FileWithCollection[] = [];
|
||||
let filesWithCollectionToUpload: FileWithCollection[] = [];
|
||||
const collections: Collection[] = [];
|
||||
let collectionNameToFilesMap = new Map<
|
||||
string,
|
||||
|
@ -302,6 +377,9 @@ export default function Uploader(props: Props) {
|
|||
toUploadFiles.current
|
||||
);
|
||||
}
|
||||
addLogLine(
|
||||
`upload collections - [${[...collectionNameToFilesMap.keys()]}]`
|
||||
);
|
||||
try {
|
||||
const existingCollection = getUserOwnedCollections(
|
||||
await syncCollections()
|
||||
|
@ -320,13 +398,14 @@ export default function Uploader(props: Props) {
|
|||
...existingCollection,
|
||||
...collections,
|
||||
]);
|
||||
filesWithCollectionToUpload.push(
|
||||
filesWithCollectionToUpload = [
|
||||
...filesWithCollectionToUpload,
|
||||
...files.map((file) => ({
|
||||
localID: index++,
|
||||
collectionID: collection.id,
|
||||
file,
|
||||
}))
|
||||
);
|
||||
})),
|
||||
];
|
||||
}
|
||||
} catch (e) {
|
||||
closeUploadProgress();
|
||||
|
@ -348,13 +427,18 @@ export default function Uploader(props: Props) {
|
|||
|
||||
const waitInQueueAndUploadFiles = (
|
||||
filesWithCollectionToUploadIn: FileWithCollection[],
|
||||
collections: Collection[]
|
||||
collections: Collection[],
|
||||
uploaderName?: string
|
||||
) => {
|
||||
const currentPromise = currentUploadPromise.current;
|
||||
currentUploadPromise.current = waitAndRun(
|
||||
currentPromise,
|
||||
async () =>
|
||||
await uploadFiles(filesWithCollectionToUploadIn, collections)
|
||||
await uploadFiles(
|
||||
filesWithCollectionToUploadIn,
|
||||
collections,
|
||||
uploaderName
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -372,9 +456,11 @@ export default function Uploader(props: Props) {
|
|||
|
||||
const uploadFiles = async (
|
||||
filesWithCollectionToUploadIn: FileWithCollection[],
|
||||
collections: Collection[]
|
||||
collections: Collection[],
|
||||
uploaderName?: string
|
||||
) => {
|
||||
try {
|
||||
addLogLine('uploadFiles called');
|
||||
preUploadAction();
|
||||
if (
|
||||
isElectron() &&
|
||||
|
@ -399,7 +485,8 @@ export default function Uploader(props: Props) {
|
|||
const shouldCloseUploadProgress =
|
||||
await uploadManager.queueFilesForUpload(
|
||||
filesWithCollectionToUploadIn,
|
||||
collections
|
||||
collections,
|
||||
uploaderName
|
||||
);
|
||||
if (shouldCloseUploadProgress) {
|
||||
closeUploadProgress();
|
||||
|
@ -416,6 +503,7 @@ export default function Uploader(props: Props) {
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
logError(err, 'failed to upload files');
|
||||
showUserFacingError(err.message);
|
||||
closeUploadProgress();
|
||||
throw err;
|
||||
|
@ -426,14 +514,18 @@ export default function Uploader(props: Props) {
|
|||
|
||||
const retryFailed = async () => {
|
||||
try {
|
||||
addLogLine('user retrying failed upload');
|
||||
const filesWithCollections =
|
||||
await uploadManager.getFailedFilesWithCollections();
|
||||
uploadManager.getFailedFilesWithCollections();
|
||||
const uploaderName = uploadManager.getUploaderName();
|
||||
await preUploadAction();
|
||||
await uploadManager.queueFilesForUpload(
|
||||
filesWithCollections.files,
|
||||
filesWithCollections.collections
|
||||
filesWithCollections.collections,
|
||||
uploaderName
|
||||
);
|
||||
} catch (err) {
|
||||
logError(err, 'retry failed files failed');
|
||||
showUserFacingError(err.message);
|
||||
closeUploadProgress();
|
||||
} finally {
|
||||
|
@ -449,35 +541,33 @@ export default function Uploader(props: Props) {
|
|||
case CustomError.SUBSCRIPTION_EXPIRED:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.SUBSCRIPTION_EXPIRED,
|
||||
action: {
|
||||
text: constants.RENEW_NOW,
|
||||
callback: billingService.redirectToCustomerPortal,
|
||||
},
|
||||
subtext: constants.SUBSCRIPTION_EXPIRED,
|
||||
message: constants.RENEW_NOW,
|
||||
onClick: () => billingService.redirectToCustomerPortal(),
|
||||
};
|
||||
break;
|
||||
case CustomError.STORAGE_QUOTA_EXCEEDED:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.STORAGE_QUOTA_EXCEEDED,
|
||||
action: {
|
||||
text: constants.UPGRADE_NOW,
|
||||
callback: galleryContext.showPlanSelectorModal,
|
||||
},
|
||||
icon: <DiscFullIcon fontSize="large" />,
|
||||
subtext: constants.STORAGE_QUOTA_EXCEEDED,
|
||||
message: constants.UPGRADE_NOW,
|
||||
onClick: () => galleryContext.showPlanSelectorModal(),
|
||||
startIcon: <DiscFullIcon />,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.UNKNOWN_ERROR,
|
||||
onClick: () => null,
|
||||
};
|
||||
}
|
||||
galleryContext.setNotificationAttributes(notification);
|
||||
appContext.setNotificationAttributes(notification);
|
||||
}
|
||||
|
||||
const uploadToSingleNewCollection = (collectionName: string) => {
|
||||
if (collectionName) {
|
||||
addLogLine(`upload to single collection - "${collectionName}"`);
|
||||
uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.SINGLE_COLLECTION,
|
||||
collectionName
|
||||
|
@ -495,45 +585,75 @@ export default function Uploader(props: Props) {
|
|||
});
|
||||
};
|
||||
|
||||
const handleCollectionCreationAndUpload = (
|
||||
const handleCollectionCreationAndUpload = async (
|
||||
importSuggestion: ImportSuggestion,
|
||||
isFirstUpload: boolean,
|
||||
pickedUploadType: PICKED_UPLOAD_TYPE
|
||||
pickedUploadType: PICKED_UPLOAD_TYPE,
|
||||
accessedThroughSharedURL?: boolean
|
||||
) => {
|
||||
if (isPendingDesktopUpload.current) {
|
||||
isPendingDesktopUpload.current = false;
|
||||
if (pendingDesktopUploadCollectionName.current) {
|
||||
uploadToSingleNewCollection(
|
||||
pendingDesktopUploadCollectionName.current
|
||||
try {
|
||||
if (accessedThroughSharedURL) {
|
||||
addLogLine(
|
||||
`uploading files to pulbic collection - ${props.uploadCollection.name} - ${props.uploadCollection.id}`
|
||||
);
|
||||
pendingDesktopUploadCollectionName.current = null;
|
||||
} else {
|
||||
const uploaderName = await getPublicCollectionUploaderName(
|
||||
getPublicCollectionUID(publicCollectionGalleryContext.token)
|
||||
);
|
||||
uploaderNameRef.current = uploaderName;
|
||||
showUserNameInputDialog();
|
||||
return;
|
||||
}
|
||||
if (isPendingDesktopUpload.current) {
|
||||
isPendingDesktopUpload.current = false;
|
||||
if (pendingDesktopUploadCollectionName.current) {
|
||||
addLogLine(
|
||||
`upload pending files to collection - ${pendingDesktopUploadCollectionName.current}`
|
||||
);
|
||||
uploadToSingleNewCollection(
|
||||
pendingDesktopUploadCollectionName.current
|
||||
);
|
||||
pendingDesktopUploadCollectionName.current = null;
|
||||
} else {
|
||||
addLogLine(
|
||||
`pending upload - strategy - "multiple collections" `
|
||||
);
|
||||
uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (isElectron() && pickedUploadType === PICKED_UPLOAD_TYPE.ZIPS) {
|
||||
addLogLine('uploading zip files');
|
||||
uploadFilesToNewCollections(
|
||||
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) => {
|
||||
let files: ElectronFile[];
|
||||
pickedUploadType.current = type;
|
||||
|
@ -547,6 +667,9 @@ export default function Uploader(props: Props) {
|
|||
zipPaths.current = response.zipPaths;
|
||||
}
|
||||
if (files?.length > 0) {
|
||||
addLogLine(
|
||||
` desktop upload for type:${type} and fileCount: ${files?.length} requested`
|
||||
);
|
||||
setElectronFiles(files);
|
||||
props.closeUploadTypeSelector();
|
||||
}
|
||||
|
@ -579,26 +702,57 @@ export default function Uploader(props: Props) {
|
|||
const handleFolderUpload = handleUpload(PICKED_UPLOAD_TYPE.FOLDERS);
|
||||
const handleZipUpload = handleUpload(PICKED_UPLOAD_TYPE.ZIPS);
|
||||
|
||||
const handlePublicUpload = async (
|
||||
uploaderName: string,
|
||||
skipSave?: boolean
|
||||
) => {
|
||||
try {
|
||||
if (!skipSave) {
|
||||
savePublicCollectionUploaderName(
|
||||
getPublicCollectionUID(
|
||||
publicCollectionGalleryContext.token
|
||||
),
|
||||
uploaderName
|
||||
);
|
||||
}
|
||||
await uploadFilesToExistingCollection(
|
||||
props.uploadCollection,
|
||||
uploaderName
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'public upload failed ');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadToSingleCollection = () => {
|
||||
uploadToSingleNewCollection(importSuggestion.rootFolderName);
|
||||
};
|
||||
|
||||
const handleUploadToMultipleCollections = () => {
|
||||
if (importSuggestion.hasRootLevelFileWithFolder) {
|
||||
appContext.setDialogMessage(
|
||||
getRootLevelFileWithFolderNotAllowMessage()
|
||||
);
|
||||
return;
|
||||
}
|
||||
uploadFilesToNewCollections(UPLOAD_STRATEGY.COLLECTION_PER_FOLDER);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<UploadStrategyChoiceModal
|
||||
open={choiceModalView}
|
||||
onClose={() => setChoiceModalView(false)}
|
||||
uploadToSingleCollection={() =>
|
||||
uploadToSingleNewCollection(importSuggestion.rootFolderName)
|
||||
}
|
||||
uploadToMultipleCollection={() =>
|
||||
uploadFilesToNewCollections(
|
||||
UPLOAD_STRATEGY.COLLECTION_PER_FOLDER
|
||||
)
|
||||
}
|
||||
onClose={handleChoiceModalClose}
|
||||
uploadToSingleCollection={handleUploadToSingleCollection}
|
||||
uploadToMultipleCollection={handleUploadToMultipleCollections}
|
||||
/>
|
||||
<UploadTypeSelector
|
||||
show={props.uploadTypeSelectorView}
|
||||
onHide={props.closeUploadTypeSelector}
|
||||
onClose={props.closeUploadTypeSelector}
|
||||
uploadFiles={handleFileUpload}
|
||||
uploadFolders={handleFolderUpload}
|
||||
uploadGoogleTakeoutZips={handleZipUpload}
|
||||
hideZipUploadOption={props.zipUploadDisabled}
|
||||
/>
|
||||
<UploadProgress
|
||||
open={uploadProgressView}
|
||||
|
@ -613,6 +767,13 @@ export default function Uploader(props: Props) {
|
|||
finishedUploads={finishedUploads}
|
||||
cancelUploads={cancelUploads}
|
||||
/>
|
||||
<UserNameInputDialog
|
||||
open={userNameInputDialogView}
|
||||
onClose={handleUserNameInputDialogClose}
|
||||
onNameSubmit={handlePublicUpload}
|
||||
toUploadFilesCount={toUploadFiles.current?.length}
|
||||
uploaderName={uploaderNameRef.current}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
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 constants from 'utils/strings/constants';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import SingleInputForm, {
|
||||
SingleInputFormProps,
|
||||
} from 'components/SingleInputForm';
|
||||
|
@ -10,6 +9,7 @@ import { CustomError } from 'utils/error';
|
|||
|
||||
import { Input } from '@mui/material';
|
||||
import { KeyAttributes, User } from 'types/user';
|
||||
import ComlinkCryptoWorker from 'utils/comlink/ComlinkCryptoWorker';
|
||||
|
||||
export interface VerifyMasterPasswordFormProps {
|
||||
user: User;
|
||||
|
@ -29,7 +29,7 @@ export default function VerifyMasterPasswordForm({
|
|||
setFieldError
|
||||
) => {
|
||||
try {
|
||||
const cryptoWorker = await new CryptoWorker();
|
||||
const cryptoWorker = await ComlinkCryptoWorker.getInstance();
|
||||
let kek: string = null;
|
||||
try {
|
||||
kek = await cryptoWorker.deriveKey(
|
||||
|
@ -43,7 +43,7 @@ export default function VerifyMasterPasswordForm({
|
|||
throw Error(CustomError.WEAK_DEVICE);
|
||||
}
|
||||
try {
|
||||
const key: string = await cryptoWorker.decryptB64(
|
||||
const key = await cryptoWorker.decryptB64(
|
||||
keyAttributes.encryptedKey,
|
||||
keyAttributes.keyDecryptionNonce,
|
||||
kek
|
||||
|
|
|
@ -3,15 +3,12 @@ import { Box } from '@mui/material';
|
|||
import { styled } from '@mui/material/styles';
|
||||
import VerticallyCentered from 'components/Container';
|
||||
|
||||
export const MappingsContainer = styled(Box)(({ theme }) => ({
|
||||
export const MappingsContainer = styled(Box)(() => ({
|
||||
height: '278px',
|
||||
overflow: 'auto',
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
},
|
||||
}));
|
||||
|
||||
export const NoMappingsContainer = styled(VerticallyCentered)({
|
||||
|
|
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>
|
||||
);
|
||||
}
|
|
@ -2,9 +2,8 @@ import { FluidContainer } from 'components/Container';
|
|||
import { SelectionBar } from '../../Navbar/SelectionBar';
|
||||
import constants from 'utils/strings/constants';
|
||||
import React, { useContext } from 'react';
|
||||
import { Box, IconButton, styled } from '@mui/material';
|
||||
import { Box, IconButton, styled, Tooltip } from '@mui/material';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { IconWithMessage } from 'components/IconWithMessage';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import BackButton from '@mui/icons-material/ArrowBackOutlined';
|
||||
|
@ -78,11 +77,11 @@ export default function DeduplicateOptions({
|
|||
<div>
|
||||
<VerticalLine />
|
||||
</div>
|
||||
<IconWithMessage message={constants.DELETE}>
|
||||
<Tooltip title={constants.DELETE}>
|
||||
<IconButton onClick={trashHandler}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
</Tooltip>
|
||||
</SelectionBar>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,34 +1,12 @@
|
|||
import Link, { LinkProps } from '@mui/material/Link';
|
||||
import { ButtonProps, Link, LinkProps } from '@mui/material';
|
||||
import React, { FC } from 'react';
|
||||
import { ButtonProps } from 'react-bootstrap';
|
||||
|
||||
export enum ButtonVariant {
|
||||
success = 'success',
|
||||
danger = 'danger',
|
||||
secondary = 'secondary',
|
||||
warning = 'warning',
|
||||
}
|
||||
export type LinkButtonProps = React.PropsWithChildren<{
|
||||
onClick: () => void;
|
||||
variant?: string;
|
||||
style?: React.CSSProperties;
|
||||
}>;
|
||||
|
||||
export function getVariantColor(variant: string) {
|
||||
switch (variant) {
|
||||
case ButtonVariant.success:
|
||||
return '#51cd7c';
|
||||
case ButtonVariant.danger:
|
||||
return '#c93f3f';
|
||||
case ButtonVariant.secondary:
|
||||
return '#858585';
|
||||
case ButtonVariant.warning:
|
||||
return '#D7BB63';
|
||||
default:
|
||||
return '#d1d1d1';
|
||||
}
|
||||
}
|
||||
|
||||
const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
||||
children,
|
||||
sx,
|
||||
|
@ -41,6 +19,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
|||
sx={{
|
||||
color: 'text.primary',
|
||||
textDecoration: 'underline rgba(255, 255, 255, 0.4)',
|
||||
paddingBottom: 0.5,
|
||||
'&:hover': {
|
||||
color: `${color}.main`,
|
||||
textDecoration: `underline `,
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import NavbarBase from 'components/Navbar/base';
|
||||
import SidebarToggler from 'components/Navbar/SidebarToggler';
|
||||
import { getNonTrashedUniqueUserFiles } from 'utils/file';
|
||||
import SearchBar from 'components/Search/SearchBar';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
@ -36,7 +35,7 @@ export function GalleryNavbar({
|
|||
isInSearchMode={isInSearchMode}
|
||||
setIsInSearchMode={setIsInSearchMode}
|
||||
collections={collections}
|
||||
files={getNonTrashedUniqueUserFiles(files)}
|
||||
files={files}
|
||||
setActiveCollection={setActiveCollection}
|
||||
updateSearch={updateSearch}
|
||||
/>
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
hasPaidSubscription,
|
||||
getTotalFamilyUsage,
|
||||
isPartOfFamily,
|
||||
isSubscriptionActive,
|
||||
} from 'utils/billing';
|
||||
import { reverseString } from 'utils/common';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
|
@ -68,18 +69,19 @@ function PlanSelectorCard(props: Props) {
|
|||
const main = async () => {
|
||||
try {
|
||||
props.setLoading(true);
|
||||
let plans = await billingService.getPlans();
|
||||
|
||||
const planNotListed =
|
||||
plans.filter((plan) =>
|
||||
isUserSubscribedPlan(plan, subscription)
|
||||
).length === 0;
|
||||
if (
|
||||
subscription &&
|
||||
!isOnFreePlan(subscription) &&
|
||||
planNotListed
|
||||
) {
|
||||
plans = [planForSubscription(subscription), ...plans];
|
||||
const plans = await billingService.getPlans();
|
||||
if (isSubscriptionActive(subscription)) {
|
||||
const planNotListed =
|
||||
plans.filter((plan) =>
|
||||
isUserSubscribedPlan(plan, subscription)
|
||||
).length === 0;
|
||||
if (
|
||||
subscription &&
|
||||
!isOnFreePlan(subscription) &&
|
||||
planNotListed
|
||||
) {
|
||||
plans.push(planForSubscription(subscription));
|
||||
}
|
||||
}
|
||||
setPlans(plans);
|
||||
} catch (e) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Stack } from '@mui/material';
|
|||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
import { Subscription } from 'types/billing';
|
||||
import { SetLoading } from 'types/gallery';
|
||||
import {
|
||||
activateSubscription,
|
||||
cancelSubscription,
|
||||
|
@ -15,7 +16,7 @@ import ManageSubscriptionButton from './button';
|
|||
interface Iprops {
|
||||
subscription: Subscription;
|
||||
closeModal: () => void;
|
||||
setLoading: (value: boolean) => void;
|
||||
setLoading: SetLoading;
|
||||
}
|
||||
|
||||
export function ManageSubscription({
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue