Merge branch 'main' into add-comlink-types
This commit is contained in:
commit
fb32c6de5b
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,59 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"env": {
|
"parserOptions": {
|
||||||
"browser": true,
|
"project": ["./tsconfig.json"]
|
||||||
"es2021": true,
|
|
||||||
"node": true
|
|
||||||
},
|
},
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:react/recommended",
|
"next/core-web-vitals",
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"google",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
"prettier"
|
"prettier"
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"plugins": ["@typescript-eslint"],
|
||||||
"parserOptions": {
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"ecmaVersion": 12,
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"react",
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent":"off",
|
"indent": "off",
|
||||||
"class-methods-use-this": "off",
|
"class-methods-use-this": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
"react/no-unescaped-entities": "off",
|
"react/no-unescaped-entities": "off",
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"@typescript-eslint/no-unused-vars": [
|
"@typescript-eslint/no-unused-vars": ["error"],
|
||||||
"error"
|
|
||||||
],
|
|
||||||
"require-jsdoc": "off",
|
"require-jsdoc": "off",
|
||||||
"valid-jsdoc": "off",
|
"valid-jsdoc": "off",
|
||||||
"max-len": "off",
|
"max-len": "off",
|
||||||
"new-cap": "off",
|
"new-cap": "off",
|
||||||
"no-invalid-this": "off",
|
"no-invalid-this": "off",
|
||||||
"eqeqeq": "error",
|
"eqeqeq": "error",
|
||||||
"object-curly-spacing": [
|
"object-curly-spacing": ["error", "always"],
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"space-before-function-paren": "off",
|
"space-before-function-paren": "off",
|
||||||
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
|
"operator-linebreak": [
|
||||||
},
|
"error",
|
||||||
"settings": {
|
"after",
|
||||||
"react": {
|
{ "overrides": { "?": "before", ":": "before" } }
|
||||||
"version": "detect"
|
],
|
||||||
}
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
},
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
"globals": {
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
"JSX": "readonly",
|
"@typescript-eslint/no-inferrable-types": "off",
|
||||||
"NodeJS": "readonly",
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
"ReadableStreamDefaultController": "readonly"
|
"@typescript-eslint/ban-types": "off",
|
||||||
|
"@typescript-eslint/no-floating-promises": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
"@typescript-eslint/restrict-plus-operands": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/no-empty-interface": "off",
|
||||||
|
"@typescript-eslint/no-misused-promises": "off",
|
||||||
|
"@typescript-eslint/no-empty-function": "off",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||||
|
"react-hooks/rules-of-hooks": "off",
|
||||||
|
"react-hooks/exhaustive-deps": "off",
|
||||||
|
"@next/next/no-img-element": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"jsx-a11y/alt-text": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
13
.lintstagedrc.js
Normal file
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],
|
||||||
|
};
|
|
@ -37,11 +37,6 @@ module.exports = {
|
||||||
'report-to': ' https://csp-reporter.ente.io/local',
|
'report-to': ' https://csp-reporter.ente.io/local',
|
||||||
},
|
},
|
||||||
|
|
||||||
WORKBOX_CONFIG: {
|
|
||||||
swSrc: 'src/serviceWorker.js',
|
|
||||||
exclude: [/manifest\.json$/i],
|
|
||||||
},
|
|
||||||
|
|
||||||
ALL_ROUTES: '/(.*)',
|
ALL_ROUTES: '/(.*)',
|
||||||
|
|
||||||
buildCSPHeader: (directives) => ({
|
buildCSPHeader: (directives) => ({
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||||
enabled: process.env.ANALYZE === 'true',
|
enabled: process.env.ANALYZE === 'true',
|
||||||
});
|
});
|
||||||
const withWorkbox = require('@ente-io/next-with-workbox');
|
|
||||||
|
|
||||||
const { withSentryConfig } = require('@sentry/nextjs');
|
const { withSentryConfig } = require('@sentry/nextjs');
|
||||||
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
||||||
|
@ -19,7 +18,6 @@ const {
|
||||||
COOP_COEP_HEADERS,
|
COOP_COEP_HEADERS,
|
||||||
WEB_SECURITY_HEADERS,
|
WEB_SECURITY_HEADERS,
|
||||||
CSP_DIRECTIVES,
|
CSP_DIRECTIVES,
|
||||||
WORKBOX_CONFIG,
|
|
||||||
ALL_ROUTES,
|
ALL_ROUTES,
|
||||||
getIsSentryEnabled,
|
getIsSentryEnabled,
|
||||||
} = require('./configUtil');
|
} = require('./configUtil');
|
||||||
|
@ -30,14 +28,17 @@ const IS_SENTRY_ENABLED = getIsSentryEnabled();
|
||||||
|
|
||||||
module.exports = (phase) =>
|
module.exports = (phase) =>
|
||||||
withSentryConfig(
|
withSentryConfig(
|
||||||
withWorkbox(
|
|
||||||
withBundleAnalyzer(
|
withBundleAnalyzer(
|
||||||
withTM({
|
withTM({
|
||||||
|
compiler: {
|
||||||
|
styledComponents: {
|
||||||
|
ssr: true,
|
||||||
|
displayName: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
env: {
|
env: {
|
||||||
SENTRY_RELEASE: GIT_SHA,
|
SENTRY_RELEASE: GIT_SHA,
|
||||||
NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA,
|
|
||||||
},
|
},
|
||||||
workbox: WORKBOX_CONFIG,
|
|
||||||
|
|
||||||
headers() {
|
headers() {
|
||||||
return [
|
return [
|
||||||
|
@ -60,7 +61,6 @@ module.exports = (phase) =>
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
|
||||||
),
|
),
|
||||||
{
|
{
|
||||||
release: GIT_SHA,
|
release: GIT_SHA,
|
||||||
|
|
32
package.json
32
package.json
|
@ -5,7 +5,7 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"albums": "next dev -p 3002",
|
"albums": "next dev -p 3002",
|
||||||
"lint": "eslint \"src/**/*.{js,jsx,ts,tsx}\"",
|
"lint": "next lint",
|
||||||
"prebuild": "yarn lint",
|
"prebuild": "yarn lint",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"postbuild": "next export",
|
"postbuild": "next export",
|
||||||
|
@ -15,7 +15,6 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@date-io/date-fns": "^2.14.0",
|
"@date-io/date-fns": "^2.14.0",
|
||||||
"@ente-io/next-with-workbox": "^1.0.3",
|
|
||||||
"@mui/icons-material": "^5.6.2",
|
"@mui/icons-material": "^5.6.2",
|
||||||
"@mui/material": "^5.6.2",
|
"@mui/material": "^5.6.2",
|
||||||
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
|
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
|
||||||
|
@ -39,7 +38,7 @@
|
||||||
"jszip": "3.7.1",
|
"jszip": "3.7.1",
|
||||||
"libsodium-wrappers": "^0.7.8",
|
"libsodium-wrappers": "^0.7.8",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"next": "^12.1.0",
|
"next": "^12.3.1",
|
||||||
"next-transpile-modules": "^9.0.0",
|
"next-transpile-modules": "^9.0.0",
|
||||||
"photoswipe": "file:./thirdparty/photoswipe",
|
"photoswipe": "file:./thirdparty/photoswipe",
|
||||||
"piexifjs": "^1.0.6",
|
"piexifjs": "^1.0.6",
|
||||||
|
@ -52,12 +51,8 @@
|
||||||
"react-top-loading-bar": "^2.0.1",
|
"react-top-loading-bar": "^2.0.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.2",
|
"react-virtualized-auto-sizer": "^1.0.2",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
|
"sanitize-filename": "^1.6.3",
|
||||||
"styled-components": "^5.3.5",
|
"styled-components": "^5.3.5",
|
||||||
"workbox-precaching": "^6.1.5",
|
|
||||||
"workbox-recipes": "^6.1.5",
|
|
||||||
"workbox-routing": "^6.1.5",
|
|
||||||
"workbox-strategies": "^6.1.5",
|
|
||||||
"workbox-window": "^6.1.5",
|
|
||||||
"xml-js": "^1.6.11",
|
"xml-js": "^1.6.11",
|
||||||
"yup": "^0.29.3"
|
"yup": "^0.29.3"
|
||||||
},
|
},
|
||||||
|
@ -75,17 +70,10 @@
|
||||||
"@types/react-window-infinite-loader": "^1.0.3",
|
"@types/react-window-infinite-loader": "^1.0.3",
|
||||||
"@types/styled-components": "^5.1.25",
|
"@types/styled-components": "^5.1.25",
|
||||||
"@types/yup": "^0.29.7",
|
"@types/yup": "^0.29.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^4.25.0",
|
"@typescript-eslint/eslint-plugin": "^5.43.0",
|
||||||
"@typescript-eslint/parser": "^4.25.0",
|
"eslint": "^8.28.0",
|
||||||
"babel-plugin-styled-components": "^1.11.1",
|
"eslint-config-next": "^13.0.4",
|
||||||
"eslint": "^7.27.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
"eslint-config-airbnb": "^18.2.1",
|
|
||||||
"eslint-config-google": "^0.14.0",
|
|
||||||
"eslint-config-prettier": "^8.3.0",
|
|
||||||
"eslint-plugin-import": "^2.23.3",
|
|
||||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
|
||||||
"eslint-plugin-react": "^7.23.2",
|
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
|
||||||
"husky": "^7.0.1",
|
"husky": "^7.0.1",
|
||||||
"lint-staged": "^11.1.2",
|
"lint-staged": "^11.1.2",
|
||||||
"prettier": "2.3.2",
|
"prettier": "2.3.2",
|
||||||
|
@ -95,12 +83,6 @@
|
||||||
"standard": {
|
"standard": {
|
||||||
"parser": "babel-eslint"
|
"parser": "babel-eslint"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
|
||||||
"src/**/*.{js,jsx,ts,tsx}": [
|
|
||||||
"eslint --fix",
|
|
||||||
"prettier --write --ignore-unknown"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
|
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest"
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV();
|
||||||
const SENTRY_RELEASE = getSentryRelease();
|
const SENTRY_RELEASE = getSentryRelease();
|
||||||
const IS_ENABLED = getIsSentryEnabled();
|
const IS_ENABLED = getIsSentryEnabled();
|
||||||
|
|
||||||
Sentry.setUser({ id: getSentryUserID() });
|
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: SENTRY_DSN,
|
dsn: SENTRY_DSN,
|
||||||
enabled: IS_ENABLED,
|
enabled: IS_ENABLED,
|
||||||
|
@ -39,3 +38,9 @@ Sentry.init({
|
||||||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||||
// that it will also get attached to your source maps
|
// that it will also get attached to your source maps
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Sentry.setUser({ id: await getSentryUserID() });
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
3
sentry.properties
Normal file
3
sentry.properties
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
defaults.url=https://sentry.ente.io/
|
||||||
|
defaults.org=ente
|
||||||
|
defaults.project=bada-frame
|
|
@ -6,6 +6,8 @@ import {
|
||||||
getIsSentryEnabled,
|
getIsSentryEnabled,
|
||||||
} from 'constants/sentry';
|
} from 'constants/sentry';
|
||||||
|
|
||||||
|
import { getSentryUserID } from 'utils/user';
|
||||||
|
|
||||||
const SENTRY_DSN = getSentryDSN();
|
const SENTRY_DSN = getSentryDSN();
|
||||||
const SENTRY_ENV = getSentryENV();
|
const SENTRY_ENV = getSentryENV();
|
||||||
const SENTRY_RELEASE = getSentryRelease();
|
const SENTRY_RELEASE = getSentryRelease();
|
||||||
|
@ -18,3 +20,9 @@ Sentry.init({
|
||||||
release: SENTRY_RELEASE,
|
release: SENTRY_RELEASE,
|
||||||
autoSessionTracking: false,
|
autoSessionTracking: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const main = async () => {
|
||||||
|
Sentry.setUser({ id: await getSentryUserID() });
|
||||||
|
};
|
||||||
|
|
||||||
|
main();
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
|
const ENV_DEVELOPMENT = 'development';
|
||||||
|
|
||||||
module.exports.getIsSentryEnabled = () => {
|
module.exports.getIsSentryEnabled = () => {
|
||||||
if (process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED) {
|
if (process.env.NEXT_PUBLIC_SENTRY_ENV === ENV_DEVELOPMENT) {
|
||||||
return process.env.NEXT_PUBLIC_IS_SENTRY_ENABLED === 'yes';
|
|
||||||
} else {
|
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_ENV) {
|
|
||||||
return process.env.NEXT_PUBLIC_SENTRY_ENV !== 'development';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
|
} else if (process.env.NEXT_PUBLIC_DISABLE_SENTRY === 'true') {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
|
||||||
|
|
||||||
export const Badge = styled(Paper)(({ theme }) => ({
|
export const Badge = styled(Paper)(({ theme }) => ({
|
||||||
padding: '2px 4px',
|
padding: '2px 4px',
|
||||||
backgroundColor: theme.palette.glass.main,
|
backgroundColor: theme.palette.backdrop.main,
|
||||||
color: theme.palette.glass.contrastText,
|
backdropFilter: `blur(${theme.palette.blur.muted})`,
|
||||||
|
color: theme.palette.primary.contrastText,
|
||||||
textTransform: 'uppercase',
|
textTransform: 'uppercase',
|
||||||
...(theme.typography.mini as CSSProperties),
|
...(theme.typography.mini as CSSProperties),
|
||||||
}));
|
}));
|
||||||
|
|
10
src/components/Chip.tsx
Normal file
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 constants from 'utils/strings/constants';
|
||||||
import { CopyButtonWrapper } from './styledComponents';
|
|
||||||
import DoneIcon from '@mui/icons-material/Done';
|
import DoneIcon from '@mui/icons-material/Done';
|
||||||
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
|
||||||
import { Tooltip } from '@mui/material';
|
import {
|
||||||
|
IconButton,
|
||||||
|
IconButtonProps,
|
||||||
|
SvgIconProps,
|
||||||
|
Tooltip,
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
export default function CopyButton({ code, copied, copyToClipboardHelper }) {
|
export default function CopyButton({
|
||||||
|
code,
|
||||||
|
color,
|
||||||
|
size,
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
color?: IconButtonProps['color'];
|
||||||
|
size?: SvgIconProps['fontSize'];
|
||||||
|
}) {
|
||||||
|
const [copied, setCopied] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const copyToClipboardHelper = (text: string) => () => {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
setCopied(true);
|
||||||
|
setTimeout(() => setCopied(false), 1000);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<Tooltip arrow open={copied} title={constants.COPIED}>
|
<Tooltip
|
||||||
<CopyButtonWrapper onClick={copyToClipboardHelper(code)}>
|
arrow
|
||||||
|
open={copied}
|
||||||
|
title={constants.COPIED}
|
||||||
|
PopperProps={{ sx: { zIndex: 2000 } }}>
|
||||||
|
<IconButton onClick={copyToClipboardHelper(code)} color={color}>
|
||||||
{copied ? (
|
{copied ? (
|
||||||
<DoneIcon fontSize="small" />
|
<DoneIcon fontSize={size ?? 'small'} />
|
||||||
) : (
|
) : (
|
||||||
<ContentCopyIcon fontSize="small" />
|
<ContentCopyIcon fontSize={size ?? 'small'} />
|
||||||
)}
|
)}
|
||||||
</CopyButtonWrapper>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { FreeFlowText } from '../Container';
|
import { FreeFlowText } from '../Container';
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import EnteSpinner from '../EnteSpinner';
|
import EnteSpinner from '../EnteSpinner';
|
||||||
import { Wrapper, CodeWrapper } from './styledComponents';
|
import { Wrapper, CodeWrapper, CopyButtonWrapper } from './styledComponents';
|
||||||
import CopyButton from './CopyButton';
|
import CopyButton from './CopyButton';
|
||||||
import { BoxProps } from '@mui/material';
|
import { BoxProps } from '@mui/material';
|
||||||
|
|
||||||
|
@ -15,14 +15,6 @@ export default function CodeBlock({
|
||||||
wordBreak,
|
wordBreak,
|
||||||
...props
|
...props
|
||||||
}: BoxProps<'div', Iprops>) {
|
}: BoxProps<'div', Iprops>) {
|
||||||
const [copied, setCopied] = useState<boolean>(false);
|
|
||||||
|
|
||||||
const copyToClipboardHelper = (text: string) => () => {
|
|
||||||
navigator.clipboard.writeText(text);
|
|
||||||
setCopied(true);
|
|
||||||
setTimeout(() => setCopied(false), 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
|
@ -37,11 +29,9 @@ export default function CodeBlock({
|
||||||
{code}
|
{code}
|
||||||
</FreeFlowText>
|
</FreeFlowText>
|
||||||
</CodeWrapper>
|
</CodeWrapper>
|
||||||
<CopyButton
|
<CopyButtonWrapper>
|
||||||
code={code}
|
<CopyButton code={code} />
|
||||||
copied={copied}
|
</CopyButtonWrapper>
|
||||||
copyToClipboardHelper={copyToClipboardHelper}
|
|
||||||
/>
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents';
|
||||||
import { shouldShowOptions } from 'utils/collection';
|
import { shouldShowOptions } from 'utils/collection';
|
||||||
import { CollectionSummaryType } from 'constants/collection';
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
|
||||||
import Delete from '@mui/icons-material/Delete';
|
import Delete from '@mui/icons-material/Delete';
|
||||||
|
import { ArchiveOutlined } from '@mui/icons-material';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
activeCollection: Collection;
|
activeCollection: Collection;
|
||||||
|
@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({
|
||||||
return <Favorite />;
|
return <Favorite />;
|
||||||
case CollectionSummaryType.archived:
|
case CollectionSummaryType.archived:
|
||||||
case CollectionSummaryType.archive:
|
case CollectionSummaryType.archive:
|
||||||
return <VisibilityOff />;
|
return <ArchiveOutlined />;
|
||||||
case CollectionSummaryType.trash:
|
case CollectionSummaryType.trash:
|
||||||
return <Delete />;
|
return <Delete />;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -11,7 +11,7 @@ import TruncateText from 'components/TruncateText';
|
||||||
import { Box } from '@mui/material';
|
import { Box } from '@mui/material';
|
||||||
import { CollectionSummaryType } from 'constants/collection';
|
import { CollectionSummaryType } from 'constants/collection';
|
||||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
import { ArchiveOutlined } from '@mui/icons-material';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
active: boolean;
|
active: boolean;
|
||||||
|
@ -50,7 +50,7 @@ function CollectionCardIcon({ collectionType }) {
|
||||||
<CollectionBarTileIcon>
|
<CollectionBarTileIcon>
|
||||||
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
||||||
{collectionType === CollectionSummaryType.archived && (
|
{collectionType === CollectionSummaryType.archived && (
|
||||||
<VisibilityOff />
|
<ArchiveOutlined />
|
||||||
)}
|
)}
|
||||||
</CollectionBarTileIcon>
|
</CollectionBarTileIcon>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,11 +4,10 @@ import React from 'react';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import EditIcon from '@mui/icons-material/Edit';
|
||||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
|
||||||
import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
|
||||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { CollectionActions } from '.';
|
import { CollectionActions } from '.';
|
||||||
|
import { ArchiveOutlined, Unarchive } from '@mui/icons-material';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
IsArchived: boolean;
|
IsArchived: boolean;
|
||||||
|
@ -53,13 +52,13 @@ export function AlbumCollectionOption({
|
||||||
onClick={handleCollectionAction(
|
onClick={handleCollectionAction(
|
||||||
CollectionActions.UNARCHIVE
|
CollectionActions.UNARCHIVE
|
||||||
)}
|
)}
|
||||||
startIcon={<VisibilityOnOutlinedIcon />}>
|
startIcon={<Unarchive />}>
|
||||||
{constants.UNARCHIVE}
|
{constants.UNARCHIVE}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
) : (
|
) : (
|
||||||
<OverflowMenuOption
|
<OverflowMenuOption
|
||||||
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
||||||
startIcon={<VisibilityOffOutlinedIcon />}>
|
startIcon={<ArchiveOutlined />}>
|
||||||
{constants.ARCHIVE}
|
{constants.ARCHIVE}
|
||||||
</OverflowMenuOption>
|
</OverflowMenuOption>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { shareCollection } from 'services/collectionService';
|
import { shareCollection } from 'services/collectionService';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { handleSharingErrors } from 'utils/error';
|
import { handleSharingErrors } from 'utils/error/ui';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { CollectionShareSharees } from './sharees';
|
import { CollectionShareSharees } from './sharees';
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { FlexWrapper } from 'components/Container';
|
import { FlexWrapper } from 'components/Container';
|
||||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import React, { useContext, useState } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
|
@ -8,7 +7,7 @@ import {
|
||||||
deleteShareableURL,
|
deleteShareableURL,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import { Collection, PublicURL } from 'types/collection';
|
import { Collection, PublicURL } from 'types/collection';
|
||||||
import { handleSharingErrors } from 'utils/error';
|
import { handleSharingErrors } from 'utils/error/ui';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import PublicShareSwitch from './switch';
|
import PublicShareSwitch from './switch';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
|
@ -60,7 +59,7 @@ export default function PublicShareControl({
|
||||||
proceed: {
|
proceed: {
|
||||||
text: constants.DISABLE,
|
text: constants.DISABLE,
|
||||||
action: disablePublicSharing,
|
action: disablePublicSharing,
|
||||||
variant: ButtonVariant.danger,
|
variant: 'danger',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
@ -34,7 +33,7 @@ export function ManageDownloadAccess({
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
enableDownload: false,
|
enableDownload: false,
|
||||||
}),
|
}),
|
||||||
variant: ButtonVariant.danger,
|
variant: 'danger',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,13 +8,13 @@ import React, { useContext, useState } from 'react';
|
||||||
import { updateShareableURL } from 'services/collectionService';
|
import { updateShareableURL } from 'services/collectionService';
|
||||||
import { UpdatePublicURL } from 'types/collection';
|
import { UpdatePublicURL } from 'types/collection';
|
||||||
import { sleep } from 'utils/common';
|
import { sleep } from 'utils/common';
|
||||||
import { handleSharingErrors } from 'utils/error';
|
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import {
|
import {
|
||||||
ManageSectionLabel,
|
ManageSectionLabel,
|
||||||
ManageSectionOptions,
|
ManageSectionOptions,
|
||||||
} from '../../styledComponents';
|
} from '../../styledComponents';
|
||||||
import { ManageDownloadAccess } from './downloadAccess';
|
import { ManageDownloadAccess } from './downloadAccess';
|
||||||
|
import { handleSharingErrors } from 'utils/error/ui';
|
||||||
|
|
||||||
export default function PublicShareManage({
|
export default function PublicShareManage({
|
||||||
publicShareProp,
|
publicShareProp,
|
||||||
|
|
|
@ -4,7 +4,7 @@ import Select from 'react-select';
|
||||||
import { linkExpiryStyle } from 'styles/linkExpiry';
|
import { linkExpiryStyle } from 'styles/linkExpiry';
|
||||||
import { shareExpiryOptions } from 'utils/collection';
|
import { shareExpiryOptions } from 'utils/collection';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { dateStringWithMMH } from 'utils/time';
|
import { formatDateTime } from 'utils/time/format';
|
||||||
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
import { OptionWithDivider } from './selectComponents/OptionWithDivider';
|
||||||
|
|
||||||
export function ManageLinkExpiry({
|
export function ManageLinkExpiry({
|
||||||
|
@ -31,7 +31,7 @@ export function ManageLinkExpiry({
|
||||||
}}
|
}}
|
||||||
placeholder={
|
placeholder={
|
||||||
publicShareProp?.validTill
|
publicShareProp?.validTill
|
||||||
? dateStringWithMMH(publicShareProp?.validTill)
|
? formatDateTime(publicShareProp?.validTill / 1000)
|
||||||
: 'never'
|
: 'never'
|
||||||
}
|
}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { Box, Typography } from '@mui/material';
|
import { Box, Typography } from '@mui/material';
|
||||||
import { ButtonVariant } from 'components/pages/gallery/LinkButton';
|
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
@ -32,7 +31,7 @@ export function ManageLinkPassword({
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
disablePassword: true,
|
disablePassword: true,
|
||||||
}),
|
}),
|
||||||
variant: ButtonVariant.danger,
|
variant: 'danger',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
padding: theme.spacing(1, 1.5),
|
||||||
maxWidth: '346px',
|
maxWidth: '346px',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'& .DialogIcon': {
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
paddingBottom: theme.spacing(1),
|
||||||
|
},
|
||||||
|
|
||||||
'& .MuiDialogTitle-root': {
|
'& .MuiDialogTitle-root': {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
paddingBottom: theme.spacing(1),
|
paddingBottom: theme.spacing(1),
|
||||||
|
@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
||||||
'& .MuiDialogContent-root': {
|
'& .MuiDialogContent-root': {
|
||||||
padding: theme.spacing(2),
|
padding: theme.spacing(2),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
'.DialogIcon + .MuiDialogTitle-root': {
|
||||||
|
paddingTop: 0,
|
||||||
|
},
|
||||||
|
|
||||||
'.MuiDialogTitle-root + .MuiDialogContent-root': {
|
'.MuiDialogTitle-root + .MuiDialogContent-root': {
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
},
|
},
|
||||||
|
|
|
@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, {
|
||||||
} from './TitleWithCloseButton';
|
} from './TitleWithCloseButton';
|
||||||
import DialogBoxBase from './base';
|
import DialogBoxBase from './base';
|
||||||
import { DialogBoxAttributes } from 'types/dialogBox';
|
import { DialogBoxAttributes } from 'types/dialogBox';
|
||||||
|
import DialogIcon from './DialogIcon';
|
||||||
|
|
||||||
type IProps = React.PropsWithChildren<
|
type IProps = React.PropsWithChildren<
|
||||||
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
||||||
|
@ -48,6 +49,7 @@ export default function DialogBox({
|
||||||
maxWidth={size}
|
maxWidth={size}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
{...props}>
|
{...props}>
|
||||||
|
{attributes.icon && <DialogIcon icon={attributes.icon} />}
|
||||||
{attributes.title && (
|
{attributes.title && (
|
||||||
<DialogTitleWithCloseButton
|
<DialogTitleWithCloseButton
|
||||||
onClose={
|
onClose={
|
||||||
|
|
|
@ -4,7 +4,6 @@ import {
|
||||||
MIN_EDITED_CREATION_TIME,
|
MIN_EDITED_CREATION_TIME,
|
||||||
MAX_EDITED_CREATION_TIME,
|
MAX_EDITED_CREATION_TIME,
|
||||||
} from 'constants/file';
|
} from 'constants/file';
|
||||||
import { TextField } from '@mui/material';
|
|
||||||
import {
|
import {
|
||||||
LocalizationProvider,
|
LocalizationProvider,
|
||||||
MobileDateTimePicker,
|
MobileDateTimePicker,
|
||||||
|
@ -60,14 +59,7 @@ const EnteDateTimePicker = ({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
renderInput={(params) => (
|
renderInput={() => <></>}
|
||||||
<TextField
|
|
||||||
{...params}
|
|
||||||
hiddenLabel
|
|
||||||
margin="none"
|
|
||||||
variant="standard"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
</LocalizationProvider>
|
</LocalizationProvider>
|
||||||
);
|
);
|
||||||
|
|
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 { Button, DialogActions, DialogContent, Stack } from '@mui/material';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ExportStats } from 'types/export';
|
import { ExportStats } from 'types/export';
|
||||||
import { formatDateTime } from 'utils/time';
|
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
import { formatDateTime } from 'utils/time/format';
|
||||||
import { FlexWrapper, Label, Value } from './Container';
|
import { FlexWrapper, Label, Value } from './Container';
|
||||||
import { ComfySpan } from './ExportInProgress';
|
import { ComfySpan } from './ExportInProgress';
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
|
@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
attributes.action?.callback();
|
attributes.onClick();
|
||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
|
@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
horizontal: 'right',
|
horizontal: 'right',
|
||||||
vertical: 'bottom',
|
vertical: 'bottom',
|
||||||
}}>
|
}}
|
||||||
|
sx={{ backgroundColor: '#000', width: '320px' }}>
|
||||||
<Paper
|
<Paper
|
||||||
component={Button}
|
component={Button}
|
||||||
color={attributes.variant}
|
color={attributes.variant}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
sx={{
|
sx={{
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
width: '320px',
|
flex: '1',
|
||||||
padding: (theme) => theme.spacing(1.5, 2),
|
padding: (theme) => theme.spacing(1.5, 2),
|
||||||
}}>
|
}}>
|
||||||
<Stack
|
<Stack
|
||||||
|
@ -55,34 +56,38 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
||||||
spacing={2}
|
spacing={2}
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems={'center'}>
|
alignItems={'center'}>
|
||||||
<Box>
|
<Box sx={{ svg: { fontSize: '36px' } }}>
|
||||||
{attributes?.icon ?? <InfoIcon fontSize="large" />}
|
{attributes.startIcon ?? <InfoIcon />}
|
||||||
</Box>
|
</Box>
|
||||||
<Box sx={{ flex: 1 }}>
|
|
||||||
<Typography
|
<Stack
|
||||||
variant="body2"
|
direction={'column'}
|
||||||
color="rgba(255, 255, 255, 0.7)"
|
spacing={0.5}
|
||||||
mb={0.5}>
|
flex={1}
|
||||||
{attributes.message}{' '}
|
textAlign="left">
|
||||||
</Typography>
|
{attributes.subtext && (
|
||||||
{attributes?.action && (
|
<Typography variant="body2">
|
||||||
<Typography
|
{attributes.subtext}
|
||||||
mb={0.5}
|
|
||||||
variant="button"
|
|
||||||
fontWeight={'bold'}>
|
|
||||||
{attributes?.action.text}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</Box>
|
{attributes.message && (
|
||||||
<Box>
|
<Typography variant="button">
|
||||||
|
{attributes.message}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{attributes.endIcon ? (
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleClose}
|
onClick={attributes.onClick}
|
||||||
sx={{
|
sx={{ fontSize: '36px' }}>
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
{attributes?.endIcon}
|
||||||
}}>
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<IconButton onClick={handleClose}>
|
||||||
<CloseIcon />
|
<CloseIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Box>
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Snackbar>
|
</Snackbar>
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import PreviewCard from './pages/gallery/PreviewCard';
|
import PreviewCard from './pages/gallery/PreviewCard';
|
||||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import DownloadManager from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import PhotoSwipe from 'components/PhotoSwipe';
|
import PhotoViewer from 'components/PhotoViewer';
|
||||||
import {
|
import {
|
||||||
ALL_SECTION,
|
ALL_SECTION,
|
||||||
ARCHIVE_SECTION,
|
ARCHIVE_SECTION,
|
||||||
|
@ -15,7 +15,7 @@ import {
|
||||||
import { isSharedFile } from 'utils/file';
|
import { isSharedFile } from 'utils/file';
|
||||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||||
import { PhotoList } from './PhotoList';
|
import { PhotoList } from './PhotoList';
|
||||||
import { SetFiles, SelectedState } from 'types/gallery';
|
import { SelectedState } from 'types/gallery';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||||
|
@ -30,6 +30,8 @@ import { logError } from 'utils/sentry';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Collection } from 'types/collection';
|
||||||
|
|
||||||
const Container = styled('div')`
|
const Container = styled('div')`
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -48,7 +50,7 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: EnteFile[];
|
files: EnteFile[];
|
||||||
setFiles: SetFiles;
|
collections?: Collection[];
|
||||||
syncWithRemote: () => Promise<void>;
|
syncWithRemote: () => Promise<void>;
|
||||||
favItemIds?: Set<number>;
|
favItemIds?: Set<number>;
|
||||||
archivedCollections?: Set<number>;
|
archivedCollections?: Set<number>;
|
||||||
|
@ -60,7 +62,8 @@ interface Props {
|
||||||
openUploader?;
|
openUploader?;
|
||||||
isInSearchMode?: boolean;
|
isInSearchMode?: boolean;
|
||||||
search?: Search;
|
search?: Search;
|
||||||
deleted?: number[];
|
deletedFileIds?: Set<number>;
|
||||||
|
setDeletedFileIds?: (value: Set<number>) => void;
|
||||||
activeCollection: number;
|
activeCollection: number;
|
||||||
isSharedCollection?: boolean;
|
isSharedCollection?: boolean;
|
||||||
enableDownload?: boolean;
|
enableDownload?: boolean;
|
||||||
|
@ -69,13 +72,15 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceURL = {
|
type SourceURL = {
|
||||||
imageURL?: string;
|
originalImageURL?: string;
|
||||||
videoURL?: string;
|
originalVideoURL?: string;
|
||||||
|
convertedImageURL?: string;
|
||||||
|
convertedVideoURL?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PhotoFrame = ({
|
const PhotoFrame = ({
|
||||||
files,
|
files,
|
||||||
setFiles,
|
collections,
|
||||||
syncWithRemote,
|
syncWithRemote,
|
||||||
favItemIds,
|
favItemIds,
|
||||||
archivedCollections,
|
archivedCollections,
|
||||||
|
@ -86,7 +91,8 @@ const PhotoFrame = ({
|
||||||
isInSearchMode,
|
isInSearchMode,
|
||||||
search,
|
search,
|
||||||
resetSearch,
|
resetSearch,
|
||||||
deleted,
|
deletedFileIds,
|
||||||
|
setDeletedFileIds,
|
||||||
activeCollection,
|
activeCollection,
|
||||||
isSharedCollection,
|
isSharedCollection,
|
||||||
enableDownload,
|
enableDownload,
|
||||||
|
@ -104,75 +110,26 @@ const PhotoFrame = ({
|
||||||
const [rangeStart, setRangeStart] = useState(null);
|
const [rangeStart, setRangeStart] = useState(null);
|
||||||
const [currentHover, setCurrentHover] = useState(null);
|
const [currentHover, setCurrentHover] = useState(null);
|
||||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||||
const filteredDataRef = useRef<EnteFile[]>([]);
|
|
||||||
const filteredData = filteredDataRef?.current ?? [];
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
const [isSourceLoaded, setIsSourceLoaded] = useState(false);
|
||||||
useEffect(() => {
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Shift') {
|
|
||||||
setIsShiftKeyPressed(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleKeyUp = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Shift') {
|
|
||||||
setIsShiftKeyPressed(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
document.addEventListener('keydown', handleKeyDown, false);
|
|
||||||
document.addEventListener('keyup', handleKeyUp, false);
|
|
||||||
router.events.on('hashChangeComplete', (url: string) => {
|
|
||||||
const start = url.indexOf('#');
|
|
||||||
const hash = url.slice(start !== -1 ? start : url.length);
|
|
||||||
const shouldPhotoSwipeBeOpened = hash.endsWith(
|
|
||||||
PHOTOSWIPE_HASH_SUFFIX
|
|
||||||
);
|
|
||||||
if (shouldPhotoSwipeBeOpened) {
|
|
||||||
setOpen(true);
|
|
||||||
} else {
|
|
||||||
setOpen(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return () => {
|
|
||||||
document.addEventListener('keydown', handleKeyDown, false);
|
|
||||||
document.addEventListener('keyup', handleKeyUp, false);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const filteredData = useMemo(() => {
|
||||||
if (!isNaN(search?.file)) {
|
|
||||||
const filteredDataIdx = filteredData.findIndex((file) => {
|
|
||||||
return file.id === search.file;
|
|
||||||
});
|
|
||||||
if (!isNaN(filteredDataIdx)) {
|
|
||||||
onThumbnailClick(filteredDataIdx)();
|
|
||||||
}
|
|
||||||
resetSearch();
|
|
||||||
}
|
|
||||||
}, [search, filteredData]);
|
|
||||||
|
|
||||||
const resetFetching = () => {
|
|
||||||
setFetching({});
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selected.count === 0) {
|
|
||||||
setRangeStart(null);
|
|
||||||
}
|
|
||||||
}, [selected]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const idSet = new Set();
|
const idSet = new Set();
|
||||||
const user: User = getData(LS_KEYS.USER);
|
const user: User = getData(LS_KEYS.USER);
|
||||||
filteredDataRef.current = files
|
|
||||||
|
return files
|
||||||
.map((item, index) => ({
|
.map((item, index) => ({
|
||||||
...item,
|
...item,
|
||||||
dataIndex: index,
|
dataIndex: index,
|
||||||
w: window.innerWidth,
|
w: window.innerWidth,
|
||||||
h: window.innerHeight,
|
h: window.innerHeight,
|
||||||
|
title: item.pubMagicMetadata?.data.caption,
|
||||||
}))
|
}))
|
||||||
.filter((item) => {
|
.filter((item) => {
|
||||||
if (deleted?.includes(item.id)) {
|
if (
|
||||||
|
deletedFileIds?.has(item.id) &&
|
||||||
|
activeCollection !== TRASH_SECTION
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -230,7 +187,31 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}, [files, deleted, search, activeCollection]);
|
}, [files, deletedFileIds, search, activeCollection]);
|
||||||
|
|
||||||
|
const fileToCollectionsMap = useMemo(() => {
|
||||||
|
const fileToCollectionsMap = new Map<number, number[]>();
|
||||||
|
files.forEach((file) => {
|
||||||
|
if (!fileToCollectionsMap.get(file.id)) {
|
||||||
|
fileToCollectionsMap.set(file.id, []);
|
||||||
|
}
|
||||||
|
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||||
|
});
|
||||||
|
return fileToCollectionsMap;
|
||||||
|
}, [files]);
|
||||||
|
|
||||||
|
const collectionNameMap = useMemo(() => {
|
||||||
|
if (collections) {
|
||||||
|
return new Map<number, string>(
|
||||||
|
collections.map((collection) => [
|
||||||
|
collection.id,
|
||||||
|
collection.name,
|
||||||
|
])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return new Map();
|
||||||
|
}
|
||||||
|
}, [collections]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const currentURL = new URL(window.location.href);
|
const currentURL = new URL(window.location.href);
|
||||||
|
@ -247,6 +228,59 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
}, [open]);
|
}, [open]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
setIsShiftKeyPressed(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleKeyUp = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Shift') {
|
||||||
|
setIsShiftKeyPressed(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
document.addEventListener('keyup', handleKeyUp, false);
|
||||||
|
router.events.on('hashChangeComplete', (url: string) => {
|
||||||
|
const start = url.indexOf('#');
|
||||||
|
const hash = url.slice(start !== -1 ? start : url.length);
|
||||||
|
const shouldPhotoSwipeBeOpened = hash.endsWith(
|
||||||
|
PHOTOSWIPE_HASH_SUFFIX
|
||||||
|
);
|
||||||
|
if (shouldPhotoSwipeBeOpened) {
|
||||||
|
setOpen(true);
|
||||||
|
} else {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
document.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
document.addEventListener('keyup', handleKeyUp, false);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isNaN(search?.file)) {
|
||||||
|
const filteredDataIdx = filteredData.findIndex((file) => {
|
||||||
|
return file.id === search.file;
|
||||||
|
});
|
||||||
|
if (!isNaN(filteredDataIdx)) {
|
||||||
|
onThumbnailClick(filteredDataIdx)();
|
||||||
|
}
|
||||||
|
resetSearch();
|
||||||
|
}
|
||||||
|
}, [search, filteredData]);
|
||||||
|
|
||||||
|
const resetFetching = () => {
|
||||||
|
setFetching({});
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selected.count === 0) {
|
||||||
|
setRangeStart(null);
|
||||||
|
}
|
||||||
|
}, [selected]);
|
||||||
|
|
||||||
const getFileIndexFromID = (files: EnteFile[], id: number) => {
|
const getFileIndexFromID = (files: EnteFile[], id: number) => {
|
||||||
const index = files.findIndex((file) => file.id === id);
|
const index = files.findIndex((file) => file.id === id);
|
||||||
if (index === -1) {
|
if (index === -1) {
|
||||||
|
@ -257,12 +291,10 @@ const PhotoFrame = ({
|
||||||
|
|
||||||
const updateURL = (id: number) => (url: string) => {
|
const updateURL = (id: number) => (url: string) => {
|
||||||
const updateFile = (file: EnteFile) => {
|
const updateFile = (file: EnteFile) => {
|
||||||
file = {
|
file.msrc = url;
|
||||||
...file,
|
file.w = window.innerWidth;
|
||||||
msrc: url,
|
file.h = window.innerHeight;
|
||||||
w: window.innerWidth,
|
|
||||||
h: window.innerHeight,
|
|
||||||
};
|
|
||||||
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
|
if (file.metadata.fileType === FILE_TYPE.VIDEO && !file.html) {
|
||||||
file.html = `
|
file.html = `
|
||||||
<div class="pswp-item-container">
|
<div class="pswp-item-container">
|
||||||
|
@ -292,29 +324,30 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
setFiles((files) => {
|
|
||||||
const index = getFileIndexFromID(files, id);
|
|
||||||
files[index] = updateFile(files[index]);
|
|
||||||
return files;
|
|
||||||
});
|
|
||||||
const index = getFileIndexFromID(files, id);
|
const index = getFileIndexFromID(files, id);
|
||||||
return updateFile(files[index]);
|
return updateFile(files[index]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSrcURL = async (id: number, srcURL: SourceURL) => {
|
const updateSrcURL = async (id: number, srcURL: SourceURL) => {
|
||||||
const { videoURL, imageURL } = srcURL;
|
const {
|
||||||
const isPlayable = videoURL && (await isPlaybackPossible(videoURL));
|
originalImageURL,
|
||||||
|
convertedImageURL,
|
||||||
|
originalVideoURL,
|
||||||
|
convertedVideoURL,
|
||||||
|
} = srcURL;
|
||||||
|
const isPlayable =
|
||||||
|
convertedVideoURL && (await isPlaybackPossible(convertedVideoURL));
|
||||||
const updateFile = (file: EnteFile) => {
|
const updateFile = (file: EnteFile) => {
|
||||||
file = {
|
file.w = window.innerWidth;
|
||||||
...file,
|
file.h = window.innerHeight;
|
||||||
w: window.innerWidth,
|
file.isSourceLoaded = true;
|
||||||
h: window.innerHeight,
|
file.originalImageURL = originalImageURL;
|
||||||
};
|
file.originalVideoURL = originalVideoURL;
|
||||||
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
if (file.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
if (isPlayable) {
|
if (isPlayable) {
|
||||||
file.html = `
|
file.html = `
|
||||||
<video controls onContextMenu="return false;">
|
<video controls onContextMenu="return false;">
|
||||||
<source src="${videoURL}" />
|
<source src="${convertedVideoURL}" />
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
`;
|
`;
|
||||||
|
@ -324,7 +357,7 @@ const PhotoFrame = ({
|
||||||
<img src="${file.msrc}" onContextMenu="return false;"/>
|
<img src="${file.msrc}" onContextMenu="return false;"/>
|
||||||
<div class="download-banner" >
|
<div class="download-banner" >
|
||||||
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
${constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD}
|
||||||
<a class="btn btn-outline-success" href=${videoURL} download="${file.metadata.title}"">Download</a>
|
<a class="btn btn-outline-success" href=${convertedVideoURL} download="${file.metadata.title}"">Download</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
@ -333,9 +366,9 @@ const PhotoFrame = ({
|
||||||
if (isPlayable) {
|
if (isPlayable) {
|
||||||
file.html = `
|
file.html = `
|
||||||
<div class = 'pswp-item-container'>
|
<div class = 'pswp-item-container'>
|
||||||
<img id = "live-photo-image-${file.id}" src="${imageURL}" onContextMenu="return false;"/>
|
<img id = "live-photo-image-${file.id}" src="${convertedImageURL}" onContextMenu="return false;"/>
|
||||||
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
|
<video id = "live-photo-video-${file.id}" loop muted onContextMenu="return false;">
|
||||||
<source src="${videoURL}" />
|
<source src="${convertedVideoURL}" />
|
||||||
Your browser does not support the video tag.
|
Your browser does not support the video tag.
|
||||||
</video>
|
</video>
|
||||||
</div>
|
</div>
|
||||||
|
@ -352,15 +385,10 @@ const PhotoFrame = ({
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
file.src = imageURL;
|
file.src = convertedImageURL;
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
setFiles((files) => {
|
|
||||||
const index = getFileIndexFromID(files, id);
|
|
||||||
files[index] = updateFile(files[index]);
|
|
||||||
return files;
|
|
||||||
});
|
|
||||||
setIsSourceLoaded(true);
|
setIsSourceLoaded(true);
|
||||||
const index = getFileIndexFromID(files, id);
|
const index = getFileIndexFromID(files, id);
|
||||||
return updateFile(files[index]);
|
return updateFile(files[index]);
|
||||||
|
@ -426,7 +454,11 @@ const PhotoFrame = ({
|
||||||
handleSelect(filteredData[index].id, index)(!checked);
|
handleSelect(filteredData[index].id, index)(!checked);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const getThumbnail = (files: EnteFile[], index: number) =>
|
const getThumbnail = (
|
||||||
|
files: EnteFile[],
|
||||||
|
index: number,
|
||||||
|
isScrolling: boolean
|
||||||
|
) =>
|
||||||
files[index] ? (
|
files[index] ? (
|
||||||
<PreviewCard
|
<PreviewCard
|
||||||
key={`tile-${files[index].id}-selected-${
|
key={`tile-${files[index].id}-selected-${
|
||||||
|
@ -450,6 +482,7 @@ const PhotoFrame = ({
|
||||||
(index >= currentHover && index <= rangeStart)
|
(index >= currentHover && index <= rangeStart)
|
||||||
}
|
}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
|
showPlaceholder={isScrolling}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
@ -484,6 +517,9 @@ const PhotoFrame = ({
|
||||||
item.msrc = newFile.msrc;
|
item.msrc = newFile.msrc;
|
||||||
item.html = newFile.html;
|
item.html = newFile.html;
|
||||||
item.src = newFile.src;
|
item.src = newFile.src;
|
||||||
|
item.isSourceLoaded = newFile.isSourceLoaded;
|
||||||
|
item.originalImageURL = newFile.originalImageURL;
|
||||||
|
item.originalVideoURL = newFile.originalVideoURL;
|
||||||
item.w = newFile.w;
|
item.w = newFile.w;
|
||||||
item.h = newFile.h;
|
item.h = newFile.h;
|
||||||
|
|
||||||
|
@ -506,10 +542,13 @@ const PhotoFrame = ({
|
||||||
if (!fetching[item.id]) {
|
if (!fetching[item.id]) {
|
||||||
try {
|
try {
|
||||||
fetching[item.id] = true;
|
fetching[item.id] = true;
|
||||||
let urls: string[];
|
let urls: { original: string[]; converted: string[] };
|
||||||
if (galleryContext.files.has(item.id)) {
|
if (galleryContext.files.has(item.id)) {
|
||||||
const mergedURL = galleryContext.files.get(item.id);
|
const mergedURL = galleryContext.files.get(item.id);
|
||||||
urls = mergedURL.split(',');
|
urls = {
|
||||||
|
original: mergedURL.original.split(','),
|
||||||
|
converted: mergedURL.converted.split(','),
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
appContext.startLoading();
|
appContext.startLoading();
|
||||||
if (
|
if (
|
||||||
|
@ -525,26 +564,39 @@ const PhotoFrame = ({
|
||||||
urls = await DownloadManager.getFile(item, true);
|
urls = await DownloadManager.getFile(item, true);
|
||||||
}
|
}
|
||||||
appContext.finishLoading();
|
appContext.finishLoading();
|
||||||
const mergedURL = urls.join(',');
|
const mergedURL = {
|
||||||
|
original: urls.original.join(','),
|
||||||
|
converted: urls.converted.join(','),
|
||||||
|
};
|
||||||
galleryContext.files.set(item.id, mergedURL);
|
galleryContext.files.set(item.id, mergedURL);
|
||||||
}
|
}
|
||||||
let imageURL;
|
let originalImageURL;
|
||||||
let videoURL;
|
let originalVideoURL;
|
||||||
|
let convertedImageURL;
|
||||||
|
let convertedVideoURL;
|
||||||
|
|
||||||
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
if (item.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
|
||||||
[imageURL, videoURL] = urls;
|
[originalImageURL, originalVideoURL] = urls.converted;
|
||||||
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
} else if (item.metadata.fileType === FILE_TYPE.VIDEO) {
|
||||||
[videoURL] = urls;
|
[originalVideoURL] = urls.original;
|
||||||
|
[convertedVideoURL] = urls.converted;
|
||||||
} else {
|
} else {
|
||||||
[imageURL] = urls;
|
[originalImageURL] = urls.original;
|
||||||
|
[convertedImageURL] = urls.converted;
|
||||||
}
|
}
|
||||||
setIsSourceLoaded(false);
|
setIsSourceLoaded(false);
|
||||||
const newFile = await updateSrcURL(item.id, {
|
const newFile = await updateSrcURL(item.id, {
|
||||||
imageURL,
|
originalImageURL,
|
||||||
videoURL,
|
originalVideoURL,
|
||||||
|
convertedImageURL,
|
||||||
|
convertedVideoURL,
|
||||||
});
|
});
|
||||||
item.msrc = newFile.msrc;
|
item.msrc = newFile.msrc;
|
||||||
item.html = newFile.html;
|
item.html = newFile.html;
|
||||||
item.src = newFile.src;
|
item.src = newFile.src;
|
||||||
|
item.isSourceLoaded = newFile.isSourceLoaded;
|
||||||
|
item.originalImageURL = newFile.originalImageURL;
|
||||||
|
item.originalVideoURL = newFile.originalVideoURL;
|
||||||
item.w = newFile.w;
|
item.w = newFile.w;
|
||||||
item.h = newFile.h;
|
item.h = newFile.h;
|
||||||
try {
|
try {
|
||||||
|
@ -591,17 +643,21 @@ const PhotoFrame = ({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
<PhotoSwipe
|
<PhotoViewer
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
items={filteredData}
|
items={filteredData}
|
||||||
currentIndex={currentIndex}
|
currentIndex={currentIndex}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
gettingData={getSlideData}
|
gettingData={getSlideData}
|
||||||
favItemIds={favItemIds}
|
favItemIds={favItemIds}
|
||||||
|
deletedFileIds={deletedFileIds}
|
||||||
|
setDeletedFileIds={setDeletedFileIds}
|
||||||
isSharedCollection={isSharedCollection}
|
isSharedCollection={isSharedCollection}
|
||||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||||
enableDownload={enableDownload}
|
enableDownload={enableDownload}
|
||||||
isSourceLoaded={isSourceLoaded}
|
isSourceLoaded={isSourceLoaded}
|
||||||
|
fileToCollectionsMap={fileToCollectionsMap}
|
||||||
|
collectionNameMap={collectionNameMap}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useRef, useEffect, useContext } from 'react';
|
import React, { useRef, useEffect, useContext } from 'react';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
import { VariableSizeList as List } from 'react-window';
|
||||||
import { Box, styled } from '@mui/material';
|
import { Box, Link, styled } from '@mui/material';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import {
|
import {
|
||||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||||
|
@ -15,16 +15,15 @@ import {
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||||
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
import { ENTE_WEBSITE_LINK } from 'constants/urls';
|
||||||
import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton';
|
|
||||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||||
import { DeduplicateContext } from 'pages/deduplicate';
|
import { DeduplicateContext } from 'pages/deduplicate';
|
||||||
import { FlexWrapper } from './Container';
|
import { FlexWrapper } from './Container';
|
||||||
import { Typography } from '@mui/material';
|
import { Typography } from '@mui/material';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import { SpecialPadding } from 'styles/SpecialPadding';
|
import { SpecialPadding } from 'styles/SpecialPadding';
|
||||||
|
import { formatDate } from 'utils/time/format';
|
||||||
|
|
||||||
const A_DAY = 24 * 60 * 60 * 1000;
|
const A_DAY = 24 * 60 * 60 * 1000;
|
||||||
const NO_OF_PAGES = 2;
|
|
||||||
const FOOTER_HEIGHT = 90;
|
const FOOTER_HEIGHT = 90;
|
||||||
|
|
||||||
export enum ITEM_TYPE {
|
export enum ITEM_TYPE {
|
||||||
|
@ -153,7 +152,11 @@ interface Props {
|
||||||
width: number;
|
width: number;
|
||||||
filteredData: EnteFile[];
|
filteredData: EnteFile[];
|
||||||
showAppDownloadBanner: boolean;
|
showAppDownloadBanner: boolean;
|
||||||
getThumbnail: (files: EnteFile[], index: number) => JSX.Element;
|
getThumbnail: (
|
||||||
|
files: EnteFile[],
|
||||||
|
index: number,
|
||||||
|
isScrolling?: boolean
|
||||||
|
) => JSX.Element;
|
||||||
activeCollection: number;
|
activeCollection: number;
|
||||||
resetFetching: () => void;
|
resetFetching: () => void;
|
||||||
}
|
}
|
||||||
|
@ -244,6 +247,10 @@ export function PhotoList({
|
||||||
filteredData,
|
filteredData,
|
||||||
showAppDownloadBanner,
|
showAppDownloadBanner,
|
||||||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||||
|
galleryContext.photoListHeader,
|
||||||
|
publicCollectionGalleryContext.photoListHeader,
|
||||||
|
deduplicateContext.isOnDeduplicatePage,
|
||||||
|
deduplicateContext.fileSizeMap,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||||
|
@ -298,22 +305,17 @@ export function PhotoList({
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
currentDate = item.metadata.creationTime / 1000;
|
currentDate = item.metadata.creationTime / 1000;
|
||||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
|
||||||
weekday: 'short',
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
});
|
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
itemType: ITEM_TYPE.TIME,
|
itemType: ITEM_TYPE.TIME,
|
||||||
date: isSameDay(new Date(currentDate), new Date())
|
date: isSameDay(new Date(currentDate), new Date())
|
||||||
? 'Today'
|
? constants.TODAY
|
||||||
: isSameDay(
|
: isSameDay(
|
||||||
new Date(currentDate),
|
new Date(currentDate),
|
||||||
new Date(Date.now() - A_DAY)
|
new Date(Date.now() - A_DAY)
|
||||||
)
|
)
|
||||||
? 'Yesterday'
|
? constants.YESTERDAY
|
||||||
: dateTimeFormat.format(currentDate),
|
: formatDate(currentDate),
|
||||||
id: currentDate.toString(),
|
id: currentDate.toString(),
|
||||||
});
|
});
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
|
@ -403,15 +405,9 @@ export function PhotoList({
|
||||||
<FooterContainer span={columns}>
|
<FooterContainer span={columns}>
|
||||||
<p>
|
<p>
|
||||||
{constants.PRESERVED_BY}{' '}
|
{constants.PRESERVED_BY}{' '}
|
||||||
<a
|
<Link target="_blank" href={ENTE_WEBSITE_LINK}>
|
||||||
target="_blank"
|
|
||||||
style={{
|
|
||||||
color: getVariantColor(ButtonVariant.success),
|
|
||||||
}}
|
|
||||||
href={ENTE_WEBSITE_LINK}
|
|
||||||
rel="noreferrer">
|
|
||||||
{constants.ENTE_IO}
|
{constants.ENTE_IO}
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
</FooterContainer>
|
</FooterContainer>
|
||||||
),
|
),
|
||||||
|
@ -453,9 +449,10 @@ export function PhotoList({
|
||||||
date: currItem.date,
|
date: currItem.date,
|
||||||
span: items[index + 1].items.length,
|
span: items[index + 1].items.length,
|
||||||
});
|
});
|
||||||
newList[newIndex + 1].items = newList[
|
newList[newIndex + 1].items = [
|
||||||
newIndex + 1
|
...newList[newIndex + 1].items,
|
||||||
].items.concat(items[index + 1].items);
|
...items[index + 1].items,
|
||||||
|
];
|
||||||
index += 2;
|
index += 2;
|
||||||
} else {
|
} else {
|
||||||
// Adding items would exceed the number of columns.
|
// Adding items would exceed the number of columns.
|
||||||
|
@ -512,10 +509,6 @@ export function PhotoList({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const extraRowsToRender = Math.ceil(
|
|
||||||
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
|
|
||||||
);
|
|
||||||
|
|
||||||
const generateKey = (index) => {
|
const generateKey = (index) => {
|
||||||
switch (timeStampList[index].itemType) {
|
switch (timeStampList[index].itemType) {
|
||||||
case ITEM_TYPE.FILE:
|
case ITEM_TYPE.FILE:
|
||||||
|
@ -527,7 +520,10 @@ export function PhotoList({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderListItem = (listItem: TimeStampListItem) => {
|
const renderListItem = (
|
||||||
|
listItem: TimeStampListItem,
|
||||||
|
isScrolling: boolean
|
||||||
|
) => {
|
||||||
switch (listItem.itemType) {
|
switch (listItem.itemType) {
|
||||||
case ITEM_TYPE.TIME:
|
case ITEM_TYPE.TIME:
|
||||||
return listItem.dates ? (
|
return listItem.dates ? (
|
||||||
|
@ -556,7 +552,8 @@ export function PhotoList({
|
||||||
const ret = listItem.items.map((item, idx) =>
|
const ret = listItem.items.map((item, idx) =>
|
||||||
getThumbnail(
|
getThumbnail(
|
||||||
filteredDataCopy,
|
filteredDataCopy,
|
||||||
listItem.itemStartIndex + idx
|
listItem.itemStartIndex + idx,
|
||||||
|
isScrolling
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
if (listItem.groups) {
|
if (listItem.groups) {
|
||||||
|
@ -587,14 +584,15 @@ export function PhotoList({
|
||||||
width={width}
|
width={width}
|
||||||
itemCount={timeStampList.length}
|
itemCount={timeStampList.length}
|
||||||
itemKey={generateKey}
|
itemKey={generateKey}
|
||||||
overscanCount={extraRowsToRender}>
|
overscanCount={0}
|
||||||
{({ index, style }) => (
|
useIsScrolling>
|
||||||
|
{({ index, style, isScrolling }) => (
|
||||||
<ListItem style={style}>
|
<ListItem style={style}>
|
||||||
<ListContainer
|
<ListContainer
|
||||||
columns={columns}
|
columns={columns}
|
||||||
shrinkRatio={shrinkRatio}
|
shrinkRatio={shrinkRatio}
|
||||||
groups={timeStampList[index].groups}>
|
groups={timeStampList[index].groups}>
|
||||||
{renderListItem(timeStampList[index])}
|
{renderListItem(timeStampList[index], isScrolling)}
|
||||||
</ListContainer>
|
</ListContainer>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -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,123 +0,0 @@
|
||||||
import React, { useContext, useEffect, useState } from 'react';
|
|
||||||
import constants from 'utils/strings/constants';
|
|
||||||
import { formatDateTime } from 'utils/time';
|
|
||||||
import { RenderFileName } from './RenderFileName';
|
|
||||||
import { ExifData } from './ExifData';
|
|
||||||
import { RenderCreationTime } from './RenderCreationTime';
|
|
||||||
import { RenderInfoItem } from './RenderInfoItem';
|
|
||||||
import DialogTitleWithCloseButton from 'components/DialogBox/TitleWithCloseButton';
|
|
||||||
import { Dialog, DialogContent, Link, styled, Typography } from '@mui/material';
|
|
||||||
import { AppContext } from 'pages/_app';
|
|
||||||
import { Location, Metadata } from 'types/upload';
|
|
||||||
import Photoswipe from 'photoswipe';
|
|
||||||
import { getEXIFLocation } from 'services/upload/exifService';
|
|
||||||
|
|
||||||
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
|
|
||||||
zIndex: 1501,
|
|
||||||
'& .MuiDialog-container': {
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
},
|
|
||||||
'& .MuiDialog-paper': {
|
|
||||||
padding: theme.spacing(2),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
interface Iprops {
|
|
||||||
shouldDisableEdits: boolean;
|
|
||||||
showInfo: boolean;
|
|
||||||
handleCloseInfo: () => void;
|
|
||||||
items: any[];
|
|
||||||
photoSwipe: Photoswipe<Photoswipe.Options>;
|
|
||||||
metadata: Metadata;
|
|
||||||
exif: any;
|
|
||||||
scheduleUpdate: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FileInfo({
|
|
||||||
shouldDisableEdits,
|
|
||||||
showInfo,
|
|
||||||
handleCloseInfo,
|
|
||||||
items,
|
|
||||||
photoSwipe,
|
|
||||||
metadata,
|
|
||||||
exif,
|
|
||||||
scheduleUpdate,
|
|
||||||
}: Iprops) {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
const [location, setLocation] = useState<Location>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!location && metadata) {
|
|
||||||
if (metadata.longitude || metadata.longitude === 0) {
|
|
||||||
setLocation({
|
|
||||||
latitude: metadata.latitude,
|
|
||||||
longitude: metadata.longitude,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [metadata]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!location && exif) {
|
|
||||||
const exifLocation = getEXIFLocation(exif);
|
|
||||||
if (exifLocation.latitude || exifLocation.latitude === 0) {
|
|
||||||
setLocation(exifLocation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [exif]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FileInfoDialog
|
|
||||||
open={showInfo}
|
|
||||||
onClose={handleCloseInfo}
|
|
||||||
fullScreen={appContext.isMobile}>
|
|
||||||
<DialogTitleWithCloseButton onClose={handleCloseInfo}>
|
|
||||||
{constants.INFO}
|
|
||||||
</DialogTitleWithCloseButton>
|
|
||||||
<DialogContent>
|
|
||||||
<Typography variant="subtitle" mb={1}>
|
|
||||||
{constants.METADATA}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
{RenderInfoItem(
|
|
||||||
constants.FILE_ID,
|
|
||||||
items[photoSwipe?.getCurrentIndex()]?.id
|
|
||||||
)}
|
|
||||||
{metadata?.title && (
|
|
||||||
<RenderFileName
|
|
||||||
shouldDisableEdits={shouldDisableEdits}
|
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
|
||||||
scheduleUpdate={scheduleUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{metadata?.creationTime && (
|
|
||||||
<RenderCreationTime
|
|
||||||
shouldDisableEdits={shouldDisableEdits}
|
|
||||||
file={items[photoSwipe?.getCurrentIndex()]}
|
|
||||||
scheduleUpdate={scheduleUpdate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{metadata?.modificationTime &&
|
|
||||||
RenderInfoItem(
|
|
||||||
constants.UPDATED_ON,
|
|
||||||
formatDateTime(metadata.modificationTime / 1000)
|
|
||||||
)}
|
|
||||||
{location &&
|
|
||||||
RenderInfoItem(
|
|
||||||
constants.LOCATION,
|
|
||||||
<Link
|
|
||||||
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer">
|
|
||||||
{constants.SHOW_MAP}
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{exif && (
|
|
||||||
<>
|
|
||||||
<ExifData exif={exif} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</DialogContent>
|
|
||||||
</FileInfoDialog>
|
|
||||||
);
|
|
||||||
}
|
|
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>
|
||||||
|
);
|
||||||
|
}
|
51
src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx
Normal file
51
src/components/PhotoViewer/FileInfo/FileNameEditDialog.tsx
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import { DialogContent, DialogTitle } from '@mui/material';
|
||||||
|
import DialogBoxBase from 'components/DialogBox/base';
|
||||||
|
import SingleInputForm, {
|
||||||
|
SingleInputFormProps,
|
||||||
|
} from 'components/SingleInputForm';
|
||||||
|
|
||||||
|
export interface formValues {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FileNameEditDialog = ({
|
||||||
|
isInEditMode,
|
||||||
|
closeEditMode,
|
||||||
|
filename,
|
||||||
|
extension,
|
||||||
|
saveEdits,
|
||||||
|
}) => {
|
||||||
|
const onSubmit: SingleInputFormProps['callback'] = async (
|
||||||
|
filename,
|
||||||
|
setFieldError
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
await saveEdits(filename);
|
||||||
|
closeEditMode();
|
||||||
|
} catch (e) {
|
||||||
|
setFieldError(constants.UNKNOWN_ERROR);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<DialogBoxBase
|
||||||
|
open={isInEditMode}
|
||||||
|
onClose={closeEditMode}
|
||||||
|
sx={{ zIndex: 1600 }}>
|
||||||
|
<DialogTitle>{constants.RENAME_FILE}</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<SingleInputForm
|
||||||
|
initialValue={filename}
|
||||||
|
callback={onSubmit}
|
||||||
|
placeholder={constants.ENTER_FILE_NAME}
|
||||||
|
buttonText={constants.RENAME}
|
||||||
|
fieldType="text"
|
||||||
|
caption={extension}
|
||||||
|
secondaryButtonAction={closeEditMode}
|
||||||
|
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogBoxBase>
|
||||||
|
);
|
||||||
|
};
|
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';
|
||||||
|
|
||||||
|
export interface formValues {
|
||||||
|
caption: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RenderCaption({
|
||||||
|
file,
|
||||||
|
scheduleUpdate,
|
||||||
|
refreshPhotoswipe,
|
||||||
|
}: {
|
||||||
|
shouldDisableEdits: boolean;
|
||||||
|
file: EnteFile;
|
||||||
|
scheduleUpdate: () => void;
|
||||||
|
refreshPhotoswipe: () => void;
|
||||||
|
}) {
|
||||||
|
const [caption, setCaption] = useState(
|
||||||
|
file?.pubMagicMetadata?.data.caption
|
||||||
|
);
|
||||||
|
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
const saveEdits = async (newCaption: string) => {
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
if (caption === newCaption) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCaption(newCaption);
|
||||||
|
|
||||||
|
let updatedFile = await changeCaption(file, newCaption);
|
||||||
|
updatedFile = (
|
||||||
|
await updateFilePublicMagicMetadata([updatedFile])
|
||||||
|
)[0];
|
||||||
|
updateExistingFilePubMetadata(file, updatedFile);
|
||||||
|
file.title = file.pubMagicMetadata.data.caption;
|
||||||
|
refreshPhotoswipe();
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to update caption');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSubmit = async (values: formValues) => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
await saveEdits(values.caption);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<Box p={1}>
|
||||||
|
<Formik<formValues>
|
||||||
|
initialValues={{ caption }}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
caption: Yup.string().max(
|
||||||
|
MAX_CAPTION_SIZE,
|
||||||
|
constants.CAPTION_CHARACTER_LIMIT
|
||||||
|
),
|
||||||
|
})}
|
||||||
|
validateOnBlur={false}
|
||||||
|
onSubmit={onSubmit}>
|
||||||
|
{({
|
||||||
|
values,
|
||||||
|
errors,
|
||||||
|
handleChange,
|
||||||
|
handleSubmit,
|
||||||
|
resetForm,
|
||||||
|
}) => (
|
||||||
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
|
<TextField
|
||||||
|
hiddenLabel
|
||||||
|
fullWidth
|
||||||
|
id="caption"
|
||||||
|
name="caption"
|
||||||
|
type="text"
|
||||||
|
multiline
|
||||||
|
placeholder={constants.CAPTION_PLACEHOLDER}
|
||||||
|
value={values.caption}
|
||||||
|
onChange={handleChange('caption')}
|
||||||
|
error={Boolean(errors.caption)}
|
||||||
|
helperText={errors.caption}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
{values.caption !== caption && (
|
||||||
|
<FlexWrapper justifyContent={'flex-end'}>
|
||||||
|
<IconButton type="submit" disabled={loading}>
|
||||||
|
{loading ? (
|
||||||
|
<SmallLoadingSpinner />
|
||||||
|
) : (
|
||||||
|
<Done />
|
||||||
|
)}
|
||||||
|
</IconButton>
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
resetForm({
|
||||||
|
values: { caption: caption ?? '' },
|
||||||
|
touched: { caption: false },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
disabled={loading}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
</FlexWrapper>
|
||||||
|
)}
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,18 +1,16 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import constants from 'utils/strings/constants';
|
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
|
||||||
import {
|
import {
|
||||||
changeFileCreationTime,
|
changeFileCreationTime,
|
||||||
updateExistingFilePubMetadata,
|
updateExistingFilePubMetadata,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
import { formatDateTime } from 'utils/time';
|
import { formatDate, formatTime } from 'utils/time/format';
|
||||||
import EditIcon from '@mui/icons-material/Edit';
|
import { FlexWrapper } from 'components/Container';
|
||||||
import { Label, Row, Value } from 'components/Container';
|
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
|
||||||
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
import EnteDateTimePicker from 'components/EnteDateTimePicker';
|
||||||
import { IconButton } from '@mui/material';
|
import InfoItem from './InfoItem';
|
||||||
|
|
||||||
export function RenderCreationTime({
|
export function RenderCreationTime({
|
||||||
shouldDisableEdits,
|
shouldDisableEdits,
|
||||||
|
@ -59,39 +57,24 @@ export function RenderCreationTime({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Row>
|
<FlexWrapper>
|
||||||
<Label width="30%">{constants.CREATION_TIME}</Label>
|
<InfoItem
|
||||||
<Value
|
icon={<CalendarTodayIcon />}
|
||||||
width={
|
title={formatDate(originalCreationTime)}
|
||||||
!shouldDisableEdits ? !isInEditMode && '60%' : '70%'
|
caption={formatTime(originalCreationTime)}
|
||||||
}>
|
openEditor={openEditMode}
|
||||||
{isInEditMode ? (
|
loading={loading}
|
||||||
|
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||||
|
/>
|
||||||
|
{isInEditMode && (
|
||||||
<EnteDateTimePicker
|
<EnteDateTimePicker
|
||||||
initialValue={originalCreationTime}
|
initialValue={originalCreationTime}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
onSubmit={saveEdits}
|
onSubmit={saveEdits}
|
||||||
onClose={closeEditMode}
|
onClose={closeEditMode}
|
||||||
/>
|
/>
|
||||||
) : (
|
|
||||||
formatDateTime(originalCreationTime)
|
|
||||||
)}
|
)}
|
||||||
</Value>
|
</FlexWrapper>
|
||||||
{!shouldDisableEdits && !isInEditMode && (
|
|
||||||
<Value
|
|
||||||
width={'10%'}
|
|
||||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
|
||||||
{loading ? (
|
|
||||||
<IconButton>
|
|
||||||
<SmallLoadingSpinner />
|
|
||||||
</IconButton>
|
|
||||||
) : (
|
|
||||||
<IconButton onClick={openEditMode}>
|
|
||||||
<EditIcon />
|
|
||||||
</IconButton>
|
|
||||||
)}
|
|
||||||
</Value>
|
|
||||||
)}
|
|
||||||
</Row>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
121
src/components/PhotoViewer/FileInfo/RenderFileName.tsx
Normal file
121
src/components/PhotoViewer/FileInfo/RenderFileName.tsx
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||||
|
import { EnteFile } from 'types/file';
|
||||||
|
import {
|
||||||
|
changeFileName,
|
||||||
|
splitFilenameAndExtension,
|
||||||
|
updateExistingFilePubMetadata,
|
||||||
|
} from 'utils/file';
|
||||||
|
import { FlexWrapper } from 'components/Container';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
import { FILE_TYPE } from 'constants/file';
|
||||||
|
import { PhotoOutlined, VideoFileOutlined } from '@mui/icons-material';
|
||||||
|
import InfoItem from './InfoItem';
|
||||||
|
import { makeHumanReadableStorage } from 'utils/billing';
|
||||||
|
import Box from '@mui/material/Box';
|
||||||
|
import { FileNameEditDialog } from './FileNameEditDialog';
|
||||||
|
|
||||||
|
const getFileTitle = (filename, extension) => {
|
||||||
|
if (extension) {
|
||||||
|
return filename + '.' + extension;
|
||||||
|
} else {
|
||||||
|
return filename;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCaption = (file: EnteFile, parsedExifData) => {
|
||||||
|
const megaPixels = parsedExifData?.['megaPixels'];
|
||||||
|
const resolution = parsedExifData?.['resolution'];
|
||||||
|
const fileSize = file.info?.fileSize;
|
||||||
|
|
||||||
|
const captionParts = [];
|
||||||
|
if (megaPixels) {
|
||||||
|
captionParts.push(megaPixels);
|
||||||
|
}
|
||||||
|
if (resolution) {
|
||||||
|
captionParts.push(resolution);
|
||||||
|
}
|
||||||
|
if (fileSize) {
|
||||||
|
captionParts.push(makeHumanReadableStorage(fileSize));
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FlexWrapper gap={1}>
|
||||||
|
{captionParts.map((caption) => (
|
||||||
|
<Box key={caption}> {caption}</Box>
|
||||||
|
))}
|
||||||
|
</FlexWrapper>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RenderFileName({
|
||||||
|
parsedExifData,
|
||||||
|
shouldDisableEdits,
|
||||||
|
file,
|
||||||
|
scheduleUpdate,
|
||||||
|
}: {
|
||||||
|
parsedExifData: Record<string, any>;
|
||||||
|
shouldDisableEdits: boolean;
|
||||||
|
file: EnteFile;
|
||||||
|
scheduleUpdate: () => void;
|
||||||
|
}) {
|
||||||
|
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||||
|
const openEditMode = () => setIsInEditMode(true);
|
||||||
|
const closeEditMode = () => setIsInEditMode(false);
|
||||||
|
const [filename, setFilename] = useState<string>();
|
||||||
|
const [extension, setExtension] = useState<string>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const [filename, extension] = splitFilenameAndExtension(
|
||||||
|
file.metadata.title
|
||||||
|
);
|
||||||
|
setFilename(filename);
|
||||||
|
setExtension(extension);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveEdits = async (newFilename: string) => {
|
||||||
|
try {
|
||||||
|
if (file) {
|
||||||
|
if (filename === newFilename) {
|
||||||
|
closeEditMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setFilename(newFilename);
|
||||||
|
const newTitle = getFileTitle(newFilename, extension);
|
||||||
|
let updatedFile = await changeFileName(file, newTitle);
|
||||||
|
updatedFile = (
|
||||||
|
await updateFilePublicMagicMetadata([updatedFile])
|
||||||
|
)[0];
|
||||||
|
updateExistingFilePubMetadata(file, updatedFile);
|
||||||
|
scheduleUpdate();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to update file name');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InfoItem
|
||||||
|
icon={
|
||||||
|
file.metadata.fileType === FILE_TYPE.IMAGE ? (
|
||||||
|
<PhotoOutlined />
|
||||||
|
) : (
|
||||||
|
<VideoFileOutlined />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
title={getFileTitle(filename, extension)}
|
||||||
|
caption={getCaption(file, parsedExifData)}
|
||||||
|
openEditor={openEditMode}
|
||||||
|
hideEditOption={shouldDisableEdits || isInEditMode}
|
||||||
|
/>
|
||||||
|
<FileNameEditDialog
|
||||||
|
isInEditMode={isInEditMode}
|
||||||
|
closeEditMode={closeEditMode}
|
||||||
|
filename={filename}
|
||||||
|
extension={extension}
|
||||||
|
saveEdits={saveEdits}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
280
src/components/PhotoViewer/FileInfo/index.tsx
Normal file
280
src/components/PhotoViewer/FileInfo/index.tsx
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import { RenderFileName } from './RenderFileName';
|
||||||
|
import { RenderCreationTime } from './RenderCreationTime';
|
||||||
|
import { Box, DialogProps, Link, Stack, styled } from '@mui/material';
|
||||||
|
import { Location } from 'types/upload';
|
||||||
|
import { getEXIFLocation } from 'services/upload/exifService';
|
||||||
|
import { RenderCaption } from './RenderCaption';
|
||||||
|
import {
|
||||||
|
BackupOutlined,
|
||||||
|
CameraOutlined,
|
||||||
|
FolderOutlined,
|
||||||
|
LocationOnOutlined,
|
||||||
|
TextSnippetOutlined,
|
||||||
|
} from '@mui/icons-material';
|
||||||
|
import CopyButton from 'components/CodeBlock/CopyButton';
|
||||||
|
import { formatDate, formatTime } from 'utils/time/format';
|
||||||
|
import Titlebar from 'components/Titlebar';
|
||||||
|
import InfoItem from './InfoItem';
|
||||||
|
import { FlexWrapper } from 'components/Container';
|
||||||
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
import { EnteFile } from 'types/file';
|
||||||
|
import { Chip } from 'components/Chip';
|
||||||
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
|
import { ExifData } from './ExifData';
|
||||||
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
|
|
||||||
|
export const FileInfoSidebar = styled((props: DialogProps) => (
|
||||||
|
<EnteDrawer {...props} anchor="right" />
|
||||||
|
))({
|
||||||
|
zIndex: 1501,
|
||||||
|
'& .MuiPaper-root': {
|
||||||
|
padding: 8,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
shouldDisableEdits: boolean;
|
||||||
|
showInfo: boolean;
|
||||||
|
handleCloseInfo: () => void;
|
||||||
|
file: EnteFile;
|
||||||
|
exif: any;
|
||||||
|
scheduleUpdate: () => void;
|
||||||
|
refreshPhotoswipe: () => void;
|
||||||
|
fileToCollectionsMap: Map<number, number[]>;
|
||||||
|
collectionNameMap: Map<number, string>;
|
||||||
|
isTrashCollection: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function BasicDeviceCamera({
|
||||||
|
parsedExifData,
|
||||||
|
}: {
|
||||||
|
parsedExifData: Record<string, any>;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<FlexWrapper gap={1}>
|
||||||
|
<Box>{parsedExifData['fNumber']}</Box>
|
||||||
|
<Box>{parsedExifData['exposureTime']}</Box>
|
||||||
|
<Box>{parsedExifData['ISO']}</Box>
|
||||||
|
</FlexWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOpenStreetMapLink(location: {
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
}) {
|
||||||
|
return `https://www.openstreetmap.org/?mlat=${location.latitude}&mlon=${location.longitude}#map=15/${location.latitude}/${location.longitude}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FileInfo({
|
||||||
|
shouldDisableEdits,
|
||||||
|
showInfo,
|
||||||
|
handleCloseInfo,
|
||||||
|
file,
|
||||||
|
exif,
|
||||||
|
scheduleUpdate,
|
||||||
|
refreshPhotoswipe,
|
||||||
|
fileToCollectionsMap,
|
||||||
|
collectionNameMap,
|
||||||
|
isTrashCollection,
|
||||||
|
}: Iprops) {
|
||||||
|
const [location, setLocation] = useState<Location>(null);
|
||||||
|
const [parsedExifData, setParsedExifData] = useState<Record<string, any>>();
|
||||||
|
const [showExif, setShowExif] = useState(false);
|
||||||
|
|
||||||
|
const openExif = () => setShowExif(true);
|
||||||
|
const closeExif = () => setShowExif(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!location && file && file.metadata) {
|
||||||
|
if (file.metadata.longitude || file.metadata.longitude === 0) {
|
||||||
|
setLocation({
|
||||||
|
latitude: file.metadata.latitude,
|
||||||
|
longitude: file.metadata.longitude,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [file]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!location && exif) {
|
||||||
|
const exifLocation = getEXIFLocation(exif);
|
||||||
|
if (exifLocation.latitude || exifLocation.latitude === 0) {
|
||||||
|
setLocation(exifLocation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [exif]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!exif) {
|
||||||
|
setParsedExifData({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parsedExifData = {};
|
||||||
|
if (exif['fNumber']) {
|
||||||
|
parsedExifData['fNumber'] = `f/${Math.ceil(exif['FNumber'])}`;
|
||||||
|
} else if (exif['ApertureValue'] && exif['FocalLength']) {
|
||||||
|
parsedExifData['fNumber'] = `f/${Math.ceil(
|
||||||
|
exif['FocalLength'] / exif['ApertureValue']
|
||||||
|
)}`;
|
||||||
|
}
|
||||||
|
const imageWidth = exif['ImageWidth'] ?? exif['ExifImageWidth'];
|
||||||
|
const imageHeight = exif['ImageHeight'] ?? exif['ExifImageHeight'];
|
||||||
|
if (imageWidth && imageHeight) {
|
||||||
|
parsedExifData['resolution'] = `${imageWidth} x ${imageHeight}`;
|
||||||
|
const megaPixels = Math.round((imageWidth * imageHeight) / 1000000);
|
||||||
|
if (megaPixels) {
|
||||||
|
parsedExifData['megaPixels'] = `${Math.round(
|
||||||
|
(imageWidth * imageHeight) / 1000000
|
||||||
|
)}MP`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (exif['Make'] && exif['Model']) {
|
||||||
|
parsedExifData[
|
||||||
|
'takenOnDevice'
|
||||||
|
] = `${exif['Make']} ${exif['Model']}`;
|
||||||
|
}
|
||||||
|
if (exif['ExposureTime']) {
|
||||||
|
parsedExifData['exposureTime'] = `1/${
|
||||||
|
1 / parseFloat(exif['ExposureTime'])
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
if (exif['ISO']) {
|
||||||
|
parsedExifData['ISO'] = `ISO${exif['ISO']}`;
|
||||||
|
}
|
||||||
|
setParsedExifData(parsedExifData);
|
||||||
|
}, [exif]);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FileInfoSidebar open={showInfo} onClose={handleCloseInfo}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={handleCloseInfo}
|
||||||
|
title={constants.INFO}
|
||||||
|
backIsClose
|
||||||
|
/>
|
||||||
|
<Stack pt={1} pb={3} spacing={'20px'}>
|
||||||
|
<RenderCaption
|
||||||
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
|
file={file}
|
||||||
|
scheduleUpdate={scheduleUpdate}
|
||||||
|
refreshPhotoswipe={refreshPhotoswipe}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RenderCreationTime
|
||||||
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
|
file={file}
|
||||||
|
scheduleUpdate={scheduleUpdate}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<RenderFileName
|
||||||
|
parsedExifData={parsedExifData}
|
||||||
|
shouldDisableEdits={shouldDisableEdits}
|
||||||
|
file={file}
|
||||||
|
scheduleUpdate={scheduleUpdate}
|
||||||
|
/>
|
||||||
|
{parsedExifData && parsedExifData['takenOnDevice'] && (
|
||||||
|
<InfoItem
|
||||||
|
icon={<CameraOutlined />}
|
||||||
|
title={parsedExifData['takenOnDevice']}
|
||||||
|
caption={
|
||||||
|
<BasicDeviceCamera
|
||||||
|
parsedExifData={parsedExifData}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
hideEditOption
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{location && (
|
||||||
|
<InfoItem
|
||||||
|
icon={<LocationOnOutlined />}
|
||||||
|
title={constants.LOCATION}
|
||||||
|
caption={
|
||||||
|
<Link
|
||||||
|
href={getOpenStreetMapLink({
|
||||||
|
latitude: file.metadata.latitude,
|
||||||
|
longitude: file.metadata.longitude,
|
||||||
|
})}
|
||||||
|
target="_blank"
|
||||||
|
sx={{ fontWeight: 'bold' }}>
|
||||||
|
{constants.SHOW_ON_MAP}
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
|
customEndButton={
|
||||||
|
<CopyButton
|
||||||
|
code={getOpenStreetMapLink({
|
||||||
|
latitude: file.metadata.latitude,
|
||||||
|
longitude: file.metadata.longitude,
|
||||||
|
})}
|
||||||
|
color="secondary"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<InfoItem
|
||||||
|
icon={<TextSnippetOutlined />}
|
||||||
|
title={constants.DETAILS}
|
||||||
|
caption={
|
||||||
|
typeof exif === 'undefined' ? (
|
||||||
|
<EnteSpinner size={11.33} />
|
||||||
|
) : exif !== null ? (
|
||||||
|
<LinkButton
|
||||||
|
onClick={openExif}
|
||||||
|
sx={{
|
||||||
|
textDecoration: 'none',
|
||||||
|
color: 'text.secondary',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
}}>
|
||||||
|
{constants.VIEW_EXIF}
|
||||||
|
</LinkButton>
|
||||||
|
) : (
|
||||||
|
constants.NO_EXIF
|
||||||
|
)
|
||||||
|
}
|
||||||
|
hideEditOption
|
||||||
|
/>
|
||||||
|
<InfoItem
|
||||||
|
icon={<BackupOutlined />}
|
||||||
|
title={formatDate(file.metadata.modificationTime / 1000)}
|
||||||
|
caption={formatTime(file.metadata.modificationTime / 1000)}
|
||||||
|
hideEditOption
|
||||||
|
/>
|
||||||
|
{!isTrashCollection && (
|
||||||
|
<InfoItem icon={<FolderOutlined />} hideEditOption>
|
||||||
|
<Box
|
||||||
|
display={'flex'}
|
||||||
|
gap={1}
|
||||||
|
flexWrap="wrap"
|
||||||
|
justifyContent={'flex-start'}
|
||||||
|
alignItems={'flex-start'}>
|
||||||
|
{fileToCollectionsMap
|
||||||
|
.get(file.id)
|
||||||
|
?.filter((collectionID) =>
|
||||||
|
collectionNameMap.has(collectionID)
|
||||||
|
)
|
||||||
|
?.map((collectionID) => (
|
||||||
|
<Chip key={collectionID}>
|
||||||
|
{collectionNameMap.get(collectionID)}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</InfoItem>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
<ExifData
|
||||||
|
exif={exif}
|
||||||
|
open={showExif}
|
||||||
|
onClose={closeExif}
|
||||||
|
onInfoClose={handleCloseInfo}
|
||||||
|
filename={file.metadata.title}
|
||||||
|
/>
|
||||||
|
</FileInfoSidebar>
|
||||||
|
);
|
||||||
|
}
|
|
@ -9,26 +9,49 @@ import {
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
import events from './events';
|
import { downloadFile, copyFileToClipboard } from 'utils/file';
|
||||||
import { downloadFile } from 'utils/file';
|
|
||||||
import { prettyPrintExif } from 'utils/exif';
|
|
||||||
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { sleep } from 'utils/common';
|
import { isClipboardItemPresent } from 'utils/common';
|
||||||
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
import { playVideo, pauseVideo } from 'utils/photoFrame';
|
||||||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import { FileInfo } from './InfoDialog';
|
import { FileInfo } from './FileInfo';
|
||||||
import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe';
|
import {
|
||||||
|
defaultLivePhotoDefaultOptions,
|
||||||
|
photoSwipeV4Events,
|
||||||
|
} from 'constants/photoViewer';
|
||||||
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
|
import { LivePhotoBtn } from './styledComponents/LivePhotoBtn';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import InfoIcon from '@mui/icons-material/InfoOutlined';
|
import InfoIcon from '@mui/icons-material/InfoOutlined';
|
||||||
import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
|
import FavoriteIcon from '@mui/icons-material/FavoriteRounded';
|
||||||
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
|
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorderRounded';
|
||||||
import ChevronRight from '@mui/icons-material/ChevronRight';
|
import ChevronRight from '@mui/icons-material/ChevronRight';
|
||||||
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
|
import { trashFiles } from 'services/fileService';
|
||||||
|
import { getTrashFileMessage } from 'utils/ui';
|
||||||
|
import { ChevronLeft, ContentCopy } from '@mui/icons-material';
|
||||||
|
import { styled } from '@mui/material';
|
||||||
|
import { addLocalLog } from 'utils/logging';
|
||||||
|
|
||||||
|
interface PhotoswipeFullscreenAPI {
|
||||||
|
enter: () => void;
|
||||||
|
exit: () => void;
|
||||||
|
isFullscreen: () => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CaptionContainer = styled('div')(({ theme }) => ({
|
||||||
|
padding: theme.spacing(2),
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
textAlign: 'right',
|
||||||
|
maxWidth: '375px',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '17px',
|
||||||
|
backgroundColor: theme.palette.backdrop.light,
|
||||||
|
backdropFilter: `blur(${theme.palette.blur.base})`,
|
||||||
|
}));
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
items: any[];
|
items: any[];
|
||||||
|
@ -38,13 +61,17 @@ interface Iprops {
|
||||||
id?: string;
|
id?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
favItemIds: Set<number>;
|
favItemIds: Set<number>;
|
||||||
|
deletedFileIds: Set<number>;
|
||||||
|
setDeletedFileIds?: (value: Set<number>) => void;
|
||||||
isSharedCollection: boolean;
|
isSharedCollection: boolean;
|
||||||
isTrashCollection: boolean;
|
isTrashCollection: boolean;
|
||||||
enableDownload: boolean;
|
enableDownload: boolean;
|
||||||
isSourceLoaded: boolean;
|
isSourceLoaded: boolean;
|
||||||
|
fileToCollectionsMap: Map<number, number[]>;
|
||||||
|
collectionNameMap: Map<number, string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PhotoSwipe(props: Iprops) {
|
function PhotoViewer(props: Iprops) {
|
||||||
const pswpElement = useRef<HTMLDivElement>();
|
const pswpElement = useRef<HTMLDivElement>();
|
||||||
const [photoSwipe, setPhotoSwipe] =
|
const [photoSwipe, setPhotoSwipe] =
|
||||||
useState<Photoswipe<Photoswipe.Options>>();
|
useState<Photoswipe<Photoswipe.Options>>();
|
||||||
|
@ -52,8 +79,9 @@ function PhotoSwipe(props: Iprops) {
|
||||||
const { isOpen, items, isSourceLoaded } = props;
|
const { isOpen, items, isSourceLoaded } = props;
|
||||||
const [isFav, setIsFav] = useState(false);
|
const [isFav, setIsFav] = useState(false);
|
||||||
const [showInfo, setShowInfo] = useState(false);
|
const [showInfo, setShowInfo] = useState(false);
|
||||||
const [metadata, setMetaData] = useState<EnteFile['metadata']>(null);
|
const [exif, setExif] =
|
||||||
const [exif, setExif] = useState<any>(null);
|
useState<{ key: string; value: Record<string, any> }>();
|
||||||
|
const exifCopy = useRef(null);
|
||||||
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
const [livePhotoBtnOptions, setLivePhotoBtnOptions] = useState(
|
||||||
defaultLivePhotoDefaultOptions
|
defaultLivePhotoDefaultOptions
|
||||||
);
|
);
|
||||||
|
@ -63,6 +91,9 @@ function PhotoSwipe(props: Iprops) {
|
||||||
);
|
);
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
|
|
||||||
|
const exifExtractionInProgress = useRef<string>(null);
|
||||||
|
const [shouldShowCopyOption] = useState(isClipboardItemPresent());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!pswpElement) return;
|
if (!pswpElement) return;
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
@ -76,6 +107,61 @@ function PhotoSwipe(props: Iprops) {
|
||||||
};
|
};
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!photoSwipe) return;
|
||||||
|
function handleCopyEvent() {
|
||||||
|
copyToClipboardHelper(photoSwipe.currItem as EnteFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeyUp(event: KeyboardEvent) {
|
||||||
|
if (!isOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addLocalLog(() => 'Event: ' + event.key);
|
||||||
|
if (event.key === 'i' || event.key === 'I') {
|
||||||
|
if (!showInfo) {
|
||||||
|
setShowInfo(true);
|
||||||
|
} else {
|
||||||
|
setShowInfo(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (showInfo) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch (event.key) {
|
||||||
|
case 'Backspace':
|
||||||
|
case 'Delete':
|
||||||
|
confirmTrashFile(photoSwipe?.currItem as EnteFile);
|
||||||
|
break;
|
||||||
|
case 'd':
|
||||||
|
case 'D':
|
||||||
|
downloadFileHelper(photoSwipe?.currItem as EnteFile);
|
||||||
|
break;
|
||||||
|
case 'f':
|
||||||
|
case 'F':
|
||||||
|
toggleFullscreen(photoSwipe);
|
||||||
|
break;
|
||||||
|
case 'l':
|
||||||
|
case 'L':
|
||||||
|
onFavClick(photoSwipe?.currItem as EnteFile);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('keyup', handleKeyUp);
|
||||||
|
if (shouldShowCopyOption) {
|
||||||
|
window.addEventListener('copy', handleCopyEvent);
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('keyup', handleKeyUp);
|
||||||
|
if (shouldShowCopyOption) {
|
||||||
|
window.removeEventListener('copy', handleCopyEvent);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isOpen, photoSwipe, showInfo]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateItems(items);
|
updateItems(items);
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
@ -152,8 +238,12 @@ function PhotoSwipe(props: Iprops) {
|
||||||
}
|
}
|
||||||
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
}, [photoSwipe?.currItem, isOpen, isSourceLoaded]);
|
||||||
|
|
||||||
function updateFavButton() {
|
useEffect(() => {
|
||||||
setIsFav(isInFav(this?.currItem));
|
exifCopy.current = exif;
|
||||||
|
}, [exif]);
|
||||||
|
|
||||||
|
function updateFavButton(file: EnteFile) {
|
||||||
|
setIsFav(isInFav(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
const openPhotoSwipe = () => {
|
const openPhotoSwipe = () => {
|
||||||
|
@ -198,7 +288,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
items,
|
items,
|
||||||
options
|
options
|
||||||
);
|
);
|
||||||
events.forEach((event) => {
|
photoSwipeV4Events.forEach((event) => {
|
||||||
const callback = props[event];
|
const callback = props[event];
|
||||||
if (callback || event === 'destroy') {
|
if (callback || event === 'destroy') {
|
||||||
photoSwipe.listen(event, function (...args) {
|
photoSwipe.listen(event, function (...args) {
|
||||||
|
@ -215,11 +305,31 @@ function PhotoSwipe(props: Iprops) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
photoSwipe.listen('beforeChange', function () {
|
photoSwipe.listen('beforeChange', () => {
|
||||||
updateInfo.call(this);
|
const currItem = photoSwipe?.currItem as EnteFile;
|
||||||
updateFavButton.call(this);
|
if (
|
||||||
|
!currItem ||
|
||||||
|
!exifCopy?.current?.value === null ||
|
||||||
|
exifCopy?.current?.key === currItem.src
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExif({ key: currItem.src, value: undefined });
|
||||||
|
checkExifAvailable(currItem);
|
||||||
|
updateFavButton(currItem);
|
||||||
|
});
|
||||||
|
photoSwipe.listen('resize', () => {
|
||||||
|
const currItem = photoSwipe?.currItem as EnteFile;
|
||||||
|
if (
|
||||||
|
!currItem ||
|
||||||
|
!exifCopy?.current?.value === null ||
|
||||||
|
exifCopy?.current?.key === currItem.src
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setExif({ key: currItem.src, value: undefined });
|
||||||
|
checkExifAvailable(currItem);
|
||||||
});
|
});
|
||||||
photoSwipe.listen('resize', checkExifAvailable);
|
|
||||||
photoSwipe.init();
|
photoSwipe.init();
|
||||||
needUpdate.current = false;
|
needUpdate.current = false;
|
||||||
setPhotoSwipe(photoSwipe);
|
setPhotoSwipe(photoSwipe);
|
||||||
|
@ -240,7 +350,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
}
|
}
|
||||||
handleCloseInfo();
|
handleCloseInfo();
|
||||||
};
|
};
|
||||||
const isInFav = (file) => {
|
const isInFav = (file: EnteFile) => {
|
||||||
const { favItemIds } = props;
|
const { favItemIds } = props;
|
||||||
if (favItemIds && file) {
|
if (favItemIds && file) {
|
||||||
return favItemIds.has(file.id);
|
return favItemIds.has(file.id);
|
||||||
|
@ -248,7 +358,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onFavClick = async (file) => {
|
const onFavClick = async (file: EnteFile) => {
|
||||||
const { favItemIds } = props;
|
const { favItemIds } = props;
|
||||||
if (!isInFav(file)) {
|
if (!isInFav(file)) {
|
||||||
favItemIds.add(file.id);
|
favItemIds.add(file.id);
|
||||||
|
@ -262,46 +372,78 @@ function PhotoSwipe(props: Iprops) {
|
||||||
needUpdate.current = true;
|
needUpdate.current = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const trashFile = async (file: EnteFile) => {
|
||||||
|
const { deletedFileIds, setDeletedFileIds } = props;
|
||||||
|
deletedFileIds.add(file.id);
|
||||||
|
setDeletedFileIds(new Set(deletedFileIds));
|
||||||
|
await trashFiles([file]);
|
||||||
|
needUpdate.current = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmTrashFile = (file: EnteFile) => {
|
||||||
|
if (props.isSharedCollection || props.isTrashCollection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appContext.setDialogMessage(getTrashFileMessage(() => trashFile(file)));
|
||||||
|
};
|
||||||
|
|
||||||
const updateItems = (items = []) => {
|
const updateItems = (items = []) => {
|
||||||
if (photoSwipe) {
|
if (photoSwipe) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
photoSwipe.close();
|
||||||
|
}
|
||||||
photoSwipe.items.length = 0;
|
photoSwipe.items.length = 0;
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
photoSwipe.items.push(item);
|
photoSwipe.items.push(item);
|
||||||
});
|
});
|
||||||
photoSwipe.invalidateCurrItems();
|
photoSwipe.invalidateCurrItems();
|
||||||
// photoSwipe.updateSize(true);
|
if (isOpen) {
|
||||||
|
photoSwipe.updateSize(true);
|
||||||
|
if (photoSwipe.getCurrentIndex() >= photoSwipe.items.length) {
|
||||||
|
photoSwipe.goTo(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkExifAvailable = async () => {
|
const refreshPhotoswipe = () => {
|
||||||
setExif(null);
|
photoSwipe.invalidateCurrItems();
|
||||||
await sleep(100);
|
if (isOpen) {
|
||||||
|
photoSwipe.updateSize(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkExifAvailable = async (file: EnteFile) => {
|
||||||
try {
|
try {
|
||||||
const img: HTMLImageElement = document.querySelector(
|
if (exifExtractionInProgress.current === file.src) {
|
||||||
'.pswp__img:not(.pswp__img--placeholder)'
|
|
||||||
);
|
|
||||||
if (img) {
|
|
||||||
const exifData = await exifr.parse(img);
|
|
||||||
if (!exifData) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
exifData.raw = prettyPrintExif(exifData);
|
try {
|
||||||
setExif(exifData);
|
if (file.isSourceLoaded) {
|
||||||
|
exifExtractionInProgress.current = file.src;
|
||||||
|
const imageBlob = await (
|
||||||
|
await fetch(file.originalImageURL)
|
||||||
|
).blob();
|
||||||
|
const exifData = (await exifr.parse(imageBlob)) as Record<
|
||||||
|
string,
|
||||||
|
any
|
||||||
|
>;
|
||||||
|
if (exifExtractionInProgress.current === file.src) {
|
||||||
|
if (exifData) {
|
||||||
|
setExif({ key: file.src, value: exifData });
|
||||||
|
} else {
|
||||||
|
setExif({ key: file.src, value: null });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
exifExtractionInProgress.current = null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'exifr parsing failed');
|
logError(e, 'exifr parsing failed');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function updateInfo() {
|
|
||||||
const file: EnteFile = this?.currItem;
|
|
||||||
if (file?.metadata) {
|
|
||||||
setMetaData(file.metadata);
|
|
||||||
setExif(null);
|
|
||||||
checkExifAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseInfo = () => {
|
const handleCloseInfo = () => {
|
||||||
setShowInfo(false);
|
setShowInfo(false);
|
||||||
};
|
};
|
||||||
|
@ -310,6 +452,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadFileHelper = async (file) => {
|
const downloadFileHelper = async (file) => {
|
||||||
|
if (props.enableDownload) {
|
||||||
appContext.startLoading();
|
appContext.startLoading();
|
||||||
await downloadFile(
|
await downloadFile(
|
||||||
file,
|
file,
|
||||||
|
@ -317,8 +460,29 @@ function PhotoSwipe(props: Iprops) {
|
||||||
publicCollectionGalleryContext.token,
|
publicCollectionGalleryContext.token,
|
||||||
publicCollectionGalleryContext.passwordToken
|
publicCollectionGalleryContext.passwordToken
|
||||||
);
|
);
|
||||||
|
|
||||||
appContext.finishLoading();
|
appContext.finishLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToClipboardHelper = async (file: EnteFile) => {
|
||||||
|
if (props.enableDownload && shouldShowCopyOption) {
|
||||||
|
appContext.startLoading();
|
||||||
|
await copyFileToClipboard(file.src);
|
||||||
|
appContext.finishLoading();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleFullscreen = (photoSwipe) => {
|
||||||
|
const fullScreenApi: PhotoswipeFullscreenAPI =
|
||||||
|
photoSwipe?.ui?.getFullscreenAPI();
|
||||||
|
if (!fullScreenApi) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (fullScreenApi.isFullscreen()) {
|
||||||
|
fullScreenApi.exit();
|
||||||
|
} else {
|
||||||
|
fullScreenApi.enter();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const scheduleUpdate = () => (needUpdate.current = true);
|
const scheduleUpdate = () => (needUpdate.current = true);
|
||||||
const { id } = props;
|
const { id } = props;
|
||||||
|
@ -355,33 +519,74 @@ function PhotoSwipe(props: Iprops) {
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--close"
|
className="pswp__button pswp__button--close"
|
||||||
title={constants.CLOSE}
|
title={constants.CLOSE_OPTION}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{props.enableDownload && (
|
{props.enableDownload && (
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--custom"
|
className="pswp__button pswp__button--custom"
|
||||||
title={constants.DOWNLOAD}
|
title={constants.DOWNLOAD_OPTION}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
downloadFileHelper(photoSwipe.currItem)
|
downloadFileHelper(photoSwipe.currItem)
|
||||||
}>
|
}>
|
||||||
<DownloadIcon fontSize="small" />
|
<DownloadIcon fontSize="small" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
{props.enableDownload && shouldShowCopyOption && (
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--fs"
|
className="pswp__button pswp__button--custom"
|
||||||
title={constants.TOGGLE_FULLSCREEN}
|
title={constants.COPY_OPTION}
|
||||||
/>
|
onClick={() =>
|
||||||
<button
|
copyToClipboardHelper(
|
||||||
className="pswp__button pswp__button--zoom"
|
photoSwipe.currItem as EnteFile
|
||||||
title={constants.ZOOM_IN_OUT}
|
)
|
||||||
/>
|
}>
|
||||||
|
<ContentCopy fontSize="small" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
{!props.isSharedCollection &&
|
{!props.isSharedCollection &&
|
||||||
!props.isTrashCollection && (
|
!props.isTrashCollection && (
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--custom"
|
className="pswp__button pswp__button--custom"
|
||||||
|
title={constants.DELETE_OPTION}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onFavClick(photoSwipe?.currItem);
|
confirmTrashFile(
|
||||||
|
photoSwipe?.currItem as EnteFile
|
||||||
|
);
|
||||||
|
}}>
|
||||||
|
<DeleteIcon fontSize="small" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
className="pswp__button pswp__button--zoom"
|
||||||
|
title={constants.ZOOM_IN_OUT}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="pswp__button pswp__button--fs"
|
||||||
|
title={constants.TOGGLE_FULLSCREEN}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{!props.isSharedCollection && (
|
||||||
|
<button
|
||||||
|
className="pswp__button pswp__button--custom"
|
||||||
|
title={constants.INFO_OPTION}
|
||||||
|
onClick={handleOpenInfo}>
|
||||||
|
<InfoIcon fontSize="small" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!props.isSharedCollection &&
|
||||||
|
!props.isTrashCollection && (
|
||||||
|
<button
|
||||||
|
title={
|
||||||
|
isFav
|
||||||
|
? constants.UNFAVORITE_OPTION
|
||||||
|
: constants.FAVORITE_OPTION
|
||||||
|
}
|
||||||
|
className="pswp__button pswp__button--custom"
|
||||||
|
onClick={() => {
|
||||||
|
onFavClick(
|
||||||
|
photoSwipe?.currItem as EnteFile
|
||||||
|
);
|
||||||
}}>
|
}}>
|
||||||
{isFav ? (
|
{isFav ? (
|
||||||
<FavoriteIcon fontSize="small" />
|
<FavoriteIcon fontSize="small" />
|
||||||
|
@ -390,14 +595,7 @@ function PhotoSwipe(props: Iprops) {
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{!props.isSharedCollection && (
|
|
||||||
<button
|
|
||||||
className="pswp__button pswp__button--custom"
|
|
||||||
title={constants.INFO}
|
|
||||||
onClick={handleOpenInfo}>
|
|
||||||
<InfoIcon fontSize="small" />
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
<div className="pswp__preloader">
|
<div className="pswp__preloader">
|
||||||
<div className="pswp__preloader__icn">
|
<div className="pswp__preloader__icn">
|
||||||
<div className="pswp__preloader__cut">
|
<div className="pswp__preloader__cut">
|
||||||
|
@ -411,36 +609,34 @@ function PhotoSwipe(props: Iprops) {
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--arrow--left"
|
className="pswp__button pswp__button--arrow--left"
|
||||||
title={constants.PREVIOUS}
|
title={constants.PREVIOUS}>
|
||||||
onClick={photoSwipe?.prev}>
|
<ChevronLeft sx={{ pointerEvents: 'none' }} />
|
||||||
<ChevronRight
|
|
||||||
sx={{ transform: 'rotate(180deg)' }}
|
|
||||||
/>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="pswp__button pswp__button--arrow--right"
|
className="pswp__button pswp__button--arrow--right"
|
||||||
title={constants.NEXT}
|
title={constants.NEXT}>
|
||||||
onClick={photoSwipe?.next}>
|
<ChevronRight sx={{ pointerEvents: 'none' }} />
|
||||||
<ChevronRight />
|
|
||||||
</button>
|
</button>
|
||||||
<div className="pswp__caption">
|
<div className="pswp__caption pswp-custom-caption-container">
|
||||||
<div />
|
<CaptionContainer />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<FileInfo
|
<FileInfo
|
||||||
|
isTrashCollection={props.isTrashCollection}
|
||||||
shouldDisableEdits={props.isSharedCollection}
|
shouldDisableEdits={props.isSharedCollection}
|
||||||
showInfo={showInfo}
|
showInfo={showInfo}
|
||||||
handleCloseInfo={handleCloseInfo}
|
handleCloseInfo={handleCloseInfo}
|
||||||
items={items}
|
file={photoSwipe?.currItem as EnteFile}
|
||||||
photoSwipe={photoSwipe}
|
exif={exif?.value}
|
||||||
metadata={metadata}
|
|
||||||
exif={exif}
|
|
||||||
scheduleUpdate={scheduleUpdate}
|
scheduleUpdate={scheduleUpdate}
|
||||||
|
refreshPhotoswipe={refreshPhotoswipe}
|
||||||
|
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||||
|
collectionNameMap={props.collectionNameMap}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PhotoSwipe;
|
export default PhotoViewer;
|
|
@ -37,7 +37,9 @@ export default function SearchInput(props: Iprops) {
|
||||||
setValue(value);
|
setValue(value);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => search(value), [value]);
|
useEffect(() => {
|
||||||
|
search(value);
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const resetSearch = () => {
|
const resetSearch = () => {
|
||||||
if (props.isOpen) {
|
if (props.isOpen) {
|
||||||
|
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
72
src/components/Sidebar/DebugSection.tsx
Normal file
72
src/components/Sidebar/DebugSection.tsx
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
import { AppContext } from 'pages/_app';
|
||||||
|
import React, { useContext, useEffect, useState } from 'react';
|
||||||
|
import { downloadAsFile } from 'utils/file';
|
||||||
|
import constants from 'utils/strings/constants';
|
||||||
|
import { addLogLine, getDebugLogs } from 'utils/logging';
|
||||||
|
import SidebarButton from './Button';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import ElectronService from 'services/electron/common';
|
||||||
|
import { testUpload } from 'tests/upload.test';
|
||||||
|
import Typography from '@mui/material/Typography';
|
||||||
|
import { isInternalUser } from 'utils/user';
|
||||||
|
|
||||||
|
export default function DebugSection() {
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
const [appVersion, setAppVersion] = useState<string>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const main = async () => {
|
||||||
|
if (isElectron()) {
|
||||||
|
const appVersion = await ElectronService.getAppVersion();
|
||||||
|
setAppVersion(appVersion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmLogDownload = () =>
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: constants.DOWNLOAD_LOGS,
|
||||||
|
content: constants.DOWNLOAD_LOGS_MESSAGE(),
|
||||||
|
proceed: {
|
||||||
|
text: constants.DOWNLOAD,
|
||||||
|
variant: 'accent',
|
||||||
|
action: downloadDebugLogs,
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
text: constants.CANCEL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadDebugLogs = () => {
|
||||||
|
addLogLine('exporting logs');
|
||||||
|
if (isElectron()) {
|
||||||
|
ElectronService.openLogDirectory();
|
||||||
|
} else {
|
||||||
|
const logs = getDebugLogs();
|
||||||
|
|
||||||
|
downloadAsFile(`debug_logs_${Date.now()}.txt`, logs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SidebarButton
|
||||||
|
onClick={confirmLogDownload}
|
||||||
|
typographyVariant="caption"
|
||||||
|
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
|
||||||
|
{constants.DOWNLOAD_UPLOAD_LOGS}
|
||||||
|
</SidebarButton>
|
||||||
|
{appVersion && (
|
||||||
|
<Typography p={1.5} color="text.secondary" variant="caption">
|
||||||
|
{appVersion}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{isInternalUser() && (
|
||||||
|
<SidebarButton onClick={testUpload}>
|
||||||
|
{constants.RUN_TESTS}
|
||||||
|
</SidebarButton>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import { getDownloadAppMessage } from 'utils/ui';
|
import { getDownloadAppMessage } from 'utils/ui';
|
||||||
import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
|
import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
|
||||||
|
import { openLink } from 'utils/common';
|
||||||
|
|
||||||
export default function HelpSection() {
|
export default function HelpSection() {
|
||||||
const [exportModalView, setExportModalView] = useState(false);
|
const [exportModalView, setExportModalView] = useState(false);
|
||||||
|
@ -20,8 +21,7 @@ export default function HelpSection() {
|
||||||
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
|
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
|
||||||
getToken()
|
getToken()
|
||||||
)}`;
|
)}`;
|
||||||
const win = window.open(feedbackURL, '_blank');
|
openLink(feedbackURL, true);
|
||||||
win.focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportFiles() {
|
function exportFiles() {
|
||||||
|
|
|
@ -2,10 +2,9 @@ import React, { useContext } from 'react';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
|
||||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
|
||||||
import { CollectionSummaries } from 'types/collection';
|
import { CollectionSummaries } from 'types/collection';
|
||||||
import ShortcutButton from './ShortcutButton';
|
import ShortcutButton from './ShortcutButton';
|
||||||
|
import { ArchiveOutlined, DeleteOutline } from '@mui/icons-material';
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
closeSidebar: () => void;
|
closeSidebar: () => void;
|
||||||
collectionSummaries: CollectionSummaries;
|
collectionSummaries: CollectionSummaries;
|
||||||
|
@ -30,13 +29,13 @@ export default function ShortcutSection({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ShortcutButton
|
<ShortcutButton
|
||||||
startIcon={<DeleteIcon />}
|
startIcon={<DeleteOutline />}
|
||||||
label={constants.TRASH}
|
label={constants.TRASH}
|
||||||
count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
|
count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
|
||||||
onClick={openTrashSection}
|
onClick={openTrashSection}
|
||||||
/>
|
/>
|
||||||
<ShortcutButton
|
<ShortcutButton
|
||||||
startIcon={<VisibilityOffIcon />}
|
startIcon={<ArchiveOutlined />}
|
||||||
label={constants.ARCHIVE_SECTION_NAME}
|
label={constants.ARCHIVE_SECTION_NAME}
|
||||||
count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
|
count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
|
||||||
onClick={openArchiveSection}
|
onClick={openArchiveSection}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
import React, { useContext, useMemo } from 'react';
|
import React, { MouseEventHandler, useContext, useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
hasPaidSubscription,
|
hasPaidSubscription,
|
||||||
isFamilyAdmin,
|
isFamilyAdmin,
|
||||||
|
@ -43,19 +43,23 @@ export default function SubscriptionStatus({
|
||||||
}, [userDetails]);
|
}, [userDetails]);
|
||||||
|
|
||||||
const handleClick = useMemo(() => {
|
const handleClick = useMemo(() => {
|
||||||
|
const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
if (userDetails) {
|
if (userDetails) {
|
||||||
if (isSubscriptionActive(userDetails.subscription)) {
|
if (isSubscriptionActive(userDetails.subscription)) {
|
||||||
if (hasExceededStorageQuota(userDetails)) {
|
if (hasExceededStorageQuota(userDetails)) {
|
||||||
return showPlanSelectorModal;
|
showPlanSelectorModal();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (hasStripeSubscription(userDetails.subscription)) {
|
if (hasStripeSubscription(userDetails.subscription)) {
|
||||||
return billingService.redirectToCustomerPortal;
|
billingService.redirectToCustomerPortal();
|
||||||
} else {
|
} else {
|
||||||
return showPlanSelectorModal;
|
showPlanSelectorModal();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
return eventHandler;
|
||||||
}, [userDetails]);
|
}, [userDetails]);
|
||||||
|
|
||||||
if (!hasAMessage) {
|
if (!hasAMessage) {
|
||||||
|
@ -80,13 +84,9 @@ export default function SubscriptionStatus({
|
||||||
)
|
)
|
||||||
: hasExceededStorageQuota(userDetails) &&
|
: hasExceededStorageQuota(userDetails) &&
|
||||||
constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO(
|
constants.STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO(
|
||||||
showPlanSelectorModal
|
handleClick
|
||||||
)
|
)
|
||||||
: constants.SUBSCRIPTION_EXPIRED_MESSAGE(
|
: constants.SUBSCRIPTION_EXPIRED_MESSAGE(handleClick)}
|
||||||
hasStripeSubscription(userDetails.subscription)
|
|
||||||
? billingService.redirectToCustomerPortal
|
|
||||||
: showPlanSelectorModal
|
|
||||||
)}
|
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
|
||||||
import UtilitySection from './UtilitySection';
|
import UtilitySection from './UtilitySection';
|
||||||
import HelpSection from './HelpSection';
|
import HelpSection from './HelpSection';
|
||||||
import ExitSection from './ExitSection';
|
import ExitSection from './ExitSection';
|
||||||
import DebugLogs from './DebugLogs';
|
import DebugSection from './DebugSection';
|
||||||
import { DrawerSidebar } from './styledComponents';
|
import { DrawerSidebar } from './styledComponents';
|
||||||
import HeaderSection from './Header';
|
import HeaderSection from './Header';
|
||||||
import { CollectionSummaries } from 'types/collection';
|
import { CollectionSummaries } from 'types/collection';
|
||||||
|
@ -37,7 +37,7 @@ export default function Sidebar({
|
||||||
<Divider />
|
<Divider />
|
||||||
<ExitSection />
|
<ExitSection />
|
||||||
<Divider />
|
<Divider />
|
||||||
<DebugLogs />
|
<DebugSection />
|
||||||
</Stack>
|
</Stack>
|
||||||
</DrawerSidebar>
|
</DrawerSidebar>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Drawer, styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import CircleIcon from '@mui/icons-material/Circle';
|
import CircleIcon from '@mui/icons-material/Circle';
|
||||||
|
import { EnteDrawer } from 'components/EnteDrawer';
|
||||||
|
|
||||||
export const DrawerSidebar = styled(Drawer)(({ theme }) => ({
|
export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
|
||||||
'& .MuiPaper-root': {
|
'& .MuiPaper-root': {
|
||||||
maxWidth: '375px',
|
|
||||||
width: '100%',
|
|
||||||
scrollbarWidth: 'thin',
|
|
||||||
padding: theme.spacing(1.5),
|
padding: theme.spacing(1.5),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -6,7 +6,7 @@ import SubmitButton from './SubmitButton';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import ShowHidePassword from './Form/ShowHidePassword';
|
import ShowHidePassword from './Form/ShowHidePassword';
|
||||||
import { FlexWrapper } from './Container';
|
import { FlexWrapper } from './Container';
|
||||||
import { Button } from '@mui/material';
|
import { Button, FormHelperText } from '@mui/material';
|
||||||
|
|
||||||
interface formValues {
|
interface formValues {
|
||||||
inputValue: string;
|
inputValue: string;
|
||||||
|
@ -24,6 +24,7 @@ export interface SingleInputFormProps {
|
||||||
secondaryButtonAction?: () => void;
|
secondaryButtonAction?: () => void;
|
||||||
disableAutoFocus?: boolean;
|
disableAutoFocus?: boolean;
|
||||||
hiddenPreInput?: any;
|
hiddenPreInput?: any;
|
||||||
|
caption?: any;
|
||||||
hiddenPostInput?: any;
|
hiddenPostInput?: any;
|
||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
}
|
}
|
||||||
|
@ -113,6 +114,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
||||||
),
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<FormHelperText
|
||||||
|
sx={{
|
||||||
|
position: 'relative',
|
||||||
|
top: errors.inputValue ? '-22px' : '0',
|
||||||
|
float: 'right',
|
||||||
|
padding: '0 8px',
|
||||||
|
}}>
|
||||||
|
{props.caption}
|
||||||
|
</FormHelperText>
|
||||||
{props.hiddenPostInput}
|
{props.hiddenPostInput}
|
||||||
<FlexWrapper justifyContent={'flex-end'}>
|
<FlexWrapper justifyContent={'flex-end'}>
|
||||||
{props.secondaryButtonAction && (
|
{props.secondaryButtonAction && (
|
||||||
|
@ -120,13 +130,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
||||||
onClick={props.secondaryButtonAction}
|
onClick={props.secondaryButtonAction}
|
||||||
size="large"
|
size="large"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }}
|
sx={{
|
||||||
|
'&&&': { mt: 2, mb: 4, mr: 1, ...buttonSx },
|
||||||
|
}}
|
||||||
{...restSubmitButtonProps}>
|
{...restSubmitButtonProps}>
|
||||||
{constants.CANCEL}
|
{constants.CANCEL}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
<SubmitButton
|
<SubmitButton
|
||||||
sx={{ mt: 2, ...buttonSx }}
|
sx={{ '&&&': { mt: 2, ...buttonSx } }}
|
||||||
buttonText={props.buttonText}
|
buttonText={props.buttonText}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
{...restSubmitButtonProps}
|
{...restSubmitButtonProps}
|
||||||
|
|
56
src/components/Titlebar.tsx
Normal file
56
src/components/Titlebar.tsx
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import { ArrowBack, Close } from '@mui/icons-material';
|
||||||
|
import { Box, IconButton, Typography } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
import { FlexWrapper } from './Container';
|
||||||
|
|
||||||
|
interface Iprops {
|
||||||
|
title: string;
|
||||||
|
caption?: string;
|
||||||
|
onClose: () => void;
|
||||||
|
backIsClose?: boolean;
|
||||||
|
onRootClose?: () => void;
|
||||||
|
actionButton?: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Titlebar({
|
||||||
|
title,
|
||||||
|
caption,
|
||||||
|
onClose,
|
||||||
|
backIsClose,
|
||||||
|
actionButton,
|
||||||
|
onRootClose,
|
||||||
|
}: Iprops): JSX.Element {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FlexWrapper
|
||||||
|
height={48}
|
||||||
|
alignItems={'center'}
|
||||||
|
justifyContent="space-between">
|
||||||
|
<IconButton
|
||||||
|
onClick={onClose}
|
||||||
|
color={backIsClose ? 'secondary' : 'primary'}>
|
||||||
|
{backIsClose ? <Close /> : <ArrowBack />}
|
||||||
|
</IconButton>
|
||||||
|
<Box display={'flex'} gap="4px">
|
||||||
|
{actionButton && actionButton}
|
||||||
|
{!backIsClose && (
|
||||||
|
<IconButton onClick={onRootClose} color={'secondary'}>
|
||||||
|
<Close />
|
||||||
|
</IconButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</FlexWrapper>
|
||||||
|
<Box py={0.5} px={2}>
|
||||||
|
<Typography variant="h3" fontWeight={'bold'}>
|
||||||
|
{title}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color="text.secondary"
|
||||||
|
sx={{ wordBreak: 'break-all', minHeight: '17px' }}>
|
||||||
|
{caption}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,9 +7,9 @@ import { UploadProgressHeader } from './header';
|
||||||
import { InProgressSection } from './inProgressSection';
|
import { InProgressSection } from './inProgressSection';
|
||||||
import { ResultSection } from './resultSection';
|
import { ResultSection } from './resultSection';
|
||||||
import { NotUploadSectionHeader } from './styledComponents';
|
import { NotUploadSectionHeader } from './styledComponents';
|
||||||
import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
|
|
||||||
import UploadProgressContext from 'contexts/uploadProgress';
|
import UploadProgressContext from 'contexts/uploadProgress';
|
||||||
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
||||||
|
import { APP_DOWNLOAD_URL } from 'utils/common';
|
||||||
|
|
||||||
export function UploadProgressDialog() {
|
export function UploadProgressDialog() {
|
||||||
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
||||||
|
@ -40,12 +40,16 @@ export function UploadProgressDialog() {
|
||||||
<Dialog maxWidth="xs" open={open} onClose={handleClose}>
|
<Dialog maxWidth="xs" open={open} onClose={handleClose}>
|
||||||
<UploadProgressHeader />
|
<UploadProgressHeader />
|
||||||
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||||
uploadStage === UPLOAD_STAGES.FINISH) && (
|
uploadStage === UPLOAD_STAGES.FINISH ||
|
||||||
|
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
|
||||||
<DialogContent sx={{ '&&&': { px: 0 } }}>
|
<DialogContent sx={{ '&&&': { px: 0 } }}>
|
||||||
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||||
|
uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA) && (
|
||||||
<InProgressSection />
|
<InProgressSection />
|
||||||
)}
|
)}
|
||||||
|
{(uploadStage === UPLOAD_STAGES.UPLOADING ||
|
||||||
|
uploadStage === UPLOAD_STAGES.FINISH) && (
|
||||||
|
<>
|
||||||
<ResultSection
|
<ResultSection
|
||||||
uploadResult={UPLOAD_RESULT.UPLOADED}
|
uploadResult={UPLOAD_RESULT.UPLOADED}
|
||||||
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
sectionTitle={constants.SUCCESSFUL_UPLOADS}
|
||||||
|
@ -57,7 +61,9 @@ export function UploadProgressDialog() {
|
||||||
sectionTitle={
|
sectionTitle={
|
||||||
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
|
constants.THUMBNAIL_GENERATION_FAILED_UPLOADS
|
||||||
}
|
}
|
||||||
sectionInfo={constants.THUMBNAIL_GENERATION_FAILED_INFO}
|
sectionInfo={
|
||||||
|
constants.THUMBNAIL_GENERATION_FAILED_INFO
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{uploadStage === UPLOAD_STAGES.FINISH &&
|
{uploadStage === UPLOAD_STAGES.FINISH &&
|
||||||
|
@ -71,7 +77,7 @@ export function UploadProgressDialog() {
|
||||||
uploadResult={UPLOAD_RESULT.BLOCKED}
|
uploadResult={UPLOAD_RESULT.BLOCKED}
|
||||||
sectionTitle={constants.BLOCKED_UPLOADS}
|
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||||
sectionInfo={constants.ETAGS_BLOCKED(
|
sectionInfo={constants.ETAGS_BLOCKED(
|
||||||
getOSSpecificDesktopAppDownloadLink()
|
APP_DOWNLOAD_URL
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<ResultSection
|
<ResultSection
|
||||||
|
@ -104,6 +110,8 @@ export function UploadProgressDialog() {
|
||||||
sectionTitle={constants.TOO_LARGE_UPLOADS}
|
sectionTitle={constants.TOO_LARGE_UPLOADS}
|
||||||
sectionInfo={constants.TOO_LARGE_INFO}
|
sectionInfo={constants.TOO_LARGE_INFO}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
)}
|
)}
|
||||||
{uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}
|
{uploadStage === UPLOAD_STAGES.FINISH && <UploadProgressFooter />}
|
||||||
|
|
|
@ -10,17 +10,19 @@ import {
|
||||||
} from './section';
|
} from './section';
|
||||||
import UploadProgressContext from 'contexts/uploadProgress';
|
import UploadProgressContext from 'contexts/uploadProgress';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
|
import { UPLOAD_STAGES } from 'constants/upload';
|
||||||
|
|
||||||
export const InProgressSection = () => {
|
export const InProgressSection = () => {
|
||||||
const { inProgressUploads, hasLivePhotos, uploadFileNames } = useContext(
|
const { inProgressUploads, hasLivePhotos, uploadFileNames, uploadStage } =
|
||||||
UploadProgressContext
|
useContext(UploadProgressContext);
|
||||||
);
|
|
||||||
const fileList = inProgressUploads ?? [];
|
const fileList = inProgressUploads ?? [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<UploadProgressSection defaultExpanded>
|
<UploadProgressSection>
|
||||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||||
{constants.INPROGRESS_UPLOADS}
|
{uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||||
|
? constants.INPROGRESS_METADATA_EXTRACTION
|
||||||
|
: constants.INPROGRESS_UPLOADS}
|
||||||
</UploadProgressSectionTitle>
|
</UploadProgressSectionTitle>
|
||||||
<UploadProgressSectionContent>
|
<UploadProgressSectionContent>
|
||||||
{hasLivePhotos && (
|
{hasLivePhotos && (
|
||||||
|
@ -30,8 +32,13 @@ export const InProgressSection = () => {
|
||||||
fileList={fileList.map(({ localFileID, progress }) => (
|
fileList={fileList.map(({ localFileID, progress }) => (
|
||||||
<InProgressItemContainer key={localFileID}>
|
<InProgressItemContainer key={localFileID}>
|
||||||
<span>{uploadFileNames.get(localFileID)}</span>
|
<span>{uploadFileNames.get(localFileID)}</span>
|
||||||
|
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
<span className="separator">{`-`}</span>
|
<span className="separator">{`-`}</span>
|
||||||
<span>{`${progress}%`}</span>
|
<span>{`${progress}%`}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</InProgressItemContainer>
|
</InProgressItemContainer>
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -20,6 +20,8 @@ function UploadProgressSubtitleText() {
|
||||||
return (
|
return (
|
||||||
<Typography color="text.secondary">
|
<Typography color="text.secondary">
|
||||||
{uploadStage === UPLOAD_STAGES.UPLOADING
|
{uploadStage === UPLOAD_STAGES.UPLOADING
|
||||||
|
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
|
||||||
|
: uploadStage === UPLOAD_STAGES.EXTRACTING_METADATA
|
||||||
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
|
? constants.UPLOAD_STAGE_MESSAGE[uploadStage](uploadCounter)
|
||||||
: constants.UPLOAD_STAGE_MESSAGE[uploadStage]}
|
: constants.UPLOAD_STAGE_MESSAGE[uploadStage]}
|
||||||
</Typography>
|
</Typography>
|
||||||
|
|
|
@ -49,6 +49,7 @@ import {
|
||||||
} from 'utils/upload';
|
} from 'utils/upload';
|
||||||
import { getUserOwnedCollections } from 'utils/collection';
|
import { getUserOwnedCollections } from 'utils/collection';
|
||||||
import billingService from 'services/billingService';
|
import billingService from 'services/billingService';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
|
||||||
const FIRST_ALBUM_NAME = 'My First Album';
|
const FIRST_ALBUM_NAME = 'My First Album';
|
||||||
|
|
||||||
|
@ -286,7 +287,7 @@ export default function Uploader(props: Props) {
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
await preCollectionCreationAction();
|
await preCollectionCreationAction();
|
||||||
const filesWithCollectionToUpload: FileWithCollection[] = [];
|
let filesWithCollectionToUpload: FileWithCollection[] = [];
|
||||||
const collections: Collection[] = [];
|
const collections: Collection[] = [];
|
||||||
let collectionNameToFilesMap = new Map<
|
let collectionNameToFilesMap = new Map<
|
||||||
string,
|
string,
|
||||||
|
@ -320,13 +321,14 @@ export default function Uploader(props: Props) {
|
||||||
...existingCollection,
|
...existingCollection,
|
||||||
...collections,
|
...collections,
|
||||||
]);
|
]);
|
||||||
filesWithCollectionToUpload.push(
|
filesWithCollectionToUpload = [
|
||||||
|
...filesWithCollectionToUpload,
|
||||||
...files.map((file) => ({
|
...files.map((file) => ({
|
||||||
localID: index++,
|
localID: index++,
|
||||||
collectionID: collection.id,
|
collectionID: collection.id,
|
||||||
file,
|
file,
|
||||||
}))
|
})),
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
closeUploadProgress();
|
closeUploadProgress();
|
||||||
|
@ -426,6 +428,7 @@ export default function Uploader(props: Props) {
|
||||||
|
|
||||||
const retryFailed = async () => {
|
const retryFailed = async () => {
|
||||||
try {
|
try {
|
||||||
|
addLogLine('user retrying failed upload');
|
||||||
const filesWithCollections =
|
const filesWithCollections =
|
||||||
await uploadManager.getFailedFilesWithCollections();
|
await uploadManager.getFailedFilesWithCollections();
|
||||||
await preUploadAction();
|
await preUploadAction();
|
||||||
|
@ -449,31 +452,28 @@ export default function Uploader(props: Props) {
|
||||||
case CustomError.SUBSCRIPTION_EXPIRED:
|
case CustomError.SUBSCRIPTION_EXPIRED:
|
||||||
notification = {
|
notification = {
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
message: constants.SUBSCRIPTION_EXPIRED,
|
subtext: constants.SUBSCRIPTION_EXPIRED,
|
||||||
action: {
|
message: constants.RENEW_NOW,
|
||||||
text: constants.RENEW_NOW,
|
onClick: () => billingService.redirectToCustomerPortal(),
|
||||||
callback: billingService.redirectToCustomerPortal,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case CustomError.STORAGE_QUOTA_EXCEEDED:
|
case CustomError.STORAGE_QUOTA_EXCEEDED:
|
||||||
notification = {
|
notification = {
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
message: constants.STORAGE_QUOTA_EXCEEDED,
|
subtext: constants.STORAGE_QUOTA_EXCEEDED,
|
||||||
action: {
|
message: constants.UPGRADE_NOW,
|
||||||
text: constants.UPGRADE_NOW,
|
onClick: () => galleryContext.showPlanSelectorModal(),
|
||||||
callback: galleryContext.showPlanSelectorModal,
|
startIcon: <DiscFullIcon />,
|
||||||
},
|
|
||||||
icon: <DiscFullIcon fontSize="large" />,
|
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
notification = {
|
notification = {
|
||||||
variant: 'danger',
|
variant: 'danger',
|
||||||
message: constants.UNKNOWN_ERROR,
|
message: constants.UNKNOWN_ERROR,
|
||||||
|
onClick: () => null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
galleryContext.setNotificationAttributes(notification);
|
appContext.setNotificationAttributes(notification);
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadToSingleNewCollection = (collectionName: string) => {
|
const uploadToSingleNewCollection = (collectionName: string) => {
|
||||||
|
|
|
@ -3,15 +3,12 @@ import { Box } from '@mui/material';
|
||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import VerticallyCentered from 'components/Container';
|
import VerticallyCentered from 'components/Container';
|
||||||
|
|
||||||
export const MappingsContainer = styled(Box)(({ theme }) => ({
|
export const MappingsContainer = styled(Box)(() => ({
|
||||||
height: '278px',
|
height: '278px',
|
||||||
overflow: 'auto',
|
overflow: 'auto',
|
||||||
'&::-webkit-scrollbar': {
|
'&::-webkit-scrollbar': {
|
||||||
width: '4px',
|
width: '4px',
|
||||||
},
|
},
|
||||||
'&::-webkit-scrollbar-thumb': {
|
|
||||||
backgroundColor: theme.palette.secondary.main,
|
|
||||||
},
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const NoMappingsContainer = styled(VerticallyCentered)({
|
export const NoMappingsContainer = styled(VerticallyCentered)({
|
||||||
|
|
|
@ -2,9 +2,8 @@ import { FluidContainer } from 'components/Container';
|
||||||
import { SelectionBar } from '../../Navbar/SelectionBar';
|
import { SelectionBar } from '../../Navbar/SelectionBar';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import React, { useContext } from 'react';
|
import React, { useContext } from 'react';
|
||||||
import { Box, IconButton, styled } from '@mui/material';
|
import { Box, IconButton, styled, Tooltip } from '@mui/material';
|
||||||
import { DeduplicateContext } from 'pages/deduplicate';
|
import { DeduplicateContext } from 'pages/deduplicate';
|
||||||
import { IconWithMessage } from 'components/IconWithMessage';
|
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
import BackButton from '@mui/icons-material/ArrowBackOutlined';
|
import BackButton from '@mui/icons-material/ArrowBackOutlined';
|
||||||
|
@ -78,11 +77,11 @@ export default function DeduplicateOptions({
|
||||||
<div>
|
<div>
|
||||||
<VerticalLine />
|
<VerticalLine />
|
||||||
</div>
|
</div>
|
||||||
<IconWithMessage message={constants.DELETE}>
|
<Tooltip title={constants.DELETE}>
|
||||||
<IconButton onClick={trashHandler}>
|
<IconButton onClick={trashHandler}>
|
||||||
<DeleteIcon />
|
<DeleteIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</IconWithMessage>
|
</Tooltip>
|
||||||
</SelectionBar>
|
</SelectionBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,34 +1,12 @@
|
||||||
import { Link, LinkProps } from '@mui/material';
|
import { ButtonProps, Link, LinkProps } from '@mui/material';
|
||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { ButtonProps } from 'react-bootstrap';
|
|
||||||
|
|
||||||
export enum ButtonVariant {
|
|
||||||
success = 'success',
|
|
||||||
danger = 'danger',
|
|
||||||
secondary = 'secondary',
|
|
||||||
warning = 'warning',
|
|
||||||
}
|
|
||||||
export type LinkButtonProps = React.PropsWithChildren<{
|
export type LinkButtonProps = React.PropsWithChildren<{
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
variant?: string;
|
variant?: string;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export function getVariantColor(variant: string) {
|
|
||||||
switch (variant) {
|
|
||||||
case ButtonVariant.success:
|
|
||||||
return '#51cd7c';
|
|
||||||
case ButtonVariant.danger:
|
|
||||||
return '#c93f3f';
|
|
||||||
case ButtonVariant.secondary:
|
|
||||||
return '#858585';
|
|
||||||
case ButtonVariant.warning:
|
|
||||||
return '#D7BB63';
|
|
||||||
default:
|
|
||||||
return '#d1d1d1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
||||||
children,
|
children,
|
||||||
sx,
|
sx,
|
||||||
|
@ -41,6 +19,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
||||||
sx={{
|
sx={{
|
||||||
color: 'text.primary',
|
color: 'text.primary',
|
||||||
textDecoration: 'underline rgba(255, 255, 255, 0.4)',
|
textDecoration: 'underline rgba(255, 255, 255, 0.4)',
|
||||||
|
paddingBottom: 0.5,
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
color: `${color}.main`,
|
color: `${color}.main`,
|
||||||
textDecoration: `underline `,
|
textDecoration: `underline `,
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
hasPaidSubscription,
|
hasPaidSubscription,
|
||||||
getTotalFamilyUsage,
|
getTotalFamilyUsage,
|
||||||
isPartOfFamily,
|
isPartOfFamily,
|
||||||
|
isSubscriptionActive,
|
||||||
} from 'utils/billing';
|
} from 'utils/billing';
|
||||||
import { reverseString } from 'utils/common';
|
import { reverseString } from 'utils/common';
|
||||||
import { GalleryContext } from 'pages/gallery';
|
import { GalleryContext } from 'pages/gallery';
|
||||||
|
@ -68,8 +69,8 @@ function PlanSelectorCard(props: Props) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
props.setLoading(true);
|
props.setLoading(true);
|
||||||
let plans = await billingService.getPlans();
|
const plans = await billingService.getPlans();
|
||||||
|
if (isSubscriptionActive(subscription)) {
|
||||||
const planNotListed =
|
const planNotListed =
|
||||||
plans.filter((plan) =>
|
plans.filter((plan) =>
|
||||||
isUserSubscribedPlan(plan, subscription)
|
isUserSubscribedPlan(plan, subscription)
|
||||||
|
@ -79,7 +80,8 @@ function PlanSelectorCard(props: Props) {
|
||||||
!isOnFreePlan(subscription) &&
|
!isOnFreePlan(subscription) &&
|
||||||
planNotListed
|
planNotListed
|
||||||
) {
|
) {
|
||||||
plans = [planForSubscription(subscription), ...plans];
|
plans.push(planForSubscription(subscription));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setPlans(plans);
|
setPlans(plans);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
|
import React, { useContext, useLayoutEffect, useState } from 'react';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
|
import PlayCircleOutlineOutlinedIcon from '@mui/icons-material/PlayCircleOutlineOutlined';
|
||||||
|
@ -14,22 +14,22 @@ import { DeduplicateContext } from 'pages/deduplicate';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { Overlay } from 'components/Container';
|
import { Overlay } from 'components/Container';
|
||||||
import { TRASH_SECTION } from 'constants/collection';
|
import { TRASH_SECTION } from 'constants/collection';
|
||||||
import { formatDateRelative } from 'utils/time';
|
import { formatDateRelative } from 'utils/time/format';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
file: EnteFile;
|
file: EnteFile;
|
||||||
updateURL?: (url: string) => EnteFile;
|
updateURL: (url: string) => EnteFile;
|
||||||
onClick?: () => void;
|
onClick: () => void;
|
||||||
forcedEnable?: boolean;
|
selectable: boolean;
|
||||||
selectable?: boolean;
|
selected: boolean;
|
||||||
selected?: boolean;
|
onSelect: (checked: boolean) => void;
|
||||||
onSelect?: (checked: boolean) => void;
|
onHover: () => void;
|
||||||
onHover?: () => void;
|
onRangeSelect: () => void;
|
||||||
onRangeSelect?: () => void;
|
isRangeSelectActive: boolean;
|
||||||
isRangeSelectActive?: boolean;
|
selectOnClick: boolean;
|
||||||
selectOnClick?: boolean;
|
isInsSelectRange: boolean;
|
||||||
isInsSelectRange?: boolean;
|
activeCollection: number;
|
||||||
activeCollection?: number;
|
showPlaceholder: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Check = styled('input')<{ active: boolean }>`
|
const Check = styled('input')<{ active: boolean }>`
|
||||||
|
@ -203,7 +203,6 @@ export default function PreviewCard(props: IProps) {
|
||||||
file,
|
file,
|
||||||
onClick,
|
onClick,
|
||||||
updateURL,
|
updateURL,
|
||||||
forcedEnable,
|
|
||||||
selectable,
|
selectable,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
|
@ -213,14 +212,13 @@ export default function PreviewCard(props: IProps) {
|
||||||
isRangeSelectActive,
|
isRangeSelectActive,
|
||||||
isInsSelectRange,
|
isInsSelectRange,
|
||||||
} = props;
|
} = props;
|
||||||
const isMounted = useRef(true);
|
|
||||||
const publicCollectionGalleryContext = useContext(
|
const publicCollectionGalleryContext = useContext(
|
||||||
PublicCollectionGalleryContext
|
PublicCollectionGalleryContext
|
||||||
);
|
);
|
||||||
const deduplicateContext = useContext(DeduplicateContext);
|
const deduplicateContext = useContext(DeduplicateContext);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (file && !file.msrc) {
|
if (file && !file.msrc && !props.showPlaceholder) {
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
try {
|
try {
|
||||||
let url;
|
let url;
|
||||||
|
@ -236,18 +234,14 @@ export default function PreviewCard(props: IProps) {
|
||||||
} else {
|
} else {
|
||||||
url = await DownloadManager.getThumbnail(file);
|
url = await DownloadManager.getThumbnail(file);
|
||||||
}
|
}
|
||||||
if (isMounted.current) {
|
|
||||||
setImgSrc(url);
|
setImgSrc(url);
|
||||||
thumbs.set(file.id, url);
|
thumbs.set(file.id, url);
|
||||||
if (updateURL) {
|
|
||||||
const newFile = updateURL(url);
|
const newFile = updateURL(url);
|
||||||
file.msrc = newFile.msrc;
|
file.msrc = newFile.msrc;
|
||||||
file.html = newFile.html;
|
file.html = newFile.html;
|
||||||
file.src = newFile.src;
|
file.src = newFile.src;
|
||||||
file.w = newFile.w;
|
file.w = newFile.w;
|
||||||
file.h = newFile.h;
|
file.h = newFile.h;
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'preview card useEffect failed');
|
logError(e, 'preview card useEffect failed');
|
||||||
// no-op
|
// no-op
|
||||||
|
@ -257,17 +251,17 @@ export default function PreviewCard(props: IProps) {
|
||||||
if (thumbs.has(file.id)) {
|
if (thumbs.has(file.id)) {
|
||||||
const thumbImgSrc = thumbs.get(file.id);
|
const thumbImgSrc = thumbs.get(file.id);
|
||||||
setImgSrc(thumbImgSrc);
|
setImgSrc(thumbImgSrc);
|
||||||
file.msrc = thumbImgSrc;
|
const newFile = updateURL(thumbImgSrc);
|
||||||
|
file.msrc = newFile.msrc;
|
||||||
|
file.html = newFile.html;
|
||||||
|
file.src = newFile.src;
|
||||||
|
file.w = newFile.w;
|
||||||
|
file.h = newFile.h;
|
||||||
} else {
|
} else {
|
||||||
main();
|
main();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}, [file, props.showPlaceholder]);
|
||||||
return () => {
|
|
||||||
// cool cool cool
|
|
||||||
isMounted.current = false;
|
|
||||||
};
|
|
||||||
}, [file]);
|
|
||||||
|
|
||||||
const handleClick = () => {
|
const handleClick = () => {
|
||||||
if (selectOnClick) {
|
if (selectOnClick) {
|
||||||
|
@ -300,10 +294,10 @@ export default function PreviewCard(props: IProps) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Cont
|
<Cont
|
||||||
id={`thumb-${file?.id}`}
|
id={`thumb-${file?.id}-${props.showPlaceholder}`}
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onMouseEnter={handleHover}
|
onMouseEnter={handleHover}
|
||||||
disabled={!forcedEnable && !file?.msrc && !imgSrc}
|
disabled={!file?.msrc && !imgSrc}
|
||||||
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
|
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<Check
|
<Check
|
||||||
|
|
|
@ -18,8 +18,8 @@ import AddIcon from '@mui/icons-material/Add';
|
||||||
import DeleteIcon from '@mui/icons-material/Delete';
|
import DeleteIcon from '@mui/icons-material/Delete';
|
||||||
import ClockIcon from '@mui/icons-material/AccessTime';
|
import ClockIcon from '@mui/icons-material/AccessTime';
|
||||||
import DownloadIcon from '@mui/icons-material/Download';
|
import DownloadIcon from '@mui/icons-material/Download';
|
||||||
import UnArchiveIcon from '@mui/icons-material/Visibility';
|
import UnArchiveIcon from '@mui/icons-material/Unarchive';
|
||||||
import ArchiveIcon from '@mui/icons-material/VisibilityOff';
|
import ArchiveIcon from '@mui/icons-material/ArchiveOutlined';
|
||||||
import MoveIcon from '@mui/icons-material/ArrowForward';
|
import MoveIcon from '@mui/icons-material/ArrowForward';
|
||||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||||
import { getTrashFilesMessage } from 'utils/ui';
|
import { getTrashFilesMessage } from 'utils/ui';
|
||||||
|
|
|
@ -1 +1,4 @@
|
||||||
export const DESKTOP_REDIRECT_URL = 'https://payments.ente.io/desktop-redirect';
|
import { getPaymentsURL } from 'utils/common/apiUtil';
|
||||||
|
|
||||||
|
export const getDesktopRedirectURL = () =>
|
||||||
|
`${getPaymentsURL()}/desktop-redirect`;
|
||||||
|
|
3
src/constants/ffmpeg/index.ts
Normal file
3
src/constants/ffmpeg/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
export const INPUT_PATH_PLACEHOLDER = 'INPUT';
|
||||||
|
export const FFMPEG_PLACEHOLDER = 'FFMPEG';
|
||||||
|
export const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT';
|
|
@ -2,6 +2,7 @@ export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||||
|
|
||||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||||
|
export const MAX_CAPTION_SIZE = 280;
|
||||||
export const MAX_TRASH_BATCH_SIZE = 1000;
|
export const MAX_TRASH_BATCH_SIZE = 1000;
|
||||||
|
|
||||||
export const TYPE_HEIC = 'heic';
|
export const TYPE_HEIC = 'heic';
|
||||||
|
|
|
@ -1,4 +1,12 @@
|
||||||
export default [
|
export const defaultLivePhotoDefaultOptions = {
|
||||||
|
click: () => {},
|
||||||
|
hide: () => {},
|
||||||
|
show: () => {},
|
||||||
|
loading: false,
|
||||||
|
visible: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const photoSwipeV4Events = [
|
||||||
'beforeChange',
|
'beforeChange',
|
||||||
'afterChange',
|
'afterChange',
|
||||||
'imageLoadComplete',
|
'imageLoadComplete',
|
|
@ -1,7 +0,0 @@
|
||||||
export const defaultLivePhotoDefaultOptions = {
|
|
||||||
click: () => {},
|
|
||||||
hide: () => {},
|
|
||||||
show: () => {},
|
|
||||||
loading: false,
|
|
||||||
visible: false,
|
|
||||||
};
|
|
|
@ -1,10 +1,15 @@
|
||||||
|
export const ENV_DEVELOPMENT = 'development';
|
||||||
|
export const ENV_PRODUCTION = 'production';
|
||||||
|
|
||||||
export const getSentryDSN = () =>
|
export const getSentryDSN = () =>
|
||||||
process.env.NEXT_PUBLIC_SENTRY_DSN ??
|
process.env.NEXT_PUBLIC_SENTRY_DSN ??
|
||||||
'https://60abb33b597c42f6a3fb27cd82c55101@sentry.ente.io/2';
|
'https://60abb33b597c42f6a3fb27cd82c55101@sentry.ente.io/2';
|
||||||
|
|
||||||
export const getSentryENV = () =>
|
export const getSentryENV = () =>
|
||||||
process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
|
process.env.NEXT_PUBLIC_SENTRY_ENV ?? ENV_PRODUCTION;
|
||||||
|
|
||||||
export const getSentryRelease = () => process.env.SENTRY_RELEASE;
|
export const getSentryRelease = () => process.env.SENTRY_RELEASE;
|
||||||
|
|
||||||
export { getIsSentryEnabled } from '../../../sentryConfigUtil';
|
export { getIsSentryEnabled } from '../../../sentryConfigUtil';
|
||||||
|
|
||||||
|
export const isDEVSentryENV = () => getSentryENV() === ENV_DEVELOPMENT;
|
||||||
|
|
|
@ -8,12 +8,10 @@ import 'photoswipe/dist/photoswipe.css';
|
||||||
import 'styles/global.css';
|
import 'styles/global.css';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import { logError } from '../utils/sentry';
|
import { logError } from '../utils/sentry';
|
||||||
// import { Workbox } from 'workbox-window';
|
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import HTTPService from 'services/HTTPService';
|
import HTTPService from 'services/HTTPService';
|
||||||
import FlashMessageBar from 'components/FlashMessageBar';
|
import FlashMessageBar from 'components/FlashMessageBar';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { addLogLine } from 'utils/logging';
|
|
||||||
import LoadingBar from 'react-top-loading-bar';
|
import LoadingBar from 'react-top-loading-bar';
|
||||||
import DialogBox from 'components/DialogBox';
|
import DialogBox from 'components/DialogBox';
|
||||||
import { styled, ThemeProvider } from '@mui/material/styles';
|
import { styled, ThemeProvider } from '@mui/material/styles';
|
||||||
|
@ -27,6 +25,25 @@ import {
|
||||||
getRoadmapRedirectURL,
|
getRoadmapRedirectURL,
|
||||||
} from 'services/userService';
|
} from 'services/userService';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
|
import {
|
||||||
|
addLogLine,
|
||||||
|
clearLogsIfLocalStorageLimitExceeded,
|
||||||
|
} from 'utils/logging';
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import ElectronUpdateService from 'services/electron/update';
|
||||||
|
import {
|
||||||
|
getUpdateAvailableForDownloadMessage,
|
||||||
|
getUpdateReadyToInstallMessage,
|
||||||
|
} from 'utils/ui';
|
||||||
|
import Notification from 'components/Notification';
|
||||||
|
import {
|
||||||
|
NotificationAttributes,
|
||||||
|
SetNotificationAttributes,
|
||||||
|
} from 'types/Notification';
|
||||||
|
import ArrowForward from '@mui/icons-material/ArrowForward';
|
||||||
|
import { AppUpdateInfo } from 'types/electron';
|
||||||
|
import { getSentryUserID } from 'utils/user';
|
||||||
|
import { User } from 'types/user';
|
||||||
|
|
||||||
export const MessageContainer = styled('div')`
|
export const MessageContainer = styled('div')`
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
|
@ -52,6 +69,7 @@ type AppContextType = {
|
||||||
finishLoading: () => void;
|
finishLoading: () => void;
|
||||||
closeMessageDialog: () => void;
|
closeMessageDialog: () => void;
|
||||||
setDialogMessage: SetDialogBoxAttributes;
|
setDialogMessage: SetDialogBoxAttributes;
|
||||||
|
setNotificationAttributes: SetNotificationAttributes;
|
||||||
isFolderSyncRunning: boolean;
|
isFolderSyncRunning: boolean;
|
||||||
setIsFolderSyncRunning: (isRunning: boolean) => void;
|
setIsFolderSyncRunning: (isRunning: boolean) => void;
|
||||||
watchFolderView: boolean;
|
watchFolderView: boolean;
|
||||||
|
@ -97,34 +115,12 @@ export default function App({ Component, err }) {
|
||||||
const [watchFolderView, setWatchFolderView] = useState(false);
|
const [watchFolderView, setWatchFolderView] = useState(false);
|
||||||
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
|
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
|
||||||
const isMobile = useMediaQuery('(max-width:428px)');
|
const isMobile = useMediaQuery('(max-width:428px)');
|
||||||
|
const [notificationView, setNotificationView] = useState(false);
|
||||||
|
const closeNotification = () => setNotificationView(false);
|
||||||
|
const [notificationAttributes, setNotificationAttributes] =
|
||||||
|
useState<NotificationAttributes>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
|
||||||
!('serviceWorker' in navigator) ||
|
|
||||||
process.env.NODE_ENV !== 'production'
|
|
||||||
) {
|
|
||||||
console.warn('Progressive Web App support is disabled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// const wb = new Workbox('sw.js', { scope: '/' });
|
|
||||||
// wb.register();
|
|
||||||
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
navigator.serviceWorker.onmessage = (event) => {
|
|
||||||
if (event.data.action === 'upload-files') {
|
|
||||||
const files = event.data.files;
|
|
||||||
setSharedFiles(files);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
navigator.serviceWorker
|
|
||||||
.getRegistrations()
|
|
||||||
.then(function (registrations) {
|
|
||||||
for (const registration of registrations) {
|
|
||||||
registration.unregister();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
HTTPService.getInterceptors().response.use(
|
HTTPService.getInterceptors().response.use(
|
||||||
(resp) => resp,
|
(resp) => resp,
|
||||||
(error) => {
|
(error) => {
|
||||||
|
@ -132,6 +128,34 @@ export default function App({ Component, err }) {
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
clearLogsIfLocalStorageLimitExceeded();
|
||||||
|
const main = async () => {
|
||||||
|
addLogLine(`userID: ${(getData(LS_KEYS.USER) as User)?.id}`);
|
||||||
|
addLogLine(`sentryID: ${await getSentryUserID()}`);
|
||||||
|
addLogLine(`sentry release ID: ${process.env.SENTRY_RELEASE}`);
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isElectron()) {
|
||||||
|
const showUpdateDialog = (updateInfo: AppUpdateInfo) => {
|
||||||
|
if (updateInfo.autoUpdatable) {
|
||||||
|
setDialogMessage(getUpdateReadyToInstallMessage());
|
||||||
|
} else {
|
||||||
|
setNotificationAttributes({
|
||||||
|
endIcon: <ArrowForward />,
|
||||||
|
variant: 'secondary',
|
||||||
|
message: constants.UPDATE_AVAILABLE,
|
||||||
|
onClick: () =>
|
||||||
|
setDialogMessage(
|
||||||
|
getUpdateAvailableForDownloadMessage(updateInfo)
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ElectronUpdateService.registerUpdateEventListener(showUpdateDialog);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const setUserOnline = () => setOffline(false);
|
const setUserOnline = () => setOffline(false);
|
||||||
|
@ -206,10 +230,12 @@ export default function App({ Component, err }) {
|
||||||
}, [redirectName]);
|
}, [redirectName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
addLogLine(`app started`);
|
setMessageDialogView(true);
|
||||||
}, []);
|
}, [dialogMessage]);
|
||||||
|
|
||||||
useEffect(() => setMessageDialogView(true), [dialogMessage]);
|
useEffect(() => {
|
||||||
|
setNotificationView(true);
|
||||||
|
}, [notificationAttributes]);
|
||||||
|
|
||||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||||
const setDisappearingFlashMessage = (flashMessages: FlashMessage) => {
|
const setDisappearingFlashMessage = (flashMessages: FlashMessage) => {
|
||||||
|
@ -239,7 +265,7 @@ export default function App({ Component, err }) {
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<ThemeProvider theme={darkThemeOptions}>
|
<ThemeProvider theme={darkThemeOptions}>
|
||||||
<CssBaseline />
|
<CssBaseline enableColorScheme />
|
||||||
{showNavbar && <AppNavbar />}
|
{showNavbar && <AppNavbar />}
|
||||||
<MessageContainer>
|
<MessageContainer>
|
||||||
{offline && constants.OFFLINE_MSG}
|
{offline && constants.OFFLINE_MSG}
|
||||||
|
@ -265,11 +291,17 @@ export default function App({ Component, err }) {
|
||||||
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
||||||
|
|
||||||
<DialogBox
|
<DialogBox
|
||||||
|
sx={{ zIndex: 1600 }}
|
||||||
size="xs"
|
size="xs"
|
||||||
open={messageDialogView}
|
open={messageDialogView}
|
||||||
onClose={closeMessageDialog}
|
onClose={closeMessageDialog}
|
||||||
attributes={dialogMessage}
|
attributes={dialogMessage}
|
||||||
/>
|
/>
|
||||||
|
<Notification
|
||||||
|
open={notificationView}
|
||||||
|
onClose={closeNotification}
|
||||||
|
attributes={notificationAttributes}
|
||||||
|
/>
|
||||||
|
|
||||||
<AppContext.Provider
|
<AppContext.Provider
|
||||||
value={{
|
value={{
|
||||||
|
@ -290,6 +322,7 @@ export default function App({ Component, err }) {
|
||||||
watchFolderFiles,
|
watchFolderFiles,
|
||||||
setWatchFolderFiles,
|
setWatchFolderFiles,
|
||||||
isMobile,
|
isMobile,
|
||||||
|
setNotificationAttributes,
|
||||||
}}>
|
}}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<VerticallyCentered>
|
<VerticallyCentered>
|
||||||
|
|
|
@ -24,6 +24,9 @@ import router from 'next/router';
|
||||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { syncCollections } from 'services/collectionService';
|
import { syncCollections } from 'services/collectionService';
|
||||||
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
import VerticallyCentered from 'components/Container';
|
||||||
|
import { Collection } from 'types/collection';
|
||||||
|
|
||||||
export const DeduplicateContext = createContext<DeduplicateContextType>(
|
export const DeduplicateContext = createContext<DeduplicateContextType>(
|
||||||
DefaultDeduplicateContext
|
DefaultDeduplicateContext
|
||||||
|
@ -42,6 +45,7 @@ export default function Deduplicate() {
|
||||||
setRedirectURL,
|
setRedirectURL,
|
||||||
} = useContext(AppContext);
|
} = useContext(AppContext);
|
||||||
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
|
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
|
||||||
|
const [collections, setCollection] = useState<Collection[]>([]);
|
||||||
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
|
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
|
||||||
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
|
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
|
||||||
const [collectionNameMap, setCollectionNameMap] = useState(
|
const [collectionNameMap, setCollectionNameMap] = useState(
|
||||||
|
@ -72,6 +76,7 @@ export default function Deduplicate() {
|
||||||
const syncWithRemote = async () => {
|
const syncWithRemote = async () => {
|
||||||
startLoading();
|
startLoading();
|
||||||
const collections = await syncCollections();
|
const collections = await syncCollections();
|
||||||
|
setCollection(collections);
|
||||||
const collectionNameMap = new Map<number, string>();
|
const collectionNameMap = new Map<number, string>();
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
collectionNameMap.set(collection.id, collection.name);
|
collectionNameMap.set(collection.id, collection.name);
|
||||||
|
@ -87,11 +92,12 @@ export default function Deduplicate() {
|
||||||
let toSelectFileIDs: number[] = [];
|
let toSelectFileIDs: number[] = [];
|
||||||
let count = 0;
|
let count = 0;
|
||||||
for (const dupe of duplicates) {
|
for (const dupe of duplicates) {
|
||||||
allDuplicateFiles = allDuplicateFiles.concat(dupe.files);
|
allDuplicateFiles = [...allDuplicateFiles, ...dupe.files];
|
||||||
// select all except first file
|
// select all except first file
|
||||||
toSelectFileIDs = toSelectFileIDs.concat(
|
toSelectFileIDs = [
|
||||||
dupe.files.slice(1).map((f) => f.id)
|
...toSelectFileIDs,
|
||||||
);
|
...dupe.files.slice(1).map((f) => f.id),
|
||||||
|
];
|
||||||
count += dupe.files.length - 1;
|
count += dupe.files.length - 1;
|
||||||
|
|
||||||
for (const file of dupe.files) {
|
for (const file of dupe.files) {
|
||||||
|
@ -143,7 +149,13 @@ export default function Deduplicate() {
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!duplicateFiles) {
|
if (!duplicateFiles) {
|
||||||
return <></>;
|
return (
|
||||||
|
<VerticallyCentered>
|
||||||
|
<EnteSpinner>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</EnteSpinner>
|
||||||
|
</VerticallyCentered>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -165,7 +177,7 @@ export default function Deduplicate() {
|
||||||
)}
|
)}
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={duplicateFiles}
|
files={duplicateFiles}
|
||||||
setFiles={setDuplicateFiles}
|
collections={collections}
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
setSelected={setSelected}
|
setSelected={setSelected}
|
||||||
selected={selected}
|
selected={selected}
|
||||||
|
|
|
@ -27,14 +27,14 @@ import { checkSubscriptionPurchase } from 'utils/billing';
|
||||||
|
|
||||||
import FullScreenDropZone from 'components/FullScreenDropZone';
|
import FullScreenDropZone from 'components/FullScreenDropZone';
|
||||||
import Sidebar from 'components/Sidebar';
|
import Sidebar from 'components/Sidebar';
|
||||||
import { checkConnectivity, preloadImage } from 'utils/common';
|
import { preloadImage } from 'utils/common';
|
||||||
import {
|
import {
|
||||||
isFirstLogin,
|
isFirstLogin,
|
||||||
justSignedUp,
|
justSignedUp,
|
||||||
setIsFirstLogin,
|
setIsFirstLogin,
|
||||||
setJustSignedUp,
|
setJustSignedUp,
|
||||||
} from 'utils/storage';
|
} from 'utils/storage';
|
||||||
import { isTokenValid, logoutUser } from 'services/userService';
|
import { isTokenValid, logoutUser, validateKey } from 'services/userService';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||||
|
@ -89,18 +89,17 @@ import { Collection, CollectionSummaries } from 'types/collection';
|
||||||
import { EnteFile } from 'types/file';
|
import { EnteFile } from 'types/file';
|
||||||
import { GalleryContextType, SelectedState } from 'types/gallery';
|
import { GalleryContextType, SelectedState } from 'types/gallery';
|
||||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||||
import Notification from 'components/Notification';
|
|
||||||
import Collections from 'components/Collections';
|
import Collections from 'components/Collections';
|
||||||
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
||||||
import { Search, SearchResultSummary, UpdateSearch } from 'types/search';
|
import { Search, SearchResultSummary, UpdateSearch } from 'types/search';
|
||||||
import SearchResultInfo from 'components/Search/SearchResultInfo';
|
import SearchResultInfo from 'components/Search/SearchResultInfo';
|
||||||
import { NotificationAttributes } from 'types/Notification';
|
|
||||||
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
||||||
import UploadInputs from 'components/UploadSelectorInputs';
|
import UploadInputs from 'components/UploadSelectorInputs';
|
||||||
import useFileInput from 'hooks/useFileInput';
|
import useFileInput from 'hooks/useFileInput';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||||
import { CenteredFlex } from 'components/Container';
|
import { CenteredFlex } from 'components/Container';
|
||||||
|
import { checkConnectivity } from 'utils/error/ui';
|
||||||
|
|
||||||
export const DeadCenter = styled('div')`
|
export const DeadCenter = styled('div')`
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
@ -117,7 +116,6 @@ const defaultGalleryContext: GalleryContextType = {
|
||||||
showPlanSelectorModal: () => null,
|
showPlanSelectorModal: () => null,
|
||||||
setActiveCollection: () => null,
|
setActiveCollection: () => null,
|
||||||
syncWithRemote: () => null,
|
syncWithRemote: () => null,
|
||||||
setNotificationAttributes: () => null,
|
|
||||||
setBlockingLoad: () => null,
|
setBlockingLoad: () => null,
|
||||||
photoListHeader: null,
|
photoListHeader: null,
|
||||||
};
|
};
|
||||||
|
@ -180,7 +178,9 @@ export default function Gallery() {
|
||||||
useState<SearchResultSummary>(null);
|
useState<SearchResultSummary>(null);
|
||||||
const syncInProgress = useRef(true);
|
const syncInProgress = useRef(true);
|
||||||
const resync = useRef(false);
|
const resync = useRef(false);
|
||||||
const [deleted, setDeleted] = useState<number[]>([]);
|
const [deletedFileIds, setDeletedFileIds] = useState<Set<number>>(
|
||||||
|
new Set<number>()
|
||||||
|
);
|
||||||
const { startLoading, finishLoading, setDialogMessage, ...appContext } =
|
const { startLoading, finishLoading, setDialogMessage, ...appContext } =
|
||||||
useContext(AppContext);
|
useContext(AppContext);
|
||||||
const [collectionSummaries, setCollectionSummaries] =
|
const [collectionSummaries, setCollectionSummaries] =
|
||||||
|
@ -190,13 +190,6 @@ export default function Gallery() {
|
||||||
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||||
useState<FixCreationTimeAttributes>(null);
|
useState<FixCreationTimeAttributes>(null);
|
||||||
|
|
||||||
const [notificationView, setNotificationView] = useState(false);
|
|
||||||
|
|
||||||
const closeNotification = () => setNotificationView(false);
|
|
||||||
|
|
||||||
const [notificationAttributes, setNotificationAttributes] =
|
|
||||||
useState<NotificationAttributes>(null);
|
|
||||||
|
|
||||||
const [archivedCollections, setArchivedCollections] =
|
const [archivedCollections, setArchivedCollections] =
|
||||||
useState<Set<number>>();
|
useState<Set<number>>();
|
||||||
|
|
||||||
|
@ -233,6 +226,10 @@ export default function Gallery() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
const valid = await validateKey();
|
||||||
|
if (!valid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setActiveCollection(ALL_SECTION);
|
setActiveCollection(ALL_SECTION);
|
||||||
setIsFirstLoad(isFirstLogin());
|
setIsFirstLoad(isFirstLogin());
|
||||||
setIsFirstFetch(true);
|
setIsFirstFetch(true);
|
||||||
|
@ -241,10 +238,10 @@ export default function Gallery() {
|
||||||
}
|
}
|
||||||
setIsFirstLogin(false);
|
setIsFirstLogin(false);
|
||||||
const user = getData(LS_KEYS.USER);
|
const user = getData(LS_KEYS.USER);
|
||||||
const files = mergeMetadata(await getLocalFiles());
|
let files = mergeMetadata(await getLocalFiles());
|
||||||
const collections = await getLocalCollections();
|
const collections = await getLocalCollections();
|
||||||
const trash = await getLocalTrash();
|
const trash = await getLocalTrash();
|
||||||
files.push(...getTrashedFiles(trash));
|
files = [...files, ...getTrashedFiles(trash)];
|
||||||
setUser(user);
|
setUser(user);
|
||||||
setFiles(sortFiles(files));
|
setFiles(sortFiles(files));
|
||||||
setCollections(collections);
|
setCollections(collections);
|
||||||
|
@ -264,24 +261,16 @@ export default function Gallery() {
|
||||||
setDerivativeState(user, collections, files);
|
setDerivativeState(user, collections, files);
|
||||||
}, [collections, files]);
|
}, [collections, files]);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => collectionSelectorAttributes && setCollectionSelectorView(true),
|
collectionSelectorAttributes && setCollectionSelectorView(true);
|
||||||
[collectionSelectorAttributes]
|
}, [collectionSelectorAttributes]);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => collectionNamerAttributes && setCollectionNamerView(true),
|
collectionNamerAttributes && setCollectionNamerView(true);
|
||||||
[collectionNamerAttributes]
|
}, [collectionNamerAttributes]);
|
||||||
);
|
useEffect(() => {
|
||||||
useEffect(
|
fixCreationTimeAttributes && setFixCreationTimeView(true);
|
||||||
() => fixCreationTimeAttributes && setFixCreationTimeView(true),
|
}, [fixCreationTimeAttributes]);
|
||||||
[fixCreationTimeAttributes]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => notificationAttributes && setNotificationView(true),
|
|
||||||
[notificationAttributes]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (typeof activeCollection === 'undefined') {
|
if (typeof activeCollection === 'undefined') {
|
||||||
|
@ -341,9 +330,9 @@ export default function Gallery() {
|
||||||
!silent && startLoading();
|
!silent && startLoading();
|
||||||
const collections = await syncCollections();
|
const collections = await syncCollections();
|
||||||
setCollections(collections);
|
setCollections(collections);
|
||||||
const files = await syncFiles(collections, setFiles);
|
let files = await syncFiles(collections, setFiles);
|
||||||
const trash = await syncTrash(collections, setFiles, files);
|
const trash = await syncTrash(collections, setFiles, files);
|
||||||
files.push(...getTrashedFiles(trash));
|
files = [...files, ...getTrashedFiles(trash)];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'syncWithRemote failed');
|
logError(e, 'syncWithRemote failed');
|
||||||
switch (e.message) {
|
switch (e.message) {
|
||||||
|
@ -490,12 +479,12 @@ export default function Gallery() {
|
||||||
startLoading();
|
startLoading();
|
||||||
try {
|
try {
|
||||||
const selectedFiles = getSelectedFiles(selected, files);
|
const selectedFiles = getSelectedFiles(selected, files);
|
||||||
|
setDeletedFileIds((deletedFileIds) => {
|
||||||
|
selectedFiles.forEach((file) => deletedFileIds.add(file.id));
|
||||||
|
return new Set(deletedFileIds);
|
||||||
|
});
|
||||||
if (permanent) {
|
if (permanent) {
|
||||||
await deleteFromTrash(selectedFiles.map((file) => file.id));
|
await deleteFromTrash(selectedFiles.map((file) => file.id));
|
||||||
setDeleted([
|
|
||||||
...deleted,
|
|
||||||
...selectedFiles.map((file) => file.id),
|
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
await trashFiles(selectedFiles);
|
await trashFiles(selectedFiles);
|
||||||
}
|
}
|
||||||
|
@ -574,7 +563,6 @@ export default function Gallery() {
|
||||||
showPlanSelectorModal,
|
showPlanSelectorModal,
|
||||||
setActiveCollection,
|
setActiveCollection,
|
||||||
syncWithRemote,
|
syncWithRemote,
|
||||||
setNotificationAttributes,
|
|
||||||
setBlockingLoad,
|
setBlockingLoad,
|
||||||
photoListHeader: photoListHeader,
|
photoListHeader: photoListHeader,
|
||||||
}}>
|
}}>
|
||||||
|
@ -602,11 +590,6 @@ export default function Gallery() {
|
||||||
closeModal={() => setPlanModalView(false)}
|
closeModal={() => setPlanModalView(false)}
|
||||||
setLoading={setBlockingLoad}
|
setLoading={setBlockingLoad}
|
||||||
/>
|
/>
|
||||||
<Notification
|
|
||||||
open={notificationView}
|
|
||||||
onClose={closeNotification}
|
|
||||||
attributes={notificationAttributes}
|
|
||||||
/>
|
|
||||||
<CollectionNamer
|
<CollectionNamer
|
||||||
show={collectionNamerView}
|
show={collectionNamerView}
|
||||||
onHide={setCollectionNamerView.bind(null, false)}
|
onHide={setCollectionNamerView.bind(null, false)}
|
||||||
|
@ -685,10 +668,9 @@ export default function Gallery() {
|
||||||
sidebarView={sidebarView}
|
sidebarView={sidebarView}
|
||||||
closeSidebar={closeSidebar}
|
closeSidebar={closeSidebar}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={files}
|
files={files}
|
||||||
setFiles={setFiles}
|
collections={collections}
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
favItemIds={favItemIds}
|
favItemIds={favItemIds}
|
||||||
archivedCollections={archivedCollections}
|
archivedCollections={archivedCollections}
|
||||||
|
@ -698,7 +680,8 @@ export default function Gallery() {
|
||||||
openUploader={openUploader}
|
openUploader={openUploader}
|
||||||
isInSearchMode={isInSearchMode}
|
isInSearchMode={isInSearchMode}
|
||||||
search={search}
|
search={search}
|
||||||
deleted={deleted}
|
deletedFileIds={deletedFileIds}
|
||||||
|
setDeletedFileIds={setDeletedFileIds}
|
||||||
activeCollection={activeCollection}
|
activeCollection={activeCollection}
|
||||||
isSharedCollection={isSharedCollection(
|
isSharedCollection={isSharedCollection(
|
||||||
activeCollection,
|
activeCollection,
|
||||||
|
|
|
@ -16,7 +16,6 @@ import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
import VerticallyCentered from 'components/Container';
|
import VerticallyCentered from 'components/Container';
|
||||||
import { Button } from 'react-bootstrap';
|
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||||
|
@ -24,6 +23,7 @@ import { KeyAttributes, User } from 'types/user';
|
||||||
import FormPaper from 'components/Form/FormPaper';
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
const bip39 = require('bip39');
|
const bip39 = require('bip39');
|
||||||
// mobile client library only supports english.
|
// mobile client library only supports english.
|
||||||
bip39.setDefaultWordlist('english');
|
bip39.setDefaultWordlist('english');
|
||||||
|
@ -59,9 +59,15 @@ export default function Recover() {
|
||||||
setFieldError
|
setFieldError
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
recoveryKey = recoveryKey
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => !!part)
|
||||||
|
.join(' ');
|
||||||
// check if user is entering mnemonic recovery key
|
// check if user is entering mnemonic recovery key
|
||||||
if (recoveryKey.trim().indexOf(' ') > 0) {
|
if (recoveryKey.indexOf(' ') > 0) {
|
||||||
if (recoveryKey.trim().split(' ').length !== 24) {
|
if (recoveryKey.split(' ').length !== 24) {
|
||||||
throw new Error('recovery code should have 24 words');
|
throw new Error('recovery code should have 24 words');
|
||||||
}
|
}
|
||||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
|
@ -102,12 +108,12 @@ export default function Recover() {
|
||||||
buttonText={constants.RECOVER}
|
buttonText={constants.RECOVER}
|
||||||
/>
|
/>
|
||||||
<FormPaperFooter style={{ justifyContent: 'space-between' }}>
|
<FormPaperFooter style={{ justifyContent: 'space-between' }}>
|
||||||
<Button variant="link" onClick={showNoRecoveryKeyMessage}>
|
<LinkButton onClick={showNoRecoveryKeyMessage}>
|
||||||
{constants.NO_RECOVERY_KEY}
|
{constants.NO_RECOVERY_KEY}
|
||||||
</Button>
|
</LinkButton>
|
||||||
<Button variant="link" onClick={router.back}>
|
<LinkButton onClick={router.back}>
|
||||||
{constants.GO_BACK}
|
{constants.GO_BACK}
|
||||||
</Button>
|
</LinkButton>
|
||||||
</FormPaperFooter>
|
</FormPaperFooter>
|
||||||
</FormPaper>
|
</FormPaper>
|
||||||
</VerticallyCentered>
|
</VerticallyCentered>
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default function PublicCollectionGallery() {
|
||||||
const url = useRef<string>(null);
|
const url = useRef<string>(null);
|
||||||
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
|
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
|
||||||
const [publicCollection, setPublicCollection] = useState<Collection>(null);
|
const [publicCollection, setPublicCollection] = useState<Collection>(null);
|
||||||
const [errorMessage, setErrorMessage] = useState<String>(null);
|
const [errorMessage, setErrorMessage] = useState<string>(null);
|
||||||
const appContext = useContext(AppContext);
|
const appContext = useContext(AppContext);
|
||||||
const [abuseReportFormView, setAbuseReportFormView] = useState(false);
|
const [abuseReportFormView, setAbuseReportFormView] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
@ -128,8 +128,7 @@ export default function PublicCollectionGallery() {
|
||||||
main();
|
main();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() =>
|
|
||||||
publicCollection &&
|
publicCollection &&
|
||||||
publicFiles &&
|
publicFiles &&
|
||||||
setPhotoListHeader({
|
setPhotoListHeader({
|
||||||
|
@ -143,9 +142,8 @@ export default function PublicCollectionGallery() {
|
||||||
),
|
),
|
||||||
itemType: ITEM_TYPE.OTHER,
|
itemType: ITEM_TYPE.OTHER,
|
||||||
height: 68,
|
height: 68,
|
||||||
}),
|
});
|
||||||
[publicCollection, publicFiles]
|
}, [publicCollection, publicFiles]);
|
||||||
);
|
|
||||||
|
|
||||||
const syncWithRemote = async () => {
|
const syncWithRemote = async () => {
|
||||||
const collectionUID = getPublicCollectionUID(token.current);
|
const collectionUID = getPublicCollectionUID(token.current);
|
||||||
|
@ -309,16 +307,10 @@ export default function PublicCollectionGallery() {
|
||||||
<SharedAlbumNavbar />
|
<SharedAlbumNavbar />
|
||||||
<PhotoFrame
|
<PhotoFrame
|
||||||
files={publicFiles}
|
files={publicFiles}
|
||||||
setFiles={setPublicFiles}
|
|
||||||
syncWithRemote={syncWithRemote}
|
syncWithRemote={syncWithRemote}
|
||||||
favItemIds={null}
|
|
||||||
setSelected={() => null}
|
setSelected={() => null}
|
||||||
selected={{ count: 0, collectionID: null }}
|
selected={{ count: 0, collectionID: null }}
|
||||||
isFirstLoad={true}
|
isFirstLoad={true}
|
||||||
openUploader={() => null}
|
|
||||||
isInSearchMode={false}
|
|
||||||
search={{}}
|
|
||||||
deleted={[]}
|
|
||||||
activeCollection={ALL_SECTION}
|
activeCollection={ALL_SECTION}
|
||||||
isSharedCollection
|
isSharedCollection
|
||||||
enableDownload={
|
enableDownload={
|
||||||
|
|
|
@ -7,7 +7,6 @@ import SingleInputForm, {
|
||||||
SingleInputFormProps,
|
SingleInputFormProps,
|
||||||
} from 'components/SingleInputForm';
|
} from 'components/SingleInputForm';
|
||||||
import VerticallyCentered from 'components/Container';
|
import VerticallyCentered from 'components/Container';
|
||||||
import { Button } from 'react-bootstrap';
|
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
|
||||||
import { AppContext } from 'pages/_app';
|
import { AppContext } from 'pages/_app';
|
||||||
|
@ -15,6 +14,7 @@ import { PAGES } from 'constants/pages';
|
||||||
import FormPaper from 'components/Form/FormPaper';
|
import FormPaper from 'components/Form/FormPaper';
|
||||||
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
import FormPaperTitle from 'components/Form/FormPaper/Title';
|
||||||
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
import FormPaperFooter from 'components/Form/FormPaper/Footer';
|
||||||
|
import LinkButton from 'components/pages/gallery/LinkButton';
|
||||||
const bip39 = require('bip39');
|
const bip39 = require('bip39');
|
||||||
// mobile client library only supports english.
|
// mobile client library only supports english.
|
||||||
bip39.setDefaultWordlist('english');
|
bip39.setDefaultWordlist('english');
|
||||||
|
@ -52,9 +52,15 @@ export default function Recover() {
|
||||||
setFieldError
|
setFieldError
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
|
recoveryKey = recoveryKey
|
||||||
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.filter((part) => !!part)
|
||||||
|
.join(' ');
|
||||||
// check if user is entering mnemonic recovery key
|
// check if user is entering mnemonic recovery key
|
||||||
if (recoveryKey.trim().indexOf(' ') > 0) {
|
if (recoveryKey.indexOf(' ') > 0) {
|
||||||
if (recoveryKey.trim().split(' ').length !== 24) {
|
if (recoveryKey.split(' ').length !== 24) {
|
||||||
throw new Error('recovery code should have 24 words');
|
throw new Error('recovery code should have 24 words');
|
||||||
}
|
}
|
||||||
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
recoveryKey = bip39.mnemonicToEntropy(recoveryKey);
|
||||||
|
@ -101,12 +107,12 @@ export default function Recover() {
|
||||||
buttonText={constants.RECOVER}
|
buttonText={constants.RECOVER}
|
||||||
/>
|
/>
|
||||||
<FormPaperFooter style={{ justifyContent: 'space-between' }}>
|
<FormPaperFooter style={{ justifyContent: 'space-between' }}>
|
||||||
<Button variant="link" onClick={showNoRecoveryKeyMessage}>
|
<LinkButton onClick={showNoRecoveryKeyMessage}>
|
||||||
{constants.NO_RECOVERY_KEY}
|
{constants.NO_RECOVERY_KEY}
|
||||||
</Button>
|
</LinkButton>
|
||||||
<Button variant="link" onClick={router.back}>
|
<LinkButton onClick={router.back}>
|
||||||
{constants.GO_BACK}
|
{constants.GO_BACK}
|
||||||
</Button>
|
</LinkButton>
|
||||||
</FormPaperFooter>
|
</FormPaperFooter>
|
||||||
</FormPaper>
|
</FormPaper>
|
||||||
</VerticallyCentered>
|
</VerticallyCentered>
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { precacheAndRoute, cleanupOutdatedCaches } from 'workbox-precaching';
|
|
||||||
import { registerRoute, setDefaultHandler } from 'workbox-routing';
|
|
||||||
import { NetworkOnly } from 'workbox-strategies';
|
|
||||||
import { pageCache, offlineFallback } from 'workbox-recipes';
|
|
||||||
|
|
||||||
pageCache();
|
|
||||||
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
cleanupOutdatedCaches();
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
'/share-target',
|
|
||||||
async ({ event }) => {
|
|
||||||
event.waitUntil(
|
|
||||||
(async function () {
|
|
||||||
const data = await event.request.formData();
|
|
||||||
const client = await self.clients.get(
|
|
||||||
event.resultingClientId || event.clientId
|
|
||||||
);
|
|
||||||
const files = data.getAll('files');
|
|
||||||
setTimeout(() => {
|
|
||||||
client.postMessage({ files, action: 'upload-files' });
|
|
||||||
}, 1000);
|
|
||||||
})()
|
|
||||||
);
|
|
||||||
return Response.redirect('./');
|
|
||||||
},
|
|
||||||
'POST'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use a stale-while-revalidate strategy for all other requests.
|
|
||||||
setDefaultHandler(new NetworkOnly());
|
|
||||||
|
|
||||||
offlineFallback();
|
|
|
@ -6,7 +6,7 @@ import { logError } from 'utils/sentry';
|
||||||
import { getPaymentToken } from './userService';
|
import { getPaymentToken } from './userService';
|
||||||
import { Plan, Subscription } from 'types/billing';
|
import { Plan, Subscription } from 'types/billing';
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { DESKTOP_REDIRECT_URL } from 'constants/billing';
|
import { getDesktopRedirectURL } from 'constants/billing';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
|
|
||||||
|
@ -170,12 +170,7 @@ class billingService {
|
||||||
action: string
|
action: string
|
||||||
) {
|
) {
|
||||||
try {
|
try {
|
||||||
let redirectURL;
|
const redirectURL = this.getRedirectURL();
|
||||||
if (isElectron()) {
|
|
||||||
redirectURL = DESKTOP_REDIRECT_URL;
|
|
||||||
} else {
|
|
||||||
redirectURL = `${window.location.origin}/gallery`;
|
|
||||||
}
|
|
||||||
window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`;
|
window.location.href = `${getPaymentsURL()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&redirectURL=${redirectURL}`;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logError(e, 'unable to get payments url');
|
logError(e, 'unable to get payments url');
|
||||||
|
@ -185,9 +180,10 @@ class billingService {
|
||||||
|
|
||||||
public async redirectToCustomerPortal() {
|
public async redirectToCustomerPortal() {
|
||||||
try {
|
try {
|
||||||
|
const redirectURL = this.getRedirectURL();
|
||||||
const response = await HTTPService.get(
|
const response = await HTTPService.get(
|
||||||
`${ENDPOINT}/billing/stripe/customer-portal`,
|
`${ENDPOINT}/billing/stripe/customer-portal`,
|
||||||
{ redirectURL: `${window.location.origin}/gallery` },
|
{ redirectURL },
|
||||||
{
|
{
|
||||||
'X-Auth-Token': getToken(),
|
'X-Auth-Token': getToken(),
|
||||||
}
|
}
|
||||||
|
@ -198,6 +194,14 @@ class billingService {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRedirectURL() {
|
||||||
|
if (isElectron()) {
|
||||||
|
return getDesktopRedirectURL();
|
||||||
|
} else {
|
||||||
|
return `${window.location.origin}/gallery`;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new billingService();
|
export default new billingService();
|
||||||
|
|
|
@ -42,6 +42,7 @@ import { EncryptionResult } from 'types/upload';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import { IsArchived } from 'utils/magicMetadata';
|
import { IsArchived } from 'utils/magicMetadata';
|
||||||
import { User } from 'types/user';
|
import { User } from 'types/user';
|
||||||
|
import { getNonHiddenCollections } from 'utils/collection';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const COLLECTION_TABLE = 'collections';
|
const COLLECTION_TABLE = 'collections';
|
||||||
|
@ -143,7 +144,7 @@ const getCollections = async (
|
||||||
export const getLocalCollections = async (): Promise<Collection[]> => {
|
export const getLocalCollections = async (): Promise<Collection[]> => {
|
||||||
const collections: Collection[] =
|
const collections: Collection[] =
|
||||||
(await localForage.getItem(COLLECTION_TABLE)) ?? [];
|
(await localForage.getItem(COLLECTION_TABLE)) ?? [];
|
||||||
return collections;
|
return getNonHiddenCollections(collections);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCollectionUpdationTime = async (): Promise<number> =>
|
export const getCollectionUpdationTime = async (): Promise<number> =>
|
||||||
|
@ -188,7 +189,7 @@ export const syncCollections = async () => {
|
||||||
|
|
||||||
await localForage.setItem(COLLECTION_TABLE, collections);
|
await localForage.setItem(COLLECTION_TABLE, collections);
|
||||||
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
|
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
|
||||||
return collections;
|
return getNonHiddenCollections(collections);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCollection = async (
|
export const getCollection = async (
|
||||||
|
|
|
@ -33,7 +33,7 @@ export async function getDuplicateFiles(
|
||||||
fileMap.set(file.id, file);
|
fileMap.set(file.id, file);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result: DuplicateFiles[] = [];
|
let result: DuplicateFiles[] = [];
|
||||||
|
|
||||||
for (const dupe of dupes) {
|
for (const dupe of dupes) {
|
||||||
let duplicateFiles: EnteFile[] = [];
|
let duplicateFiles: EnteFile[] = [];
|
||||||
|
@ -48,12 +48,13 @@ export async function getDuplicateFiles(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (duplicateFiles.length > 1) {
|
if (duplicateFiles.length > 1) {
|
||||||
result.push(
|
result = [
|
||||||
|
...result,
|
||||||
...getDupesGroupedBySameFileHashes(
|
...getDupesGroupedBySameFileHashes(
|
||||||
duplicateFiles,
|
duplicateFiles,
|
||||||
dupe.size
|
dupe.size
|
||||||
)
|
),
|
||||||
);
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,11 +13,22 @@ import { logError } from 'utils/sentry';
|
||||||
import { FILE_TYPE } from 'constants/file';
|
import { FILE_TYPE } from 'constants/file';
|
||||||
import { CustomError } from 'utils/error';
|
import { CustomError } from 'utils/error';
|
||||||
import { openThumbnailCache } from './cacheService';
|
import { openThumbnailCache } from './cacheService';
|
||||||
|
import QueueProcessor, { PROCESSING_STRATEGY } from './queueProcessor';
|
||||||
|
|
||||||
|
const MAX_PARALLEL_DOWNLOADS = 10;
|
||||||
|
|
||||||
class DownloadManager {
|
class DownloadManager {
|
||||||
private fileObjectURLPromise = new Map<string, Promise<string[]>>();
|
private fileObjectURLPromise = new Map<
|
||||||
|
string,
|
||||||
|
Promise<{ original: string[]; converted: string[] }>
|
||||||
|
>();
|
||||||
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
|
||||||
|
|
||||||
|
private thumbnailDownloadRequestsProcessor = new QueueProcessor<any>(
|
||||||
|
MAX_PARALLEL_DOWNLOADS,
|
||||||
|
PROCESSING_STRATEGY.LIFO
|
||||||
|
);
|
||||||
|
|
||||||
public async getThumbnail(file: EnteFile) {
|
public async getThumbnail(file: EnteFile) {
|
||||||
try {
|
try {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
@ -34,7 +45,10 @@ class DownloadManager {
|
||||||
if (cacheResp) {
|
if (cacheResp) {
|
||||||
return URL.createObjectURL(await cacheResp.blob());
|
return URL.createObjectURL(await cacheResp.blob());
|
||||||
}
|
}
|
||||||
const thumb = await this.downloadThumb(token, file);
|
const thumb =
|
||||||
|
await this.thumbnailDownloadRequestsProcessor.queueUpRequest(
|
||||||
|
() => this.downloadThumb(token, file)
|
||||||
|
).promise;
|
||||||
const thumbBlob = new Blob([thumb]);
|
const thumbBlob = new Blob([thumb]);
|
||||||
|
|
||||||
thumbnailCache
|
thumbnailCache
|
||||||
|
@ -84,12 +98,11 @@ class DownloadManager {
|
||||||
if (forPreview) {
|
if (forPreview) {
|
||||||
return await getRenderableFileURL(file, fileBlob);
|
return await getRenderableFileURL(file, fileBlob);
|
||||||
} else {
|
} else {
|
||||||
return [
|
const fileURL = await createTypedObjectURL(
|
||||||
await createTypedObjectURL(
|
|
||||||
fileBlob,
|
fileBlob,
|
||||||
file.metadata.title
|
file.metadata.title
|
||||||
),
|
);
|
||||||
];
|
return { converted: [fileURL], original: [fileURL] };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if (!this.fileObjectURLPromise.get(fileKey)) {
|
if (!this.fileObjectURLPromise.get(fileKey)) {
|
||||||
|
|
|
@ -1,18 +1,38 @@
|
||||||
import isElectron from 'is-electron';
|
import isElectron from 'is-electron';
|
||||||
import { ElectronAPIs } from 'types/electron';
|
import { ElectronAPIs } from 'types/electron';
|
||||||
import { runningInBrowser } from 'utils/common';
|
|
||||||
|
|
||||||
class ElectronService {
|
class ElectronService {
|
||||||
private electronAPIs: ElectronAPIs;
|
private electronAPIs: ElectronAPIs;
|
||||||
private isBundledApp: boolean = false;
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.electronAPIs = runningInBrowser() && window['ElectronAPIs'];
|
this.electronAPIs = globalThis['ElectronAPIs'];
|
||||||
this.isBundledApp = !!this.electronAPIs?.openDiskCache;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkIsBundledApp() {
|
checkIsBundledApp() {
|
||||||
return isElectron() && this.isBundledApp;
|
return isElectron() && !!this.electronAPIs?.openDiskCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
logToDisk(msg: string) {
|
||||||
|
if (this.electronAPIs?.logToDisk) {
|
||||||
|
this.electronAPIs.logToDisk(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openLogDirectory() {
|
||||||
|
if (this.electronAPIs?.openLogDirectory) {
|
||||||
|
this.electronAPIs.openLogDirectory();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSentryUserID() {
|
||||||
|
if (this.electronAPIs?.getSentryUserID) {
|
||||||
|
return this.electronAPIs.getSentryUserID();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getAppVersion() {
|
||||||
|
if (this.electronAPIs?.getAppVersion) {
|
||||||
|
return this.electronAPIs.getAppVersion();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
26
src/services/electron/ffmpeg.ts
Normal file
26
src/services/electron/ffmpeg.ts
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import { IFFmpeg } from 'services/ffmpeg/ffmpegFactory';
|
||||||
|
import { ElectronAPIs } from 'types/electron';
|
||||||
|
import { ElectronFile } from 'types/upload';
|
||||||
|
import { runningInBrowser } from 'utils/common';
|
||||||
|
|
||||||
|
export class ElectronFFmpeg implements IFFmpeg {
|
||||||
|
private electronAPIs: ElectronAPIs;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.electronAPIs = runningInBrowser() && globalThis['ElectronAPIs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async run(
|
||||||
|
cmd: string[],
|
||||||
|
inputFile: ElectronFile | File,
|
||||||
|
outputFilename: string
|
||||||
|
) {
|
||||||
|
if (this.electronAPIs?.runFFmpegCmd) {
|
||||||
|
return this.electronAPIs.runFFmpegCmd(
|
||||||
|
cmd,
|
||||||
|
inputFile,
|
||||||
|
outputFilename
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
src/services/electron/heicConvertor.ts
Normal file
46
src/services/electron/heicConvertor.ts
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { ElectronAPIs } from 'types/electron';
|
||||||
|
import { makeHumanReadableStorage } from 'utils/billing';
|
||||||
|
import { addLogLine } from 'utils/logging';
|
||||||
|
import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
|
class ElectronHEICConverter {
|
||||||
|
private electronAPIs: ElectronAPIs;
|
||||||
|
private allElectronAPIExists: boolean;
|
||||||
|
constructor() {
|
||||||
|
this.electronAPIs = globalThis['ElectronAPIs'];
|
||||||
|
this.allElectronAPIExists = !!this.electronAPIs?.convertHEIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
apiExists() {
|
||||||
|
return this.allElectronAPIExists;
|
||||||
|
}
|
||||||
|
|
||||||
|
async convert(fileBlob: Blob): Promise<Blob> {
|
||||||
|
try {
|
||||||
|
if (this.allElectronAPIExists) {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const inputFileData = new Uint8Array(
|
||||||
|
await fileBlob.arrayBuffer()
|
||||||
|
);
|
||||||
|
const convertedFileData = await this.electronAPIs.convertHEIC(
|
||||||
|
inputFileData
|
||||||
|
);
|
||||||
|
addLogLine(
|
||||||
|
`originalFileSize:${makeHumanReadableStorage(
|
||||||
|
fileBlob?.size
|
||||||
|
)},convertedFileSize:${makeHumanReadableStorage(
|
||||||
|
convertedFileData?.length
|
||||||
|
)}, native heic conversion time: ${
|
||||||
|
Date.now() - startTime
|
||||||
|
}ms `
|
||||||
|
);
|
||||||
|
return new Blob([convertedFileData]);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'failed to convert heic natively');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ElectronHEICConverter();
|
31
src/services/electron/update.ts
Normal file
31
src/services/electron/update.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import { AppUpdateInfo, ElectronAPIs } from 'types/electron';
|
||||||
|
|
||||||
|
class ElectronUpdateService {
|
||||||
|
private electronAPIs: ElectronAPIs;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.electronAPIs = globalThis['ElectronAPIs'];
|
||||||
|
}
|
||||||
|
|
||||||
|
registerUpdateEventListener(
|
||||||
|
showUpdateDialog: (updateInfo: AppUpdateInfo) => void
|
||||||
|
) {
|
||||||
|
if (this.electronAPIs?.registerUpdateEventListener) {
|
||||||
|
this.electronAPIs.registerUpdateEventListener(showUpdateDialog);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAndRestart() {
|
||||||
|
if (this.electronAPIs?.updateAndRestart) {
|
||||||
|
this.electronAPIs.updateAndRestart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
skipAppVersion(version: string) {
|
||||||
|
if (this.electronAPIs?.skipAppVersion) {
|
||||||
|
this.electronAPIs.skipAppVersion(version);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new ElectronUpdateService();
|
|
@ -39,7 +39,6 @@ import {
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
|
|
||||||
import { updateFileCreationDateInEXIF } from './upload/exifService';
|
import { updateFileCreationDateInEXIF } from './upload/exifService';
|
||||||
import { Metadata } from 'types/upload';
|
|
||||||
import QueueProcessor from './queueProcessor';
|
import QueueProcessor from './queueProcessor';
|
||||||
import { Collection } from 'types/collection';
|
import { Collection } from 'types/collection';
|
||||||
import {
|
import {
|
||||||
|
@ -245,10 +244,7 @@ class ExportService {
|
||||||
file,
|
file,
|
||||||
RecordType.FAILED
|
RecordType.FAILED
|
||||||
);
|
);
|
||||||
console.log(
|
|
||||||
`export failed for fileID:${file.id}, reason:`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
logError(
|
logError(
|
||||||
e,
|
e,
|
||||||
'download and save failed for file during export'
|
'download and save failed for file during export'
|
||||||
|
@ -460,11 +456,7 @@ class ExportService {
|
||||||
await this.exportMotionPhoto(fileStream, file, collectionPath);
|
await this.exportMotionPhoto(fileStream, file, collectionPath);
|
||||||
} else {
|
} else {
|
||||||
this.saveMediaFile(collectionPath, fileSaveName, fileStream);
|
this.saveMediaFile(collectionPath, fileSaveName, fileStream);
|
||||||
await this.saveMetadataFile(
|
await this.saveMetadataFile(collectionPath, fileSaveName, file);
|
||||||
collectionPath,
|
|
||||||
fileSaveName,
|
|
||||||
file.metadata
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -483,11 +475,7 @@ class ExportService {
|
||||||
file.id
|
file.id
|
||||||
);
|
);
|
||||||
this.saveMediaFile(collectionPath, imageSaveName, imageStream);
|
this.saveMediaFile(collectionPath, imageSaveName, imageStream);
|
||||||
await this.saveMetadataFile(
|
await this.saveMetadataFile(collectionPath, imageSaveName, file);
|
||||||
collectionPath,
|
|
||||||
imageSaveName,
|
|
||||||
file.metadata
|
|
||||||
);
|
|
||||||
|
|
||||||
const videoStream = generateStreamFromArrayBuffer(motionPhoto.video);
|
const videoStream = generateStreamFromArrayBuffer(motionPhoto.video);
|
||||||
const videoSaveName = getUniqueFileSaveName(
|
const videoSaveName = getUniqueFileSaveName(
|
||||||
|
@ -496,11 +484,7 @@ class ExportService {
|
||||||
file.id
|
file.id
|
||||||
);
|
);
|
||||||
this.saveMediaFile(collectionPath, videoSaveName, videoStream);
|
this.saveMediaFile(collectionPath, videoSaveName, videoStream);
|
||||||
await this.saveMetadataFile(
|
await this.saveMetadataFile(collectionPath, videoSaveName, file);
|
||||||
collectionPath,
|
|
||||||
videoSaveName,
|
|
||||||
file.metadata
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private saveMediaFile(
|
private saveMediaFile(
|
||||||
|
@ -516,11 +500,11 @@ class ExportService {
|
||||||
private async saveMetadataFile(
|
private async saveMetadataFile(
|
||||||
collectionFolderPath: string,
|
collectionFolderPath: string,
|
||||||
fileSaveName: string,
|
fileSaveName: string,
|
||||||
metadata: Metadata
|
file: EnteFile
|
||||||
) {
|
) {
|
||||||
await this.electronAPIs.saveFileToDisk(
|
await this.electronAPIs.saveFileToDisk(
|
||||||
getFileMetadataSavePath(collectionFolderPath, fileSaveName),
|
getFileMetadataSavePath(collectionFolderPath, fileSaveName),
|
||||||
getGoogleLikeMetadataFile(fileSaveName, metadata)
|
getGoogleLikeMetadataFile(fileSaveName, file)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -637,7 +621,6 @@ class ExportService {
|
||||||
oldFileSavePath,
|
oldFileSavePath,
|
||||||
newFileSavePath
|
newFileSavePath
|
||||||
);
|
);
|
||||||
console.log(oldFileMetadataSavePath, newFileMetadataSavePath);
|
|
||||||
await this.electronAPIs.checkExistsAndRename(
|
await this.electronAPIs.checkExistsAndRename(
|
||||||
oldFileMetadataSavePath,
|
oldFileMetadataSavePath,
|
||||||
newFileMetadataSavePath
|
newFileMetadataSavePath
|
||||||
|
|
|
@ -1,114 +0,0 @@
|
||||||
import { createFFmpeg, FFmpeg } from 'ffmpeg-wasm';
|
|
||||||
import { getUint8ArrayView } from 'services/readerService';
|
|
||||||
import {
|
|
||||||
parseFFmpegExtractedMetadata,
|
|
||||||
splitFilenameAndExtension,
|
|
||||||
} from 'utils/ffmpeg';
|
|
||||||
|
|
||||||
class FFmpegClient {
|
|
||||||
private ffmpeg: FFmpeg;
|
|
||||||
private ready: Promise<void> = null;
|
|
||||||
constructor() {
|
|
||||||
this.ffmpeg = createFFmpeg({
|
|
||||||
corePath: '/js/ffmpeg/ffmpeg-core.js',
|
|
||||||
mt: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ready = this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async init() {
|
|
||||||
if (!this.ffmpeg.isLoaded()) {
|
|
||||||
await this.ffmpeg.load();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateThumbnail(file: File) {
|
|
||||||
await this.ready;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, ext] = splitFilenameAndExtension(file.name);
|
|
||||||
const inputFileName = `${Date.now().toString()}-input.${ext}`;
|
|
||||||
const thumbFileName = `${Date.now().toString()}-thumb.jpeg`;
|
|
||||||
this.ffmpeg.FS(
|
|
||||||
'writeFile',
|
|
||||||
inputFileName,
|
|
||||||
await getUint8ArrayView(file)
|
|
||||||
);
|
|
||||||
let seekTime = 1.0;
|
|
||||||
let thumb = null;
|
|
||||||
while (seekTime > 0) {
|
|
||||||
try {
|
|
||||||
await this.ffmpeg.run(
|
|
||||||
'-i',
|
|
||||||
inputFileName,
|
|
||||||
'-ss',
|
|
||||||
`00:00:0${seekTime.toFixed(3)}`,
|
|
||||||
'-vframes',
|
|
||||||
'1',
|
|
||||||
'-vf',
|
|
||||||
'scale=-1:720',
|
|
||||||
thumbFileName
|
|
||||||
);
|
|
||||||
thumb = this.ffmpeg.FS('readFile', thumbFileName);
|
|
||||||
this.ffmpeg.FS('unlink', thumbFileName);
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
seekTime = Number((seekTime / 10).toFixed(3));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.ffmpeg.FS('unlink', inputFileName);
|
|
||||||
return thumb;
|
|
||||||
}
|
|
||||||
|
|
||||||
async extractVideoMetadata(file: File) {
|
|
||||||
await this.ready;
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
const [_, ext] = splitFilenameAndExtension(file.name);
|
|
||||||
const inputFileName = `${Date.now().toString()}-input.${ext}`;
|
|
||||||
const outFileName = `${Date.now().toString()}-metadata.txt`;
|
|
||||||
this.ffmpeg.FS(
|
|
||||||
'writeFile',
|
|
||||||
inputFileName,
|
|
||||||
await getUint8ArrayView(file)
|
|
||||||
);
|
|
||||||
let metadata = null;
|
|
||||||
|
|
||||||
// https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
|
|
||||||
// -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
|
|
||||||
// -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
|
|
||||||
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
|
|
||||||
await this.ffmpeg.run(
|
|
||||||
'-i',
|
|
||||||
inputFileName,
|
|
||||||
'-c',
|
|
||||||
'copy',
|
|
||||||
'-map_metadata',
|
|
||||||
'0',
|
|
||||||
'-f',
|
|
||||||
'ffmetadata',
|
|
||||||
outFileName
|
|
||||||
);
|
|
||||||
metadata = this.ffmpeg.FS('readFile', outFileName);
|
|
||||||
this.ffmpeg.FS('unlink', outFileName);
|
|
||||||
this.ffmpeg.FS('unlink', inputFileName);
|
|
||||||
return parseFFmpegExtractedMetadata(metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
async convertToMP4(file: Uint8Array, inputFileName: string) {
|
|
||||||
await this.ready;
|
|
||||||
this.ffmpeg.FS('writeFile', inputFileName, file);
|
|
||||||
await this.ffmpeg.run(
|
|
||||||
'-i',
|
|
||||||
inputFileName,
|
|
||||||
'-preset',
|
|
||||||
'ultrafast',
|
|
||||||
'output.mp4'
|
|
||||||
);
|
|
||||||
const convertedFile = this.ffmpeg.FS('readFile', 'output.mp4');
|
|
||||||
this.ffmpeg.FS('unlink', inputFileName);
|
|
||||||
this.ffmpeg.FS('unlink', 'output.mp4');
|
|
||||||
return convertedFile;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default FFmpegClient;
|
|
28
src/services/ffmpeg/ffmpegFactory.ts
Normal file
28
src/services/ffmpeg/ffmpegFactory.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import isElectron from 'is-electron';
|
||||||
|
import { ElectronFFmpeg } from 'services/electron/ffmpeg';
|
||||||
|
import { ElectronFile } from 'types/upload';
|
||||||
|
import { FFmpegWorker } from 'utils/comlink';
|
||||||
|
|
||||||
|
export interface IFFmpeg {
|
||||||
|
run: (
|
||||||
|
cmd: string[],
|
||||||
|
inputFile: File | ElectronFile,
|
||||||
|
outputFilename: string
|
||||||
|
) => Promise<File | ElectronFile>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FFmpegFactory {
|
||||||
|
private client: IFFmpeg;
|
||||||
|
|
||||||
|
async getFFmpegClient() {
|
||||||
|
if (!this.client) {
|
||||||
|
if (isElectron()) {
|
||||||
|
this.client = new ElectronFFmpeg();
|
||||||
|
} else {
|
||||||
|
this.client = await new FFmpegWorker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export default new FFmpegFactory();
|
|
@ -1,93 +1,99 @@
|
||||||
import { CustomError } from 'utils/error';
|
import {
|
||||||
|
FFMPEG_PLACEHOLDER,
|
||||||
|
INPUT_PATH_PLACEHOLDER,
|
||||||
|
OUTPUT_PATH_PLACEHOLDER,
|
||||||
|
} from 'constants/ffmpeg';
|
||||||
|
import { ElectronFile } from 'types/upload';
|
||||||
|
import { parseFFmpegExtractedMetadata } from 'utils/ffmpeg';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import QueueProcessor from 'services/queueProcessor';
|
import ffmpegFactory from './ffmpegFactory';
|
||||||
import { ParsedExtractedMetadata } from 'types/upload';
|
|
||||||
|
|
||||||
import { FFmpegWorker } from 'utils/comlink';
|
export async function generateVideoThumbnail(
|
||||||
import { promiseWithTimeout } from 'utils/common';
|
file: File | ElectronFile
|
||||||
|
): Promise<File | ElectronFile> {
|
||||||
const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000;
|
|
||||||
|
|
||||||
class FFmpegService {
|
|
||||||
private ffmpegWorker = null;
|
|
||||||
private ffmpegTaskQueue = new QueueProcessor<any>(1);
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
this.ffmpegWorker = await new FFmpegWorker();
|
|
||||||
}
|
|
||||||
|
|
||||||
async generateThumbnail(file: File): Promise<Uint8Array> {
|
|
||||||
if (!this.ffmpegWorker) {
|
|
||||||
await this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = this.ffmpegTaskQueue.queueUpRequest(() =>
|
|
||||||
promiseWithTimeout(
|
|
||||||
this.ffmpegWorker.generateThumbnail(file),
|
|
||||||
FFMPEG_EXECUTION_WAIT_TIME
|
|
||||||
)
|
|
||||||
);
|
|
||||||
try {
|
try {
|
||||||
return await response.promise;
|
let seekTime = 1.0;
|
||||||
|
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||||
|
while (seekTime > 0) {
|
||||||
|
try {
|
||||||
|
return await ffmpegClient.run(
|
||||||
|
[
|
||||||
|
FFMPEG_PLACEHOLDER,
|
||||||
|
'-i',
|
||||||
|
INPUT_PATH_PLACEHOLDER,
|
||||||
|
'-ss',
|
||||||
|
`00:00:0${seekTime.toFixed(3)}`,
|
||||||
|
'-vframes',
|
||||||
|
'1',
|
||||||
|
'-vf',
|
||||||
|
'scale=-1:720',
|
||||||
|
OUTPUT_PATH_PLACEHOLDER,
|
||||||
|
],
|
||||||
|
file,
|
||||||
|
'thumb.jpeg'
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
if (seekTime <= 0) {
|
||||||
// ignore
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
logError(e, 'ffmpeg thumbnail generation failed');
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
seekTime = Number((seekTime / 10).toFixed(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
async extractMetadata(file: File): Promise<ParsedExtractedMetadata> {
|
|
||||||
if (!this.ffmpegWorker) {
|
|
||||||
await this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = this.ffmpegTaskQueue.queueUpRequest(() =>
|
|
||||||
promiseWithTimeout(
|
|
||||||
this.ffmpegWorker.extractVideoMetadata(file),
|
|
||||||
FFMPEG_EXECUTION_WAIT_TIME
|
|
||||||
)
|
|
||||||
);
|
|
||||||
try {
|
|
||||||
return await response.promise;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
logError(e, 'ffmpeg generateVideoThumbnail failed');
|
||||||
// ignore
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
logError(e, 'ffmpeg metadata extraction failed');
|
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async convertToMP4(
|
|
||||||
file: Uint8Array,
|
|
||||||
fileName: string
|
|
||||||
): Promise<Uint8Array> {
|
|
||||||
if (!this.ffmpegWorker) {
|
|
||||||
await this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = this.ffmpegTaskQueue.queueUpRequest(
|
|
||||||
async () => await this.ffmpegWorker.convertToMP4(file, fileName)
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await response.promise;
|
|
||||||
} catch (e) {
|
|
||||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
|
||||||
// ignore
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
logError(e, 'ffmpeg MP4 conversion failed');
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new FFmpegService();
|
export async function extractVideoMetadata(file: File | ElectronFile) {
|
||||||
|
try {
|
||||||
|
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||||
|
// https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
|
||||||
|
// -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
|
||||||
|
// -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
|
||||||
|
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
|
||||||
|
const metadata = await ffmpegClient.run(
|
||||||
|
[
|
||||||
|
FFMPEG_PLACEHOLDER,
|
||||||
|
'-i',
|
||||||
|
INPUT_PATH_PLACEHOLDER,
|
||||||
|
'-c',
|
||||||
|
'copy',
|
||||||
|
'-map_metadata',
|
||||||
|
'0',
|
||||||
|
'-f',
|
||||||
|
'ffmetadata',
|
||||||
|
OUTPUT_PATH_PLACEHOLDER,
|
||||||
|
],
|
||||||
|
file,
|
||||||
|
`metadata.txt`
|
||||||
|
);
|
||||||
|
return parseFFmpegExtractedMetadata(
|
||||||
|
new Uint8Array(await metadata.arrayBuffer())
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'ffmpeg extractVideoMetadata failed');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function convertToMP4(file: File | ElectronFile) {
|
||||||
|
try {
|
||||||
|
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||||
|
return await ffmpegClient.run(
|
||||||
|
[
|
||||||
|
FFMPEG_PLACEHOLDER,
|
||||||
|
'-i',
|
||||||
|
INPUT_PATH_PLACEHOLDER,
|
||||||
|
'-preset',
|
||||||
|
'ultrafast',
|
||||||
|
OUTPUT_PATH_PLACEHOLDER,
|
||||||
|
],
|
||||||
|
file,
|
||||||
|
'output.mp4'
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'ffmpeg convertToMP4 failed');
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@ import { SetFiles } from 'types/gallery';
|
||||||
import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
|
import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
|
||||||
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||||
import { addLogLine } from 'utils/logging';
|
import { addLogLine } from 'utils/logging';
|
||||||
|
import { isCollectionHidden } from 'utils/collection';
|
||||||
|
import { CustomError } from 'utils/error';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const FILES_TABLE = 'files';
|
const FILES_TABLE = 'files';
|
||||||
|
@ -63,13 +65,16 @@ export const syncFiles = async (
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (isCollectionHidden(collection)) {
|
||||||
|
throw Error(CustomError.HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED);
|
||||||
|
}
|
||||||
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||||
if (collection.updationTime === lastSyncTime) {
|
if (collection.updationTime === lastSyncTime) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const fetchedFiles =
|
const fetchedFiles =
|
||||||
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
|
(await getFiles(collection, lastSyncTime, files, setFiles)) ?? [];
|
||||||
files.push(...fetchedFiles);
|
files = [...files, ...fetchedFiles];
|
||||||
const latestVersionFiles = new Map<string, EnteFile>();
|
const latestVersionFiles = new Map<string, EnteFile>();
|
||||||
files.forEach((file) => {
|
files.forEach((file) => {
|
||||||
const uid = `${file.collectionID}-${file.id}`;
|
const uid = `${file.collectionID}-${file.id}`;
|
||||||
|
@ -105,7 +110,7 @@ export const getFiles = async (
|
||||||
setFiles: SetFiles
|
setFiles: SetFiles
|
||||||
): Promise<EnteFile[]> => {
|
): Promise<EnteFile[]> => {
|
||||||
try {
|
try {
|
||||||
const decryptedFiles: EnteFile[] = [];
|
let decryptedFiles: EnteFile[] = [];
|
||||||
let time = sinceTime;
|
let time = sinceTime;
|
||||||
let resp;
|
let resp;
|
||||||
do {
|
do {
|
||||||
|
@ -124,7 +129,8 @@ export const getFiles = async (
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
decryptedFiles.push(
|
decryptedFiles = [
|
||||||
|
...decryptedFiles,
|
||||||
...(await Promise.all(
|
...(await Promise.all(
|
||||||
resp.data.diff.map(async (file: EnteFile) => {
|
resp.data.diff.map(async (file: EnteFile) => {
|
||||||
if (!file.isDeleted) {
|
if (!file.isDeleted) {
|
||||||
|
@ -132,8 +138,8 @@ export const getFiles = async (
|
||||||
}
|
}
|
||||||
return file;
|
return file;
|
||||||
}) as Promise<EnteFile>[]
|
}) as Promise<EnteFile>[]
|
||||||
))
|
)),
|
||||||
);
|
];
|
||||||
|
|
||||||
if (resp.data.diff.length) {
|
if (resp.data.diff.length) {
|
||||||
time = resp.data.diff.slice(-1)[0].updationTime;
|
time = resp.data.diff.slice(-1)[0].updationTime;
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue