Merge branch 'master' into update-file-title
This commit is contained in:
commit
91825db617
|
@ -1,5 +1,4 @@
|
|||
import {
|
||||
DeadCenter,
|
||||
GalleryContext,
|
||||
Search,
|
||||
SelectedState,
|
||||
|
@ -14,18 +13,9 @@ import styled from 'styled-components';
|
|||
import DownloadManager from 'services/downloadManager';
|
||||
import constants from 'utils/strings/constants';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||
import { SetDialogMessage } from './MessageDialog';
|
||||
import {
|
||||
GAP_BTW_TILES,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
MIN_COLUMNS,
|
||||
SPACE_BTW_DATES,
|
||||
} from 'types';
|
||||
import { fileIsArchived, formatDateRelative } from 'utils/file';
|
||||
import {
|
||||
ALL_SECTION,
|
||||
|
@ -34,24 +24,7 @@ import {
|
|||
} from './pages/gallery/Collections';
|
||||
import { isSharedFile } from 'utils/file';
|
||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||
|
||||
const NO_OF_PAGES = 2;
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: File[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
dates?: {
|
||||
date: string;
|
||||
span: number;
|
||||
}[];
|
||||
groups?: number[];
|
||||
banner?: any;
|
||||
id?: string;
|
||||
height?: number;
|
||||
}
|
||||
import { PhotoList } from './PhotoList';
|
||||
|
||||
const Container = styled.div`
|
||||
display: block;
|
||||
|
@ -66,60 +39,6 @@ const Container = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
const ListItem = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
||||
if (groups) {
|
||||
const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
if (sum < columns) {
|
||||
groups[groups.length - 1] += columns - sum;
|
||||
}
|
||||
return groups
|
||||
.map((x) => `repeat(${x}, 1fr)`)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns}, 1fr)`;
|
||||
}
|
||||
};
|
||||
|
||||
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
||||
user-select: none;
|
||||
display: grid;
|
||||
grid-template-columns: ${({ columns, groups }) =>
|
||||
getTemplateColumns(columns, groups)};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const DateContainer = styled.div<{ span: number }>`
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-column: span ${(props) => props.span};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
`;
|
||||
|
||||
const BannerContainer = styled.div<{ span: number }>`
|
||||
color: #979797;
|
||||
text-align: center;
|
||||
grid-column: span ${(props) => props.span};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
const EmptyScreen = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
@ -133,12 +52,6 @@ const EmptyScreen = styled.div`
|
|||
}
|
||||
`;
|
||||
|
||||
enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
TILE = 'TILE',
|
||||
BANNER = 'BANNER',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
files: File[];
|
||||
setFiles: SetFiles;
|
||||
|
@ -182,11 +95,11 @@ const PhotoFrame = ({
|
|||
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
||||
const startTime = Date.now();
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const listRef = useRef(null);
|
||||
const [rangeStart, setRangeStart] = useState(null);
|
||||
const [currentHover, setCurrentHover] = useState(null);
|
||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||
|
||||
const filteredDataRef = useRef([]);
|
||||
const filteredData = filteredDataRef?.current ?? [];
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Shift') {
|
||||
|
@ -223,10 +136,9 @@ const PhotoFrame = ({
|
|||
}
|
||||
}, [search]);
|
||||
|
||||
useEffect(() => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
const resetFetching = () => {
|
||||
setFetching({});
|
||||
}, [files, search, deleted]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (selected.count === 0) {
|
||||
|
@ -234,6 +146,71 @@ const PhotoFrame = ({
|
|||
}
|
||||
}, [selected]);
|
||||
|
||||
useEffect(() => {
|
||||
const idSet = new Set();
|
||||
filteredDataRef.current = files
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
...(item.deleteBy && {
|
||||
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
|
||||
formatDateRelative(item.deleteBy / 1000)
|
||||
),
|
||||
}),
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (deleted.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search.date &&
|
||||
!isSameDayAnyYear(search.date)(
|
||||
new Date(item.metadata.creationTime / 1000)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search.location &&
|
||||
!isInsideBox(item.metadata, search.location)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === ALL_SECTION && fileIsArchived(item)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
activeCollection === ARCHIVE_SECTION &&
|
||||
!fileIsArchived(item)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSharedFile(item) && !isSharedCollection) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection !== TRASH_SECTION && item.isTrashed) {
|
||||
return false;
|
||||
}
|
||||
if (!idSet.has(item.id)) {
|
||||
if (
|
||||
activeCollection === ALL_SECTION ||
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === TRASH_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [files, deleted, search, activeCollection]);
|
||||
|
||||
const updateUrl = (index: number) => (url: string) => {
|
||||
files[index] = {
|
||||
...files[index],
|
||||
|
@ -310,9 +287,7 @@ const PhotoFrame = ({
|
|||
if (selected.collectionID !== activeCollection) {
|
||||
setSelected({ count: 0, collectionID: 0 });
|
||||
}
|
||||
if (rangeStart || rangeStart === 0) {
|
||||
setRangeStart(null);
|
||||
} else if (checked) {
|
||||
if (checked) {
|
||||
setRangeStart(index);
|
||||
}
|
||||
|
||||
|
@ -332,11 +307,11 @@ const PhotoFrame = ({
|
|||
let leftEnd = -1;
|
||||
let rightEnd = -1;
|
||||
if (index < rangeStart) {
|
||||
leftEnd = index;
|
||||
rightEnd = rangeStart;
|
||||
leftEnd = index + 1;
|
||||
rightEnd = rangeStart - 1;
|
||||
} else {
|
||||
leftEnd = rangeStart;
|
||||
rightEnd = index;
|
||||
leftEnd = rangeStart + 1;
|
||||
rightEnd = index - 1;
|
||||
}
|
||||
for (let i = leftEnd; i <= rightEnd; i++) {
|
||||
handleSelect(filteredData[i].id)(true);
|
||||
|
@ -364,8 +339,8 @@ const PhotoFrame = ({
|
|||
isShiftKeyPressed && (rangeStart || rangeStart === 0)
|
||||
}
|
||||
isInsSelectRange={
|
||||
(index >= rangeStart + 1 && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart - 1)
|
||||
(index >= rangeStart && index <= currentHover) ||
|
||||
(index >= currentHover && index <= rangeStart)
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
@ -426,149 +401,6 @@ const PhotoFrame = ({
|
|||
}
|
||||
};
|
||||
|
||||
const idSet = new Set();
|
||||
const filteredData = files
|
||||
.map((item, index) => ({
|
||||
...item,
|
||||
dataIndex: index,
|
||||
...(item.deleteBy && {
|
||||
title: constants.AUTOMATIC_BIN_DELETE_MESSAGE(
|
||||
formatDateRelative(item.deleteBy / 1000)
|
||||
),
|
||||
}),
|
||||
}))
|
||||
.filter((item) => {
|
||||
if (deleted.includes(item.id)) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search.date &&
|
||||
!isSameDayAnyYear(search.date)(
|
||||
new Date(item.metadata.creationTime / 1000)
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
search.location &&
|
||||
!isInsideBox(item.metadata, search.location)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === ALL_SECTION && fileIsArchived(item)) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === ARCHIVE_SECTION && !fileIsArchived(item)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isSharedFile(item) && !isSharedCollection) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection === TRASH_SECTION && !item.isTrashed) {
|
||||
return false;
|
||||
}
|
||||
if (activeCollection !== TRASH_SECTION && item.isTrashed) {
|
||||
return false;
|
||||
}
|
||||
if (!idSet.has(item.id)) {
|
||||
if (
|
||||
activeCollection === ALL_SECTION ||
|
||||
activeCollection === ARCHIVE_SECTION ||
|
||||
activeCollection === TRASH_SECTION ||
|
||||
activeCollection === item.collectionID
|
||||
) {
|
||||
idSet.add(item.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate();
|
||||
|
||||
/**
|
||||
* Checks and merge multiple dates into a single row.
|
||||
*
|
||||
* @param items
|
||||
* @param columns
|
||||
* @returns
|
||||
*/
|
||||
const mergeTimeStampList = (
|
||||
items: TimeStampListItem[],
|
||||
columns: number
|
||||
): TimeStampListItem[] => {
|
||||
const newList: TimeStampListItem[] = [];
|
||||
let index = 0;
|
||||
let newIndex = 0;
|
||||
while (index < items.length) {
|
||||
const currItem = items[index];
|
||||
// If the current item is of type time, then it is not part of an ongoing date.
|
||||
// So, there is a possibility of merge.
|
||||
if (currItem.itemType === ITEM_TYPE.TIME) {
|
||||
// If new list pointer is not at the end of list then
|
||||
// we can add more items to the same list.
|
||||
if (newList[newIndex]) {
|
||||
// Check if items can be added to same list
|
||||
if (
|
||||
newList[newIndex + 1].items.length +
|
||||
items[index + 1].items.length <=
|
||||
columns
|
||||
) {
|
||||
newList[newIndex].dates.push({
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
});
|
||||
newList[newIndex + 1].items = newList[
|
||||
newIndex + 1
|
||||
].items.concat(items[index + 1].items);
|
||||
index += 2;
|
||||
} else {
|
||||
// Adding items would exceed the number of columns.
|
||||
// So, move new list pointer to the end. Hence, in next iteration,
|
||||
// items will be added to a new list.
|
||||
newIndex += 2;
|
||||
}
|
||||
} else {
|
||||
// New list pointer was at the end of list so simply add new items to the list.
|
||||
newList.push({
|
||||
...currItem,
|
||||
date: null,
|
||||
dates: [
|
||||
{
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
},
|
||||
],
|
||||
});
|
||||
newList.push(items[index + 1]);
|
||||
index += 2;
|
||||
}
|
||||
} else {
|
||||
// Merge cannot happen. Simply add all items to new list
|
||||
// and set new list point to the end of list.
|
||||
newList.push(currItem);
|
||||
index++;
|
||||
newIndex = newList.length;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
const currItem = newList[i];
|
||||
const nextItem = newList[i + 1];
|
||||
if (currItem.itemType === ITEM_TYPE.TIME) {
|
||||
if (currItem.dates.length > 1) {
|
||||
currItem.groups = currItem.dates.map((item) => item.span);
|
||||
nextItem.groups = currItem.groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
||||
|
@ -591,217 +423,22 @@ const PhotoFrame = ({
|
|||
{constants.UPLOAD_FIRST_PHOTO}
|
||||
</Button>
|
||||
</EmptyScreen>
|
||||
) : filteredData.length ? (
|
||||
) : (
|
||||
<Container>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => {
|
||||
let columns = Math.floor(
|
||||
width / IMAGE_CONTAINER_MAX_WIDTH
|
||||
);
|
||||
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
|
||||
let skipMerge = false;
|
||||
if (columns < MIN_COLUMNS) {
|
||||
columns = MIN_COLUMNS;
|
||||
listItemHeight = width / MIN_COLUMNS;
|
||||
skipMerge = true;
|
||||
}
|
||||
|
||||
let timeStampList: TimeStampListItem[] = [];
|
||||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
filteredData.forEach((item, index) => {
|
||||
if (
|
||||
!isSameDay(
|
||||
new Date(
|
||||
item.metadata.creationTime / 1000
|
||||
),
|
||||
new Date(currentDate)
|
||||
)
|
||||
) {
|
||||
currentDate =
|
||||
item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat =
|
||||
new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date()
|
||||
)
|
||||
? 'Today'
|
||||
: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(Date.now() - A_DAY)
|
||||
)
|
||||
? 'Yesterday'
|
||||
: dateTimeFormat.format(
|
||||
currentDate
|
||||
),
|
||||
id: currentDate.toString(),
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
listItemIndex = 1;
|
||||
} else if (listItemIndex < columns) {
|
||||
timeStampList[
|
||||
timeStampList.length - 1
|
||||
].items.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipMerge) {
|
||||
timeStampList = mergeTimeStampList(
|
||||
timeStampList,
|
||||
columns
|
||||
);
|
||||
}
|
||||
|
||||
const getItemSize = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return DATE_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.TILE:
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index].height;
|
||||
}
|
||||
};
|
||||
|
||||
const photoFrameHeight = (() => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < timeStampList.length; i++) {
|
||||
sum += getItemSize(i);
|
||||
}
|
||||
return sum;
|
||||
})();
|
||||
files.length < 30 &&
|
||||
!isInSearchMode &&
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
<BannerContainer span={columns}>
|
||||
<p>
|
||||
{constants.INSTALL_MOBILE_APP()}
|
||||
</p>
|
||||
</BannerContainer>
|
||||
),
|
||||
id: 'install-banner',
|
||||
height: Math.max(
|
||||
48,
|
||||
height - photoFrameHeight
|
||||
),
|
||||
});
|
||||
const extraRowsToRender = Math.ceil(
|
||||
(NO_OF_PAGES * height) /
|
||||
IMAGE_CONTAINER_MAX_HEIGHT
|
||||
);
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TILE:
|
||||
return `${
|
||||
timeStampList[index].items[0].id
|
||||
}-${
|
||||
timeStampList[index].items.slice(
|
||||
-1
|
||||
)[0].id
|
||||
}`;
|
||||
default:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderListItem = (
|
||||
listItem: TimeStampListItem
|
||||
) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return listItem.dates ? (
|
||||
listItem.dates.map((item) => (
|
||||
<>
|
||||
<DateContainer
|
||||
key={item.date}
|
||||
span={item.span}>
|
||||
{item.date}
|
||||
</DateContainer>
|
||||
<div />
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<DateContainer span={columns}>
|
||||
{listItem.date}
|
||||
</DateContainer>
|
||||
);
|
||||
case ITEM_TYPE.BANNER:
|
||||
return listItem.banner;
|
||||
default: {
|
||||
const ret = listItem.items.map(
|
||||
(item, idx) =>
|
||||
getThumbnail(
|
||||
filteredData,
|
||||
listItem.itemStartIndex +
|
||||
idx
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
let sum = 0;
|
||||
for (
|
||||
let i = 0;
|
||||
i < listItem.groups.length - 1;
|
||||
i++
|
||||
) {
|
||||
sum = sum + listItem.groups[i];
|
||||
ret.splice(sum, 0, <div />);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
key={`${columns}-${listItemHeight}-${activeCollection}`}
|
||||
ref={listRef}
|
||||
itemSize={getItemSize}
|
||||
height={height}
|
||||
{({ height, width }) => (
|
||||
<PhotoList
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={extraRowsToRender}>
|
||||
{({ index, style }) => (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
groups={
|
||||
timeStampList[index].groups
|
||||
}>
|
||||
{renderListItem(
|
||||
timeStampList[index]
|
||||
height={height}
|
||||
getThumbnail={getThumbnail}
|
||||
filteredData={filteredData}
|
||||
activeCollection={activeCollection}
|
||||
showBanner={
|
||||
files.length < 30 && !isInSearchMode
|
||||
}
|
||||
resetFetching={resetFetching}
|
||||
/>
|
||||
)}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
<PhotoSwipe
|
||||
isOpen={open}
|
||||
|
@ -815,10 +452,6 @@ const PhotoFrame = ({
|
|||
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||
/>
|
||||
</Container>
|
||||
) : (
|
||||
<DeadCenter>
|
||||
<div>{constants.NOTHING_HERE}</div>
|
||||
</DeadCenter>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
409
src/components/PhotoList.tsx
Normal file
409
src/components/PhotoList.tsx
Normal file
|
@ -0,0 +1,409 @@
|
|||
import React, { useRef, useEffect } from 'react';
|
||||
import { VariableSizeList as List } from 'react-window';
|
||||
import styled from 'styled-components';
|
||||
import { File } from 'services/fileService';
|
||||
import {
|
||||
IMAGE_CONTAINER_MAX_WIDTH,
|
||||
IMAGE_CONTAINER_MAX_HEIGHT,
|
||||
MIN_COLUMNS,
|
||||
DATE_CONTAINER_HEIGHT,
|
||||
GAP_BTW_TILES,
|
||||
SPACE_BTW_DATES,
|
||||
} from 'types';
|
||||
import constants from 'utils/strings/constants';
|
||||
|
||||
const A_DAY = 24 * 60 * 60 * 1000;
|
||||
const NO_OF_PAGES = 2;
|
||||
|
||||
enum ITEM_TYPE {
|
||||
TIME = 'TIME',
|
||||
TILE = 'TILE',
|
||||
BANNER = 'BANNER',
|
||||
}
|
||||
|
||||
interface TimeStampListItem {
|
||||
itemType: ITEM_TYPE;
|
||||
items?: File[];
|
||||
itemStartIndex?: number;
|
||||
date?: string;
|
||||
dates?: {
|
||||
date: string;
|
||||
span: number;
|
||||
}[];
|
||||
groups?: number[];
|
||||
banner?: any;
|
||||
id?: string;
|
||||
height?: number;
|
||||
}
|
||||
|
||||
const ListItem = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const getTemplateColumns = (columns: number, groups?: number[]): string => {
|
||||
if (groups) {
|
||||
const sum = groups.reduce((acc, item) => acc + item, 0);
|
||||
if (sum < columns) {
|
||||
groups[groups.length - 1] += columns - sum;
|
||||
}
|
||||
return groups
|
||||
.map((x) => `repeat(${x}, 1fr)`)
|
||||
.join(` ${SPACE_BTW_DATES}px `);
|
||||
} else {
|
||||
return `repeat(${columns}, 1fr)`;
|
||||
}
|
||||
};
|
||||
|
||||
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
|
||||
user-select: none;
|
||||
display: grid;
|
||||
grid-template-columns: ${({ columns, groups }) =>
|
||||
getTemplateColumns(columns, groups)};
|
||||
grid-column-gap: ${GAP_BTW_TILES}px;
|
||||
padding: 0 24px;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const DateContainer = styled.div<{ span: number }>`
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
grid-column: span ${(props) => props.span};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: ${DATE_CONTAINER_HEIGHT}px;
|
||||
`;
|
||||
|
||||
const BannerContainer = styled.div<{ span: number }>`
|
||||
color: #979797;
|
||||
text-align: center;
|
||||
grid-column: span ${(props) => props.span};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
`;
|
||||
|
||||
const NothingContainer = styled.div<{ span: number }>`
|
||||
color: #979797;
|
||||
text-align: center;
|
||||
grid-column: span ${(props) => props.span};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
height: number;
|
||||
width: number;
|
||||
filteredData: File[];
|
||||
showBanner: boolean;
|
||||
getThumbnail: (file: File[], index: number) => JSX.Element;
|
||||
activeCollection: number;
|
||||
resetFetching: () => void;
|
||||
}
|
||||
|
||||
export function PhotoList({
|
||||
height,
|
||||
width,
|
||||
filteredData,
|
||||
showBanner,
|
||||
getThumbnail,
|
||||
activeCollection,
|
||||
resetFetching,
|
||||
}: Props) {
|
||||
const timeStampListRef = useRef([]);
|
||||
const timeStampList = timeStampListRef?.current ?? [];
|
||||
const filteredDataCopyRef = useRef([]);
|
||||
const filteredDataCopy = filteredDataCopyRef.current ?? [];
|
||||
const listRef = useRef(null);
|
||||
|
||||
let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH);
|
||||
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
|
||||
|
||||
let skipMerge = false;
|
||||
if (columns < MIN_COLUMNS) {
|
||||
columns = MIN_COLUMNS;
|
||||
listItemHeight = width / MIN_COLUMNS;
|
||||
skipMerge = true;
|
||||
}
|
||||
|
||||
const refreshList = () => {
|
||||
listRef.current?.resetAfterIndex(0);
|
||||
resetFetching();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let timeStampList: TimeStampListItem[] = [];
|
||||
let listItemIndex = 0;
|
||||
let currentDate = -1;
|
||||
|
||||
filteredData.forEach((item, index) => {
|
||||
if (
|
||||
!isSameDay(
|
||||
new Date(item.metadata.creationTime / 1000),
|
||||
new Date(currentDate)
|
||||
)
|
||||
) {
|
||||
currentDate = item.metadata.creationTime / 1000;
|
||||
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
|
||||
weekday: 'short',
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TIME,
|
||||
date: isSameDay(new Date(currentDate), new Date())
|
||||
? 'Today'
|
||||
: isSameDay(
|
||||
new Date(currentDate),
|
||||
new Date(Date.now() - A_DAY)
|
||||
)
|
||||
? 'Yesterday'
|
||||
: dateTimeFormat.format(currentDate),
|
||||
id: currentDate.toString(),
|
||||
});
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
listItemIndex = 1;
|
||||
} else if (listItemIndex < columns) {
|
||||
timeStampList[timeStampList.length - 1].items.push(item);
|
||||
listItemIndex++;
|
||||
} else {
|
||||
listItemIndex = 1;
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.TILE,
|
||||
items: [item],
|
||||
itemStartIndex: index,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (!skipMerge) {
|
||||
timeStampList = mergeTimeStampList(timeStampList, columns);
|
||||
}
|
||||
if (timeStampList.length === 0) {
|
||||
timeStampList.push(getEmptyListItem());
|
||||
}
|
||||
if (showBanner) {
|
||||
timeStampList.push(getBannerItem(timeStampList));
|
||||
}
|
||||
|
||||
timeStampListRef.current = timeStampList;
|
||||
filteredDataCopyRef.current = filteredData;
|
||||
refreshList();
|
||||
}, [width, height, filteredData, showBanner]);
|
||||
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate();
|
||||
|
||||
const getEmptyListItem = () => {
|
||||
return {
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
<NothingContainer span={columns}>
|
||||
<div>{constants.NOTHING_HERE}</div>
|
||||
</NothingContainer>
|
||||
),
|
||||
id: 'empty-list-banner',
|
||||
height: height - 48,
|
||||
};
|
||||
};
|
||||
|
||||
const getBannerItem = (timeStampList) => {
|
||||
const photoFrameHeight = (() => {
|
||||
let sum = 0;
|
||||
const getCurrentItemSize = getItemSize(timeStampList);
|
||||
for (let i = 0; i < timeStampList.length; i++) {
|
||||
sum += getCurrentItemSize(i);
|
||||
}
|
||||
return sum;
|
||||
})();
|
||||
return {
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
<BannerContainer span={columns}>
|
||||
<p>{constants.INSTALL_MOBILE_APP()}</p>
|
||||
</BannerContainer>
|
||||
),
|
||||
id: 'install-banner',
|
||||
height: Math.max(48, height - photoFrameHeight),
|
||||
};
|
||||
};
|
||||
/**
|
||||
* Checks and merge multiple dates into a single row.
|
||||
*
|
||||
* @param items
|
||||
* @param columns
|
||||
* @returns
|
||||
*/
|
||||
const mergeTimeStampList = (
|
||||
items: TimeStampListItem[],
|
||||
columns: number
|
||||
): TimeStampListItem[] => {
|
||||
const newList: TimeStampListItem[] = [];
|
||||
let index = 0;
|
||||
let newIndex = 0;
|
||||
while (index < items.length) {
|
||||
const currItem = items[index];
|
||||
// If the current item is of type time, then it is not part of an ongoing date.
|
||||
// So, there is a possibility of merge.
|
||||
if (currItem.itemType === ITEM_TYPE.TIME) {
|
||||
// If new list pointer is not at the end of list then
|
||||
// we can add more items to the same list.
|
||||
if (newList[newIndex]) {
|
||||
// Check if items can be added to same list
|
||||
if (
|
||||
newList[newIndex + 1].items.length +
|
||||
items[index + 1].items.length <=
|
||||
columns
|
||||
) {
|
||||
newList[newIndex].dates.push({
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
});
|
||||
newList[newIndex + 1].items = newList[
|
||||
newIndex + 1
|
||||
].items.concat(items[index + 1].items);
|
||||
index += 2;
|
||||
} else {
|
||||
// Adding items would exceed the number of columns.
|
||||
// So, move new list pointer to the end. Hence, in next iteration,
|
||||
// items will be added to a new list.
|
||||
newIndex += 2;
|
||||
}
|
||||
} else {
|
||||
// New list pointer was at the end of list so simply add new items to the list.
|
||||
newList.push({
|
||||
...currItem,
|
||||
date: null,
|
||||
dates: [
|
||||
{
|
||||
date: currItem.date,
|
||||
span: items[index + 1].items.length,
|
||||
},
|
||||
],
|
||||
});
|
||||
newList.push(items[index + 1]);
|
||||
index += 2;
|
||||
}
|
||||
} else {
|
||||
// Merge cannot happen. Simply add all items to new list
|
||||
// and set new list point to the end of list.
|
||||
newList.push(currItem);
|
||||
index++;
|
||||
newIndex = newList.length;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < newList.length; i++) {
|
||||
const currItem = newList[i];
|
||||
const nextItem = newList[i + 1];
|
||||
if (currItem.itemType === ITEM_TYPE.TIME) {
|
||||
if (currItem.dates.length > 1) {
|
||||
currItem.groups = currItem.dates.map((item) => item.span);
|
||||
nextItem.groups = currItem.groups;
|
||||
}
|
||||
}
|
||||
}
|
||||
return newList;
|
||||
};
|
||||
|
||||
const getItemSize = (timeStampList) => (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return DATE_CONTAINER_HEIGHT;
|
||||
case ITEM_TYPE.TILE:
|
||||
return listItemHeight;
|
||||
default:
|
||||
return timeStampList[index].height;
|
||||
}
|
||||
};
|
||||
|
||||
const extraRowsToRender = Math.ceil(
|
||||
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT
|
||||
);
|
||||
|
||||
const generateKey = (index) => {
|
||||
switch (timeStampList[index].itemType) {
|
||||
case ITEM_TYPE.TILE:
|
||||
return `${timeStampList[index].items[0].id}-${
|
||||
timeStampList[index].items.slice(-1)[0].id
|
||||
}`;
|
||||
default:
|
||||
return `${timeStampList[index].id}-${index}`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderListItem = (listItem: TimeStampListItem) => {
|
||||
switch (listItem.itemType) {
|
||||
case ITEM_TYPE.TIME:
|
||||
return listItem.dates ? (
|
||||
listItem.dates.map((item) => (
|
||||
<>
|
||||
<DateContainer key={item.date} span={item.span}>
|
||||
{item.date}
|
||||
</DateContainer>
|
||||
<div />
|
||||
</>
|
||||
))
|
||||
) : (
|
||||
<DateContainer span={columns}>
|
||||
{listItem.date}
|
||||
</DateContainer>
|
||||
);
|
||||
case ITEM_TYPE.BANNER:
|
||||
return listItem.banner;
|
||||
default: {
|
||||
const ret = listItem.items.map((item, idx) =>
|
||||
getThumbnail(
|
||||
filteredDataCopy,
|
||||
listItem.itemStartIndex + idx
|
||||
)
|
||||
);
|
||||
if (listItem.groups) {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < listItem.groups.length - 1; i++) {
|
||||
sum = sum + listItem.groups[i];
|
||||
ret.splice(sum, 0, <div />);
|
||||
sum += 1;
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<List
|
||||
key={`${activeCollection}`}
|
||||
ref={listRef}
|
||||
itemSize={getItemSize(timeStampList)}
|
||||
height={height}
|
||||
width={width}
|
||||
itemCount={timeStampList.length}
|
||||
itemKey={generateKey}
|
||||
overscanCount={extraRowsToRender}>
|
||||
{({ index, style }) => (
|
||||
<ListItem style={style}>
|
||||
<ListContainer
|
||||
columns={columns}
|
||||
groups={timeStampList[index].groups}>
|
||||
{renderListItem(timeStampList[index])}
|
||||
</ListContainer>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}
|
|
@ -8,8 +8,10 @@ import {
|
|||
removeFromFavorites,
|
||||
} from 'services/collectionService';
|
||||
import {
|
||||
ALL_TIME,
|
||||
File,
|
||||
MAX_EDITED_FILE_NAME_LENGTH,
|
||||
MAX_EDITED_CREATION_TIME,
|
||||
MIN_EDITED_CREATION_TIME,
|
||||
updatePublicMagicMetadata,
|
||||
} from 'services/fileService';
|
||||
|
@ -40,8 +42,8 @@ import { logError } from 'utils/sentry';
|
|||
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
import CloseIcon from 'components/icons/CloseIcon';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
|
@ -73,11 +75,6 @@ const Pre = styled.pre`
|
|||
padding: 7px 15px;
|
||||
`;
|
||||
|
||||
const ButtonContainer = styled.div`
|
||||
margin-left: auto;
|
||||
width: 200px;
|
||||
padding: 5px 10px;
|
||||
`;
|
||||
const WarningMessage = styled.div`
|
||||
width: 100%;
|
||||
margin-top: 0.25rem;
|
||||
|
@ -92,6 +89,11 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
|||
</Row>
|
||||
);
|
||||
|
||||
const isSameDay = (first, second) =>
|
||||
first.getFullYear() === second.getFullYear() &&
|
||||
first.getMonth() === second.getMonth() &&
|
||||
first.getDate() === second.getDate();
|
||||
|
||||
function RenderCreationTime({
|
||||
file,
|
||||
scheduleUpdate,
|
||||
|
@ -106,6 +108,7 @@ function RenderCreationTime({
|
|||
|
||||
const openEditMode = () => setIsInEditMode(true);
|
||||
const closeEditMode = () => setIsInEditMode(false);
|
||||
|
||||
const saveEdits = async () => {
|
||||
try {
|
||||
if (isInEditMode && file) {
|
||||
|
@ -150,35 +153,18 @@ function RenderCreationTime({
|
|||
onChange={handleChange}
|
||||
timeInputLabel="Time:"
|
||||
dateFormat="dd/MM/yyyy h:mm aa"
|
||||
showTimeInput
|
||||
showTimeSelect
|
||||
autoFocus
|
||||
shouldCloseOnSelect={false}
|
||||
onClickOutside={discardEdits}
|
||||
minDate={new Date(MIN_EDITED_CREATION_TIME)}
|
||||
maxDate={new Date()}
|
||||
showYearDropdown
|
||||
showMonthDropdown
|
||||
withPortal>
|
||||
<ButtonContainer
|
||||
style={{
|
||||
marginLeft: 'auto',
|
||||
width: '200px',
|
||||
justifyContent: 'flex-end',
|
||||
padding: '5px 10px ',
|
||||
}}>
|
||||
<Button
|
||||
style={{ marginRight: '20px' }}
|
||||
variant="outline-secondary"
|
||||
onClick={discardEdits}>
|
||||
{constants.CANCEL}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-success"
|
||||
onClick={saveEdits}>
|
||||
{constants.SAVE}
|
||||
</Button>
|
||||
</ButtonContainer>
|
||||
</DatePicker>
|
||||
minDate={MIN_EDITED_CREATION_TIME}
|
||||
maxDate={MAX_EDITED_CREATION_TIME}
|
||||
maxTime={
|
||||
isSameDay(pickedTime, new Date())
|
||||
? MAX_EDITED_CREATION_TIME
|
||||
: ALL_TIME
|
||||
}
|
||||
minTime={MIN_EDITED_CREATION_TIME}
|
||||
fixedHeight
|
||||
withPortal></DatePicker>
|
||||
) : (
|
||||
formatDateTime(pickedTime)
|
||||
)}
|
||||
|
@ -186,9 +172,20 @@ function RenderCreationTime({
|
|||
<Value
|
||||
width={isInEditMode ? '20%' : '10%'}
|
||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||
{!isInEditMode ? (
|
||||
<IconButton onClick={openEditMode}>
|
||||
<EditIcon />
|
||||
</IconButton>
|
||||
) : (
|
||||
<>
|
||||
<IconButton onClick={saveEdits}>
|
||||
<TickIcon />
|
||||
</IconButton>
|
||||
<IconButton onClick={discardEdits}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
</Value>
|
||||
</Row>
|
||||
</>
|
||||
|
|
|
@ -14,7 +14,7 @@ export default function TickIcon(props) {
|
|||
}
|
||||
|
||||
TickIcon.defaultProps = {
|
||||
height: 28,
|
||||
height: 20,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Label, Value } from 'components/Container';
|
||||
import { Value } from 'components/Container';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
import React from 'react';
|
||||
import { ListGroup, Popover, Row } from 'react-bootstrap';
|
||||
|
@ -23,13 +23,13 @@ const SortByOptionCreator =
|
|||
(
|
||||
<MenuItem>
|
||||
<Row>
|
||||
<Label width="20px">
|
||||
<Value width="20px">
|
||||
{activeSortBy === props.sortBy && (
|
||||
<TickWrapper>
|
||||
<TickIcon />
|
||||
</TickWrapper>
|
||||
)}
|
||||
</Label>
|
||||
</Value>
|
||||
<Value width="165px">
|
||||
<MenuLink
|
||||
onClick={() => setCollectionSortBy(props.sortBy)}
|
||||
|
|
|
@ -217,9 +217,8 @@ export default function PreviewCard(props: IProps) {
|
|||
if (selectOnClick) {
|
||||
if (isRangeSelectActive) {
|
||||
onRangeSelect();
|
||||
} else {
|
||||
onSelect?.(!selected);
|
||||
}
|
||||
onSelect?.(!selected);
|
||||
} else if (file?.msrc || imgSrc) {
|
||||
onClick?.();
|
||||
}
|
||||
|
|
|
@ -413,6 +413,39 @@ const GlobalStyles = createGlobalStyle`
|
|||
.react-datepicker__input-container > input {
|
||||
width:100%;
|
||||
}
|
||||
.react-datepicker__navigation{
|
||||
top:14px;
|
||||
}
|
||||
.react-datepicker, .react-datepicker__header,.react-datepicker__time-container .react-datepicker__time,.react-datepicker-time__header{
|
||||
background-color: #202020;
|
||||
color:#fff;
|
||||
border-color: #444;
|
||||
}
|
||||
.react-datepicker__current-month,.react-datepicker__day-name, .react-datepicker__day, .react-datepicker__time-name{
|
||||
color:#fff;
|
||||
}
|
||||
.react-datepicker__day--disabled{
|
||||
color:#5b5656;
|
||||
}
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item:hover{
|
||||
background-color:#686868
|
||||
}
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled :hover{
|
||||
background-color: #202020;
|
||||
}
|
||||
|
||||
.react-datepicker__time-container .react-datepicker__time .react-datepicker__time-box ul.react-datepicker__time-list li.react-datepicker__time-list-item--disabled{
|
||||
color:#5b5656;
|
||||
}
|
||||
.react-datepicker{
|
||||
padding-bottom:10px;
|
||||
}
|
||||
.react-datepicker__day:hover {
|
||||
background-color:#686868
|
||||
}
|
||||
.react-datepicker__day--disabled:hover {
|
||||
background-color: #202020;
|
||||
}
|
||||
`;
|
||||
|
||||
export const LogoImage = styled.img`
|
||||
|
|
|
@ -10,19 +10,16 @@ import {
|
|||
import { Collection } from './collectionService';
|
||||
import HTTPService from './HTTPService';
|
||||
import { logError } from 'utils/sentry';
|
||||
import {
|
||||
appendPhotoSwipeProps,
|
||||
decryptFile,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
const FILES = 'files';
|
||||
const FILES_TABLE = 'files';
|
||||
|
||||
export const MIN_EDITED_CREATION_TIME = '1800-01-01T00:00:00.000Z';
|
||||
export const MIN_EDITED_CREATION_TIME = new Date(1800, 0, 1);
|
||||
export const MAX_EDITED_CREATION_TIME = new Date();
|
||||
export const ALL_TIME = new Date(1800, 0, 1, 23, 59, 59);
|
||||
|
||||
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||
|
||||
|
@ -133,10 +130,18 @@ interface TrashRequestItems {
|
|||
collectionID: number;
|
||||
}
|
||||
export const getLocalFiles = async () => {
|
||||
const files: Array<File> = (await localForage.getItem<File[]>(FILES)) || [];
|
||||
const files: Array<File> =
|
||||
(await localForage.getItem<File[]>(FILES_TABLE)) || [];
|
||||
return files;
|
||||
};
|
||||
|
||||
export const setLocalFiles = async (files: File[]) => {
|
||||
await localForage.setItem(FILES_TABLE, files);
|
||||
};
|
||||
|
||||
const getCollectionLastSyncTime = async (collection: Collection) =>
|
||||
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
||||
|
||||
export const syncFiles = async (
|
||||
collections: Collection[],
|
||||
setFiles: (files: File[]) => void
|
||||
|
@ -144,15 +149,14 @@ export const syncFiles = async (
|
|||
const localFiles = await getLocalFiles();
|
||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||
if (files.length !== localFiles.length) {
|
||||
await localForage.setItem('files', files);
|
||||
await setLocalFiles(files);
|
||||
setFiles(sortFiles(mergeMetadata(files)));
|
||||
}
|
||||
for (const collection of collections) {
|
||||
if (!getToken()) {
|
||||
continue;
|
||||
}
|
||||
const lastSyncTime =
|
||||
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
||||
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||
if (collection.updationTime === lastSyncTime) {
|
||||
continue;
|
||||
}
|
||||
|
@ -177,15 +181,14 @@ export const syncFiles = async (
|
|||
}
|
||||
files.push(file);
|
||||
}
|
||||
await localForage.setItem('files', files);
|
||||
await setLocalFiles(files);
|
||||
await localForage.setItem(
|
||||
`${collection.id}-time`,
|
||||
collection.updationTime
|
||||
);
|
||||
files = sortFiles(mergeMetadata(appendPhotoSwipeProps(files)));
|
||||
setFiles(files);
|
||||
setFiles(sortFiles(mergeMetadata(files)));
|
||||
}
|
||||
return mergeMetadata(appendPhotoSwipeProps(files));
|
||||
return mergeMetadata(files);
|
||||
};
|
||||
|
||||
export const getFiles = async (
|
||||
|
@ -196,10 +199,7 @@ export const getFiles = async (
|
|||
): Promise<File[]> => {
|
||||
try {
|
||||
const decryptedFiles: File[] = [];
|
||||
let time =
|
||||
sinceTime ||
|
||||
(await localForage.getItem<number>(`${collection.id}-time`)) ||
|
||||
0;
|
||||
let time = sinceTime;
|
||||
let resp;
|
||||
do {
|
||||
const token = getToken();
|
||||
|
|
|
@ -10,6 +10,7 @@ import uploadHttpClient from 'services/upload/uploadHttpClient';
|
|||
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
|
||||
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||
import { getFileType } from './upload/readFileService';
|
||||
import { getLocalTrash, getTrashedFiles } from './trashService';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
||||
|
@ -43,10 +44,14 @@ export async function replaceThumbnail(
|
|||
const token = getToken();
|
||||
const worker = await new CryptoWorker();
|
||||
const files = await getLocalFiles();
|
||||
const largeThumbnailFiles = files.filter((file) =>
|
||||
const trash = await getLocalTrash();
|
||||
const trashFiles = getTrashedFiles(trash);
|
||||
const largeThumbnailFiles = [...files, ...trashFiles].filter((file) =>
|
||||
largeThumbnailFileIDs.has(file.id)
|
||||
);
|
||||
|
||||
if (largeThumbnailFileIDs.size !== largeThumbnailFiles.length) {
|
||||
logError(Error(), 'all large thumbnail files not found locally');
|
||||
}
|
||||
if (largeThumbnailFiles.length === 0) {
|
||||
return completedWithError;
|
||||
}
|
||||
|
|
|
@ -1,12 +1,7 @@
|
|||
import { SetFiles } from 'pages/gallery';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
import {
|
||||
appendPhotoSwipeProps,
|
||||
decryptFile,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
} from 'utils/file';
|
||||
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import { Collection, getCollection } from './collectionService';
|
||||
|
@ -167,14 +162,12 @@ function removeRestoredOrDeletedTrashItems(trash: Trash) {
|
|||
|
||||
export function getTrashedFiles(trash: Trash) {
|
||||
return mergeMetadata(
|
||||
appendPhotoSwipeProps(
|
||||
trash.map((trashedFile) => ({
|
||||
...trashedFile.file,
|
||||
updationTime: trashedFile.updatedAt,
|
||||
isTrashed: true,
|
||||
deleteBy: trashedFile.deleteBy,
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import exifr from 'exifr';
|
||||
|
||||
import { logError } from 'utils/sentry';
|
||||
import { NULL_LOCATION, Location } from './metadataService';
|
||||
|
||||
const EXIF_TAGS_NEEDED = [
|
||||
|
@ -20,7 +19,6 @@ interface ParsedEXIFData {
|
|||
export async function getExifData(
|
||||
receivedFile: globalThis.File
|
||||
): Promise<ParsedEXIFData> {
|
||||
try {
|
||||
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
||||
if (!exifData) {
|
||||
return { location: NULL_LOCATION, creationTime: null };
|
||||
|
@ -30,10 +28,6 @@ export async function getExifData(
|
|||
creationTime: getUNIXTime(exifData),
|
||||
};
|
||||
return parsedEXIFData;
|
||||
} catch (e) {
|
||||
logError(e, 'error reading exif data');
|
||||
// ignore exif parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
function getUNIXTime(exifData: any) {
|
||||
|
|
|
@ -34,7 +34,14 @@ export async function extractMetadata(
|
|||
) {
|
||||
let exifData = null;
|
||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||
try {
|
||||
exifData = await getExifData(receivedFile);
|
||||
} catch (e) {
|
||||
logError(e, 'file missing exif data ', {
|
||||
fileType: fileTypeInfo.exactType,
|
||||
});
|
||||
// ignore exif parsing errors
|
||||
}
|
||||
}
|
||||
|
||||
const extractedMetadata: MetadataObject = {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { File, getLocalFiles } from '../fileService';
|
||||
import { File, getLocalFiles, setLocalFiles } from '../fileService';
|
||||
import { Collection, getLocalCollections } from '../collectionService';
|
||||
import { SetFiles } from 'pages/gallery';
|
||||
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
||||
|
@ -8,7 +8,6 @@ import {
|
|||
removeUnnecessaryFileProps,
|
||||
} from 'utils/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import localForage from 'utils/storage/localForage';
|
||||
import {
|
||||
getMetadataMapKey,
|
||||
ParsedMetaDataJSON,
|
||||
|
@ -185,8 +184,7 @@ class UploadManager {
|
|||
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
||||
this.existingFiles.push(file);
|
||||
this.existingFiles = sortFiles(this.existingFiles);
|
||||
await localForage.setItem(
|
||||
'files',
|
||||
await setLocalFiles(
|
||||
removeUnnecessaryFileProps(this.existingFiles)
|
||||
);
|
||||
this.setFiles(this.existingFiles);
|
||||
|
|
|
@ -420,13 +420,6 @@ export function mergeMetadata(files: File[]): File[] {
|
|||
},
|
||||
}));
|
||||
}
|
||||
export function appendPhotoSwipeProps(files: File[]) {
|
||||
return files.map((file) => ({
|
||||
...file,
|
||||
w: window.innerWidth,
|
||||
h: window.innerHeight,
|
||||
})) as File[];
|
||||
}
|
||||
|
||||
export function updateExistingFilePubMetadata(
|
||||
existingFile: File,
|
||||
|
|
Loading…
Reference in a new issue