Merge branch 'gallery-refactor' into hidden-support
This commit is contained in:
commit
477848fb16
|
@ -1,15 +0,0 @@
|
|||
const path = require('path');
|
||||
|
||||
const buildEslintCommand = (filenames) =>
|
||||
`yarn lint --fix --file ${filenames
|
||||
.map((f) => path.relative(process.cwd(), f))
|
||||
.join(' --file ')} --`;
|
||||
|
||||
const buildPrettierCommand = (filenames) =>
|
||||
`yarn prettier --write --ignore-unknown ${filenames.join(' ')}`;
|
||||
|
||||
module.exports = {
|
||||
'apps/**/*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand],
|
||||
'packages/**/*.{js,jsx,ts,tsx}': [buildEslintCommand, buildPrettierCommand],
|
||||
'**/*.{json,css,scss,md,html,yml,yaml}': [buildPrettierCommand],
|
||||
};
|
|
@ -84,7 +84,7 @@
|
|||
"PREVIOUS": "Previous (←)",
|
||||
"NEXT": "Next (→)",
|
||||
"TITLE_PHOTOS": "ente Photos",
|
||||
"TITLE_ALBUMS": "ente Albums",
|
||||
"TITLE_ALBUMS": "ente Photos",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Upload your first photo",
|
||||
"IMPORT_YOUR_FOLDERS": "Import your folders",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"PREVIOUS": "Anterior (←)",
|
||||
"NEXT": "Siguiente (→)",
|
||||
"TITLE_PHOTOS": "ente Fotos",
|
||||
"TITLE_ALBUMS": "ente Álbumes",
|
||||
"TITLE_ALBUMS": "ente Fotos",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Carga tu primer archivo",
|
||||
"IMPORT_YOUR_FOLDERS": "Importar tus carpetas",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"PREVIOUS": "Précédent (←)",
|
||||
"NEXT": "Suivant (→)",
|
||||
"TITLE_PHOTOS": "ente Photos",
|
||||
"TITLE_ALBUMS": "ente Albums",
|
||||
"TITLE_ALBUMS": "ente Photos",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Chargez votre 1ere photo",
|
||||
"IMPORT_YOUR_FOLDERS": "Importez vos dossiers",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"PREVIOUS": "Precedente (←)",
|
||||
"NEXT": "Successivo (→)",
|
||||
"TITLE_PHOTOS": "ente Photos",
|
||||
"TITLE_ALBUMS": "ente Album",
|
||||
"TITLE_ALBUMS": "ente Photos",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Carica la tua prima foto",
|
||||
"IMPORT_YOUR_FOLDERS": "Importa una cartella",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"PREVIOUS": "Vorige (←)",
|
||||
"NEXT": "Volgende (→)",
|
||||
"TITLE_PHOTOS": "ente Photos",
|
||||
"TITLE_ALBUMS": "ente Albums",
|
||||
"TITLE_ALBUMS": "ente Photos",
|
||||
"TITLE_AUTH": "ente Auth",
|
||||
"UPLOAD_FIRST_PHOTO": "Je eerste foto uploaden",
|
||||
"IMPORT_YOUR_FOLDERS": "Importeer uw mappen",
|
||||
|
|
|
@ -83,7 +83,7 @@
|
|||
"PREVIOUS": "上一个 (←)",
|
||||
"NEXT": "下一个 (→)",
|
||||
"TITLE_PHOTOS": "ente 照片",
|
||||
"TITLE_ALBUMS": "ente 相册",
|
||||
"TITLE_ALBUMS": "ente 照片",
|
||||
"TITLE_AUTH": "ente 验证器",
|
||||
"UPLOAD_FIRST_PHOTO": "上传您的第一张照片",
|
||||
"IMPORT_YOUR_FOLDERS": "导入您的文件夹",
|
||||
|
|
|
@ -1,18 +1,12 @@
|
|||
import { GalleryContext } from 'pages/gallery';
|
||||
import PreviewCard from './pages/gallery/PreviewCard';
|
||||
import React, { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { useContext, useEffect, useRef, useState } from 'react';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { styled } from '@mui/material';
|
||||
import DownloadManager from 'services/downloadManager';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import PhotoViewer from 'components/PhotoViewer';
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
CollectionType,
|
||||
TRASH_SECTION,
|
||||
} from 'constants/collection';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
import { TRASH_SECTION } from 'constants/collection';
|
||||
import { updateFileMsrcProps, updateFileSrcProps } from 'utils/photoFrame';
|
||||
import { PhotoList } from './PhotoList';
|
||||
import { MergedSourceURL, SelectedState } from 'types/gallery';
|
||||
|
@ -20,19 +14,11 @@ import PublicCollectionDownloadManager from 'services/publicCollectionDownloadMa
|
|||
import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery';
|
||||
import { useRouter } from 'next/router';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { DeduplicateContext } from 'pages/deduplicate';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
import { isSameDayAnyYear, isInsideBox } from 'utils/search';
|
||||
import { Search } from 'types/search';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { useMemo } from 'react';
|
||||
import { Collection } from 'types/collection';
|
||||
import { addLogLine } from 'utils/logging';
|
||||
import PhotoSwipe from 'photoswipe';
|
||||
import { isHiddenCollection } from 'utils/collection';
|
||||
import { t } from 'i18next';
|
||||
|
||||
const Container = styled('div')`
|
||||
display: block;
|
||||
|
@ -51,41 +37,36 @@ const PHOTOSWIPE_HASH_SUFFIX = '&opened';
|
|||
|
||||
interface Props {
|
||||
files: EnteFile[];
|
||||
collections?: Collection[];
|
||||
syncWithRemote: () => Promise<void>;
|
||||
favItemIds?: Set<number>;
|
||||
archivedCollections?: Set<number>;
|
||||
setSelected: (
|
||||
selected: SelectedState | ((selected: SelectedState) => SelectedState)
|
||||
) => void;
|
||||
selected: SelectedState;
|
||||
isInSearchMode?: boolean;
|
||||
search?: Search;
|
||||
deletedFileIds?: Set<number>;
|
||||
setDeletedFileIds?: (value: Set<number>) => void;
|
||||
activeCollection: number;
|
||||
isIncomingSharedCollection?: boolean;
|
||||
enableDownload?: boolean;
|
||||
isDeduplicating?: boolean;
|
||||
resetSearch?: () => void;
|
||||
fileToCollectionsMap: Map<number, number[]>;
|
||||
collectionNameMap: Map<number, string>;
|
||||
showAppDownloadBanner?: boolean;
|
||||
}
|
||||
|
||||
const PhotoFrame = ({
|
||||
files,
|
||||
collections,
|
||||
syncWithRemote,
|
||||
favItemIds,
|
||||
archivedCollections,
|
||||
setSelected,
|
||||
selected,
|
||||
isInSearchMode,
|
||||
search,
|
||||
deletedFileIds,
|
||||
setDeletedFileIds,
|
||||
activeCollection,
|
||||
isIncomingSharedCollection,
|
||||
enableDownload,
|
||||
isDeduplicating,
|
||||
fileToCollectionsMap,
|
||||
collectionNameMap,
|
||||
showAppDownloadBanner,
|
||||
}: Props) => {
|
||||
const [user, setUser] = useState<User>(null);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
@ -93,7 +74,6 @@ const PhotoFrame = ({
|
|||
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const appContext = useContext(AppContext);
|
||||
const deduplicateContext = useContext(DeduplicateContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
PublicCollectionGalleryContext
|
||||
);
|
||||
|
@ -106,7 +86,8 @@ const PhotoFrame = ({
|
|||
const updateInProgress = useRef(false);
|
||||
const updateRequired = useRef(false);
|
||||
|
||||
const [filteredData, setFilteredData] = useState<EnteFile[]>([]);
|
||||
const [displayFiles, setDisplayFiles] = useState<EnteFile[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
setUser(user);
|
||||
|
@ -119,142 +100,33 @@ const PhotoFrame = ({
|
|||
return;
|
||||
}
|
||||
updateInProgress.current = true;
|
||||
const idSet = new Set();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
const filteredData = files
|
||||
.filter((item) => {
|
||||
// only show single copy of a file
|
||||
if (idSet.has(item.id)) {
|
||||
return false;
|
||||
const displayFiles = files.map((item) => {
|
||||
const filteredItem = {
|
||||
...item,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
};
|
||||
try {
|
||||
if (galleryContext.thumbs.has(item.id)) {
|
||||
updateFileMsrcProps(
|
||||
filteredItem,
|
||||
galleryContext.thumbs.get(item.id)
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
activeCollection === TRASH_SECTION ||
|
||||
isDeduplicating ||
|
||||
activeCollection === HIDDEN_SECTION ||
|
||||
isIncomingSharedCollection
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
updateFileSrcProps(
|
||||
filteredItem,
|
||||
galleryContext.files.get(item.id)
|
||||
);
|
||||
}
|
||||
|
||||
// SEARCH MODE
|
||||
if (isInSearchMode) {
|
||||
if (
|
||||
search?.date &&
|
||||
!isSameDayAnyYear(search.date)(
|
||||
new Date(item.metadata.creationTime / 1000)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.location &&
|
||||
!isInsideBox(
|
||||
{
|
||||
latitude: item.metadata.latitude,
|
||||
longitude: item.metadata.longitude,
|
||||
},
|
||||
search.location
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.person &&
|
||||
search.person.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.thing &&
|
||||
search.thing.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.text &&
|
||||
search.text.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.files &&
|
||||
search.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// shared files can only be seen in their respective shared collection
|
||||
if (isSharedFile(user, item)) {
|
||||
if (activeCollection === item.collectionID) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Archived files/collection files can only be seen in archive section or their respective collection
|
||||
if (
|
||||
IsArchived(item) ||
|
||||
archivedCollections.has(item.collectionID)
|
||||
) {
|
||||
if (
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ALL SECTION - show all files
|
||||
if (activeCollection === ALL_SECTION) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// COLLECTION SECTION - show files in the active collection
|
||||
if (activeCollection === item.collectionID) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.map((item) => {
|
||||
const filteredItem = {
|
||||
...item,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
title: item.pubMagicMetadata?.data.caption,
|
||||
};
|
||||
try {
|
||||
if (galleryContext.thumbs.has(item.id)) {
|
||||
updateFileMsrcProps(
|
||||
filteredItem,
|
||||
galleryContext.thumbs.get(item.id)
|
||||
);
|
||||
}
|
||||
if (galleryContext.files.has(item.id)) {
|
||||
updateFileSrcProps(
|
||||
filteredItem,
|
||||
galleryContext.files.get(item.id)
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'PhotoFrame url prefill failed');
|
||||
}
|
||||
return filteredItem;
|
||||
});
|
||||
setFilteredData(filteredData);
|
||||
} catch (e) {
|
||||
logError(e, 'PhotoFrame url prefill failed');
|
||||
}
|
||||
return filteredItem;
|
||||
});
|
||||
setDisplayFiles(displayFiles);
|
||||
updateInProgress.current = false;
|
||||
if (updateRequired.current) {
|
||||
updateRequired.current = false;
|
||||
|
@ -264,54 +136,11 @@ const PhotoFrame = ({
|
|||
}
|
||||
};
|
||||
main();
|
||||
}, [
|
||||
files,
|
||||
deletedFileIds,
|
||||
search?.date,
|
||||
search?.files,
|
||||
search?.location,
|
||||
search?.person,
|
||||
search?.thing,
|
||||
search?.text,
|
||||
activeCollection,
|
||||
]);
|
||||
}, [files]);
|
||||
|
||||
useEffect(() => {
|
||||
setFetching({});
|
||||
}, [filteredData]);
|
||||
|
||||
const fileToCollectionsMap = useMemo(() => {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
files.forEach((file) => {
|
||||
if (!fileToCollectionsMap.get(file.id)) {
|
||||
fileToCollectionsMap.set(file.id, []);
|
||||
}
|
||||
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||
});
|
||||
return fileToCollectionsMap;
|
||||
}, [files]);
|
||||
|
||||
const collectionNameMap = useMemo(() => {
|
||||
if (collections) {
|
||||
return new Map<number, string>(
|
||||
collections.map((collection) => {
|
||||
if (isHiddenCollection(collection)) {
|
||||
return [collection.id, t('HIDDEN')];
|
||||
} else if (collection.type === CollectionType.favorites) {
|
||||
return [collection.id, t('FAVORITES')];
|
||||
} else if (
|
||||
collection.type === CollectionType.uncategorized
|
||||
) {
|
||||
return [collection.id, t('UNCATEGORIZED')];
|
||||
} else {
|
||||
return [collection.id, collection.name];
|
||||
}
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return new Map();
|
||||
}
|
||||
}, [collections]);
|
||||
}, [displayFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentURL = new URL(window.location.href);
|
||||
|
@ -368,7 +197,7 @@ const PhotoFrame = ({
|
|||
}, [selected]);
|
||||
|
||||
const updateURL = (index: number) => (id: number, url: string) => {
|
||||
const file = filteredData[index];
|
||||
const file = displayFiles[index];
|
||||
// this is to prevent outdated updateURL call from updating the wrong file
|
||||
if (file.id !== id) {
|
||||
addLogLine(
|
||||
|
@ -394,7 +223,7 @@ const PhotoFrame = ({
|
|||
id: number,
|
||||
mergedSrcURL: MergedSourceURL
|
||||
) => {
|
||||
const file = filteredData[index];
|
||||
const file = displayFiles[index];
|
||||
// this is to prevent outdate updateSrcURL call from updating the wrong file
|
||||
if (file.id !== id) {
|
||||
addLogLine(
|
||||
|
@ -488,7 +317,7 @@ const PhotoFrame = ({
|
|||
(index - i) * direction >= 0;
|
||||
i += direction
|
||||
) {
|
||||
checked = checked && !!selected[filteredData[i].id];
|
||||
checked = checked && !!selected[displayFiles[i].id];
|
||||
}
|
||||
for (
|
||||
let i = rangeStart;
|
||||
|
@ -496,13 +325,13 @@ const PhotoFrame = ({
|
|||
i += direction
|
||||
) {
|
||||
handleSelect(
|
||||
filteredData[i].id,
|
||||
filteredData[i].ownerID === user?.id
|
||||
displayFiles[i].id,
|
||||
displayFiles[i].ownerID === user?.id
|
||||
)(!checked);
|
||||
}
|
||||
handleSelect(
|
||||
filteredData[index].id,
|
||||
filteredData[index].ownerID === user?.id,
|
||||
displayFiles[index].id,
|
||||
displayFiles[index].ownerID === user?.id,
|
||||
index
|
||||
)(!checked);
|
||||
}
|
||||
|
@ -670,19 +499,15 @@ const PhotoFrame = ({
|
|||
width={width}
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
filteredData={filteredData}
|
||||
displayFiles={displayFiles}
|
||||
activeCollection={activeCollection}
|
||||
showAppDownloadBanner={
|
||||
files.length < 30 &&
|
||||
!isInSearchMode &&
|
||||
!deduplicateContext.isOnDeduplicatePage
|
||||
}
|
||||
showAppDownloadBanner={showAppDownloadBanner}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<PhotoViewer
|
||||
isOpen={open}
|
||||
items={filteredData}
|
||||
items={displayFiles}
|
||||
currentIndex={currentIndex}
|
||||
onClose={handleClose}
|
||||
gettingData={getSlideData}
|
||||
|
|
|
@ -163,7 +163,7 @@ const NothingContainer = styled(ListItemContainer)`
|
|||
interface Props {
|
||||
height: number;
|
||||
width: number;
|
||||
filteredData: EnteFile[];
|
||||
displayFiles: EnteFile[];
|
||||
showAppDownloadBanner: boolean;
|
||||
getThumbnail: (
|
||||
file: EnteFile,
|
||||
|
@ -176,7 +176,7 @@ interface Props {
|
|||
export function PhotoList({
|
||||
height,
|
||||
width,
|
||||
filteredData,
|
||||
displayFiles,
|
||||
showAppDownloadBanner,
|
||||
getThumbnail,
|
||||
activeCollection,
|
||||
|
@ -266,7 +266,7 @@ export function PhotoList({
|
|||
}, [
|
||||
width,
|
||||
height,
|
||||
filteredData,
|
||||
displayFiles,
|
||||
deduplicateContext.isOnDeduplicatePage,
|
||||
deduplicateContext.fileSizeMap,
|
||||
]);
|
||||
|
@ -340,19 +340,19 @@ export function PhotoList({
|
|||
|
||||
const groupByFileSize = (timeStampList: TimeStampListItem[]) => {
|
||||
let index = 0;
|
||||
while (index < filteredData.length) {
|
||||
const file = filteredData[index];
|
||||
while (index < displayFiles.length) {
|
||||
const file = displayFiles[index];
|
||||
const currentFileSize = deduplicateContext.fileSizeMap.get(file.id);
|
||||
const currentCreationTime = file.metadata.creationTime;
|
||||
let lastFileIndex = index;
|
||||
|
||||
while (lastFileIndex < filteredData.length) {
|
||||
while (lastFileIndex < displayFiles.length) {
|
||||
if (
|
||||
deduplicateContext.fileSizeMap.get(
|
||||
filteredData[lastFileIndex].id
|
||||
displayFiles[lastFileIndex].id
|
||||
) !== currentFileSize ||
|
||||
(deduplicateContext.clubSameTimeFilesOnly &&
|
||||
filteredData[lastFileIndex].metadata.creationTime !==
|
||||
displayFiles[lastFileIndex].metadata.creationTime !==
|
||||
currentCreationTime)
|
||||
) {
|
||||
break;
|
||||
|
@ -370,7 +370,7 @@ export function PhotoList({
|
|||
const tileSize = Math.min(columns, lastFileIndex - index + 1);
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.FILE,
|
||||
items: filteredData.slice(index, index + tileSize),
|
||||
items: displayFiles.slice(index, index + tileSize),
|
||||
itemStartIndex: index,
|
||||
});
|
||||
index += tileSize;
|
||||
|
@ -381,7 +381,7 @@ export function PhotoList({
|
|||
const groupByTime = (t, timeStampList: TimeStampListItem[]) => {
|
||||
let listItemIndex = 0;
|
||||
let currentDate;
|
||||
filteredData.forEach((item, index) => {
|
||||
displayFiles.forEach((item, index) => {
|
||||
if (
|
||||
!currentDate ||
|
||||
!isSameDay(
|
||||
|
|
|
@ -43,7 +43,7 @@ export const FileInfoSidebar = styled((props: DialogProps) => (
|
|||
});
|
||||
|
||||
interface Iprops {
|
||||
shouldDisableEdits: boolean;
|
||||
shouldDisableEdits?: boolean;
|
||||
showInfo: boolean;
|
||||
handleCloseInfo: () => void;
|
||||
file: EnteFile;
|
||||
|
|
|
@ -133,7 +133,7 @@ function PhotoViewer(props: Iprops) {
|
|||
switch (event.key) {
|
||||
case 'i':
|
||||
case 'I':
|
||||
setShowInfo(true);
|
||||
!props.isIncomingSharedCollection && setShowInfo(true);
|
||||
break;
|
||||
case 'Backspace':
|
||||
case 'Delete':
|
||||
|
@ -663,18 +663,19 @@ function PhotoViewer(props: Iprops) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FileInfo
|
||||
isTrashCollection={props.isTrashCollection}
|
||||
shouldDisableEdits={props.isIncomingSharedCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
file={photoSwipe?.currItem as EnteFile}
|
||||
exif={exif?.value}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||
collectionNameMap={props.collectionNameMap}
|
||||
/>
|
||||
{!props.isIncomingSharedCollection && (
|
||||
<FileInfo
|
||||
isTrashCollection={props.isTrashCollection}
|
||||
showInfo={showInfo}
|
||||
handleCloseInfo={handleCloseInfo}
|
||||
file={photoSwipe?.currItem as EnteFile}
|
||||
exif={exif?.value}
|
||||
scheduleUpdate={scheduleUpdate}
|
||||
refreshPhotoswipe={refreshPhotoswipe}
|
||||
fileToCollectionsMap={props.fileToCollectionsMap}
|
||||
collectionNameMap={props.collectionNameMap}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ export const getAppNameAndTitle = () => {
|
|||
const albumsURL = new URL(getAlbumsURL());
|
||||
const authURL = new URL(getAuthURL());
|
||||
if (currentURL.origin === albumsURL.origin) {
|
||||
return { name: APPS.ALBUMS, title: 'ente Albums' };
|
||||
return { name: APPS.ALBUMS, title: 'ente Photos' };
|
||||
} else if (currentURL.origin === authURL.origin) {
|
||||
return { name: APPS.AUTH, title: 'ente Auth' };
|
||||
} else {
|
||||
|
|
|
@ -4,8 +4,7 @@ export enum REDIRECTS {
|
|||
}
|
||||
|
||||
export const getRedirectURL = (redirect: REDIRECTS) => {
|
||||
// open current app with query param of redirect = roadmap
|
||||
const url = new URL(window.location.href);
|
||||
const url = new URL('https://web.ente.io');
|
||||
url.searchParams.set('redirect', redirect);
|
||||
return url.href;
|
||||
};
|
||||
|
|
38
apps/photos/src/hooks/useMemoSingleThreaded.tsx
Normal file
38
apps/photos/src/hooks/useMemoSingleThreaded.tsx
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export default function useMemoSingleThreaded<T>(
|
||||
fn: () => T | Promise<T>,
|
||||
deps: any[]
|
||||
): T {
|
||||
const [result, setResult] = useState<T>(null);
|
||||
const updateInProgress = useRef(false);
|
||||
const updateRequired = useRef(false);
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (updateInProgress.current) {
|
||||
updateRequired.current = true;
|
||||
return;
|
||||
}
|
||||
updateInProgress.current = true;
|
||||
const result = fn();
|
||||
if (isPromise(result)) {
|
||||
const resultValue = await result;
|
||||
setResult(resultValue);
|
||||
} else {
|
||||
setResult(result);
|
||||
}
|
||||
updateInProgress.current = false;
|
||||
if (updateRequired.current) {
|
||||
updateRequired.current = false;
|
||||
setTimeout(main, 0);
|
||||
}
|
||||
};
|
||||
main();
|
||||
}, deps);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function isPromise<T>(obj: T | Promise<T>): obj is Promise<T> {
|
||||
return obj && typeof (obj as any).then === 'function';
|
||||
}
|
|
@ -3,7 +3,7 @@ import { t } from 'i18next';
|
|||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import { ALL_SECTION } from 'constants/collection';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import React, { createContext, useContext, useEffect, useState } from 'react';
|
||||
import { createContext, useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
getDuplicateFiles,
|
||||
clubDuplicatesByTime,
|
||||
|
@ -13,7 +13,7 @@ import { EnteFile } from 'types/file';
|
|||
import { SelectedState } from 'types/gallery';
|
||||
|
||||
import { ServerErrorCodes } from 'utils/error';
|
||||
import { getSelectedFiles } from 'utils/file';
|
||||
import { constructFileToCollectionMap, getSelectedFiles } from 'utils/file';
|
||||
import {
|
||||
DeduplicateContextType,
|
||||
DefaultDeduplicateContext,
|
||||
|
@ -27,8 +27,8 @@ import { styled } from '@mui/material';
|
|||
import { syncCollections } from 'services/collectionService';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import VerticallyCentered from 'components/Container';
|
||||
import { Collection } from 'types/collection';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import useMemoSingleThreaded from 'hooks/useMemoSingleThreaded';
|
||||
|
||||
export const DeduplicateContext = createContext<DeduplicateContextType>(
|
||||
DefaultDeduplicateContext
|
||||
|
@ -47,7 +47,6 @@ export default function Deduplicate() {
|
|||
setRedirectURL,
|
||||
} = useContext(AppContext);
|
||||
const [duplicateFiles, setDuplicateFiles] = useState<EnteFile[]>(null);
|
||||
const [collections, setCollection] = useState<Collection[]>([]);
|
||||
const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false);
|
||||
const [fileSizeMap, setFileSizeMap] = useState(new Map<number, number>());
|
||||
const [collectionNameMap, setCollectionNameMap] = useState(
|
||||
|
@ -75,10 +74,13 @@ export default function Deduplicate() {
|
|||
syncWithRemote();
|
||||
}, [clubSameTimeFilesOnly]);
|
||||
|
||||
const fileToCollectionsMap = useMemoSingleThreaded(() => {
|
||||
return constructFileToCollectionMap(duplicateFiles);
|
||||
}, [duplicateFiles]);
|
||||
|
||||
const syncWithRemote = async () => {
|
||||
startLoading();
|
||||
const collections = await syncCollections();
|
||||
setCollection(collections);
|
||||
const collectionNameMap = new Map<number, string>();
|
||||
for (const collection of collections) {
|
||||
collectionNameMap.set(collection.id, collection.name);
|
||||
|
@ -189,12 +191,12 @@ export default function Deduplicate() {
|
|||
) : (
|
||||
<PhotoFrame
|
||||
files={duplicateFiles}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
activeCollection={ALL_SECTION}
|
||||
isDeduplicating
|
||||
fileToCollectionsMap={fileToCollectionsMap}
|
||||
collectionNameMap={collectionNameMap}
|
||||
/>
|
||||
)}
|
||||
<DeduplicateOptions
|
||||
|
|
|
@ -39,15 +39,17 @@ import {
|
|||
setIsFirstLogin,
|
||||
setJustSignedUp,
|
||||
} from 'utils/storage';
|
||||
import { isTokenValid, logoutUser, validateKey } from 'services/userService';
|
||||
import { isTokenValid, validateKey } from 'services/userService';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { LoadingOverlay } from 'components/LoadingOverlay';
|
||||
import PhotoFrame from 'components/PhotoFrame';
|
||||
import {
|
||||
changeFilesVisibility,
|
||||
constructFileToCollectionMap,
|
||||
downloadFiles,
|
||||
getSelectedFiles,
|
||||
isSharedFile,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
|
@ -78,8 +80,8 @@ import {
|
|||
getSelectedCollection,
|
||||
getArchivedCollections,
|
||||
hasNonSystemCollections,
|
||||
getSearchableCollections,
|
||||
splitNormalAndHiddenCollections,
|
||||
constructCollectionNameMap,
|
||||
} from 'utils/collection';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { getLocalTrashedFiles, syncTrash } from 'services/trashService';
|
||||
|
@ -113,6 +115,10 @@ import { getToken } from 'utils/common/key';
|
|||
import ExportModal from 'components/ExportModal';
|
||||
import GalleryEmptyState from 'components/GalleryEmptyState';
|
||||
import AuthenticateUserModal from 'components/AuthenticateUserModal';
|
||||
import useMemoSingleThreaded from 'hooks/useMemoSingleThreaded';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
import { isSameDayAnyYear, isInsideBox } from 'utils/search';
|
||||
import { getSessionExpiredMessage } from 'utils/ui';
|
||||
|
||||
export const DeadCenter = styled('div')`
|
||||
flex: 1;
|
||||
|
@ -242,38 +248,6 @@ export default function Gallery() {
|
|||
const closeAuthenticateUserModal = () =>
|
||||
setAuthenticateUserModalView(false);
|
||||
|
||||
const showSessionExpiredMessage = () =>
|
||||
setDialogMessage({
|
||||
title: t('SESSION_EXPIRED'),
|
||||
content: t('SESSION_EXPIRED_MESSAGE'),
|
||||
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
text: t('LOGIN'),
|
||||
action: logoutUser,
|
||||
variant: 'accent',
|
||||
},
|
||||
});
|
||||
|
||||
const displayFiles = useMemo(() => {
|
||||
if (!files || !hiddenFiles || !trashedFiles) {
|
||||
return [];
|
||||
}
|
||||
if (activeCollection === HIDDEN_SECTION) {
|
||||
return hiddenFiles;
|
||||
} else if (activeCollection === TRASH_SECTION) {
|
||||
const markedForDeletionFiles = files.filter((file) =>
|
||||
deletedFileIds.has(file.id)
|
||||
);
|
||||
return [...markedForDeletionFiles, ...trashedFiles];
|
||||
} else {
|
||||
const nonMarkedForDeletion = files.filter(
|
||||
(file) => !deletedFileIds.has(file.id)
|
||||
);
|
||||
return nonMarkedForDeletion;
|
||||
}
|
||||
}, [activeCollection, files, hiddenFiles, trashedFiles]);
|
||||
|
||||
useEffect(() => {
|
||||
appContext.showNavBar(true);
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
|
@ -393,6 +367,146 @@ export default function Gallery() {
|
|||
}
|
||||
}, [isInSearchMode, searchResultSummary]);
|
||||
|
||||
const displayFiles = useMemoSingleThreaded((): EnteFile[] => {
|
||||
if (!files || !hiddenFiles || !trashedFiles) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let displayFiles: EnteFile[] = [];
|
||||
if (activeCollection === HIDDEN_SECTION) {
|
||||
displayFiles = hiddenFiles;
|
||||
} else if (activeCollection === TRASH_SECTION) {
|
||||
const markedForDeletionFiles = files.filter((file) =>
|
||||
deletedFileIds.has(file.id)
|
||||
);
|
||||
displayFiles = [...markedForDeletionFiles, ...trashedFiles];
|
||||
} else {
|
||||
displayFiles = files.filter((item) => {
|
||||
// SEARCH MODE
|
||||
if (isInSearchMode) {
|
||||
if (
|
||||
search?.date &&
|
||||
!isSameDayAnyYear(search.date)(
|
||||
new Date(item.metadata.creationTime / 1000)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.location &&
|
||||
!isInsideBox(
|
||||
{
|
||||
latitude: item.metadata.latitude,
|
||||
longitude: item.metadata.longitude,
|
||||
},
|
||||
search.location
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.person &&
|
||||
search.person.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.thing &&
|
||||
search.thing.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search?.text &&
|
||||
search.text.files.indexOf(item.id) === -1
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (search?.files && search.files.indexOf(item.id) === -1) {
|
||||
return false;
|
||||
}
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// shared files can only be seen in their respective shared collection
|
||||
if (isSharedFile(user, item)) {
|
||||
if (activeCollection === item.collectionID) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Archived files/collection files can only be seen in archive section or their respective collection
|
||||
if (
|
||||
IsArchived(item) ||
|
||||
archivedCollections.has(item.collectionID)
|
||||
) {
|
||||
if (
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ALL SECTION - show all files
|
||||
if (activeCollection === ALL_SECTION) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
|
||||
// COLLECTION SECTION - show files in the active collection
|
||||
if (activeCollection === item.collectionID) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const idSet = new Set();
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
const uniqueFiles = displayFiles.filter((item) => {
|
||||
if (idSet.has(item.id)) {
|
||||
return false;
|
||||
}
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
});
|
||||
|
||||
return uniqueFiles;
|
||||
}, [
|
||||
files,
|
||||
deletedFileIds,
|
||||
search?.date,
|
||||
search?.files,
|
||||
search?.location,
|
||||
search?.person,
|
||||
search?.thing,
|
||||
search?.text,
|
||||
activeCollection,
|
||||
]);
|
||||
|
||||
const fileToCollectionsMap = useMemoSingleThreaded(() => {
|
||||
return constructFileToCollectionMap(files);
|
||||
}, [files]);
|
||||
|
||||
const collectionNameMap = useMemo(() => {
|
||||
return constructCollectionNameMap(collections);
|
||||
}, [collections]);
|
||||
|
||||
const showSessionExpiredMessage = () => {
|
||||
setDialogMessage(getSessionExpiredMessage());
|
||||
};
|
||||
|
||||
const syncWithRemote = async (force = false, silent = false) => {
|
||||
if (syncInProgress.current && !force) {
|
||||
resync.current = { force, silent };
|
||||
|
@ -690,8 +804,6 @@ export default function Gallery() {
|
|||
setExportModalView(false);
|
||||
};
|
||||
|
||||
console.log('rendering gallery', displayFiles);
|
||||
|
||||
return (
|
||||
<GalleryContext.Provider
|
||||
value={{
|
||||
|
@ -752,7 +864,7 @@ export default function Gallery() {
|
|||
setIsInSearchMode={setIsInSearchMode}
|
||||
openUploader={openUploader}
|
||||
isInSearchMode={isInSearchMode}
|
||||
collections={getSearchableCollections(collections)}
|
||||
collections={collections}
|
||||
files={files}
|
||||
setActiveCollection={setActiveCollection}
|
||||
updateSearch={updateSearch}
|
||||
|
@ -815,14 +927,10 @@ export default function Gallery() {
|
|||
) : (
|
||||
<PhotoFrame
|
||||
files={displayFiles}
|
||||
collections={collections}
|
||||
syncWithRemote={syncWithRemote}
|
||||
favItemIds={favItemIds}
|
||||
archivedCollections={archivedCollections}
|
||||
setSelected={setSelected}
|
||||
selected={selected}
|
||||
isInSearchMode={isInSearchMode}
|
||||
search={search}
|
||||
deletedFileIds={deletedFileIds}
|
||||
setDeletedFileIds={setDeletedFileIds}
|
||||
activeCollection={activeCollection}
|
||||
|
@ -831,6 +939,11 @@ export default function Gallery() {
|
|||
CollectionSummaryType.incomingShare
|
||||
}
|
||||
enableDownload={true}
|
||||
fileToCollectionsMap={fileToCollectionsMap}
|
||||
collectionNameMap={collectionNameMap}
|
||||
showAppDownloadBanner={
|
||||
files.length < 30 && !isInSearchMode
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{selected.count > 0 &&
|
||||
|
|
|
@ -406,6 +406,8 @@ export default function PublicCollectionGallery() {
|
|||
publicCollection?.publicURLs?.[0]?.enableDownload ??
|
||||
true
|
||||
}
|
||||
fileToCollectionsMap={null}
|
||||
collectionNameMap={null}
|
||||
/>
|
||||
{blockingLoad && (
|
||||
<LoadingOverlay>
|
||||
|
|
|
@ -59,7 +59,7 @@ import {
|
|||
getCollectionNameMap,
|
||||
getNonEmptyPersonalCollections,
|
||||
} from 'utils/collection';
|
||||
import { migrateExportJSON } from './migration';
|
||||
import { migrateExport } from './migration';
|
||||
|
||||
const EXPORT_RECORD_FILE_NAME = 'export_status.json';
|
||||
|
||||
|
@ -130,10 +130,7 @@ class ExportService {
|
|||
async runMigration(exportDir: string, exportRecord: ExportRecord) {
|
||||
try {
|
||||
addLogLine('running migration');
|
||||
this.migrationInProgress = migrateExportJSON(
|
||||
exportDir,
|
||||
exportRecord
|
||||
);
|
||||
this.migrationInProgress = migrateExport(exportDir, exportRecord);
|
||||
await this.migrationInProgress;
|
||||
addLogLine('migration completed');
|
||||
this.migrationInProgress = null;
|
||||
|
@ -292,6 +289,11 @@ class ExportService {
|
|||
failed: 0,
|
||||
total: 0,
|
||||
});
|
||||
if (this.migrationInProgress) {
|
||||
addLogLine('migration in progress, waiting for it to complete');
|
||||
await this.migrationInProgress;
|
||||
this.migrationInProgress = null;
|
||||
}
|
||||
}
|
||||
|
||||
async postExport() {
|
||||
|
@ -333,11 +335,6 @@ class ExportService {
|
|||
addLogLine('export not in progress, starting export');
|
||||
}
|
||||
this.exportInProgress = true;
|
||||
if (this.migrationInProgress) {
|
||||
addLogLine('migration in progress, waiting for it to complete');
|
||||
await this.migrationInProgress;
|
||||
this.migrationInProgress = null;
|
||||
}
|
||||
try {
|
||||
const exportFolder = this.getExportSettings()?.folder;
|
||||
await this.preExport(exportFolder);
|
||||
|
|
|
@ -39,29 +39,7 @@ import { decodeLivePhoto } from 'services/livePhotoService';
|
|||
import downloadManager from 'services/downloadManager';
|
||||
import { retryAsyncFunction } from 'utils/network';
|
||||
|
||||
export async function migrateExportJSON(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecord
|
||||
) {
|
||||
try {
|
||||
if (!exportDir) {
|
||||
return;
|
||||
}
|
||||
await migrateExport(exportDir, exportRecord);
|
||||
} catch (e) {
|
||||
logError(e, 'migrateExportJSON failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
this function migrates the exportRecord file to apply any schema changes.
|
||||
currently we apply only a single migration to update file and collection name to newer format
|
||||
so there is just a if condition check,
|
||||
later this will be converted to a loop which applies the migration one by one
|
||||
till the files reaches the latest version
|
||||
*/
|
||||
async function migrateExport(
|
||||
export async function migrateExport(
|
||||
exportDir: string,
|
||||
exportRecord: ExportRecordV1 | ExportRecordV2 | ExportRecord
|
||||
) {
|
||||
|
@ -94,6 +72,7 @@ async function migrateExport(
|
|||
addLogLine(`Record at latest version`);
|
||||
} catch (e) {
|
||||
logError(e, 'export record migration failed');
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -305,12 +305,6 @@ export function getNonEmptyPersonalCollections(
|
|||
return personalCollections;
|
||||
}
|
||||
|
||||
export function getSearchableCollections(
|
||||
collections: Collection[]
|
||||
): Collection[] {
|
||||
return collections.filter((collection) => !isHiddenCollection(collection));
|
||||
}
|
||||
|
||||
export function getNonHiddenCollections(
|
||||
collections: Collection[]
|
||||
): Collection[] {
|
||||
|
@ -334,3 +328,14 @@ export async function splitNormalAndHiddenCollections(
|
|||
}
|
||||
return { normalCollections, hiddenCollections };
|
||||
}
|
||||
|
||||
export function constructCollectionNameMap(
|
||||
collections: Collection[]
|
||||
): Map<number, string> {
|
||||
return new Map<number, string>(
|
||||
(collections ?? []).map((collection) => [
|
||||
collection.id,
|
||||
collection.name,
|
||||
])
|
||||
);
|
||||
}
|
||||
|
|
|
@ -579,3 +579,14 @@ export function getPersonalFiles(files: EnteFile[], user: User) {
|
|||
export function getIDBasedSortedFiles(files: EnteFile[]) {
|
||||
return files.sort((a, b) => a.id - b.id);
|
||||
}
|
||||
|
||||
export function constructFileToCollectionMap(files: EnteFile[]) {
|
||||
const fileToCollectionsMap = new Map<number, number[]>();
|
||||
(files ?? []).forEach((file) => {
|
||||
if (!fileToCollectionsMap.get(file.id)) {
|
||||
fileToCollectionsMap.set(file.id, []);
|
||||
}
|
||||
fileToCollectionsMap.get(file.id).push(file.collectionID);
|
||||
});
|
||||
return fileToCollectionsMap;
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ import { AppUpdateInfo } from 'types/electron';
|
|||
import InfoOutlined from '@mui/icons-material/InfoRounded';
|
||||
import { Trans } from 'react-i18next';
|
||||
import { Subscription } from 'types/billing';
|
||||
import { logoutUser } from 'services/userService';
|
||||
export const getDownloadAppMessage = (): DialogBoxAttributes => {
|
||||
return {
|
||||
title: t('DOWNLOAD_APP'),
|
||||
|
@ -116,3 +117,15 @@ export const getSubscriptionPurchaseSuccessMessage = (
|
|||
/>
|
||||
),
|
||||
});
|
||||
|
||||
export const getSessionExpiredMessage = (): DialogBoxAttributes => ({
|
||||
title: t('SESSION_EXPIRED'),
|
||||
content: t('SESSION_EXPIRED_MESSAGE'),
|
||||
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
text: t('LOGIN'),
|
||||
action: logoutUser,
|
||||
variant: 'accent',
|
||||
},
|
||||
});
|
||||
|
|
15
package.json
15
package.json
|
@ -27,5 +27,18 @@
|
|||
"workspaces": [
|
||||
"packages/*",
|
||||
"apps/*"
|
||||
]
|
||||
],
|
||||
"lint-staged": {
|
||||
"apps/**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write --ignore-unknown"
|
||||
],
|
||||
"packages/**/*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
"prettier --write --ignore-unknown"
|
||||
],
|
||||
"**/*.{json,css,scss,md,html,yml,yaml}": [
|
||||
"prettier --write --ignore-unknown"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue