Merge branch 'master' into update-file-title
This commit is contained in:
commit
91825db617
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
DeadCenter,
|
|
||||||
GalleryContext,
|
GalleryContext,
|
||||||
Search,
|
Search,
|
||||||
SelectedState,
|
SelectedState,
|
||||||
|
@ -14,18 +13,9 @@ import styled from 'styled-components';
|
||||||
import DownloadManager from 'services/downloadManager';
|
import DownloadManager from 'services/downloadManager';
|
||||||
import constants from 'utils/strings/constants';
|
import constants from 'utils/strings/constants';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { VariableSizeList as List } from 'react-window';
|
|
||||||
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
|
||||||
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
|
||||||
import { SetDialogMessage } from './MessageDialog';
|
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 { fileIsArchived, formatDateRelative } from 'utils/file';
|
||||||
import {
|
import {
|
||||||
ALL_SECTION,
|
ALL_SECTION,
|
||||||
|
@ -34,24 +24,7 @@ import {
|
||||||
} from './pages/gallery/Collections';
|
} from './pages/gallery/Collections';
|
||||||
import { isSharedFile } from 'utils/file';
|
import { isSharedFile } from 'utils/file';
|
||||||
import { isPlaybackPossible } from 'utils/photoFrame';
|
import { isPlaybackPossible } from 'utils/photoFrame';
|
||||||
|
import { PhotoList } from './PhotoList';
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: block;
|
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`
|
const EmptyScreen = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
@ -133,12 +52,6 @@ const EmptyScreen = styled.div`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
enum ITEM_TYPE {
|
|
||||||
TIME = 'TIME',
|
|
||||||
TILE = 'TILE',
|
|
||||||
BANNER = 'BANNER',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: File[];
|
files: File[];
|
||||||
setFiles: SetFiles;
|
setFiles: SetFiles;
|
||||||
|
@ -182,11 +95,11 @@ const PhotoFrame = ({
|
||||||
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
const [fetching, setFetching] = useState<{ [k: number]: boolean }>({});
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const galleryContext = useContext(GalleryContext);
|
const galleryContext = useContext(GalleryContext);
|
||||||
const listRef = useRef(null);
|
|
||||||
const [rangeStart, setRangeStart] = useState(null);
|
const [rangeStart, setRangeStart] = useState(null);
|
||||||
const [currentHover, setCurrentHover] = useState(null);
|
const [currentHover, setCurrentHover] = useState(null);
|
||||||
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
const [isShiftKeyPressed, setIsShiftKeyPressed] = useState(false);
|
||||||
|
const filteredDataRef = useRef([]);
|
||||||
|
const filteredData = filteredDataRef?.current ?? [];
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (e.key === 'Shift') {
|
if (e.key === 'Shift') {
|
||||||
|
@ -223,10 +136,9 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
}, [search]);
|
}, [search]);
|
||||||
|
|
||||||
useEffect(() => {
|
const resetFetching = () => {
|
||||||
listRef.current?.resetAfterIndex(0);
|
|
||||||
setFetching({});
|
setFetching({});
|
||||||
}, [files, search, deleted]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selected.count === 0) {
|
if (selected.count === 0) {
|
||||||
|
@ -234,6 +146,71 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
}, [selected]);
|
}, [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) => {
|
const updateUrl = (index: number) => (url: string) => {
|
||||||
files[index] = {
|
files[index] = {
|
||||||
...files[index],
|
...files[index],
|
||||||
|
@ -310,9 +287,7 @@ const PhotoFrame = ({
|
||||||
if (selected.collectionID !== activeCollection) {
|
if (selected.collectionID !== activeCollection) {
|
||||||
setSelected({ count: 0, collectionID: 0 });
|
setSelected({ count: 0, collectionID: 0 });
|
||||||
}
|
}
|
||||||
if (rangeStart || rangeStart === 0) {
|
if (checked) {
|
||||||
setRangeStart(null);
|
|
||||||
} else if (checked) {
|
|
||||||
setRangeStart(index);
|
setRangeStart(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -332,11 +307,11 @@ const PhotoFrame = ({
|
||||||
let leftEnd = -1;
|
let leftEnd = -1;
|
||||||
let rightEnd = -1;
|
let rightEnd = -1;
|
||||||
if (index < rangeStart) {
|
if (index < rangeStart) {
|
||||||
leftEnd = index;
|
leftEnd = index + 1;
|
||||||
rightEnd = rangeStart;
|
rightEnd = rangeStart - 1;
|
||||||
} else {
|
} else {
|
||||||
leftEnd = rangeStart;
|
leftEnd = rangeStart + 1;
|
||||||
rightEnd = index;
|
rightEnd = index - 1;
|
||||||
}
|
}
|
||||||
for (let i = leftEnd; i <= rightEnd; i++) {
|
for (let i = leftEnd; i <= rightEnd; i++) {
|
||||||
handleSelect(filteredData[i].id)(true);
|
handleSelect(filteredData[i].id)(true);
|
||||||
|
@ -364,8 +339,8 @@ const PhotoFrame = ({
|
||||||
isShiftKeyPressed && (rangeStart || rangeStart === 0)
|
isShiftKeyPressed && (rangeStart || rangeStart === 0)
|
||||||
}
|
}
|
||||||
isInsSelectRange={
|
isInsSelectRange={
|
||||||
(index >= rangeStart + 1 && index <= currentHover) ||
|
(index >= rangeStart && index <= currentHover) ||
|
||||||
(index >= currentHover && index <= rangeStart - 1)
|
(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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
{!isFirstLoad && files.length === 0 && !isInSearchMode ? (
|
||||||
|
@ -591,217 +423,22 @@ const PhotoFrame = ({
|
||||||
{constants.UPLOAD_FIRST_PHOTO}
|
{constants.UPLOAD_FIRST_PHOTO}
|
||||||
</Button>
|
</Button>
|
||||||
</EmptyScreen>
|
</EmptyScreen>
|
||||||
) : filteredData.length ? (
|
) : (
|
||||||
<Container>
|
<Container>
|
||||||
<AutoSizer>
|
<AutoSizer>
|
||||||
{({ height, width }) => {
|
{({ height, width }) => (
|
||||||
let columns = Math.floor(
|
<PhotoList
|
||||||
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}
|
|
||||||
width={width}
|
width={width}
|
||||||
itemCount={timeStampList.length}
|
height={height}
|
||||||
itemKey={generateKey}
|
getThumbnail={getThumbnail}
|
||||||
overscanCount={extraRowsToRender}>
|
filteredData={filteredData}
|
||||||
{({ index, style }) => (
|
activeCollection={activeCollection}
|
||||||
<ListItem style={style}>
|
showBanner={
|
||||||
<ListContainer
|
files.length < 30 && !isInSearchMode
|
||||||
columns={columns}
|
}
|
||||||
groups={
|
resetFetching={resetFetching}
|
||||||
timeStampList[index].groups
|
/>
|
||||||
}>
|
|
||||||
{renderListItem(
|
|
||||||
timeStampList[index]
|
|
||||||
)}
|
)}
|
||||||
</ListContainer>
|
|
||||||
</ListItem>
|
|
||||||
)}
|
|
||||||
</List>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</AutoSizer>
|
</AutoSizer>
|
||||||
<PhotoSwipe
|
<PhotoSwipe
|
||||||
isOpen={open}
|
isOpen={open}
|
||||||
|
@ -815,10 +452,6 @@ const PhotoFrame = ({
|
||||||
isTrashCollection={activeCollection === TRASH_SECTION}
|
isTrashCollection={activeCollection === TRASH_SECTION}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</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,
|
removeFromFavorites,
|
||||||
} from 'services/collectionService';
|
} from 'services/collectionService';
|
||||||
import {
|
import {
|
||||||
|
ALL_TIME,
|
||||||
File,
|
File,
|
||||||
MAX_EDITED_FILE_NAME_LENGTH,
|
MAX_EDITED_FILE_NAME_LENGTH,
|
||||||
|
MAX_EDITED_CREATION_TIME,
|
||||||
MIN_EDITED_CREATION_TIME,
|
MIN_EDITED_CREATION_TIME,
|
||||||
updatePublicMagicMetadata,
|
updatePublicMagicMetadata,
|
||||||
} from 'services/fileService';
|
} from 'services/fileService';
|
||||||
|
@ -40,8 +42,8 @@ import { logError } from 'utils/sentry';
|
||||||
|
|
||||||
import DatePicker from 'react-datepicker';
|
import DatePicker from 'react-datepicker';
|
||||||
import 'react-datepicker/dist/react-datepicker.css';
|
import 'react-datepicker/dist/react-datepicker.css';
|
||||||
import TickIcon from 'components/icons/TickIcon';
|
|
||||||
import CloseIcon from 'components/icons/CloseIcon';
|
import CloseIcon from 'components/icons/CloseIcon';
|
||||||
|
import TickIcon from 'components/icons/TickIcon';
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -73,11 +75,6 @@ const Pre = styled.pre`
|
||||||
padding: 7px 15px;
|
padding: 7px 15px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ButtonContainer = styled.div`
|
|
||||||
margin-left: auto;
|
|
||||||
width: 200px;
|
|
||||||
padding: 5px 10px;
|
|
||||||
`;
|
|
||||||
const WarningMessage = styled.div`
|
const WarningMessage = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 0.25rem;
|
margin-top: 0.25rem;
|
||||||
|
@ -92,6 +89,11 @@ const renderInfoItem = (label: string, value: string | JSX.Element) => (
|
||||||
</Row>
|
</Row>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isSameDay = (first, second) =>
|
||||||
|
first.getFullYear() === second.getFullYear() &&
|
||||||
|
first.getMonth() === second.getMonth() &&
|
||||||
|
first.getDate() === second.getDate();
|
||||||
|
|
||||||
function RenderCreationTime({
|
function RenderCreationTime({
|
||||||
file,
|
file,
|
||||||
scheduleUpdate,
|
scheduleUpdate,
|
||||||
|
@ -106,6 +108,7 @@ function RenderCreationTime({
|
||||||
|
|
||||||
const openEditMode = () => setIsInEditMode(true);
|
const openEditMode = () => setIsInEditMode(true);
|
||||||
const closeEditMode = () => setIsInEditMode(false);
|
const closeEditMode = () => setIsInEditMode(false);
|
||||||
|
|
||||||
const saveEdits = async () => {
|
const saveEdits = async () => {
|
||||||
try {
|
try {
|
||||||
if (isInEditMode && file) {
|
if (isInEditMode && file) {
|
||||||
|
@ -150,35 +153,18 @@ function RenderCreationTime({
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
timeInputLabel="Time:"
|
timeInputLabel="Time:"
|
||||||
dateFormat="dd/MM/yyyy h:mm aa"
|
dateFormat="dd/MM/yyyy h:mm aa"
|
||||||
showTimeInput
|
showTimeSelect
|
||||||
autoFocus
|
autoFocus
|
||||||
shouldCloseOnSelect={false}
|
minDate={MIN_EDITED_CREATION_TIME}
|
||||||
onClickOutside={discardEdits}
|
maxDate={MAX_EDITED_CREATION_TIME}
|
||||||
minDate={new Date(MIN_EDITED_CREATION_TIME)}
|
maxTime={
|
||||||
maxDate={new Date()}
|
isSameDay(pickedTime, new Date())
|
||||||
showYearDropdown
|
? MAX_EDITED_CREATION_TIME
|
||||||
showMonthDropdown
|
: ALL_TIME
|
||||||
withPortal>
|
}
|
||||||
<ButtonContainer
|
minTime={MIN_EDITED_CREATION_TIME}
|
||||||
style={{
|
fixedHeight
|
||||||
marginLeft: 'auto',
|
withPortal></DatePicker>
|
||||||
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>
|
|
||||||
) : (
|
) : (
|
||||||
formatDateTime(pickedTime)
|
formatDateTime(pickedTime)
|
||||||
)}
|
)}
|
||||||
|
@ -186,9 +172,20 @@ function RenderCreationTime({
|
||||||
<Value
|
<Value
|
||||||
width={isInEditMode ? '20%' : '10%'}
|
width={isInEditMode ? '20%' : '10%'}
|
||||||
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
style={{ cursor: 'pointer', marginLeft: '10px' }}>
|
||||||
|
{!isInEditMode ? (
|
||||||
<IconButton onClick={openEditMode}>
|
<IconButton onClick={openEditMode}>
|
||||||
<EditIcon />
|
<EditIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<IconButton onClick={saveEdits}>
|
||||||
|
<TickIcon />
|
||||||
|
</IconButton>
|
||||||
|
<IconButton onClick={discardEdits}>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Value>
|
</Value>
|
||||||
</Row>
|
</Row>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default function TickIcon(props) {
|
||||||
}
|
}
|
||||||
|
|
||||||
TickIcon.defaultProps = {
|
TickIcon.defaultProps = {
|
||||||
height: 28,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
viewBox: '0 0 24 24',
|
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 TickIcon from 'components/icons/TickIcon';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ListGroup, Popover, Row } from 'react-bootstrap';
|
import { ListGroup, Popover, Row } from 'react-bootstrap';
|
||||||
|
@ -23,13 +23,13 @@ const SortByOptionCreator =
|
||||||
(
|
(
|
||||||
<MenuItem>
|
<MenuItem>
|
||||||
<Row>
|
<Row>
|
||||||
<Label width="20px">
|
<Value width="20px">
|
||||||
{activeSortBy === props.sortBy && (
|
{activeSortBy === props.sortBy && (
|
||||||
<TickWrapper>
|
<TickWrapper>
|
||||||
<TickIcon />
|
<TickIcon />
|
||||||
</TickWrapper>
|
</TickWrapper>
|
||||||
)}
|
)}
|
||||||
</Label>
|
</Value>
|
||||||
<Value width="165px">
|
<Value width="165px">
|
||||||
<MenuLink
|
<MenuLink
|
||||||
onClick={() => setCollectionSortBy(props.sortBy)}
|
onClick={() => setCollectionSortBy(props.sortBy)}
|
||||||
|
|
|
@ -217,9 +217,8 @@ export default function PreviewCard(props: IProps) {
|
||||||
if (selectOnClick) {
|
if (selectOnClick) {
|
||||||
if (isRangeSelectActive) {
|
if (isRangeSelectActive) {
|
||||||
onRangeSelect();
|
onRangeSelect();
|
||||||
} else {
|
|
||||||
onSelect?.(!selected);
|
|
||||||
}
|
}
|
||||||
|
onSelect?.(!selected);
|
||||||
} else if (file?.msrc || imgSrc) {
|
} else if (file?.msrc || imgSrc) {
|
||||||
onClick?.();
|
onClick?.();
|
||||||
}
|
}
|
||||||
|
|
|
@ -413,6 +413,39 @@ const GlobalStyles = createGlobalStyle`
|
||||||
.react-datepicker__input-container > input {
|
.react-datepicker__input-container > input {
|
||||||
width:100%;
|
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`
|
export const LogoImage = styled.img`
|
||||||
|
|
|
@ -10,19 +10,16 @@ import {
|
||||||
import { Collection } from './collectionService';
|
import { Collection } from './collectionService';
|
||||||
import HTTPService from './HTTPService';
|
import HTTPService from './HTTPService';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import {
|
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||||
appendPhotoSwipeProps,
|
|
||||||
decryptFile,
|
|
||||||
mergeMetadata,
|
|
||||||
sortFiles,
|
|
||||||
} from 'utils/file';
|
|
||||||
import CryptoWorker from 'utils/crypto';
|
import CryptoWorker from 'utils/crypto';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
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;
|
export const MAX_EDITED_FILE_NAME_LENGTH = 100;
|
||||||
|
|
||||||
|
@ -133,10 +130,18 @@ interface TrashRequestItems {
|
||||||
collectionID: number;
|
collectionID: number;
|
||||||
}
|
}
|
||||||
export const getLocalFiles = async () => {
|
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;
|
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 (
|
export const syncFiles = async (
|
||||||
collections: Collection[],
|
collections: Collection[],
|
||||||
setFiles: (files: File[]) => void
|
setFiles: (files: File[]) => void
|
||||||
|
@ -144,15 +149,14 @@ export const syncFiles = async (
|
||||||
const localFiles = await getLocalFiles();
|
const localFiles = await getLocalFiles();
|
||||||
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
let files = await removeDeletedCollectionFiles(collections, localFiles);
|
||||||
if (files.length !== localFiles.length) {
|
if (files.length !== localFiles.length) {
|
||||||
await localForage.setItem('files', files);
|
await setLocalFiles(files);
|
||||||
setFiles(sortFiles(mergeMetadata(files)));
|
setFiles(sortFiles(mergeMetadata(files)));
|
||||||
}
|
}
|
||||||
for (const collection of collections) {
|
for (const collection of collections) {
|
||||||
if (!getToken()) {
|
if (!getToken()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const lastSyncTime =
|
const lastSyncTime = await getCollectionLastSyncTime(collection);
|
||||||
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
|
|
||||||
if (collection.updationTime === lastSyncTime) {
|
if (collection.updationTime === lastSyncTime) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -177,15 +181,14 @@ export const syncFiles = async (
|
||||||
}
|
}
|
||||||
files.push(file);
|
files.push(file);
|
||||||
}
|
}
|
||||||
await localForage.setItem('files', files);
|
await setLocalFiles(files);
|
||||||
await localForage.setItem(
|
await localForage.setItem(
|
||||||
`${collection.id}-time`,
|
`${collection.id}-time`,
|
||||||
collection.updationTime
|
collection.updationTime
|
||||||
);
|
);
|
||||||
files = sortFiles(mergeMetadata(appendPhotoSwipeProps(files)));
|
setFiles(sortFiles(mergeMetadata(files)));
|
||||||
setFiles(files);
|
|
||||||
}
|
}
|
||||||
return mergeMetadata(appendPhotoSwipeProps(files));
|
return mergeMetadata(files);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFiles = async (
|
export const getFiles = async (
|
||||||
|
@ -196,10 +199,7 @@ export const getFiles = async (
|
||||||
): Promise<File[]> => {
|
): Promise<File[]> => {
|
||||||
try {
|
try {
|
||||||
const decryptedFiles: File[] = [];
|
const decryptedFiles: File[] = [];
|
||||||
let time =
|
let time = sinceTime;
|
||||||
sinceTime ||
|
|
||||||
(await localForage.getItem<number>(`${collection.id}-time`)) ||
|
|
||||||
0;
|
|
||||||
let resp;
|
let resp;
|
||||||
do {
|
do {
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
|
|
|
@ -10,6 +10,7 @@ import uploadHttpClient from 'services/upload/uploadHttpClient';
|
||||||
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
|
import { EncryptionResult, UploadURL } from 'services/upload/uploadService';
|
||||||
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
import { SetProgressTracker } from 'components/FixLargeThumbnail';
|
||||||
import { getFileType } from './upload/readFileService';
|
import { getFileType } from './upload/readFileService';
|
||||||
|
import { getLocalTrash, getTrashedFiles } from './trashService';
|
||||||
|
|
||||||
const ENDPOINT = getEndpoint();
|
const ENDPOINT = getEndpoint();
|
||||||
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
const REPLACE_THUMBNAIL_THRESHOLD = 500 * 1024; // 500KB
|
||||||
|
@ -43,10 +44,14 @@ export async function replaceThumbnail(
|
||||||
const token = getToken();
|
const token = getToken();
|
||||||
const worker = await new CryptoWorker();
|
const worker = await new CryptoWorker();
|
||||||
const files = await getLocalFiles();
|
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)
|
largeThumbnailFileIDs.has(file.id)
|
||||||
);
|
);
|
||||||
|
if (largeThumbnailFileIDs.size !== largeThumbnailFiles.length) {
|
||||||
|
logError(Error(), 'all large thumbnail files not found locally');
|
||||||
|
}
|
||||||
if (largeThumbnailFiles.length === 0) {
|
if (largeThumbnailFiles.length === 0) {
|
||||||
return completedWithError;
|
return completedWithError;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,7 @@
|
||||||
import { SetFiles } from 'pages/gallery';
|
import { SetFiles } from 'pages/gallery';
|
||||||
import { getEndpoint } from 'utils/common/apiUtil';
|
import { getEndpoint } from 'utils/common/apiUtil';
|
||||||
import { getToken } from 'utils/common/key';
|
import { getToken } from 'utils/common/key';
|
||||||
import {
|
import { decryptFile, mergeMetadata, sortFiles } from 'utils/file';
|
||||||
appendPhotoSwipeProps,
|
|
||||||
decryptFile,
|
|
||||||
mergeMetadata,
|
|
||||||
sortFiles,
|
|
||||||
} from 'utils/file';
|
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import localForage from 'utils/storage/localForage';
|
import localForage from 'utils/storage/localForage';
|
||||||
import { Collection, getCollection } from './collectionService';
|
import { Collection, getCollection } from './collectionService';
|
||||||
|
@ -167,14 +162,12 @@ function removeRestoredOrDeletedTrashItems(trash: Trash) {
|
||||||
|
|
||||||
export function getTrashedFiles(trash: Trash) {
|
export function getTrashedFiles(trash: Trash) {
|
||||||
return mergeMetadata(
|
return mergeMetadata(
|
||||||
appendPhotoSwipeProps(
|
|
||||||
trash.map((trashedFile) => ({
|
trash.map((trashedFile) => ({
|
||||||
...trashedFile.file,
|
...trashedFile.file,
|
||||||
updationTime: trashedFile.updatedAt,
|
updationTime: trashedFile.updatedAt,
|
||||||
isTrashed: true,
|
isTrashed: true,
|
||||||
deleteBy: trashedFile.deleteBy,
|
deleteBy: trashedFile.deleteBy,
|
||||||
}))
|
}))
|
||||||
)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import exifr from 'exifr';
|
import exifr from 'exifr';
|
||||||
|
|
||||||
import { logError } from 'utils/sentry';
|
|
||||||
import { NULL_LOCATION, Location } from './metadataService';
|
import { NULL_LOCATION, Location } from './metadataService';
|
||||||
|
|
||||||
const EXIF_TAGS_NEEDED = [
|
const EXIF_TAGS_NEEDED = [
|
||||||
|
@ -20,7 +19,6 @@ interface ParsedEXIFData {
|
||||||
export async function getExifData(
|
export async function getExifData(
|
||||||
receivedFile: globalThis.File
|
receivedFile: globalThis.File
|
||||||
): Promise<ParsedEXIFData> {
|
): Promise<ParsedEXIFData> {
|
||||||
try {
|
|
||||||
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
const exifData = await exifr.parse(receivedFile, EXIF_TAGS_NEEDED);
|
||||||
if (!exifData) {
|
if (!exifData) {
|
||||||
return { location: NULL_LOCATION, creationTime: null };
|
return { location: NULL_LOCATION, creationTime: null };
|
||||||
|
@ -30,10 +28,6 @@ export async function getExifData(
|
||||||
creationTime: getUNIXTime(exifData),
|
creationTime: getUNIXTime(exifData),
|
||||||
};
|
};
|
||||||
return parsedEXIFData;
|
return parsedEXIFData;
|
||||||
} catch (e) {
|
|
||||||
logError(e, 'error reading exif data');
|
|
||||||
// ignore exif parsing errors
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUNIXTime(exifData: any) {
|
function getUNIXTime(exifData: any) {
|
||||||
|
|
|
@ -34,7 +34,14 @@ export async function extractMetadata(
|
||||||
) {
|
) {
|
||||||
let exifData = null;
|
let exifData = null;
|
||||||
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
if (fileTypeInfo.fileType === FILE_TYPE.IMAGE) {
|
||||||
|
try {
|
||||||
exifData = await getExifData(receivedFile);
|
exifData = await getExifData(receivedFile);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e, 'file missing exif data ', {
|
||||||
|
fileType: fileTypeInfo.exactType,
|
||||||
|
});
|
||||||
|
// ignore exif parsing errors
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractedMetadata: MetadataObject = {
|
const extractedMetadata: MetadataObject = {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { File, getLocalFiles } from '../fileService';
|
import { File, getLocalFiles, setLocalFiles } from '../fileService';
|
||||||
import { Collection, getLocalCollections } from '../collectionService';
|
import { Collection, getLocalCollections } from '../collectionService';
|
||||||
import { SetFiles } from 'pages/gallery';
|
import { SetFiles } from 'pages/gallery';
|
||||||
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
|
||||||
|
@ -8,7 +8,6 @@ import {
|
||||||
removeUnnecessaryFileProps,
|
removeUnnecessaryFileProps,
|
||||||
} from 'utils/file';
|
} from 'utils/file';
|
||||||
import { logError } from 'utils/sentry';
|
import { logError } from 'utils/sentry';
|
||||||
import localForage from 'utils/storage/localForage';
|
|
||||||
import {
|
import {
|
||||||
getMetadataMapKey,
|
getMetadataMapKey,
|
||||||
ParsedMetaDataJSON,
|
ParsedMetaDataJSON,
|
||||||
|
@ -185,8 +184,7 @@ class UploadManager {
|
||||||
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
if (fileUploadResult === FileUploadResults.UPLOADED) {
|
||||||
this.existingFiles.push(file);
|
this.existingFiles.push(file);
|
||||||
this.existingFiles = sortFiles(this.existingFiles);
|
this.existingFiles = sortFiles(this.existingFiles);
|
||||||
await localForage.setItem(
|
await setLocalFiles(
|
||||||
'files',
|
|
||||||
removeUnnecessaryFileProps(this.existingFiles)
|
removeUnnecessaryFileProps(this.existingFiles)
|
||||||
);
|
);
|
||||||
this.setFiles(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(
|
export function updateExistingFilePubMetadata(
|
||||||
existingFile: File,
|
existingFile: File,
|
||||||
|
|
Loading…
Reference in a new issue