Export pending list (#1318)
This commit is contained in:
commit
17c2be3902
|
@ -232,14 +232,14 @@
|
|||
"CAPTION_PLACEHOLDER": "Add a description",
|
||||
"LOCATION": "Location",
|
||||
"SHOW_ON_MAP": "View on OpenStreetMap",
|
||||
"MAP":"Map",
|
||||
"MAP_SETTINGS":"Map Settings",
|
||||
"ENABLE_MAPS":"Enable Maps?",
|
||||
"ENABLE_MAP":"Enable map",
|
||||
"DISABLE_MAPS":"Disable Maps?",
|
||||
"ENABLE_MAP_DESCRIPTION":"<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP_DESCRIPTION":"<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP":"Disable map",
|
||||
"MAP": "Map",
|
||||
"MAP_SETTINGS": "Map Settings",
|
||||
"ENABLE_MAPS": "Enable Maps?",
|
||||
"ENABLE_MAP": "Enable map",
|
||||
"DISABLE_MAPS": "Disable Maps?",
|
||||
"ENABLE_MAP_DESCRIPTION": "<p>This will show your photos on a world map.</p> <p>The map is hosted by <a>OpenStreetMap</a>, and the exact locations of your photos are never shared.</p> <p>You can disable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP_DESCRIPTION": "<p>This will disable the display of your photos on a world map.</p> <p>You can enable this feature anytime from Settings.</p>",
|
||||
"DISABLE_MAP": "Disable map",
|
||||
"DETAILS": "Details",
|
||||
"VIEW_EXIF": "View all EXIF data",
|
||||
"NO_EXIF": "No EXIF data",
|
||||
|
@ -357,24 +357,24 @@
|
|||
"MODIFY_SHARING": "Modify sharing",
|
||||
"ADD_COLLABORATORS": "Add collaborators",
|
||||
"ADD_NEW_EMAIL": "Add a new email",
|
||||
"shared_with_people_zero" :"Share with specific people",
|
||||
"shared_with_people_zero": "Share with specific people",
|
||||
"shared_with_people_one": "Shared with 1 person",
|
||||
"shared_with_people_other": "Shared with {{count, number}} people",
|
||||
"participants_zero": "No participants",
|
||||
"participants_one": "1 participant",
|
||||
"participants_other": "{{count, number}} participants",
|
||||
"ADD_VIEWERS":"Add viewers",
|
||||
"ADD_VIEWERS": "Add viewers",
|
||||
"PARTICIPANTS": "Participants",
|
||||
"CHANGE_PERMISSIONS_TO_VIEWER": "<p>{{selectedEmail}} will not be able to add more photos to the album</p> <p>They will still be able to remove photos added by them</p>",
|
||||
"CHANGE_PERMISSIONS_TO_COLLABORATOR": "{{selectedEmail}} will be able to add photos to the album",
|
||||
"CONVERT_TO_VIEWER": "Yes, convert to viewer",
|
||||
"CONVERT_TO_COLLABORATOR": "Yes, convert to collaborator",
|
||||
"CHANGE_PERMISSION":"Change permission?",
|
||||
"CHANGE_PERMISSION": "Change permission?",
|
||||
"REMOVE_PARTICIPANT": "Remove?",
|
||||
"CONFIRM_REMOVE":"Yes, remove",
|
||||
"MANAGE":"Manage",
|
||||
"ADDED_AS":"Added as",
|
||||
"COLLABORATOR_RIGHTS":"Collaborators can add photos and videos to the shared album",
|
||||
"CONFIRM_REMOVE": "Yes, remove",
|
||||
"MANAGE": "Manage",
|
||||
"ADDED_AS": "Added as",
|
||||
"COLLABORATOR_RIGHTS": "Collaborators can add photos and videos to the shared album",
|
||||
"REMOVE_PARTICIPANT_HEAD": "Remove participant",
|
||||
"OWNER": "Owner",
|
||||
"COLLABORATORS": "Collaborators",
|
||||
|
@ -528,6 +528,9 @@
|
|||
"STOP_EXPORT": "Stop",
|
||||
"EXPORT_PROGRESS": "<a>{{progress.success}} / {{progress.total}}</a> items synced",
|
||||
"MIGRATING_EXPORT": "Preparing...",
|
||||
"RENAMING_COLLECTION_FOLDERS": "Renaming album folders...",
|
||||
"TRASHING_DELETED_FILES": "Trashing deleted files...",
|
||||
"TRASHING_DELETED_COLLECTIONS": "Trashing deleted albums...",
|
||||
"EXPORT_NOTIFICATION": {
|
||||
"START": "Export started",
|
||||
"IN_PROGRESS": "Export already in progress",
|
||||
|
@ -564,7 +567,7 @@
|
|||
"NEWEST_FIRST": "Newest first",
|
||||
"OLDEST_FIRST": "Oldest first",
|
||||
"CONVERSION_FAILED_NOTIFICATION_MESSAGE": "This file could not be previewed. Click here to download the original.",
|
||||
"SELECT_COLLECTION":"Select album",
|
||||
"PIN_ALBUM":"Pin album",
|
||||
"UNPIN_ALBUM":"Unpin album"
|
||||
"SELECT_COLLECTION": "Select album",
|
||||
"PIN_ALBUM": "Pin album",
|
||||
"UNPIN_ALBUM": "Unpin album"
|
||||
}
|
||||
|
|
|
@ -72,8 +72,7 @@ export default function AuthenticateUserModal({
|
|||
sx={{ position: 'absolute' }}
|
||||
attributes={{
|
||||
title: t('PASSWORD'),
|
||||
}}
|
||||
PaperProps={{ sx: { padding: '8px 12px', maxWidth: '320px' } }}>
|
||||
}}>
|
||||
<VerifyMasterPasswordForm
|
||||
buttonText={t('AUTHENTICATE')}
|
||||
callback={useMasterPassword}
|
||||
|
|
|
@ -44,6 +44,7 @@ export const CollectionTile = styled('div')`
|
|||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const ActiveIndicator = styled('div')`
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Breakpoint,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogProps,
|
||||
|
@ -14,11 +13,9 @@ import { DialogBoxAttributesV2 } from 'types/dialogBox';
|
|||
import EnteButton from 'components/EnteButton';
|
||||
|
||||
type IProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, 'onClose' | 'maxSize'> & {
|
||||
Omit<DialogProps, 'onClose'> & {
|
||||
onClose: () => void;
|
||||
attributes: DialogBoxAttributesV2;
|
||||
size?: Breakpoint;
|
||||
titleCloseButton?: boolean;
|
||||
}
|
||||
>;
|
||||
|
||||
|
@ -40,17 +37,21 @@ export default function DialogBoxV2({
|
|||
onClose: onClose,
|
||||
});
|
||||
|
||||
const { PaperProps, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
PaperProps={{
|
||||
...PaperProps,
|
||||
sx: {
|
||||
padding: '8px 12px',
|
||||
maxWidth: '360px',
|
||||
...PaperProps?.sx,
|
||||
},
|
||||
}}
|
||||
{...props}>
|
||||
{...rest}>
|
||||
<Stack spacing={'36px'} p={'16px'}>
|
||||
<Stack spacing={'19px'}>
|
||||
{attributes.icon && (
|
||||
|
|
|
@ -5,20 +5,34 @@ import {
|
|||
Stack,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
import { t } from 'i18next';
|
||||
import { formatDateTime } from 'utils/time/format';
|
||||
import { SpaceBetweenFlex } from './Container';
|
||||
import { formatNumber } from 'utils/number/format';
|
||||
import ExportPendingList from './ExportPendingList';
|
||||
import { useState } from 'react';
|
||||
import LinkButton from './pages/gallery/LinkButton';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
interface Props {
|
||||
pendingFileCount: number;
|
||||
pendingExports: EnteFile[];
|
||||
collectionNameMap: Map<number, string>;
|
||||
onHide: () => void;
|
||||
lastExportTime: number;
|
||||
startExport: () => void;
|
||||
}
|
||||
|
||||
export default function ExportFinished(props: Props) {
|
||||
const [pendingFileListView, setPendingFileListView] =
|
||||
useState<boolean>(false);
|
||||
|
||||
const openPendingFileList = () => {
|
||||
setPendingFileListView(true);
|
||||
};
|
||||
|
||||
const closePendingFileList = () => {
|
||||
setPendingFileListView(false);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
|
@ -27,9 +41,15 @@ export default function ExportFinished(props: Props) {
|
|||
<Typography color={'text.muted'}>
|
||||
{t('PENDING_ITEMS')}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{formatNumber(props.pendingFileCount)}
|
||||
</Typography>
|
||||
{props.pendingExports.length ? (
|
||||
<LinkButton onClick={openPendingFileList}>
|
||||
{formatNumber(props.pendingExports.length)}
|
||||
</LinkButton>
|
||||
) : (
|
||||
<Typography>
|
||||
{formatNumber(props.pendingExports.length)}
|
||||
</Typography>
|
||||
)}
|
||||
</SpaceBetweenFlex>
|
||||
<SpaceBetweenFlex minHeight={'48px'}>
|
||||
<Typography color="text.muted">
|
||||
|
@ -54,6 +74,12 @@ export default function ExportFinished(props: Props) {
|
|||
{t('EXPORT_AGAIN')}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
<ExportPendingList
|
||||
pendingExports={props.pendingExports}
|
||||
collectionNameMap={props.collectionNameMap}
|
||||
isOpen={pendingFileListView}
|
||||
onClose={closePendingFileList}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,16 +27,33 @@ interface Props {
|
|||
}
|
||||
|
||||
export default function ExportInProgress(props: Props) {
|
||||
const isLoading = props.exportProgress.total === 0;
|
||||
const showIndeterminateProgress = () => {
|
||||
return (
|
||||
props.exportStage === ExportStage.STARTING ||
|
||||
props.exportStage === ExportStage.MIGRATION ||
|
||||
props.exportStage === ExportStage.RENAMING_COLLECTION_FOLDERS ||
|
||||
props.exportStage === ExportStage.TRASHING_DELETED_FILES ||
|
||||
props.exportStage === ExportStage.TRASHING_DELETED_COLLECTIONS
|
||||
);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<DialogContent>
|
||||
<VerticallyCentered>
|
||||
<Box mb={1.5}>
|
||||
{isLoading ? (
|
||||
{props.exportStage === ExportStage.STARTING ? (
|
||||
t('EXPORT_STARTING')
|
||||
) : props.exportStage === ExportStage.MIGRATION ? (
|
||||
t('MIGRATING_EXPORT')
|
||||
) : props.exportStage ===
|
||||
ExportStage.RENAMING_COLLECTION_FOLDERS ? (
|
||||
t('RENAMING_COLLECTION_FOLDERS')
|
||||
) : props.exportStage ===
|
||||
ExportStage.TRASHING_DELETED_FILES ? (
|
||||
t('TRASHING_DELETED_FILES')
|
||||
) : props.exportStage ===
|
||||
ExportStage.TRASHING_DELETED_COLLECTIONS ? (
|
||||
t('TRASHING_DELETED_COLLECTIONS')
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey={'EXPORT_PROGRESS'}
|
||||
|
@ -53,7 +70,7 @@ export default function ExportInProgress(props: Props) {
|
|||
<ProgressBar
|
||||
style={{ width: '100%' }}
|
||||
now={
|
||||
isLoading
|
||||
showIndeterminateProgress()
|
||||
? 100
|
||||
: Math.round(
|
||||
((props.exportProgress.success +
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import isElectron from 'is-electron';
|
||||
import React, { useEffect, useState, useContext } from 'react';
|
||||
import exportService from 'services/export';
|
||||
import { ExportProgress, ExportSettings, FileExportStats } from 'types/export';
|
||||
import { ExportProgress, ExportSettings } from 'types/export';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
@ -30,6 +30,7 @@ import { t } from 'i18next';
|
|||
import LinkButton from './pages/gallery/LinkButton';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
const ExportFolderPathContainer = styled(LinkButton)`
|
||||
width: 262px;
|
||||
|
@ -44,6 +45,7 @@ const ExportFolderPathContainer = styled(LinkButton)`
|
|||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
collectionNameMap: Map<number, string>;
|
||||
}
|
||||
export default function ExportModal(props: Props) {
|
||||
const appContext = useContext(AppContext);
|
||||
|
@ -55,10 +57,7 @@ export default function ExportModal(props: Props) {
|
|||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
const [fileExportStats, setFileExportStats] = useState<FileExportStats>({
|
||||
totalCount: 0,
|
||||
pendingCount: 0,
|
||||
});
|
||||
const [pendingExports, setPendingExports] = useState<EnteFile[]>([]);
|
||||
const [lastExportTime, setLastExportTime] = useState(0);
|
||||
|
||||
// ====================
|
||||
|
@ -72,8 +71,8 @@ export default function ExportModal(props: Props) {
|
|||
exportService.setUIUpdaters({
|
||||
setExportStage,
|
||||
setExportProgress,
|
||||
setFileExportStats,
|
||||
setLastExportTime,
|
||||
setPendingExports,
|
||||
});
|
||||
const exportSettings: ExportSettings =
|
||||
exportService.getExportSettings();
|
||||
|
@ -123,20 +122,20 @@ export default function ExportModal(props: Props) {
|
|||
const syncExportRecord = async (exportFolder: string): Promise<void> => {
|
||||
try {
|
||||
if (!exportService.exportFolderExists(exportFolder)) {
|
||||
const fileExportStats = await exportService.getFileExportStats(
|
||||
const pendingExports = await exportService.getPendingExports(
|
||||
null
|
||||
);
|
||||
setFileExportStats(fileExportStats);
|
||||
setPendingExports(pendingExports);
|
||||
}
|
||||
const exportRecord = await exportService.getExportRecord(
|
||||
exportFolder
|
||||
);
|
||||
setExportStage(exportRecord.stage);
|
||||
setLastExportTime(exportRecord.lastAttemptTimestamp);
|
||||
const fileExportStats = await exportService.getFileExportStats(
|
||||
const pendingExports = await exportService.getPendingExports(
|
||||
exportRecord
|
||||
);
|
||||
setFileExportStats(fileExportStats);
|
||||
setPendingExports(pendingExports);
|
||||
} catch (e) {
|
||||
if (e.message !== CustomError.EXPORT_FOLDER_DOES_NOT_EXIST) {
|
||||
logError(e, 'syncExportRecord failed');
|
||||
|
@ -219,8 +218,9 @@ export default function ExportModal(props: Props) {
|
|||
stopExport={stopExport}
|
||||
onHide={props.onHide}
|
||||
lastExportTime={lastExportTime}
|
||||
pendingFileCount={fileExportStats.pendingCount}
|
||||
exportProgress={exportProgress}
|
||||
pendingExports={pendingExports}
|
||||
collectionNameMap={props.collectionNameMap}
|
||||
/>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -306,23 +306,29 @@ const ExportDynamicContent = ({
|
|||
stopExport,
|
||||
onHide,
|
||||
lastExportTime,
|
||||
pendingFileCount,
|
||||
exportProgress,
|
||||
pendingExports,
|
||||
collectionNameMap,
|
||||
}: {
|
||||
exportStage: ExportStage;
|
||||
startExport: () => void;
|
||||
stopExport: () => void;
|
||||
onHide: () => void;
|
||||
lastExportTime: number;
|
||||
pendingFileCount: number;
|
||||
exportProgress: ExportProgress;
|
||||
pendingExports: EnteFile[];
|
||||
collectionNameMap: Map<number, string>;
|
||||
}) => {
|
||||
switch (exportStage) {
|
||||
case ExportStage.INIT:
|
||||
return <ExportInit startExport={startExport} />;
|
||||
|
||||
case ExportStage.INPROGRESS:
|
||||
case ExportStage.MIGRATION:
|
||||
case ExportStage.STARTING:
|
||||
case ExportStage.EXPORTING_FILES:
|
||||
case ExportStage.RENAMING_COLLECTION_FOLDERS:
|
||||
case ExportStage.TRASHING_DELETED_FILES:
|
||||
case ExportStage.TRASHING_DELETED_COLLECTIONS:
|
||||
return (
|
||||
<ExportInProgress
|
||||
exportStage={exportStage}
|
||||
|
@ -336,7 +342,8 @@ const ExportDynamicContent = ({
|
|||
<ExportFinished
|
||||
onHide={onHide}
|
||||
lastExportTime={lastExportTime}
|
||||
pendingFileCount={pendingFileCount}
|
||||
pendingExports={pendingExports}
|
||||
collectionNameMap={collectionNameMap}
|
||||
startExport={startExport}
|
||||
/>
|
||||
);
|
||||
|
|
84
apps/photos/src/components/ExportPendingList.tsx
Normal file
84
apps/photos/src/components/ExportPendingList.tsx
Normal file
|
@ -0,0 +1,84 @@
|
|||
import { EnteFile } from 'types/file';
|
||||
import ItemList from 'components/ItemList';
|
||||
import DialogBoxV2 from './DialogBoxV2';
|
||||
import { t } from 'i18next';
|
||||
import { FlexWrapper } from './Container';
|
||||
import CollectionCard from './Collections/CollectionCard';
|
||||
import { ResultPreviewTile } from './Collections/styledComponents';
|
||||
import { Box, styled } from '@mui/material';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
collectionNameMap: Map<number, string>;
|
||||
pendingExports: EnteFile[];
|
||||
}
|
||||
|
||||
export const ItemContainer = styled('div')`
|
||||
position: relative;
|
||||
top: 5px;
|
||||
display: inline-block;
|
||||
max-width: 394px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
`;
|
||||
|
||||
const ExportPendingList = (props: Iprops) => {
|
||||
const renderListItem = (file: EnteFile) => {
|
||||
return (
|
||||
<FlexWrapper>
|
||||
<Box sx={{ marginRight: '8px' }}>
|
||||
<CollectionCard
|
||||
key={file.id}
|
||||
coverFile={file}
|
||||
onClick={() => null}
|
||||
collectionTile={ResultPreviewTile}
|
||||
/>
|
||||
</Box>
|
||||
<ItemContainer>
|
||||
{`${props.collectionNameMap.get(file.collectionID)} / ${
|
||||
file.metadata.title
|
||||
}`}
|
||||
</ItemContainer>
|
||||
</FlexWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const getItemTitle = (file: EnteFile) => {
|
||||
return `${props.collectionNameMap.get(file.collectionID)} / ${
|
||||
file.metadata.title
|
||||
}`;
|
||||
};
|
||||
|
||||
const generateItemKey = (file: EnteFile) => {
|
||||
return `${file.collectionID}-${file.id}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
open={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
PaperProps={{
|
||||
sx: { maxWidth: '444px' },
|
||||
}}
|
||||
attributes={{
|
||||
title: t('PENDING_ITEMS'),
|
||||
close: {
|
||||
action: props.onClose,
|
||||
text: t('CLOSE'),
|
||||
},
|
||||
}}>
|
||||
<ItemList
|
||||
maxHeight={240}
|
||||
itemSize={50}
|
||||
items={props.pendingExports}
|
||||
renderListItem={renderListItem}
|
||||
getItemTitle={getItemTitle}
|
||||
generateItemKey={generateItemKey}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExportPendingList;
|
|
@ -1,37 +0,0 @@
|
|||
import { Box, Tooltip } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { FixedSizeList as List } from 'react-window';
|
||||
|
||||
interface Iprops {
|
||||
fileList: any[];
|
||||
}
|
||||
|
||||
export default function FileList(props: Iprops) {
|
||||
const Row = ({ index, style }) => (
|
||||
<Tooltip
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'.MuiTooltip-tooltip.MuiTooltip-tooltip.MuiTooltip-tooltip':
|
||||
{ marginTop: 0 },
|
||||
},
|
||||
}}
|
||||
title={props.fileList[index]}
|
||||
placement="bottom-start"
|
||||
enterDelay={300}
|
||||
enterNextDelay={100}>
|
||||
<div style={style}>{props.fileList[index]}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box pl={2}>
|
||||
<List
|
||||
height={Math.min(35 * props.fileList.length, 160)}
|
||||
width={'100%'}
|
||||
itemSize={35}
|
||||
itemCount={props.fileList.length}>
|
||||
{Row}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
83
apps/photos/src/components/ItemList.tsx
Normal file
83
apps/photos/src/components/ItemList.tsx
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { Box, Tooltip } from '@mui/material';
|
||||
import React from 'react';
|
||||
import {
|
||||
FixedSizeList as List,
|
||||
ListChildComponentProps,
|
||||
areEqual,
|
||||
} from 'react-window';
|
||||
import memoize from 'memoize-one';
|
||||
|
||||
interface Iprops {
|
||||
items: any[];
|
||||
generateItemKey: (item: any) => string;
|
||||
getItemTitle: (item: any) => string;
|
||||
renderListItem: (item: any) => JSX.Element;
|
||||
maxHeight?: number;
|
||||
itemSize?: number;
|
||||
}
|
||||
|
||||
interface ItemData {
|
||||
renderListItem: (item: any) => JSX.Element;
|
||||
getItemTitle: (item: any) => string;
|
||||
items: any[];
|
||||
}
|
||||
|
||||
const createItemData = memoize(
|
||||
(
|
||||
renderListItem: (item: any) => JSX.Element,
|
||||
getItemTitle: (item: any) => string,
|
||||
items: any[]
|
||||
): ItemData => ({
|
||||
renderListItem,
|
||||
getItemTitle,
|
||||
items,
|
||||
})
|
||||
);
|
||||
|
||||
const Row = React.memo(
|
||||
({ index, style, data }: ListChildComponentProps<ItemData>) => {
|
||||
const { renderListItem, items, getItemTitle } = data;
|
||||
return (
|
||||
<Tooltip
|
||||
PopperProps={{
|
||||
sx: {
|
||||
'.MuiTooltip-tooltip.MuiTooltip-tooltip.MuiTooltip-tooltip':
|
||||
{
|
||||
marginTop: 0,
|
||||
},
|
||||
},
|
||||
}}
|
||||
title={getItemTitle(items[index])}
|
||||
placement="bottom-start"
|
||||
enterDelay={300}
|
||||
enterNextDelay={100}>
|
||||
<div style={style}>{renderListItem(items[index])}</div>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
areEqual
|
||||
);
|
||||
|
||||
export default function ItemList(props: Iprops) {
|
||||
const itemData = createItemData(
|
||||
props.renderListItem,
|
||||
props.getItemTitle,
|
||||
props.items
|
||||
);
|
||||
return (
|
||||
<Box pl={2}>
|
||||
<List
|
||||
itemData={itemData}
|
||||
height={Math.min(
|
||||
props.itemSize * props.items.length,
|
||||
props.maxHeight
|
||||
)}
|
||||
width={'100%'}
|
||||
itemSize={props.itemSize}
|
||||
itemCount={props.items.length}
|
||||
itemKey={props.generateItemKey}>
|
||||
{Row}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import FileList from 'components/FileList';
|
||||
import ItemList from 'components/ItemList';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { InProgressItemContainer } from './styledComponents';
|
||||
import {
|
||||
|
@ -18,6 +18,29 @@ export const InProgressSection = () => {
|
|||
useContext(UploadProgressContext);
|
||||
const fileList = inProgressUploads ?? [];
|
||||
|
||||
const renderListItem = ({ localFileID, progress }) => {
|
||||
return (
|
||||
<InProgressItemContainer key={localFileID}>
|
||||
<span>{uploadFileNames.get(localFileID)}</span>
|
||||
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="separator">{`-`}</span>
|
||||
<span>{`${progress}%`}</span>
|
||||
</>
|
||||
)}
|
||||
</InProgressItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const getItemTitle = ({ localFileID, progress }) => {
|
||||
return `${uploadFileNames.get(localFileID)} - ${progress}%`;
|
||||
};
|
||||
|
||||
const generateItemKey = ({ localFileID, progress }) => {
|
||||
return `${localFileID}-${progress}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<UploadProgressSection>
|
||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||
|
@ -29,19 +52,13 @@ export const InProgressSection = () => {
|
|||
{hasLivePhotos && (
|
||||
<SectionInfo>{t('LIVE_PHOTOS_DETECTED')}</SectionInfo>
|
||||
)}
|
||||
<FileList
|
||||
fileList={fileList.map(({ localFileID, progress }) => (
|
||||
<InProgressItemContainer key={localFileID}>
|
||||
<span>{uploadFileNames.get(localFileID)}</span>
|
||||
{uploadStage === UPLOAD_STAGES.UPLOADING && (
|
||||
<>
|
||||
{' '}
|
||||
<span className="separator">{`-`}</span>
|
||||
<span>{`${progress}%`}</span>
|
||||
</>
|
||||
)}
|
||||
</InProgressItemContainer>
|
||||
))}
|
||||
<ItemList
|
||||
items={fileList}
|
||||
generateItemKey={generateItemKey}
|
||||
getItemTitle={getItemTitle}
|
||||
renderListItem={renderListItem}
|
||||
maxHeight={160}
|
||||
itemSize={35}
|
||||
/>
|
||||
</UploadProgressSectionContent>
|
||||
</UploadProgressSection>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useContext } from 'react';
|
||||
import FileList from 'components/FileList';
|
||||
import ItemList from 'components/ItemList';
|
||||
import { Typography } from '@mui/material';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import { ResultItemContainer } from './styledComponents';
|
||||
|
@ -26,6 +26,23 @@ export const ResultSection = (props: ResultSectionProps) => {
|
|||
if (!fileList?.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const renderListItem = (fileID) => {
|
||||
return (
|
||||
<ResultItemContainer key={fileID}>
|
||||
{uploadFileNames.get(fileID)}
|
||||
</ResultItemContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const getItemTitle = (fileID) => {
|
||||
return uploadFileNames.get(fileID);
|
||||
};
|
||||
|
||||
const generateItemKey = (fileID) => {
|
||||
return fileID;
|
||||
};
|
||||
|
||||
return (
|
||||
<UploadProgressSection>
|
||||
<UploadProgressSectionTitle expandIcon={<ExpandMoreIcon />}>
|
||||
|
@ -35,12 +52,13 @@ export const ResultSection = (props: ResultSectionProps) => {
|
|||
{props.sectionInfo && (
|
||||
<SectionInfo>{props.sectionInfo}</SectionInfo>
|
||||
)}
|
||||
<FileList
|
||||
fileList={fileList.map((fileID) => (
|
||||
<ResultItemContainer key={fileID}>
|
||||
{uploadFileNames.get(fileID)}
|
||||
</ResultItemContainer>
|
||||
))}
|
||||
<ItemList
|
||||
items={fileList}
|
||||
generateItemKey={generateItemKey}
|
||||
getItemTitle={getItemTitle}
|
||||
renderListItem={renderListItem}
|
||||
maxHeight={160}
|
||||
itemSize={35}
|
||||
/>
|
||||
</UploadProgressSectionContent>
|
||||
</UploadProgressSection>
|
||||
|
|
|
@ -197,7 +197,7 @@ const Cont = styled('div')<{ disabled: boolean }>`
|
|||
position: relative;
|
||||
flex: 1;
|
||||
cursor: ${(props) => (props.disabled ? 'not-allowed' : 'pointer')};
|
||||
|
||||
user-select: none;
|
||||
& > img {
|
||||
object-fit: cover;
|
||||
max-width: 100%;
|
||||
|
|
|
@ -4,7 +4,11 @@ export const ENTE_TRASH_FOLDER = 'Trash';
|
|||
|
||||
export enum ExportStage {
|
||||
INIT = 0,
|
||||
INPROGRESS = 1,
|
||||
MIGRATION = 2,
|
||||
FINISHED = 3,
|
||||
MIGRATION = 1,
|
||||
STARTING = 2,
|
||||
EXPORTING_FILES = 3,
|
||||
TRASHING_DELETED_FILES = 4,
|
||||
RENAMING_COLLECTION_FOLDERS = 5,
|
||||
TRASHING_DELETED_COLLECTIONS = 6,
|
||||
FINISHED = 7,
|
||||
}
|
||||
|
|
|
@ -75,9 +75,9 @@ import {
|
|||
getAppNameAndTitle,
|
||||
} from 'constants/apps';
|
||||
import exportService from 'services/export';
|
||||
import { ExportStage } from 'constants/export';
|
||||
import { REDIRECTS } from 'constants/redirects';
|
||||
import { getLocalMapEnabled, setLocalMapEnabled } from 'utils/storage';
|
||||
import { isExportInProgress } from 'utils/export';
|
||||
|
||||
const redirectMap = new Map([
|
||||
[REDIRECTS.ROADMAP, getRoadmapRedirectURL],
|
||||
|
@ -275,7 +275,7 @@ export default function App(props) {
|
|||
if (exportSettings.continuousExport) {
|
||||
exportService.enableContinuousExport();
|
||||
}
|
||||
if (exportRecord.stage === ExportStage.INPROGRESS) {
|
||||
if (isExportInProgress(exportRecord.stage)) {
|
||||
addLogLine('export was in progress, resuming');
|
||||
exportService.scheduleExport();
|
||||
}
|
||||
|
|
|
@ -1032,7 +1032,11 @@ export default function Gallery() {
|
|||
isInSearchMode={isInSearchMode}
|
||||
/>
|
||||
)}
|
||||
<ExportModal show={exportModalView} onHide={closeExportModal} />
|
||||
<ExportModal
|
||||
show={exportModalView}
|
||||
onHide={closeExportModal}
|
||||
collectionNameMap={collectionNameMap}
|
||||
/>
|
||||
<AuthenticateUserModal
|
||||
open={authenticateUserModalView}
|
||||
onClose={closeAuthenticateUserModal}
|
||||
|
|
|
@ -48,14 +48,13 @@ import {
|
|||
ExportRecord,
|
||||
ExportSettings,
|
||||
ExportUIUpdaters,
|
||||
FileExportStats,
|
||||
} from 'types/export';
|
||||
import { User } from 'types/user';
|
||||
import { FILE_TYPE, TYPE_JPEG, TYPE_JPG } from 'constants/file';
|
||||
import { ExportStage } from 'constants/export';
|
||||
import { ElectronAPIs } from 'types/electron';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { addLocalLog, addLogLine } from 'utils/logging';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import { eventBus, Events } from '../events';
|
||||
import {
|
||||
getCollectionNameMap,
|
||||
|
@ -87,7 +86,7 @@ class ExportService {
|
|||
setExportProgress: () => {},
|
||||
setExportStage: () => {},
|
||||
setLastExportTime: () => {},
|
||||
setFileExportStats: () => {},
|
||||
setPendingExports: () => {},
|
||||
};
|
||||
private currentExportProgress: ExportProgress = {
|
||||
total: 0,
|
||||
|
@ -230,9 +229,9 @@ class ExportService {
|
|||
}
|
||||
}
|
||||
|
||||
getFileExportStats = async (
|
||||
getPendingExports = async (
|
||||
exportRecord: ExportRecord
|
||||
): Promise<FileExportStats> => {
|
||||
): Promise<EnteFile[]> => {
|
||||
try {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
const files = [
|
||||
|
@ -241,44 +240,11 @@ class ExportService {
|
|||
];
|
||||
const userPersonalFiles = getPersonalFiles(files, user);
|
||||
|
||||
const collections = await getLocalCollections(true);
|
||||
const userNonEmptyPersonalCollections =
|
||||
getNonEmptyPersonalCollections(
|
||||
collections,
|
||||
userPersonalFiles,
|
||||
user
|
||||
);
|
||||
|
||||
const unExportedFiles = getUnExportedFiles(
|
||||
userPersonalFiles,
|
||||
exportRecord
|
||||
);
|
||||
const deletedExportedFiles = getDeletedExportedFiles(
|
||||
userPersonalFiles,
|
||||
exportRecord
|
||||
);
|
||||
const renamedCollections = getRenamedExportedCollections(
|
||||
userNonEmptyPersonalCollections,
|
||||
exportRecord
|
||||
);
|
||||
const deletedCollections = getDeletedExportedCollections(
|
||||
userNonEmptyPersonalCollections,
|
||||
exportRecord
|
||||
);
|
||||
|
||||
addLocalLog(
|
||||
() =>
|
||||
`personal files:${userPersonalFiles.length} unexported files: ${unExportedFiles.length}, deleted exported files: ${deletedExportedFiles.length}, renamed collections: ${renamedCollections.length}, deleted collections: ${deletedCollections.length}`
|
||||
);
|
||||
|
||||
return {
|
||||
totalCount: userPersonalFiles.length,
|
||||
pendingCount:
|
||||
unExportedFiles.length +
|
||||
deletedExportedFiles.length +
|
||||
renamedCollections.length +
|
||||
deletedCollections.length,
|
||||
};
|
||||
return unExportedFiles;
|
||||
} catch (e) {
|
||||
logError(e, 'getUpdateFileLists failed');
|
||||
throw e;
|
||||
|
@ -289,22 +255,12 @@ class ExportService {
|
|||
this.verifyExportFolderExists(exportFolder);
|
||||
const exportRecord = await this.getExportRecord(exportFolder);
|
||||
await this.updateExportStage(ExportStage.MIGRATION);
|
||||
this.updateExportProgress({
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
await this.runMigration(
|
||||
exportFolder,
|
||||
exportRecord,
|
||||
this.updateExportProgress.bind(this)
|
||||
);
|
||||
await this.updateExportStage(ExportStage.INPROGRESS);
|
||||
this.updateExportProgress({
|
||||
success: 0,
|
||||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
await this.updateExportStage(ExportStage.STARTING);
|
||||
}
|
||||
|
||||
async postExport() {
|
||||
|
@ -319,8 +275,8 @@ class ExportService {
|
|||
|
||||
const exportRecord = await this.getExportRecord(exportFolder);
|
||||
|
||||
const fileExportStats = await this.getFileExportStats(exportRecord);
|
||||
this.uiUpdater.setFileExportStats(fileExportStats);
|
||||
const pendingExports = await this.getPendingExports(exportRecord);
|
||||
this.uiUpdater.setPendingExports(pendingExports);
|
||||
} catch (e) {
|
||||
logError(e, 'postExport failed');
|
||||
}
|
||||
|
@ -442,58 +398,45 @@ class ExportService {
|
|||
this.uiUpdater.setExportProgress({
|
||||
success: success,
|
||||
failed: failed,
|
||||
total:
|
||||
removedFileUIDs.length +
|
||||
filesToExport.length +
|
||||
deletedExportedCollections.length +
|
||||
renamedCollections.length,
|
||||
total: filesToExport.length,
|
||||
});
|
||||
const incrementSuccess = () => {
|
||||
this.updateExportProgress({
|
||||
success: ++success,
|
||||
failed: failed,
|
||||
total:
|
||||
removedFileUIDs.length +
|
||||
filesToExport.length +
|
||||
deletedExportedCollections.length +
|
||||
renamedCollections.length,
|
||||
total: filesToExport.length,
|
||||
});
|
||||
};
|
||||
const incrementFailed = () => {
|
||||
this.updateExportProgress({
|
||||
success: success,
|
||||
failed: ++failed,
|
||||
total:
|
||||
removedFileUIDs.length +
|
||||
filesToExport.length +
|
||||
deletedExportedCollections.length +
|
||||
renamedCollections.length,
|
||||
total: filesToExport.length,
|
||||
});
|
||||
};
|
||||
if (renamedCollections?.length > 0) {
|
||||
this.updateExportStage(ExportStage.RENAMING_COLLECTION_FOLDERS);
|
||||
addLogLine(`renaming ${renamedCollections.length} collections`);
|
||||
await this.collectionRenamer(
|
||||
exportFolder,
|
||||
collectionIDExportNameMap,
|
||||
renamedCollections,
|
||||
incrementSuccess,
|
||||
incrementFailed,
|
||||
isCanceled
|
||||
);
|
||||
}
|
||||
|
||||
if (removedFileUIDs?.length > 0) {
|
||||
this.updateExportStage(ExportStage.TRASHING_DELETED_FILES);
|
||||
addLogLine(`trashing ${removedFileUIDs.length} files`);
|
||||
await this.fileTrasher(
|
||||
exportFolder,
|
||||
collectionIDExportNameMap,
|
||||
removedFileUIDs,
|
||||
incrementSuccess,
|
||||
incrementFailed,
|
||||
isCanceled
|
||||
);
|
||||
}
|
||||
if (filesToExport?.length > 0) {
|
||||
this.updateExportStage(ExportStage.EXPORTING_FILES);
|
||||
addLogLine(`exporting ${filesToExport.length} files`);
|
||||
await this.fileExporter(
|
||||
filesToExport,
|
||||
|
@ -506,14 +449,15 @@ class ExportService {
|
|||
);
|
||||
}
|
||||
if (deletedExportedCollections?.length > 0) {
|
||||
this.updateExportStage(
|
||||
ExportStage.TRASHING_DELETED_COLLECTIONS
|
||||
);
|
||||
addLogLine(
|
||||
`removing ${deletedExportedCollections.length} collections`
|
||||
);
|
||||
await this.collectionRemover(
|
||||
deletedExportedCollections,
|
||||
exportFolder,
|
||||
incrementSuccess,
|
||||
incrementFailed,
|
||||
isCanceled
|
||||
);
|
||||
}
|
||||
|
@ -532,8 +476,6 @@ class ExportService {
|
|||
exportFolder: string,
|
||||
collectionIDExportNameMap: Map<number, string>,
|
||||
renamedCollections: Collection[],
|
||||
incrementSuccess: () => void,
|
||||
incrementFailed: () => void,
|
||||
isCanceled: CancellationStatus
|
||||
) {
|
||||
try {
|
||||
|
@ -580,9 +522,7 @@ class ExportService {
|
|||
addLogLine(
|
||||
`renaming collection with id ${collection.id} from ${oldCollectionExportName} to ${newCollectionExportName} successful`
|
||||
);
|
||||
incrementSuccess();
|
||||
} catch (e) {
|
||||
incrementFailed();
|
||||
logError(e, 'collectionRenamer failed a collection');
|
||||
if (
|
||||
e.message ===
|
||||
|
@ -609,8 +549,6 @@ class ExportService {
|
|||
async collectionRemover(
|
||||
deletedExportedCollectionIDs: number[],
|
||||
exportFolder: string,
|
||||
incrementSuccess: () => void,
|
||||
incrementFailed: () => void,
|
||||
isCanceled: CancellationStatus
|
||||
) {
|
||||
try {
|
||||
|
@ -657,9 +595,7 @@ class ExportService {
|
|||
addLogLine(
|
||||
`removing collection with id ${collectionID} from export folder successful`
|
||||
);
|
||||
incrementSuccess();
|
||||
} catch (e) {
|
||||
incrementFailed();
|
||||
logError(e, 'collectionRemover failed a collection');
|
||||
if (
|
||||
e.message ===
|
||||
|
@ -779,8 +715,6 @@ class ExportService {
|
|||
exportDir: string,
|
||||
collectionIDExportNameMap: Map<number, string>,
|
||||
removedFileUIDs: string[],
|
||||
incrementSuccess: () => void,
|
||||
incrementFailed: () => void,
|
||||
isCanceled: CancellationStatus
|
||||
): Promise<void> {
|
||||
try {
|
||||
|
@ -878,9 +812,7 @@ class ExportService {
|
|||
}
|
||||
await this.removeFileExportedRecord(exportDir, fileUID);
|
||||
addLogLine(`trashing file with id ${fileUID} successful`);
|
||||
incrementSuccess();
|
||||
} catch (e) {
|
||||
incrementFailed();
|
||||
logError(e, 'trashing failed for a file');
|
||||
if (
|
||||
e.message ===
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { ExportStage } from 'constants/export';
|
||||
import { EnteFile } from 'types/file';
|
||||
|
||||
export interface ExportProgress {
|
||||
success: number;
|
||||
|
@ -17,11 +18,6 @@ export interface FileExportNames {
|
|||
[ID: string]: string;
|
||||
}
|
||||
|
||||
export interface FileExportStats {
|
||||
totalCount: number;
|
||||
pendingCount: number;
|
||||
}
|
||||
|
||||
export interface ExportRecordV0 {
|
||||
stage: ExportStage;
|
||||
lastAttemptTimestamp: number;
|
||||
|
@ -66,6 +62,6 @@ export interface ExportSettings {
|
|||
export interface ExportUIUpdaters {
|
||||
setExportStage: (stage: ExportStage) => void;
|
||||
setExportProgress: (progress: ExportProgress) => void;
|
||||
setFileExportStats: (fileExportStats: FileExportStats) => void;
|
||||
setLastExportTime: (exportTime: number) => void;
|
||||
setPendingExports: (pendingExports: EnteFile[]) => void;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,11 @@ import { EnteFile } from 'types/file';
|
|||
|
||||
import { Metadata } from 'types/upload';
|
||||
import { splitFilenameAndExtension } from 'utils/file';
|
||||
import { ENTE_METADATA_FOLDER, ENTE_TRASH_FOLDER } from 'constants/export';
|
||||
import {
|
||||
ENTE_METADATA_FOLDER,
|
||||
ENTE_TRASH_FOLDER,
|
||||
ExportStage,
|
||||
} from 'constants/export';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { formatDateTimeShort } from 'utils/time/format';
|
||||
import { HIDDEN_COLLECTION_NAME } from 'services/collectionService';
|
||||
|
@ -305,3 +309,6 @@ export const parseLivePhotoExportName = (
|
|||
const { image, video } = JSON.parse(livePhotoExportName);
|
||||
return { image, video };
|
||||
};
|
||||
|
||||
export const isExportInProgress = (exportStage: ExportStage) =>
|
||||
exportStage > ExportStage.INIT && exportStage < ExportStage.FINISHED;
|
||||
|
|
Loading…
Reference in a new issue