Merge branch 'main' into mouse_trap
This commit is contained in:
commit
a9c9e73d8c
|
@ -41,7 +41,6 @@ module.exports = (phase) =>
|
|||
},
|
||||
env: {
|
||||
SENTRY_RELEASE: GIT_SHA,
|
||||
NEXT_PUBLIC_LATEST_COMMIT_HASH: GIT_SHA,
|
||||
},
|
||||
workbox: WORKBOX_CONFIG,
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ const SENTRY_ENV = getSentryENV();
|
|||
const SENTRY_RELEASE = getSentryRelease();
|
||||
const IS_ENABLED = getIsSentryEnabled();
|
||||
|
||||
Sentry.setUser({ id: getSentryUserID() });
|
||||
Sentry.init({
|
||||
dsn: SENTRY_DSN,
|
||||
enabled: IS_ENABLED,
|
||||
|
@ -39,3 +38,9 @@ Sentry.init({
|
|||
// `release` value here - use the environment variable `SENTRY_RELEASE`, so
|
||||
// that it will also get attached to your source maps
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -6,6 +6,8 @@ import {
|
|||
getIsSentryEnabled,
|
||||
} from 'constants/sentry';
|
||||
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
const SENTRY_DSN = getSentryDSN();
|
||||
const SENTRY_ENV = getSentryENV();
|
||||
const SENTRY_RELEASE = getSentryRelease();
|
||||
|
@ -18,3 +20,9 @@ Sentry.init({
|
|||
release: SENTRY_RELEASE,
|
||||
autoSessionTracking: false,
|
||||
});
|
||||
|
||||
const main = async () => {
|
||||
Sentry.setUser({ id: await getSentryUserID() });
|
||||
};
|
||||
|
||||
main();
|
||||
|
|
|
@ -3,8 +3,9 @@ import { CSSProperties } from '@mui/styled-engine';
|
|||
|
||||
export const Badge = styled(Paper)(({ theme }) => ({
|
||||
padding: '2px 4px',
|
||||
backgroundColor: theme.palette.glass.main,
|
||||
color: theme.palette.glass.contrastText,
|
||||
backgroundColor: theme.palette.backdrop.main,
|
||||
backdropFilter: `blur(${theme.palette.blur.muted})`,
|
||||
color: theme.palette.primary.contrastText,
|
||||
textTransform: 'uppercase',
|
||||
...(theme.typography.mini as CSSProperties),
|
||||
}));
|
||||
|
|
|
@ -8,8 +8,8 @@ import { CollectionInfoBarWrapper } from './styledComponents';
|
|||
import { shouldShowOptions } from 'utils/collection';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import Delete from '@mui/icons-material/Delete';
|
||||
import { ArchiveOutlined } from '@mui/icons-material';
|
||||
|
||||
interface Iprops {
|
||||
activeCollection: Collection;
|
||||
|
@ -43,7 +43,7 @@ export default function CollectionInfoWithOptions({
|
|||
return <Favorite />;
|
||||
case CollectionSummaryType.archived:
|
||||
case CollectionSummaryType.archive:
|
||||
return <VisibilityOff />;
|
||||
return <ArchiveOutlined />;
|
||||
case CollectionSummaryType.trash:
|
||||
return <Delete />;
|
||||
default:
|
||||
|
|
|
@ -11,7 +11,7 @@ import TruncateText from 'components/TruncateText';
|
|||
import { Box } from '@mui/material';
|
||||
import { CollectionSummaryType } from 'constants/collection';
|
||||
import Favorite from '@mui/icons-material/FavoriteRounded';
|
||||
import VisibilityOff from '@mui/icons-material/VisibilityOff';
|
||||
import { ArchiveOutlined } from '@mui/icons-material';
|
||||
|
||||
interface Iprops {
|
||||
active: boolean;
|
||||
|
@ -50,7 +50,7 @@ function CollectionCardIcon({ collectionType }) {
|
|||
<CollectionBarTileIcon>
|
||||
{collectionType === CollectionSummaryType.favorites && <Favorite />}
|
||||
{collectionType === CollectionSummaryType.archived && (
|
||||
<VisibilityOff />
|
||||
<ArchiveOutlined />
|
||||
)}
|
||||
</CollectionBarTileIcon>
|
||||
);
|
||||
|
|
|
@ -4,11 +4,10 @@ import React from 'react';
|
|||
import EditIcon from '@mui/icons-material/Edit';
|
||||
import IosShareIcon from '@mui/icons-material/IosShare';
|
||||
import FileDownloadOutlinedIcon from '@mui/icons-material/FileDownloadOutlined';
|
||||
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
|
||||
import VisibilityOnOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
|
||||
import DeleteOutlinedIcon from '@mui/icons-material/DeleteOutlined';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CollectionActions } from '.';
|
||||
import { ArchiveOutlined, Unarchive } from '@mui/icons-material';
|
||||
|
||||
interface Iprops {
|
||||
IsArchived: boolean;
|
||||
|
@ -53,13 +52,13 @@ export function AlbumCollectionOption({
|
|||
onClick={handleCollectionAction(
|
||||
CollectionActions.UNARCHIVE
|
||||
)}
|
||||
startIcon={<VisibilityOnOutlinedIcon />}>
|
||||
startIcon={<Unarchive />}>
|
||||
{constants.UNARCHIVE}
|
||||
</OverflowMenuOption>
|
||||
) : (
|
||||
<OverflowMenuOption
|
||||
onClick={handleCollectionAction(CollectionActions.ARCHIVE)}
|
||||
startIcon={<VisibilityOffOutlinedIcon />}>
|
||||
startIcon={<ArchiveOutlined />}>
|
||||
{constants.ARCHIVE}
|
||||
</OverflowMenuOption>
|
||||
)}
|
||||
|
|
|
@ -5,7 +5,7 @@ import { GalleryContext } from 'pages/gallery';
|
|||
import React, { useContext } from 'react';
|
||||
import { shareCollection } from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { CollectionShareSharees } from './sharees';
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
deleteShareableURL,
|
||||
} from 'services/collectionService';
|
||||
import { Collection, PublicURL } from 'types/collection';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
import constants from 'utils/strings/constants';
|
||||
import PublicShareSwitch from './switch';
|
||||
interface Iprops {
|
||||
|
|
|
@ -8,13 +8,13 @@ import React, { useContext, useState } from 'react';
|
|||
import { updateShareableURL } from 'services/collectionService';
|
||||
import { UpdatePublicURL } from 'types/collection';
|
||||
import { sleep } from 'utils/common';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import constants from 'utils/strings/constants';
|
||||
import {
|
||||
ManageSectionLabel,
|
||||
ManageSectionOptions,
|
||||
} from '../../styledComponents';
|
||||
import { ManageDownloadAccess } from './downloadAccess';
|
||||
import { handleSharingErrors } from 'utils/error/ui';
|
||||
|
||||
export default function PublicShareManage({
|
||||
publicShareProp,
|
||||
|
|
18
src/components/DialogBox/DialogIcon.tsx
Normal file
18
src/components/DialogBox/DialogIcon.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { Box } from '@mui/material';
|
||||
import React from 'react';
|
||||
|
||||
export default function DialogIcon({ icon }: { icon: React.ReactNode }) {
|
||||
return (
|
||||
<Box
|
||||
className="DialogIcon"
|
||||
sx={{
|
||||
svg: {
|
||||
width: '48px',
|
||||
height: '48px',
|
||||
},
|
||||
color: 'stroke.secondary',
|
||||
}}>
|
||||
{icon}
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -5,6 +5,12 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
|||
padding: theme.spacing(1, 1.5),
|
||||
maxWidth: '346px',
|
||||
},
|
||||
|
||||
'& .DialogIcon': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
},
|
||||
|
||||
'& .MuiDialogTitle-root': {
|
||||
padding: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(1),
|
||||
|
@ -12,6 +18,11 @@ const DialogBoxBase = styled(Dialog)(({ theme }) => ({
|
|||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
|
||||
'.DialogIcon + .MuiDialogTitle-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
||||
'.MuiDialogTitle-root + .MuiDialogContent-root': {
|
||||
paddingTop: 0,
|
||||
},
|
||||
|
|
|
@ -13,6 +13,7 @@ import DialogTitleWithCloseButton, {
|
|||
} from './TitleWithCloseButton';
|
||||
import DialogBoxBase from './base';
|
||||
import { DialogBoxAttributes } from 'types/dialogBox';
|
||||
import DialogIcon from './DialogIcon';
|
||||
|
||||
type IProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
||||
|
@ -48,6 +49,7 @@ export default function DialogBox({
|
|||
maxWidth={size}
|
||||
onClose={handleClose}
|
||||
{...props}>
|
||||
{attributes.icon && <DialogIcon icon={attributes.icon} />}
|
||||
{attributes.title && (
|
||||
<DialogTitleWithCloseButton
|
||||
onClose={
|
||||
|
|
|
@ -31,7 +31,7 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
};
|
||||
|
||||
const handleClick = () => {
|
||||
attributes.action?.callback();
|
||||
attributes.onClick();
|
||||
onClose();
|
||||
};
|
||||
return (
|
||||
|
@ -40,14 +40,15 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
anchorOrigin={{
|
||||
horizontal: 'right',
|
||||
vertical: 'bottom',
|
||||
}}>
|
||||
}}
|
||||
sx={{ backgroundColor: '#000', width: '320px' }}>
|
||||
<Paper
|
||||
component={Button}
|
||||
color={attributes.variant}
|
||||
onClick={handleClick}
|
||||
sx={{
|
||||
textAlign: 'left',
|
||||
width: '320px',
|
||||
flex: '1',
|
||||
padding: (theme) => theme.spacing(1.5, 2),
|
||||
}}>
|
||||
<Stack
|
||||
|
@ -55,34 +56,38 @@ export default function Notification({ open, onClose, attributes }: Iprops) {
|
|||
spacing={2}
|
||||
direction="row"
|
||||
alignItems={'center'}>
|
||||
<Box>
|
||||
{attributes?.icon ?? <InfoIcon fontSize="large" />}
|
||||
<Box sx={{ svg: { fontSize: '36px' } }}>
|
||||
{attributes.startIcon ?? <InfoIcon />}
|
||||
</Box>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="rgba(255, 255, 255, 0.7)"
|
||||
mb={0.5}>
|
||||
{attributes.message}{' '}
|
||||
</Typography>
|
||||
{attributes?.action && (
|
||||
<Typography
|
||||
mb={0.5}
|
||||
variant="button"
|
||||
fontWeight={'bold'}>
|
||||
{attributes?.action.text}
|
||||
|
||||
<Stack
|
||||
direction={'column'}
|
||||
spacing={0.5}
|
||||
flex={1}
|
||||
textAlign="left">
|
||||
{attributes.subtext && (
|
||||
<Typography variant="body2">
|
||||
{attributes.subtext}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
<Box>
|
||||
{attributes.message && (
|
||||
<Typography variant="button">
|
||||
{attributes.message}
|
||||
</Typography>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
{attributes.endIcon ? (
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
sx={{
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
||||
}}>
|
||||
onClick={attributes.onClick}
|
||||
sx={{ fontSize: '36px' }}>
|
||||
{attributes?.endIcon}
|
||||
</IconButton>
|
||||
) : (
|
||||
<IconButton onClick={handleClose}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
</Paper>
|
||||
</Snackbar>
|
||||
|
|
|
@ -118,6 +118,7 @@ const PhotoFrame = ({
|
|||
dataIndex: index,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (
|
||||
|
|
|
@ -249,6 +249,8 @@ export function PhotoList({
|
|||
publicCollectionGalleryContext.accessedThroughSharedURL,
|
||||
galleryContext.photoListHeader,
|
||||
publicCollectionGalleryContext.photoListHeader,
|
||||
deduplicateContext.isOnDeduplicatePage,
|
||||
deduplicateContext.fileSizeMap,
|
||||
]);
|
||||
|
||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||
|
|
79
src/components/PhotoViewer/InfoDialog/CaptionEditForm.tsx
Normal file
79
src/components/PhotoViewer/InfoDialog/CaptionEditForm.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import React, { useState } from 'react';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Col, Form, FormControl } from 'react-bootstrap';
|
||||
import { 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_CAPTION_SIZE } from 'constants/file';
|
||||
import { SmallLoadingSpinner } from '../styledComponents/SmallLoadingSpinner';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export interface formValues {
|
||||
caption: string;
|
||||
}
|
||||
|
||||
export const CaptionEditForm = ({ caption, saveEdits, discardEdits }) => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const onSubmit = async (values: formValues) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
await saveEdits(values.caption);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<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 }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Row>
|
||||
<Form.Group bsPrefix="ente-form-group" as={Col} xs={9}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
placeholder={constants.CAPTION}
|
||||
value={values.caption}
|
||||
onChange={handleChange('caption')}
|
||||
isInvalid={Boolean(errors.caption)}
|
||||
autoFocus
|
||||
disabled={loading}
|
||||
/>
|
||||
<FormControl.Feedback
|
||||
type="invalid"
|
||||
style={{ textAlign: 'center' }}>
|
||||
{errors.caption}
|
||||
</FormControl.Feedback>
|
||||
</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>
|
||||
);
|
||||
};
|
100
src/components/PhotoViewer/InfoDialog/RenderCaption.tsx
Normal file
100
src/components/PhotoViewer/InfoDialog/RenderCaption.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import React, { useState } from 'react';
|
||||
import { updateFilePublicMagicMetadata } from 'services/fileService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { changeCaption, 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 { IconButton, Typography } from '@mui/material';
|
||||
import { CaptionEditForm } from './CaptionEditForm';
|
||||
|
||||
export const getFileTitle = (filename, extension) => {
|
||||
if (extension) {
|
||||
return filename + '.' + extension;
|
||||
} else {
|
||||
return filename;
|
||||
}
|
||||
};
|
||||
|
||||
export function RenderCaption({
|
||||
shouldDisableEdits,
|
||||
file,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
}: {
|
||||
shouldDisableEdits: boolean;
|
||||
file: EnteFile;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
}) {
|
||||
const [caption, setCaption] = useState(
|
||||
file?.pubMagicMetadata?.data.caption
|
||||
);
|
||||
const [isInEditMode, setIsInEditMode] = useState(false);
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async (newCaption: string) => {
|
||||
try {
|
||||
if (file) {
|
||||
if (caption === newCaption) {
|
||||
closeEditMode();
|
||||
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');
|
||||
} finally {
|
||||
closeEditMode();
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Label width="30%">{constants.CAPTION}</Label>
|
||||
{!isInEditMode ? (
|
||||
<>
|
||||
<Value width={!shouldDisableEdits ? '60%' : '70%'}>
|
||||
{caption ? (
|
||||
<FreeFlowText>{caption}</FreeFlowText>
|
||||
) : (
|
||||
<Typography color="text.secondary">
|
||||
{constants.CAPTION_PLACEHOLDER}
|
||||
</Typography>
|
||||
)}
|
||||
</Value>
|
||||
{!shouldDisableEdits && (
|
||||
<Value
|
||||
width="10%"
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
marginLeft: '10px',
|
||||
}}>
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
</Value>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<CaptionEditForm
|
||||
caption={caption}
|
||||
saveEdits={saveEdits}
|
||||
discardEdits={closeEditMode}
|
||||
/>
|
||||
)}
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -11,6 +11,7 @@ import { AppContext } from 'pages/_app';
|
|||
import { Location, Metadata } from 'types/upload';
|
||||
import Photoswipe from 'photoswipe';
|
||||
import { getEXIFLocation } from 'services/upload/exifService';
|
||||
import { RenderCaption } from './RenderCaption';
|
||||
|
||||
const FileInfoDialog = styled(Dialog)(({ theme }) => ({
|
||||
zIndex: 1501,
|
||||
|
@ -31,6 +32,7 @@ interface Iprops {
|
|||
metadata: Metadata;
|
||||
exif: any;
|
||||
scheduleUpdate: () => void;
|
||||
refreshPhotoswipe: () => void;
|
||||
}
|
||||
|
||||
export function FileInfo({
|
||||
|
@ -42,6 +44,7 @@ export function FileInfo({
|
|||
metadata,
|
||||
exif,
|
||||
scheduleUpdate,
|
||||
refreshPhotoswipe,
|
||||
}: Iprops) {
|
||||
const appContext = useContext(AppContext);
|
||||
const [location, setLocation] = useState<Location>(null);
|
||||
|
@ -78,11 +81,12 @@ export function FileInfo({
|
|||
<Typography variant="subtitle" mb={1}>
|
||||
{constants.METADATA}
|
||||
</Typography>
|
||||
|
||||
{RenderInfoItem(
|
||||
constants.FILE_ID,
|
||||
items[photoSwipe?.getCurrentIndex()]?.id
|
||||
)}
|
||||
<RenderCaption
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
file={items[photoSwipe?.getCurrentIndex()]}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
/>
|
||||
{metadata?.title && (
|
||||
<RenderFileName
|
||||
shouldDisableEdits={shouldDisableEdits}
|
||||
|
|
|
@ -33,7 +33,19 @@ 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 } from '@mui/icons-material';
|
||||
import { styled } from '@mui/material';
|
||||
|
||||
const CaptionContainer = styled('div')(({ theme }) => ({
|
||||
padding: theme.spacing(2),
|
||||
wordBreak: 'break-word',
|
||||
textAlign: 'right',
|
||||
maxWidth: '375px',
|
||||
fontSize: '14px',
|
||||
lineHeight: '17px',
|
||||
backgroundColor: theme.palette.backdrop.light,
|
||||
backdropFilter: `blur(${theme.palette.blur.base})`,
|
||||
}));
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
items: any[];
|
||||
|
@ -339,6 +351,13 @@ function PhotoViewer(props: Iprops) {
|
|||
}
|
||||
};
|
||||
|
||||
const refreshPhotoswipe = () => {
|
||||
photoSwipe.invalidateCurrItems();
|
||||
if (isOpen) {
|
||||
photoSwipe.updateSize(true);
|
||||
}
|
||||
};
|
||||
|
||||
const checkExifAvailable = async () => {
|
||||
setExif(null);
|
||||
await sleep(100);
|
||||
|
@ -497,20 +516,16 @@ function PhotoViewer(props: Iprops) {
|
|||
</div>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--left"
|
||||
title={constants.PREVIOUS}
|
||||
onClick={photoSwipe?.prev}>
|
||||
<ChevronRight
|
||||
sx={{ transform: 'rotate(180deg)' }}
|
||||
/>
|
||||
title={constants.PREVIOUS}>
|
||||
<ChevronLeft sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<button
|
||||
className="pswp__button pswp__button--arrow--right"
|
||||
title={constants.NEXT}
|
||||
onClick={photoSwipe?.next}>
|
||||
<ChevronRight />
|
||||
title={constants.NEXT}>
|
||||
<ChevronRight sx={{ pointerEvents: 'none' }} />
|
||||
</button>
|
||||
<div className="pswp__caption">
|
||||
<div />
|
||||
<div className="pswp__caption pswp-custom-caption-container">
|
||||
<CaptionContainer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -524,6 +539,7 @@ function PhotoViewer(props: Iprops) {
|
|||
metadata={metadata}
|
||||
exif={exif}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
import { AppContext } from 'pages/_app';
|
||||
import React, { useContext } from 'react';
|
||||
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 { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { User } from 'types/user';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
import isElectron from 'is-electron';
|
||||
import ElectronService from 'services/electron/common';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export default function DebugLogs() {
|
||||
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,
|
||||
|
@ -27,11 +37,6 @@ export default function DebugLogs() {
|
|||
});
|
||||
|
||||
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');
|
||||
if (isElectron()) {
|
||||
ElectronService.openLogDirectory();
|
||||
|
@ -43,11 +48,18 @@ export default function DebugLogs() {
|
|||
};
|
||||
|
||||
return (
|
||||
<SidebarButton
|
||||
onClick={confirmLogDownload}
|
||||
typographyVariant="caption"
|
||||
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
|
||||
{constants.DOWNLOAD_UPLOAD_LOGS}
|
||||
</SidebarButton>
|
||||
<>
|
||||
<SidebarButton
|
||||
onClick={confirmLogDownload}
|
||||
typographyVariant="caption"
|
||||
sx={{ fontWeight: 'normal', color: 'text.secondary' }}>
|
||||
{constants.DOWNLOAD_UPLOAD_LOGS}
|
||||
</SidebarButton>
|
||||
{appVersion && (
|
||||
<Typography p={2} color="text.secondary" variant="caption">
|
||||
{appVersion}
|
||||
</Typography>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import { AppContext } from 'pages/_app';
|
|||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { getDownloadAppMessage } from 'utils/ui';
|
||||
import { NoStyleAnchor } from 'components/pages/sharedAlbum/GoToEnte';
|
||||
import { openLink } from 'utils/common';
|
||||
|
||||
export default function HelpSection() {
|
||||
const [exportModalView, setExportModalView] = useState(false);
|
||||
|
@ -20,8 +21,7 @@ export default function HelpSection() {
|
|||
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
|
||||
getToken()
|
||||
)}`;
|
||||
const win = window.open(feedbackURL, '_blank');
|
||||
win.focus();
|
||||
openLink(feedbackURL, true);
|
||||
}
|
||||
|
||||
function exportFiles() {
|
||||
|
|
|
@ -2,10 +2,9 @@ import React, { useContext } from 'react';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { ARCHIVE_SECTION, TRASH_SECTION } from 'constants/collection';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import { CollectionSummaries } from 'types/collection';
|
||||
import ShortcutButton from './ShortcutButton';
|
||||
import { ArchiveOutlined, DeleteOutline } from '@mui/icons-material';
|
||||
interface Iprops {
|
||||
closeSidebar: () => void;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
|
@ -30,13 +29,13 @@ export default function ShortcutSection({
|
|||
return (
|
||||
<>
|
||||
<ShortcutButton
|
||||
startIcon={<DeleteIcon />}
|
||||
startIcon={<DeleteOutline />}
|
||||
label={constants.TRASH}
|
||||
count={collectionSummaries.get(TRASH_SECTION)?.fileCount}
|
||||
onClick={openTrashSection}
|
||||
/>
|
||||
<ShortcutButton
|
||||
startIcon={<VisibilityOffIcon />}
|
||||
startIcon={<ArchiveOutlined />}
|
||||
label={constants.ARCHIVE_SECTION_NAME}
|
||||
count={collectionSummaries.get(ARCHIVE_SECTION)?.fileCount}
|
||||
onClick={openArchiveSection}
|
||||
|
|
|
@ -4,7 +4,7 @@ import ShortcutSection from './ShortcutSection';
|
|||
import UtilitySection from './UtilitySection';
|
||||
import HelpSection from './HelpSection';
|
||||
import ExitSection from './ExitSection';
|
||||
import DebugLogs from './DebugLogs';
|
||||
import DebugSection from './DebugSection';
|
||||
import { DrawerSidebar } from './styledComponents';
|
||||
import HeaderSection from './Header';
|
||||
import { CollectionSummaries } from 'types/collection';
|
||||
|
@ -37,7 +37,7 @@ export default function Sidebar({
|
|||
<Divider />
|
||||
<ExitSection />
|
||||
<Divider />
|
||||
<DebugLogs />
|
||||
<DebugSection />
|
||||
</Stack>
|
||||
</DrawerSidebar>
|
||||
);
|
||||
|
|
|
@ -120,13 +120,15 @@ export default function SingleInputForm(props: SingleInputFormProps) {
|
|||
onClick={props.secondaryButtonAction}
|
||||
size="large"
|
||||
color="secondary"
|
||||
sx={{ mt: 2, mb: 4, mr: 1, ...buttonSx }}
|
||||
sx={{
|
||||
'&&&': { mt: 2, mb: 4, mr: 1, ...buttonSx },
|
||||
}}
|
||||
{...restSubmitButtonProps}>
|
||||
{constants.CANCEL}
|
||||
</Button>
|
||||
)}
|
||||
<SubmitButton
|
||||
sx={{ mt: 2, ...buttonSx }}
|
||||
sx={{ '&&&': { mt: 2, ...buttonSx } }}
|
||||
buttonText={props.buttonText}
|
||||
loading={loading}
|
||||
{...restSubmitButtonProps}
|
||||
|
|
|
@ -7,9 +7,9 @@ import { UploadProgressHeader } from './header';
|
|||
import { InProgressSection } from './inProgressSection';
|
||||
import { ResultSection } from './resultSection';
|
||||
import { NotUploadSectionHeader } from './styledComponents';
|
||||
import { getOSSpecificDesktopAppDownloadLink } from 'utils/common';
|
||||
import UploadProgressContext from 'contexts/uploadProgress';
|
||||
import { dialogCloseHandler } from 'components/DialogBox/TitleWithCloseButton';
|
||||
import { APP_DOWNLOAD_URL } from 'utils/common';
|
||||
|
||||
export function UploadProgressDialog() {
|
||||
const { open, onClose, uploadStage, finishedUploads } = useContext(
|
||||
|
@ -77,7 +77,7 @@ export function UploadProgressDialog() {
|
|||
uploadResult={UPLOAD_RESULT.BLOCKED}
|
||||
sectionTitle={constants.BLOCKED_UPLOADS}
|
||||
sectionInfo={constants.ETAGS_BLOCKED(
|
||||
getOSSpecificDesktopAppDownloadLink()
|
||||
APP_DOWNLOAD_URL
|
||||
)}
|
||||
/>
|
||||
<ResultSection
|
||||
|
|
|
@ -452,32 +452,28 @@ export default function Uploader(props: Props) {
|
|||
case CustomError.SUBSCRIPTION_EXPIRED:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.SUBSCRIPTION_EXPIRED,
|
||||
action: {
|
||||
text: constants.RENEW_NOW,
|
||||
callback: () =>
|
||||
billingService.redirectToCustomerPortal(),
|
||||
},
|
||||
subtext: constants.SUBSCRIPTION_EXPIRED,
|
||||
message: constants.RENEW_NOW,
|
||||
onClick: () => billingService.redirectToCustomerPortal(),
|
||||
};
|
||||
break;
|
||||
case CustomError.STORAGE_QUOTA_EXCEEDED:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.STORAGE_QUOTA_EXCEEDED,
|
||||
action: {
|
||||
text: constants.UPGRADE_NOW,
|
||||
callback: galleryContext.showPlanSelectorModal,
|
||||
},
|
||||
icon: <DiscFullIcon fontSize="large" />,
|
||||
subtext: constants.STORAGE_QUOTA_EXCEEDED,
|
||||
message: constants.UPGRADE_NOW,
|
||||
onClick: () => galleryContext.showPlanSelectorModal(),
|
||||
startIcon: <DiscFullIcon />,
|
||||
};
|
||||
break;
|
||||
default:
|
||||
notification = {
|
||||
variant: 'danger',
|
||||
message: constants.UNKNOWN_ERROR,
|
||||
onClick: () => null,
|
||||
};
|
||||
}
|
||||
galleryContext.setNotificationAttributes(notification);
|
||||
appContext.setNotificationAttributes(notification);
|
||||
}
|
||||
|
||||
const uploadToSingleNewCollection = (collectionName: string) => {
|
||||
|
|
|
@ -41,6 +41,7 @@ const LinkButton: FC<LinkProps<'button', { color?: ButtonProps['color'] }>> = ({
|
|||
sx={{
|
||||
color: 'text.primary',
|
||||
textDecoration: 'underline rgba(255, 255, 255, 0.4)',
|
||||
paddingBottom: 0.5,
|
||||
'&:hover': {
|
||||
color: `${color}.main`,
|
||||
textDecoration: `underline `,
|
||||
|
|
|
@ -251,7 +251,12 @@ export default function PreviewCard(props: IProps) {
|
|||
if (thumbs.has(file.id)) {
|
||||
const thumbImgSrc = thumbs.get(file.id);
|
||||
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 {
|
||||
main();
|
||||
}
|
||||
|
|
|
@ -18,8 +18,8 @@ import AddIcon from '@mui/icons-material/Add';
|
|||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import ClockIcon from '@mui/icons-material/AccessTime';
|
||||
import DownloadIcon from '@mui/icons-material/Download';
|
||||
import UnArchiveIcon from '@mui/icons-material/Visibility';
|
||||
import ArchiveIcon from '@mui/icons-material/VisibilityOff';
|
||||
import UnArchiveIcon from '@mui/icons-material/Unarchive';
|
||||
import ArchiveIcon from '@mui/icons-material/ArchiveOutlined';
|
||||
import MoveIcon from '@mui/icons-material/ArrowForward';
|
||||
import RemoveIcon from '@mui/icons-material/RemoveCircleOutline';
|
||||
import { getTrashFilesMessage } from 'utils/ui';
|
||||
|
|
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_FILE_NAME_LENGTH = 100;
|
||||
export const MAX_CAPTION_SIZE = 280;
|
||||
export const MAX_TRASH_BATCH_SIZE = 1000;
|
||||
|
||||
export const TYPE_HEIC = 'heic';
|
||||
|
|
|
@ -26,8 +26,25 @@ import {
|
|||
getRoadmapRedirectURL,
|
||||
} from 'services/userService';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { clearLogsIfLocalStorageLimitExceeded } from 'utils/logging';
|
||||
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')`
|
||||
background-color: #111;
|
||||
|
@ -53,6 +70,7 @@ type AppContextType = {
|
|||
finishLoading: () => void;
|
||||
closeMessageDialog: () => void;
|
||||
setDialogMessage: SetDialogBoxAttributes;
|
||||
setNotificationAttributes: SetNotificationAttributes;
|
||||
isFolderSyncRunning: boolean;
|
||||
setIsFolderSyncRunning: (isRunning: boolean) => void;
|
||||
watchFolderView: boolean;
|
||||
|
@ -98,6 +116,10 @@ export default function App({ Component, err }) {
|
|||
const [watchFolderView, setWatchFolderView] = useState(false);
|
||||
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
|
||||
const isMobile = useMediaQuery('(max-width:428px)');
|
||||
const [notificationView, setNotificationView] = useState(false);
|
||||
const closeNotification = () => setNotificationView(false);
|
||||
const [notificationAttributes, setNotificationAttributes] =
|
||||
useState<NotificationAttributes>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
@ -136,6 +158,33 @@ export default function App({ Component, err }) {
|
|||
}
|
||||
);
|
||||
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);
|
||||
|
@ -211,6 +260,8 @@ export default function App({ Component, err }) {
|
|||
|
||||
useEffect(() => setMessageDialogView(true), [dialogMessage]);
|
||||
|
||||
useEffect(() => setNotificationView(true), [notificationAttributes]);
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
const setDisappearingFlashMessage = (flashMessages: FlashMessage) => {
|
||||
setFlashMessage(flashMessages);
|
||||
|
@ -271,6 +322,11 @@ export default function App({ Component, err }) {
|
|||
onClose={closeMessageDialog}
|
||||
attributes={dialogMessage}
|
||||
/>
|
||||
<Notification
|
||||
open={notificationView}
|
||||
onClose={closeNotification}
|
||||
attributes={notificationAttributes}
|
||||
/>
|
||||
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
|
@ -291,6 +347,7 @@ export default function App({ Component, err }) {
|
|||
watchFolderFiles,
|
||||
setWatchFolderFiles,
|
||||
isMobile,
|
||||
setNotificationAttributes,
|
||||
}}>
|
||||
{loading ? (
|
||||
<VerticallyCentered>
|
||||
|
|
|
@ -24,6 +24,8 @@ import router from 'next/router';
|
|||
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
|
||||
import { styled } from '@mui/material';
|
||||
import { syncCollections } from 'services/collectionService';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import VerticallyCentered from 'components/Container';
|
||||
|
||||
export const DeduplicateContext = createContext<DeduplicateContextType>(
|
||||
DefaultDeduplicateContext
|
||||
|
@ -143,8 +145,14 @@ export default function Deduplicate() {
|
|||
setSelected({ count: 0, collectionID: 0 });
|
||||
};
|
||||
|
||||
if (!duplicateFiles) {
|
||||
return <></>;
|
||||
if (!duplicateFiles?.length) {
|
||||
return (
|
||||
<VerticallyCentered>
|
||||
<EnteSpinner>
|
||||
<span className="sr-only">Loading...</span>
|
||||
</EnteSpinner>
|
||||
</VerticallyCentered>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -27,7 +27,7 @@ import { checkSubscriptionPurchase } from 'utils/billing';
|
|||
|
||||
import FullScreenDropZone from 'components/FullScreenDropZone';
|
||||
import Sidebar from 'components/Sidebar';
|
||||
import { checkConnectivity, preloadImage } from 'utils/common';
|
||||
import { preloadImage } from 'utils/common';
|
||||
import {
|
||||
isFirstLogin,
|
||||
justSignedUp,
|
||||
|
@ -89,18 +89,17 @@ import { Collection, CollectionSummaries } from 'types/collection';
|
|||
import { EnteFile } from 'types/file';
|
||||
import { GalleryContextType, SelectedState } from 'types/gallery';
|
||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import Notification from 'components/Notification';
|
||||
import Collections from 'components/Collections';
|
||||
import { GalleryNavbar } from 'components/pages/gallery/Navbar';
|
||||
import { Search, SearchResultSummary, UpdateSearch } from 'types/search';
|
||||
import SearchResultInfo from 'components/Search/SearchResultInfo';
|
||||
import { NotificationAttributes } from 'types/Notification';
|
||||
import { ITEM_TYPE, TimeStampListItem } from 'components/PhotoList';
|
||||
import UploadInputs from 'components/UploadSelectorInputs';
|
||||
import useFileInput from 'hooks/useFileInput';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { CenteredFlex } from 'components/Container';
|
||||
import { checkConnectivity } from 'utils/error/ui';
|
||||
|
||||
export const DeadCenter = styled('div')`
|
||||
flex: 1;
|
||||
|
@ -117,7 +116,6 @@ const defaultGalleryContext: GalleryContextType = {
|
|||
showPlanSelectorModal: () => null,
|
||||
setActiveCollection: () => null,
|
||||
syncWithRemote: () => null,
|
||||
setNotificationAttributes: () => null,
|
||||
setBlockingLoad: () => null,
|
||||
photoListHeader: null,
|
||||
};
|
||||
|
@ -192,13 +190,6 @@ export default function Gallery() {
|
|||
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
|
||||
useState<FixCreationTimeAttributes>(null);
|
||||
|
||||
const [notificationView, setNotificationView] = useState(false);
|
||||
|
||||
const closeNotification = () => setNotificationView(false);
|
||||
|
||||
const [notificationAttributes, setNotificationAttributes] =
|
||||
useState<NotificationAttributes>(null);
|
||||
|
||||
const [archivedCollections, setArchivedCollections] =
|
||||
useState<Set<number>>();
|
||||
|
||||
|
@ -280,11 +271,6 @@ export default function Gallery() {
|
|||
[fixCreationTimeAttributes]
|
||||
);
|
||||
|
||||
useEffect(
|
||||
() => notificationAttributes && setNotificationView(true),
|
||||
[notificationAttributes]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof activeCollection === 'undefined') {
|
||||
return;
|
||||
|
@ -576,7 +562,6 @@ export default function Gallery() {
|
|||
showPlanSelectorModal,
|
||||
setActiveCollection,
|
||||
syncWithRemote,
|
||||
setNotificationAttributes,
|
||||
setBlockingLoad,
|
||||
photoListHeader: photoListHeader,
|
||||
}}>
|
||||
|
@ -604,11 +589,6 @@ export default function Gallery() {
|
|||
closeModal={() => setPlanModalView(false)}
|
||||
setLoading={setBlockingLoad}
|
||||
/>
|
||||
<Notification
|
||||
open={notificationView}
|
||||
onClose={closeNotification}
|
||||
attributes={notificationAttributes}
|
||||
/>
|
||||
<CollectionNamer
|
||||
show={collectionNamerView}
|
||||
onHide={setCollectionNamerView.bind(null, false)}
|
||||
|
@ -687,7 +667,6 @@ export default function Gallery() {
|
|||
sidebarView={sidebarView}
|
||||
closeSidebar={closeSidebar}
|
||||
/>
|
||||
|
||||
<PhotoFrame
|
||||
files={files}
|
||||
syncWithRemote={syncWithRemote}
|
||||
|
|
|
@ -43,6 +43,7 @@ import { EncryptionResult } from 'types/upload';
|
|||
import constants from 'utils/strings/constants';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
import { User } from 'types/user';
|
||||
import { getNonHiddenCollections } from 'utils/collection';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const COLLECTION_TABLE = 'collections';
|
||||
|
@ -141,7 +142,7 @@ const getCollections = async (
|
|||
export const getLocalCollections = async (): Promise<Collection[]> => {
|
||||
const collections: Collection[] =
|
||||
(await localForage.getItem(COLLECTION_TABLE)) ?? [];
|
||||
return collections;
|
||||
return getNonHiddenCollections(collections);
|
||||
};
|
||||
|
||||
export const getCollectionUpdationTime = async (): Promise<number> =>
|
||||
|
@ -186,7 +187,7 @@ export const syncCollections = async () => {
|
|||
|
||||
await localForage.setItem(COLLECTION_TABLE, collections);
|
||||
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
|
||||
return collections;
|
||||
return getNonHiddenCollections(collections);
|
||||
};
|
||||
|
||||
export const getCollection = async (
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ElectronAPIs } from 'types/electron';
|
|||
|
||||
class ElectronService {
|
||||
private electronAPIs: ElectronAPIs;
|
||||
private isBundledApp: boolean = false;
|
||||
|
||||
constructor() {
|
||||
this.electronAPIs = globalThis['ElectronAPIs'];
|
||||
|
@ -24,6 +23,17 @@ class ElectronService {
|
|||
this.electronAPIs.openLogDirectory();
|
||||
}
|
||||
}
|
||||
|
||||
getSentryUserID() {
|
||||
if (this.electronAPIs?.getSentryUserID) {
|
||||
return this.electronAPIs.getSentryUserID();
|
||||
}
|
||||
}
|
||||
getAppVersion() {
|
||||
if (this.electronAPIs?.getAppVersion) {
|
||||
return this.electronAPIs.getAppVersion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ElectronService();
|
||||
|
|
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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
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();
|
|
@ -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 QueueProcessor from 'services/queueProcessor';
|
||||
import { ParsedExtractedMetadata } from 'types/upload';
|
||||
import ffmpegFactory from './ffmpegFactory';
|
||||
|
||||
import { FFmpegWorker } from 'utils/comlink';
|
||||
import { promiseWithTimeout } from 'utils/common';
|
||||
|
||||
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 {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||
// ignore
|
||||
return null;
|
||||
} else {
|
||||
logError(e, 'ffmpeg thumbnail generation failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async extractMetadata(file: File): Promise<ParsedExtractedMetadata> {
|
||||
if (!this.ffmpegWorker) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
const response = this.ffmpegTaskQueue.queueUpRequest(() =>
|
||||
promiseWithTimeout(
|
||||
this.ffmpegWorker.extractVideoMetadata(file),
|
||||
FFMPEG_EXECUTION_WAIT_TIME
|
||||
)
|
||||
);
|
||||
try {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||
// ignore
|
||||
return null;
|
||||
} else {
|
||||
logError(e, 'ffmpeg metadata extraction failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async convertToMP4(
|
||||
file: Uint8Array,
|
||||
fileName: string
|
||||
): Promise<Uint8Array> {
|
||||
if (!this.ffmpegWorker) {
|
||||
await this.init();
|
||||
}
|
||||
|
||||
const response = this.ffmpegTaskQueue.queueUpRequest(
|
||||
async () => await this.ffmpegWorker.convertToMP4(file, fileName)
|
||||
);
|
||||
|
||||
try {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
if (e.message === CustomError.REQUEST_CANCELLED) {
|
||||
// ignore
|
||||
return null;
|
||||
} else {
|
||||
logError(e, 'ffmpeg MP4 conversion failed');
|
||||
throw e;
|
||||
export async function generateVideoThumbnail(
|
||||
file: File | ElectronFile
|
||||
): Promise<File | ElectronFile> {
|
||||
try {
|
||||
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) {
|
||||
if (seekTime <= 0) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
seekTime = Number((seekTime / 10).toFixed(3));
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'ffmpeg generateVideoThumbnail failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export default new FFmpegService();
|
||||
export async function extractVideoMetadata(file: File | ElectronFile) {
|
||||
try {
|
||||
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||
// https://stackoverflow.com/questions/9464617/retrieving-and-saving-media-metadata-using-ffmpeg
|
||||
// -c [short for codex] copy[(stream_specifier)[ffmpeg.org/ffmpeg.html#Stream-specifiers]] => copies all the stream without re-encoding
|
||||
// -map_metadata [http://ffmpeg.org/ffmpeg.html#Advanced-options search for map_metadata] => copies all stream metadata to the out
|
||||
// -f ffmetadata [https://ffmpeg.org/ffmpeg-formats.html#Metadata-1] => dump metadata from media files into a simple UTF-8-encoded INI-like text file
|
||||
const metadata = await ffmpegClient.run(
|
||||
[
|
||||
FFMPEG_PLACEHOLDER,
|
||||
'-i',
|
||||
INPUT_PATH_PLACEHOLDER,
|
||||
'-c',
|
||||
'copy',
|
||||
'-map_metadata',
|
||||
'0',
|
||||
'-f',
|
||||
'ffmetadata',
|
||||
OUTPUT_PATH_PLACEHOLDER,
|
||||
],
|
||||
file,
|
||||
`metadata.txt`
|
||||
);
|
||||
return parseFFmpegExtractedMetadata(
|
||||
new Uint8Array(await metadata.arrayBuffer())
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'ffmpeg extractVideoMetadata failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertToMP4(file: File | ElectronFile) {
|
||||
try {
|
||||
const ffmpegClient = await ffmpegFactory.getFFmpegClient();
|
||||
return await ffmpegClient.run(
|
||||
[
|
||||
FFMPEG_PLACEHOLDER,
|
||||
'-i',
|
||||
INPUT_PATH_PLACEHOLDER,
|
||||
'-preset',
|
||||
'ultrafast',
|
||||
OUTPUT_PATH_PLACEHOLDER,
|
||||
],
|
||||
file,
|
||||
'output.mp4'
|
||||
);
|
||||
} catch (e) {
|
||||
logError(e, 'ffmpeg convertToMP4 failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,8 @@ import { SetFiles } from 'types/gallery';
|
|||
import { MAX_TRASH_BATCH_SIZE } from 'constants/file';
|
||||
import { BulkUpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { isCollectionHidden } from 'utils/collection';
|
||||
import { CustomError } from 'utils/error';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const FILES_TABLE = 'files';
|
||||
|
@ -63,6 +65,9 @@ export const syncFiles = async (
|
|||
if (!getToken()) {
|
||||
continue;
|
||||
}
|
||||
if (isCollectionHidden(collection)) {
|
||||
throw Error(CustomError.HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED);
|
||||
}
|
||||
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||
if (collection.updationTime === lastSyncTime) {
|
||||
continue;
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FILE_TYPE } from 'constants/file';
|
|||
import { CustomError, errorWithContext } from 'utils/error';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { BLACK_THUMBNAIL_BASE64 } from 'constants/upload';
|
||||
import FFmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import * as FFmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import { convertBytesToHumanReadable } from 'utils/file/size';
|
||||
import { isExactTypeHEIC } from 'utils/file';
|
||||
import { ElectronFile, FileTypeInfo } from 'types/upload';
|
||||
|
@ -33,9 +33,6 @@ export async function generateThumbnail(
|
|||
let canvas = document.createElement('canvas');
|
||||
let thumbnail: Uint8Array;
|
||||
try {
|
||||
if (!(file instanceof File)) {
|
||||
file = new File([await file.blob()], file.name);
|
||||
}
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||
const isHEIC = isExactTypeHEIC(fileTypeInfo.exactType);
|
||||
canvas = await generateImageThumbnail(file, isHEIC);
|
||||
|
@ -47,17 +44,14 @@ export async function generateThumbnail(
|
|||
)}`
|
||||
);
|
||||
|
||||
const thumb = await FFmpegService.generateThumbnail(file);
|
||||
const thumbFile =
|
||||
await FFmpegService.generateVideoThumbnail(file);
|
||||
addLogLine(
|
||||
`ffmpeg thumbnail successfully generated ${getFileNameSize(
|
||||
file
|
||||
)}`
|
||||
);
|
||||
const dummyImageFile = new File([thumb], file.name);
|
||||
canvas = await generateImageThumbnail(
|
||||
dummyImageFile,
|
||||
false
|
||||
);
|
||||
canvas = await generateImageThumbnail(thumbFile, false);
|
||||
} catch (e) {
|
||||
addLogLine(
|
||||
`ffmpeg thumbnail generated failed ${getFileNameSize(
|
||||
|
@ -99,7 +93,10 @@ export async function generateThumbnail(
|
|||
}
|
||||
}
|
||||
|
||||
export async function generateImageThumbnail(file: File, isHEIC: boolean) {
|
||||
export async function generateImageThumbnail(
|
||||
file: File | ElectronFile,
|
||||
isHEIC: boolean
|
||||
) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvasCTX = canvas.getContext('2d');
|
||||
|
||||
|
@ -108,13 +105,16 @@ export async function generateImageThumbnail(file: File, isHEIC: boolean) {
|
|||
|
||||
if (isHEIC) {
|
||||
addLogLine(`HEICConverter called for ${getFileNameSize(file)}`);
|
||||
file = new File([await HeicConversionService.convert(file)], file.name);
|
||||
const convertedBlob = await HeicConversionService.convert(
|
||||
new Blob([await file.arrayBuffer()])
|
||||
);
|
||||
file = new File([convertedBlob], file.name);
|
||||
addLogLine(`${getFileNameSize(file)} successfully converted`);
|
||||
}
|
||||
let image = new Image();
|
||||
imageURL = URL.createObjectURL(file);
|
||||
image.setAttribute('src', imageURL);
|
||||
imageURL = URL.createObjectURL(new Blob([await file.arrayBuffer()]));
|
||||
await new Promise((resolve, reject) => {
|
||||
image.setAttribute('src', imageURL);
|
||||
image.onload = () => {
|
||||
try {
|
||||
URL.revokeObjectURL(imageURL);
|
||||
|
@ -154,16 +154,18 @@ export async function generateImageThumbnail(file: File, isHEIC: boolean) {
|
|||
return canvas;
|
||||
}
|
||||
|
||||
export async function generateVideoThumbnail(file: File) {
|
||||
export async function generateVideoThumbnail(file: File | ElectronFile) {
|
||||
const canvas = document.createElement('canvas');
|
||||
const canvasCTX = canvas.getContext('2d');
|
||||
|
||||
let videoURL = null;
|
||||
let timeout = null;
|
||||
let videoURL = null;
|
||||
|
||||
let video = document.createElement('video');
|
||||
videoURL = URL.createObjectURL(new Blob([await file.arrayBuffer()]));
|
||||
await new Promise((resolve, reject) => {
|
||||
let video = document.createElement('video');
|
||||
videoURL = URL.createObjectURL(file);
|
||||
video.preload = 'metadata';
|
||||
video.src = videoURL;
|
||||
video.addEventListener('loadeddata', function () {
|
||||
try {
|
||||
URL.revokeObjectURL(videoURL);
|
||||
|
@ -198,8 +200,6 @@ export async function generateVideoThumbnail(file: File) {
|
|||
reject(err);
|
||||
}
|
||||
});
|
||||
video.preload = 'metadata';
|
||||
video.src = videoURL;
|
||||
timeout = setTimeout(
|
||||
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
|
||||
WAIT_TIME_THUMBNAIL_GENERATION
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { NULL_EXTRACTED_METADATA } from 'constants/upload';
|
||||
import ffmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import * as ffmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import { ElectronFile } from 'types/upload';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getFileNameSize, addLogLine } from 'utils/logging';
|
||||
|
@ -8,16 +8,7 @@ export async function getVideoMetadata(file: File | ElectronFile) {
|
|||
let videoMetadata = NULL_EXTRACTED_METADATA;
|
||||
try {
|
||||
addLogLine(`getVideoMetadata called for ${getFileNameSize(file)}`);
|
||||
if (!(file instanceof File)) {
|
||||
addLogLine('get file blob for video metadata extraction');
|
||||
file = new File([await file.blob()], file.name, {
|
||||
lastModified: file.lastModified,
|
||||
});
|
||||
addLogLine(
|
||||
'get file blob for video metadata extraction successfully'
|
||||
);
|
||||
}
|
||||
videoMetadata = await ffmpegService.extractMetadata(file);
|
||||
videoMetadata = await ffmpegService.extractVideoMetadata(file);
|
||||
addLogLine(
|
||||
`videoMetadata successfully extracted ${getFileNameSize(file)}`
|
||||
);
|
||||
|
|
97
src/services/wasm/ffmpeg.ts
Normal file
97
src/services/wasm/ffmpeg.ts
Normal file
|
@ -0,0 +1,97 @@
|
|||
import { createFFmpeg, FFmpeg } from 'ffmpeg-wasm';
|
||||
import QueueProcessor from 'services/queueProcessor';
|
||||
import { getUint8ArrayView } from 'services/readerService';
|
||||
import { promiseWithTimeout } from 'utils/common';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { generateTempName } from 'utils/temp';
|
||||
|
||||
const INPUT_PATH_PLACEHOLDER = 'INPUT';
|
||||
const FFMPEG_PLACEHOLDER = 'FFMPEG';
|
||||
const OUTPUT_PATH_PLACEHOLDER = 'OUTPUT';
|
||||
|
||||
const FFMPEG_EXECUTION_WAIT_TIME = 30 * 1000;
|
||||
|
||||
export class WasmFFmpeg {
|
||||
private ffmpeg: FFmpeg;
|
||||
private ready: Promise<void> = null;
|
||||
private ffmpegTaskQueue = new QueueProcessor<File>(1);
|
||||
|
||||
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 run(cmd: string[], inputFile: File, outputFileName: string) {
|
||||
const response = this.ffmpegTaskQueue.queueUpRequest(() =>
|
||||
promiseWithTimeout<File>(
|
||||
this.execute(cmd, inputFile, outputFileName),
|
||||
FFMPEG_EXECUTION_WAIT_TIME
|
||||
)
|
||||
);
|
||||
try {
|
||||
return await response.promise;
|
||||
} catch (e) {
|
||||
logError(e, 'ffmpeg run failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private async execute(
|
||||
cmd: string[],
|
||||
inputFile: File,
|
||||
outputFileName: string
|
||||
) {
|
||||
let tempInputFilePath: string;
|
||||
let tempOutputFilePath: string;
|
||||
try {
|
||||
await this.ready;
|
||||
tempInputFilePath = `${generateTempName(10)}- ${inputFile.name}`;
|
||||
this.ffmpeg.FS(
|
||||
'writeFile',
|
||||
tempInputFilePath,
|
||||
await getUint8ArrayView(inputFile)
|
||||
);
|
||||
tempOutputFilePath = `${generateTempName(10)}-${outputFileName}`;
|
||||
|
||||
cmd = cmd.map((cmdPart) => {
|
||||
if (cmdPart === FFMPEG_PLACEHOLDER) {
|
||||
return '';
|
||||
} else if (cmdPart === INPUT_PATH_PLACEHOLDER) {
|
||||
return tempInputFilePath;
|
||||
} else if (cmdPart === OUTPUT_PATH_PLACEHOLDER) {
|
||||
return tempOutputFilePath;
|
||||
} else {
|
||||
return cmdPart;
|
||||
}
|
||||
});
|
||||
addLogLine(`${cmd}`);
|
||||
await this.ffmpeg.run(...cmd);
|
||||
return new File(
|
||||
[this.ffmpeg.FS('readFile', tempOutputFilePath)],
|
||||
outputFileName
|
||||
);
|
||||
} finally {
|
||||
try {
|
||||
this.ffmpeg.FS('unlink', tempInputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, 'unlink input file failed');
|
||||
}
|
||||
try {
|
||||
this.ffmpeg.FS('unlink', tempOutputFilePath);
|
||||
} catch (e) {
|
||||
logError(e, 'unlink output file failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -121,6 +121,18 @@ html, body {
|
|||
}
|
||||
|
||||
|
||||
.pswp-custom-caption-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
bottom: 56px;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.pswp__caption--empty{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
|
|
|
@ -9,10 +9,22 @@ declare module '@mui/material/styles' {
|
|||
interface TypeBackground {
|
||||
overPaper?: string;
|
||||
}
|
||||
|
||||
interface BlurStrength {
|
||||
base: string;
|
||||
muted: string;
|
||||
faint: string;
|
||||
}
|
||||
interface BlurStrengthOptions {
|
||||
base?: string;
|
||||
muted?: string;
|
||||
faint?: string;
|
||||
}
|
||||
interface Palette {
|
||||
accent: PaletteColor;
|
||||
fill: PaletteColor;
|
||||
glass: PaletteColor;
|
||||
backdrop: PaletteColor;
|
||||
blur: BlurStrength;
|
||||
danger: PaletteColor;
|
||||
stroke: TypeText;
|
||||
}
|
||||
|
@ -20,7 +32,8 @@ declare module '@mui/material/styles' {
|
|||
accent?: PaletteColorOptions;
|
||||
danger?: PaletteColorOptions;
|
||||
fill?: PaletteColorOptions;
|
||||
glass?: PaletteColorOptions;
|
||||
backdrop?: PaletteColorOptions;
|
||||
blur?: BlurStrengthOptions;
|
||||
stroke?: Partial<TypeText>;
|
||||
}
|
||||
|
||||
|
@ -74,6 +87,12 @@ declare module '@mui/material/Alert' {
|
|||
}
|
||||
}
|
||||
|
||||
declare module '@mui/material/CircularProgress' {
|
||||
export interface CircularProgressPropsColorOverrides {
|
||||
accent: true;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a theme instance.
|
||||
const darkThemeOptions = createTheme({
|
||||
components: {
|
||||
|
@ -243,6 +262,13 @@ const darkThemeOptions = createTheme({
|
|||
},
|
||||
},
|
||||
},
|
||||
MuiSnackbar: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
borderRadius: '8px',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
palette: {
|
||||
|
@ -265,11 +291,15 @@ const darkThemeOptions = createTheme({
|
|||
dark: 'rgba(256, 256, 256, 0.12)',
|
||||
light: 'rgba(256, 256, 256)',
|
||||
},
|
||||
glass: {
|
||||
main: 'rgba(256, 256, 256, 0.7)',
|
||||
dark: 'rgba(256, 256, 256, 0.9)',
|
||||
light: 'rgba(256, 256, 256,0.3)',
|
||||
contrastText: '#000',
|
||||
backdrop: {
|
||||
main: 'rgba(256, 256, 256, 0.65)',
|
||||
light: 'rgba(0, 0, 0,0.2)',
|
||||
},
|
||||
|
||||
blur: {
|
||||
base: '96px',
|
||||
muted: '48px',
|
||||
faint: '24px',
|
||||
},
|
||||
text: {
|
||||
primary: '#fff',
|
||||
|
@ -304,7 +334,7 @@ const darkThemeOptions = createTheme({
|
|||
typography: {
|
||||
body1: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '19px',
|
||||
lineHeight: '20px',
|
||||
},
|
||||
body2: {
|
||||
fontSize: '14px',
|
||||
|
@ -316,7 +346,7 @@ const darkThemeOptions = createTheme({
|
|||
},
|
||||
button: {
|
||||
fontSize: '16px',
|
||||
lineHeight: '19px',
|
||||
lineHeight: '20px',
|
||||
fontWeight: 'bold',
|
||||
textTransform: 'none',
|
||||
},
|
||||
|
|
|
@ -2,11 +2,14 @@ import { ButtonProps } from '@mui/material/Button';
|
|||
import { ReactNode } from 'react';
|
||||
|
||||
export interface NotificationAttributes {
|
||||
icon?: ReactNode;
|
||||
startIcon?: ReactNode;
|
||||
variant: ButtonProps['color'];
|
||||
message: JSX.Element | string;
|
||||
action?: {
|
||||
text: string;
|
||||
callback: () => void;
|
||||
};
|
||||
subtext?: JSX.Element | string;
|
||||
onClick: () => void;
|
||||
endIcon?: ReactNode;
|
||||
}
|
||||
|
||||
export type SetNotificationAttributes = React.Dispatch<
|
||||
React.SetStateAction<NotificationAttributes>
|
||||
>;
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
import { User } from 'types/user';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CollectionSummaryType, CollectionType } from 'constants/collection';
|
||||
import { MagicMetadataCore, VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import {
|
||||
MagicMetadataCore,
|
||||
SUB_TYPE,
|
||||
VISIBILITY_STATE,
|
||||
} from 'types/magicMetadata';
|
||||
|
||||
export interface Collection {
|
||||
id: number;
|
||||
|
@ -82,6 +86,7 @@ export interface RemoveFromCollectionRequest {
|
|||
|
||||
export interface CollectionMagicMetadataProps {
|
||||
visibility?: VISIBILITY_STATE;
|
||||
subType?: SUB_TYPE;
|
||||
}
|
||||
|
||||
export interface CollectionMagicMetadata
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { ButtonProps } from '@mui/material';
|
||||
|
||||
export interface DialogBoxAttributes {
|
||||
icon?: React.ReactNode;
|
||||
title?: string;
|
||||
staticBackdrop?: boolean;
|
||||
nonClosable?: boolean;
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { ElectronFile } from 'types/upload';
|
||||
import { WatchMapping } from 'types/watchFolder';
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
autoUpdatable: boolean;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export interface ElectronAPIs {
|
||||
exists: (path: string) => boolean;
|
||||
checkExistsAndCreateCollectionDir: (dirPath: string) => Promise<void>;
|
||||
|
@ -65,4 +70,16 @@ export interface ElectronAPIs {
|
|||
logToDisk: (msg: string) => void;
|
||||
convertHEIC(fileData: Uint8Array): Promise<Uint8Array>;
|
||||
openLogDirectory: () => void;
|
||||
registerUpdateEventListener: (
|
||||
showUpdateDialog: (updateInfo: AppUpdateInfo) => void
|
||||
) => void;
|
||||
updateAndRestart: () => void;
|
||||
skipAppVersion: (version: string) => void;
|
||||
getSentryUserID: () => Promise<string>;
|
||||
getAppVersion: () => Promise<string>;
|
||||
runFFmpegCmd: (
|
||||
cmd: string[],
|
||||
inputFile: File | ElectronFile,
|
||||
outputFileName: string
|
||||
) => Promise<File>;
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export interface FileMagicMetadata extends Omit<MagicMetadataCore, 'data'> {
|
|||
export interface FilePublicMagicMetadataProps {
|
||||
editedTime?: number;
|
||||
editedName?: string;
|
||||
caption?: string;
|
||||
}
|
||||
|
||||
export interface FilePublicMagicMetadata
|
||||
|
@ -43,6 +44,7 @@ export interface EnteFile {
|
|||
html: string;
|
||||
w: number;
|
||||
h: number;
|
||||
title: string;
|
||||
isDeleted: boolean;
|
||||
isTrashed?: boolean;
|
||||
deleteBy?: number;
|
||||
|
|
|
@ -2,7 +2,6 @@ import { CollectionSelectorAttributes } from 'components/Collections/CollectionS
|
|||
import { TimeStampListItem } from 'components/PhotoList';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { NotificationAttributes } from 'types/Notification';
|
||||
|
||||
export type SelectedState = {
|
||||
[k: number]: boolean;
|
||||
|
@ -22,7 +21,6 @@ export type GalleryContextType = {
|
|||
showPlanSelectorModal: () => void;
|
||||
setActiveCollection: (collection: number) => void;
|
||||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
setNotificationAttributes: (attributes: NotificationAttributes) => void;
|
||||
setBlockingLoad: (value: boolean) => void;
|
||||
photoListHeader: TimeStampListItem;
|
||||
};
|
||||
|
|
|
@ -11,8 +11,13 @@ export interface EncryptedMagicMetadataCore
|
|||
}
|
||||
|
||||
export enum VISIBILITY_STATE {
|
||||
VISIBLE,
|
||||
ARCHIVED,
|
||||
VISIBLE = 0,
|
||||
ARCHIVED = 1,
|
||||
HIDDEN = 2,
|
||||
}
|
||||
|
||||
export enum SUB_TYPE {
|
||||
DEFAULT_HIDDEN = 1,
|
||||
}
|
||||
|
||||
export const NEW_FILE_MAGIC_METADATA: MagicMetadataCore = {
|
||||
|
|
|
@ -30,6 +30,7 @@ import { getAlbumSiteHost } from 'constants/pages';
|
|||
import { getUnixTimeInMicroSecondsWithDelta } from 'utils/time';
|
||||
import {
|
||||
NEW_COLLECTION_MAGIC_METADATA,
|
||||
SUB_TYPE,
|
||||
VISIBILITY_STATE,
|
||||
} from 'types/magicMetadata';
|
||||
import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata';
|
||||
|
@ -227,3 +228,11 @@ export const getUserOwnedCollections = (collections: Collection[]) => {
|
|||
}
|
||||
return collections.filter((collection) => collection.owner.id === user.id);
|
||||
};
|
||||
|
||||
export const getNonHiddenCollections = (collections: Collection[]) => {
|
||||
return collections.filter((collection) => !isCollectionHidden(collection));
|
||||
};
|
||||
|
||||
export const isCollectionHidden = (collection: Collection) =>
|
||||
collection.magicMetadata?.data.visibility === VISIBILITY_STATE.HIDDEN ||
|
||||
collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN;
|
||||
|
|
|
@ -1,18 +1,6 @@
|
|||
import constants from 'utils/strings/constants';
|
||||
import { CustomError } from 'utils/error';
|
||||
import GetDeviceOS, { OS } from './deviceDetection';
|
||||
|
||||
const DESKTOP_APP_GITHUB_DOWNLOAD_URL =
|
||||
'https://github.com/ente-io/bhari-frame/releases/latest';
|
||||
|
||||
const APP_DOWNLOAD_ENTE_URL_PREFIX = 'https://ente.io/download';
|
||||
|
||||
export function checkConnectivity() {
|
||||
if (navigator.onLine) {
|
||||
return true;
|
||||
}
|
||||
throw new Error(constants.NO_INTERNET_CONNECTION);
|
||||
}
|
||||
export const APP_DOWNLOAD_URL = 'https://ente.io/download/desktop';
|
||||
|
||||
export function runningInBrowser() {
|
||||
return typeof window !== 'undefined';
|
||||
|
@ -24,22 +12,8 @@ export async function sleep(time: number) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getOSSpecificDesktopAppDownloadLink() {
|
||||
const os = GetDeviceOS();
|
||||
let url = '';
|
||||
if (os === OS.WINDOWS) {
|
||||
url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/exe`;
|
||||
} else if (os === OS.MAC) {
|
||||
url = `${APP_DOWNLOAD_ENTE_URL_PREFIX}/dmg`;
|
||||
} else {
|
||||
url = DESKTOP_APP_GITHUB_DOWNLOAD_URL;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
export function downloadApp() {
|
||||
const link = getOSSpecificDesktopAppDownloadLink();
|
||||
const win = window.open(link, '_blank');
|
||||
win.focus();
|
||||
openLink(APP_DOWNLOAD_URL, true);
|
||||
}
|
||||
|
||||
export function reverseString(title: string) {
|
||||
|
@ -54,12 +28,12 @@ export function initiateEmail(email: string) {
|
|||
a.rel = 'noreferrer noopener';
|
||||
a.click();
|
||||
}
|
||||
export const promiseWithTimeout = async (
|
||||
request: Promise<any>,
|
||||
export const promiseWithTimeout = async <T>(
|
||||
request: Promise<T>,
|
||||
timeout: number
|
||||
) => {
|
||||
): Promise<T> => {
|
||||
const timeoutRef = { current: null };
|
||||
const rejectOnTimeout = new Promise((_, reject) => {
|
||||
const rejectOnTimeout = new Promise<null>((_, reject) => {
|
||||
timeoutRef.current = setTimeout(
|
||||
() => reject(Error(CustomError.WAIT_TIME_EXCEEDED)),
|
||||
timeout
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import constants from 'utils/strings/constants';
|
||||
|
||||
export const ServerErrorCodes = {
|
||||
SESSION_EXPIRED: '401',
|
||||
NO_ACTIVE_SUBSCRIPTION: '402',
|
||||
|
@ -14,7 +12,6 @@ export const ServerErrorCodes = {
|
|||
};
|
||||
|
||||
export enum CustomError {
|
||||
UNKNOWN_ERROR = 'unknown error',
|
||||
SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed',
|
||||
THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed',
|
||||
VIDEO_PLAYBACK_FAILED = 'video playback failed',
|
||||
|
@ -47,6 +44,8 @@ export enum CustomError {
|
|||
INCORRECT_PASSWORD = 'incorrect password',
|
||||
UPLOAD_CANCELLED = 'upload cancelled',
|
||||
REQUEST_TIMEOUT = 'request taking too long',
|
||||
HIDDEN_COLLECTION_SYNC_FILE_ATTEMPTED = 'hidden collection sync file attempted',
|
||||
UNKNOWN_ERROR = 'Something went wrong, please try again',
|
||||
}
|
||||
|
||||
function parseUploadErrorCodes(error) {
|
||||
|
@ -67,7 +66,7 @@ function parseUploadErrorCodes(error) {
|
|||
parsedMessage = CustomError.FILE_TOO_LARGE;
|
||||
break;
|
||||
default:
|
||||
parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`;
|
||||
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`;
|
||||
}
|
||||
} else {
|
||||
parsedMessage = error.message;
|
||||
|
@ -120,29 +119,10 @@ export const parseSharingErrorCodes = (error) => {
|
|||
parsedMessage = CustomError.TOO_MANY_REQUESTS;
|
||||
break;
|
||||
default:
|
||||
parsedMessage = `${constants.UNKNOWN_ERROR} statusCode:${errorCode}`;
|
||||
parsedMessage = `${CustomError.UNKNOWN_ERROR} statusCode:${errorCode}`;
|
||||
}
|
||||
} else {
|
||||
parsedMessage = error.message;
|
||||
}
|
||||
return new Error(parsedMessage);
|
||||
};
|
||||
|
||||
export const handleSharingErrors = (error) => {
|
||||
const parsedError = parseSharingErrorCodes(error);
|
||||
let errorMessage = '';
|
||||
switch (parsedError.message) {
|
||||
case CustomError.BAD_REQUEST:
|
||||
errorMessage = constants.SHARING_BAD_REQUEST_ERROR;
|
||||
break;
|
||||
case CustomError.SUBSCRIPTION_NEEDED:
|
||||
errorMessage = constants.SHARING_DISABLED_FOR_FREE_ACCOUNTS;
|
||||
break;
|
||||
case CustomError.NOT_FOUND:
|
||||
errorMessage = constants.USER_DOES_NOT_EXIST;
|
||||
break;
|
||||
default:
|
||||
errorMessage = parsedError.message;
|
||||
}
|
||||
return errorMessage;
|
||||
};
|
||||
|
|
28
src/utils/error/ui.ts
Normal file
28
src/utils/error/ui.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import constants from 'utils/strings/constants';
|
||||
import { parseSharingErrorCodes, CustomError } from '.';
|
||||
|
||||
export const handleSharingErrors = (error) => {
|
||||
const parsedError = parseSharingErrorCodes(error);
|
||||
let errorMessage = '';
|
||||
switch (parsedError.message) {
|
||||
case CustomError.BAD_REQUEST:
|
||||
errorMessage = constants.SHARING_BAD_REQUEST_ERROR;
|
||||
break;
|
||||
case CustomError.SUBSCRIPTION_NEEDED:
|
||||
errorMessage = constants.SHARING_DISABLED_FOR_FREE_ACCOUNTS;
|
||||
break;
|
||||
case CustomError.NOT_FOUND:
|
||||
errorMessage = constants.USER_DOES_NOT_EXIST;
|
||||
break;
|
||||
default:
|
||||
errorMessage = parsedError.message;
|
||||
}
|
||||
return errorMessage;
|
||||
};
|
||||
|
||||
export function checkConnectivity() {
|
||||
if (navigator.onLine) {
|
||||
return true;
|
||||
}
|
||||
throw new Error(constants.NO_INTERNET_CONNECTION);
|
||||
}
|
|
@ -22,7 +22,7 @@ import {
|
|||
} from 'constants/file';
|
||||
import PublicCollectionDownloadManager from 'services/publicCollectionDownloadManager';
|
||||
import heicConversionService from 'services/heicConversionService';
|
||||
import ffmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import * as ffmpegService from 'services/ffmpeg/ffmpegService';
|
||||
import { NEW_FILE_MAGIC_METADATA, VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import { IsArchived, updateMagicMetadataProps } from 'utils/magicMetadata';
|
||||
|
||||
|
@ -319,10 +319,9 @@ async function getRenderableLivePhoto(
|
|||
|
||||
async function getPlayableVideo(videoNameTitle: string, video: Uint8Array) {
|
||||
const mp4ConvertedVideo = await ffmpegService.convertToMP4(
|
||||
video,
|
||||
videoNameTitle
|
||||
new File([video], videoNameTitle)
|
||||
);
|
||||
return new Blob([mp4ConvertedVideo]);
|
||||
return new Blob([await mp4ConvertedVideo.arrayBuffer()]);
|
||||
}
|
||||
|
||||
async function getRenderableImage(fileName: string, imageBlob: Blob) {
|
||||
|
@ -408,6 +407,19 @@ export async function changeFileName(file: EnteFile, editedName: string) {
|
|||
return file;
|
||||
}
|
||||
|
||||
export async function changeCaption(file: EnteFile, caption: string) {
|
||||
const updatedPublicMagicMetadataProps: FilePublicMagicMetadataProps = {
|
||||
caption,
|
||||
};
|
||||
|
||||
file.pubMagicMetadata = await updateMagicMetadataProps(
|
||||
file.pubMagicMetadata ?? NEW_FILE_MAGIC_METADATA,
|
||||
file.key,
|
||||
updatedPublicMagicMetadataProps
|
||||
);
|
||||
return file;
|
||||
}
|
||||
|
||||
export function isSharedFile(user: User, file: EnteFile) {
|
||||
if (!user?.id || !file?.ownerID) {
|
||||
return false;
|
||||
|
|
|
@ -34,6 +34,10 @@ export function addLogLine(log: string) {
|
|||
});
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.name === 'QuotaExceededError') {
|
||||
deleteLogs();
|
||||
addLogLine('logs cleared');
|
||||
}
|
||||
logError(e, 'failed to addLogLine', undefined, true);
|
||||
// ignore
|
||||
}
|
||||
|
@ -65,7 +69,6 @@ export const clearLogsIfLocalStorageLimitExceeded = () => {
|
|||
addLogLine(`app started`);
|
||||
} catch (e) {
|
||||
deleteLogs();
|
||||
logError(e, 'failed to log test log');
|
||||
}
|
||||
}
|
||||
addLogLine(`logs size: ${convertBytesToHumanReadable(logSize)}`);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { isDEVSentryENV } from 'constants/sentry';
|
|||
import { addLogLine } from 'utils/logging';
|
||||
import { getSentryUserID } from 'utils/user';
|
||||
|
||||
export const logError = (
|
||||
export const logError = async (
|
||||
error: any,
|
||||
msg: string,
|
||||
info?: Record<string, unknown>,
|
||||
|
@ -25,7 +25,7 @@ export const logError = (
|
|||
}
|
||||
Sentry.captureException(err, {
|
||||
level: Sentry.Severity.Info,
|
||||
user: { id: getSentryUserID() },
|
||||
user: { id: await getSentryUserID() },
|
||||
contexts: {
|
||||
...(info && {
|
||||
info: info,
|
||||
|
|
|
@ -373,7 +373,6 @@ const englishConstants = {
|
|||
<p>All files will be queued for download sequentially</p>
|
||||
</>
|
||||
),
|
||||
ARCHIVED_ALBUM: 'Archived album',
|
||||
DOWNLOAD_COLLECTION_FAILED: 'Album downloading failed, please try again',
|
||||
CREATE_ALBUM_FAILED: 'Failed to create album , please try again',
|
||||
|
||||
|
@ -436,6 +435,8 @@ const englishConstants = {
|
|||
INFO: 'Info',
|
||||
FILE_ID: 'File ID',
|
||||
FILE_NAME: 'File name',
|
||||
CAPTION: 'Description',
|
||||
CAPTION_PLACEHOLDER: 'Add a description',
|
||||
CREATION_TIME: 'Creation time',
|
||||
UPDATED_ON: 'Updated on',
|
||||
LOCATION: 'Location',
|
||||
|
@ -511,7 +512,7 @@ const englishConstants = {
|
|||
EMAIl_ALREADY_OWNED: 'Email already taken',
|
||||
EMAIL_UDPATE_SUCCESSFUL: 'Your email has been udpated successfully',
|
||||
UPLOAD_FAILED: 'Upload failed',
|
||||
ETAGS_BLOCKED: (url: string) => (
|
||||
ETAGS_BLOCKED: (link: string) => (
|
||||
<>
|
||||
<Box mb={1}>
|
||||
We were unable to upload the following files because of your
|
||||
|
@ -520,13 +521,9 @@ const englishConstants = {
|
|||
<Box>
|
||||
Please disable any addons that might be preventing ente from
|
||||
using <code>eTags</code> to upload large files, or use our{' '}
|
||||
<a
|
||||
href={url}
|
||||
style={{ color: '#51cd7c', textDecoration: 'underline' }}
|
||||
target="_blank"
|
||||
rel="noreferrer">
|
||||
<Link href={link} target="_blank">
|
||||
desktop app
|
||||
</a>{' '}
|
||||
</Link>{' '}
|
||||
for a more reliable import experience.
|
||||
</Box>
|
||||
</>
|
||||
|
@ -556,11 +553,11 @@ const englishConstants = {
|
|||
THUMBNAIL_GENERATION_FAILED_INFO:
|
||||
'These files were uploaded, but unfortunately we could not generate the thumbnails for them.',
|
||||
UPLOAD_TO_COLLECTION: 'Upload to album',
|
||||
ARCHIVE: 'Hide',
|
||||
ARCHIVE_SECTION_NAME: 'Hidden',
|
||||
ARCHIVE: 'Archive',
|
||||
ARCHIVE_SECTION_NAME: 'Archive',
|
||||
ALL_SECTION_NAME: 'All',
|
||||
MOVE_TO_COLLECTION: 'Move to album',
|
||||
UNARCHIVE: 'Unhide',
|
||||
UNARCHIVE: 'Unarchive',
|
||||
MOVE: 'Move',
|
||||
ADD: 'Add',
|
||||
SORT: 'Sort',
|
||||
|
@ -627,6 +624,7 @@ const englishConstants = {
|
|||
<>File time updation failed for some files, please retry</>
|
||||
),
|
||||
FILE_NAME_CHARACTER_LIMIT: '100 characters max',
|
||||
CAPTION_CHARACTER_LIMIT: '280 characters max',
|
||||
|
||||
DATE_TIME_ORIGINAL: 'EXIF:DateTimeOriginal',
|
||||
DATE_TIME_DIGITIZED: 'EXIF:DateTimeDigitized',
|
||||
|
@ -825,6 +823,15 @@ const englishConstants = {
|
|||
UPLOADED_TO_SINGLE_COLLECTION: 'Uploaded to single collection',
|
||||
UPLOADED_TO_SEPARATE_COLLECTIONS: 'Uploaded to separate collections',
|
||||
NEVERMIND: 'Nevermind',
|
||||
UPDATE_AVAILABLE: 'Update available',
|
||||
UPDATE_INSTALLABLE_MESSAGE:
|
||||
'A new version of ente is ready to be installed.',
|
||||
INSTALL_NOW: `Install now`,
|
||||
INSTALL_ON_NEXT_LAUNCH: 'Install on next launch',
|
||||
UPDATE_AVAILABLE_MESSAGE:
|
||||
'A new version of ente has been released, but it cannot be automatically downloaded and installed.',
|
||||
DOWNLOAD_AND_INSTALL: 'Download and install',
|
||||
IGNORE_THIS_VERSION: 'Ignore this version',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
14
src/utils/temp/index.ts
Normal file
14
src/utils/temp/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
const CHARACTERS =
|
||||
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
|
||||
export function generateTempName(length: number) {
|
||||
let result = '';
|
||||
|
||||
const charactersLength = CHARACTERS.length;
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += CHARACTERS.charAt(
|
||||
Math.floor(Math.random() * charactersLength)
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import AutoAwesomeOutlinedIcon from '@mui/icons-material/AutoAwesomeOutlined';
|
||||
import { DialogBoxAttributes } from 'types/dialogBox';
|
||||
import { downloadApp } from 'utils/common';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
import ElectronUpdateService from 'services/electron/update';
|
||||
import { AppUpdateInfo } from 'types/electron';
|
||||
export const getDownloadAppMessage = (): DialogBoxAttributes => {
|
||||
return {
|
||||
title: constants.DOWNLOAD_APP,
|
||||
|
@ -41,3 +44,36 @@ export const getTrashFileMessage = (deleteFileHelper): DialogBoxAttributes => ({
|
|||
},
|
||||
close: { text: constants.CANCEL },
|
||||
});
|
||||
|
||||
export const getUpdateReadyToInstallMessage = (): DialogBoxAttributes => ({
|
||||
icon: <AutoAwesomeOutlinedIcon />,
|
||||
title: constants.UPDATE_AVAILABLE,
|
||||
content: constants.UPDATE_INSTALLABLE_MESSAGE,
|
||||
close: {
|
||||
text: constants.INSTALL_ON_NEXT_LAUNCH,
|
||||
variant: 'secondary',
|
||||
},
|
||||
proceed: {
|
||||
action: () => ElectronUpdateService.updateAndRestart(),
|
||||
text: constants.INSTALL_NOW,
|
||||
variant: 'accent',
|
||||
},
|
||||
});
|
||||
|
||||
export const getUpdateAvailableForDownloadMessage = (
|
||||
updateInfo: AppUpdateInfo
|
||||
): DialogBoxAttributes => ({
|
||||
icon: <AutoAwesomeOutlinedIcon />,
|
||||
title: constants.UPDATE_AVAILABLE,
|
||||
content: constants.UPDATE_AVAILABLE_MESSAGE,
|
||||
close: {
|
||||
text: constants.IGNORE_THIS_VERSION,
|
||||
variant: 'secondary',
|
||||
action: () => ElectronUpdateService.skipAppVersion(updateInfo.version),
|
||||
},
|
||||
proceed: {
|
||||
action: downloadApp,
|
||||
text: constants.DOWNLOAD_AND_INSTALL,
|
||||
variant: 'accent',
|
||||
},
|
||||
});
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import isElectron from 'is-electron';
|
||||
import { UserDetails } from 'types/user';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
import ElectronService from 'services/electron/common';
|
||||
|
||||
export function makeID(length) {
|
||||
let result = '';
|
||||
|
@ -14,13 +16,17 @@ export function makeID(length) {
|
|||
return result;
|
||||
}
|
||||
|
||||
export function getSentryUserID() {
|
||||
let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id;
|
||||
if (!anonymizeUserID) {
|
||||
anonymizeUserID = makeID(6);
|
||||
setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID });
|
||||
export async function getSentryUserID() {
|
||||
if (isElectron()) {
|
||||
return await ElectronService.getSentryUserID();
|
||||
} else {
|
||||
let anonymizeUserID = getData(LS_KEYS.AnonymizedUserID)?.id;
|
||||
if (!anonymizeUserID) {
|
||||
anonymizeUserID = makeID(6);
|
||||
setData(LS_KEYS.AnonymizedUserID, { id: anonymizeUserID });
|
||||
}
|
||||
return anonymizeUserID;
|
||||
}
|
||||
return anonymizeUserID;
|
||||
}
|
||||
|
||||
export function getLocalUserDetails(): UserDetails {
|
||||
|
|
|
@ -1,20 +1,14 @@
|
|||
import * as Comlink from 'comlink';
|
||||
import FFmpegClient from 'services/ffmpeg/ffmpegClient';
|
||||
import { WasmFFmpeg } from 'services/wasm/ffmpeg';
|
||||
|
||||
export class FFmpeg {
|
||||
ffmpegClient;
|
||||
wasmFFmpeg;
|
||||
constructor() {
|
||||
this.ffmpegClient = new FFmpegClient();
|
||||
}
|
||||
async generateThumbnail(file) {
|
||||
return this.ffmpegClient.generateThumbnail(file);
|
||||
}
|
||||
async extractVideoMetadata(file) {
|
||||
return this.ffmpegClient.extractVideoMetadata(file);
|
||||
this.wasmFFmpeg = new WasmFFmpeg();
|
||||
}
|
||||
|
||||
async convertToMP4(file, inputFileName) {
|
||||
return this.ffmpegClient.convertToMP4(file, inputFileName);
|
||||
run(cmd, inputFile, outputFileName) {
|
||||
return this.wasmFFmpeg.run(cmd, inputFile, outputFileName);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue