Merge pull request #499 from ente-io/collections-redesign
Collections UI redesign
This commit is contained in:
commit
ab96dd8a3b
|
@ -9,6 +9,7 @@ const { PHASE_DEVELOPMENT_SERVER } = require('next/constants');
|
|||
const withTM = require('next-transpile-modules')([
|
||||
'@mui/material',
|
||||
'@mui/system',
|
||||
'@mui/icons-material',
|
||||
]);
|
||||
|
||||
const {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ente-io/next-with-workbox": "^1.0.3",
|
||||
"@mui/icons-material": "^5.6.2",
|
||||
"@mui/material": "^5.6.2",
|
||||
"@mui/styled-engine": "npm:@mui/styled-engine-sc@latest",
|
||||
"@mui/styled-engine-sc": "^5.6.1",
|
||||
|
@ -80,7 +81,7 @@
|
|||
"@types/react-select": "^4.0.15",
|
||||
"@types/react-window": "^1.8.2",
|
||||
"@types/react-window-infinite-loader": "^1.0.3",
|
||||
"@types/styled-components": "^5.1.3",
|
||||
"@types/styled-components": "^5.1.25",
|
||||
"@types/yup": "^0.29.7",
|
||||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"eslint": "^7.27.0",
|
||||
|
|
BIN
public/fonts/Inter-Bold.ttf
Normal file
BIN
public/fonts/Inter-Bold.ttf
Normal file
Binary file not shown.
BIN
public/fonts/Inter-Regular.ttf
Normal file
BIN
public/fonts/Inter-Regular.ttf
Normal file
Binary file not shown.
93
public/fonts/OFL.txt
Normal file
93
public/fonts/OFL.txt
Normal file
|
@ -0,0 +1,93 @@
|
|||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@ -1,96 +0,0 @@
|
|||
-------------------------------
|
||||
UBUNTU FONT LICENCE Version 1.0
|
||||
-------------------------------
|
||||
|
||||
PREAMBLE
|
||||
This licence allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely. The fonts, including any derivative works, can be
|
||||
bundled, embedded, and redistributed provided the terms of this licence
|
||||
are met. The fonts and derivatives, however, cannot be released under
|
||||
any other licence. The requirement for fonts to remain under this
|
||||
licence does not require any document created using the fonts or their
|
||||
derivatives to be published under this licence, as long as the primary
|
||||
purpose of the document is not to be a vehicle for the distribution of
|
||||
the fonts.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this licence and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Original Version" refers to the collection of Font Software components
|
||||
as received under this licence.
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to
|
||||
a new environment.
|
||||
|
||||
"Copyright Holder(s)" refers to all individuals and companies who have a
|
||||
copyright ownership of the Font Software.
|
||||
|
||||
"Substantially Changed" refers to Modified Versions which can be easily
|
||||
identified as dissimilar to the Font Software by users of the Font
|
||||
Software comparing the Original Version with the Modified Version.
|
||||
|
||||
To "Propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification and with or without charging
|
||||
a redistribution fee), making available to the public, and in some
|
||||
countries other activities as well.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
This licence does not grant any rights under trademark law and all such
|
||||
rights are reserved.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of the Font Software, to propagate the Font Software, subject to
|
||||
the below conditions:
|
||||
|
||||
1) Each copy of the Font Software must contain the above copyright
|
||||
notice and this licence. These can be included either as stand-alone
|
||||
text files, human-readable headers or in the appropriate machine-
|
||||
readable metadata fields within text or binary files as long as those
|
||||
fields can be easily viewed by the user.
|
||||
|
||||
2) The font name complies with the following:
|
||||
(a) The Original Version must retain its name, unmodified.
|
||||
(b) Modified Versions which are Substantially Changed must be renamed to
|
||||
avoid use of the name of the Original Version or similar names entirely.
|
||||
(c) Modified Versions which are not Substantially Changed must be
|
||||
renamed to both (i) retain the name of the Original Version and (ii) add
|
||||
additional naming elements to distinguish the Modified Version from the
|
||||
Original Version. The name of such Modified Versions must be the name of
|
||||
the Original Version, with "derivative X" where X represents the name of
|
||||
the new work, appended to that name.
|
||||
|
||||
3) The name(s) of the Copyright Holder(s) and any contributor to the
|
||||
Font Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except (i) as required by this licence, (ii) to
|
||||
acknowledge the contribution(s) of the Copyright Holder(s) or (iii) with
|
||||
their explicit written permission.
|
||||
|
||||
4) The Font Software, modified or unmodified, in part or in whole, must
|
||||
be distributed entirely under this licence, and must not be distributed
|
||||
under any other licence. The requirement for fonts to remain under this
|
||||
licence does not affect any document created using the Font Software,
|
||||
except any version of the Font Software extracted from a document
|
||||
created using the Font Software may only be distributed under this
|
||||
licence.
|
||||
|
||||
TERMINATION
|
||||
This licence becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF
|
||||
COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
|
||||
DEALINGS IN THE FONT SOFTWARE.
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -3,7 +3,7 @@ import { FreeFlowText, IconButton } from './Container';
|
|||
import CopyIcon from './icons/CopyIcon';
|
||||
import React, { useState } from 'react';
|
||||
import { Tooltip, OverlayTrigger } from 'react-bootstrap';
|
||||
import TickIcon from './icons/TickIcon';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import EnteSpinner from './EnteSpinner';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
|
|
37
src/components/Collections/AllCollections/CollectionCard.tsx
Normal file
37
src/components/Collections/AllCollections/CollectionCard.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import { Typography } from '@mui/material';
|
||||
import constants from 'utils/strings/constants';
|
||||
import React from 'react';
|
||||
import CollectionCard from '../CollectionCard';
|
||||
|
||||
export default function AllCollectionCard({
|
||||
onCollectionClick,
|
||||
collectionAttributes,
|
||||
latestFile,
|
||||
fileCount,
|
||||
}) {
|
||||
return (
|
||||
<CollectionCard
|
||||
large
|
||||
latestFile={latestFile}
|
||||
onClick={() => onCollectionClick(collectionAttributes.id)}>
|
||||
<div>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
line-height: 20px;
|
||||
`}>
|
||||
{collectionAttributes.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
line-height: 20px;
|
||||
`}>
|
||||
{fileCount} {constants.PHOTOS}
|
||||
</Typography>
|
||||
</div>
|
||||
</CollectionCard>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import React, { useState } from 'react';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import Menu from '@mui/material/Menu';
|
||||
import { IconButton, styled } from '@mui/material';
|
||||
import SortIcon from '@mui/icons-material/Sort';
|
||||
import CollectionSortOptions from './options';
|
||||
|
||||
export interface CollectionSortProps {
|
||||
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
|
||||
activeSortBy: COLLECTION_SORT_BY;
|
||||
}
|
||||
|
||||
const StyledMenu = styled(Menu)`
|
||||
& .MuiPaper-root {
|
||||
box-shadow: 0px 0px 10px 0px rgba(0, 0, 0, 0.16);
|
||||
}
|
||||
`;
|
||||
|
||||
export default function CollectionSort(props: CollectionSortProps) {
|
||||
const [sortByEl, setSortByEl] = useState(null);
|
||||
const handleClose = () => setSortByEl(null);
|
||||
return (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={(event) => setSortByEl(event.currentTarget)}
|
||||
aria-controls={sortByEl ? 'collection-sort' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={sortByEl ? 'true' : undefined}>
|
||||
<SortIcon />
|
||||
</IconButton>
|
||||
<StyledMenu
|
||||
id="collection-sort"
|
||||
anchorEl={sortByEl}
|
||||
open={Boolean(sortByEl)}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
disablePadding: true,
|
||||
'aria-labelledby': 'collection-sort',
|
||||
}}>
|
||||
<CollectionSortOptions {...props} close={handleClose} />
|
||||
</StyledMenu>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import { MenuItem, ListItemIcon, ListItemText } from '@mui/material';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import { CollectionSortProps } from '.';
|
||||
|
||||
export interface SortOptionProps extends CollectionSortProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
const SortByOptionCreator =
|
||||
({ setCollectionSortBy, activeSortBy, close }: SortOptionProps) =>
|
||||
(props: { sortBy: COLLECTION_SORT_BY; children: any }) => {
|
||||
const handleClick = () => {
|
||||
setCollectionSortBy(props.sortBy);
|
||||
close();
|
||||
};
|
||||
return (
|
||||
<MenuItem onClick={handleClick} style={{ paddingLeft: '5px' }}>
|
||||
<ListItemIcon style={{ minWidth: '25px' }}>
|
||||
{activeSortBy === props.sortBy && (
|
||||
<TickIcon
|
||||
css={`
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
`}
|
||||
/>
|
||||
)}
|
||||
</ListItemIcon>
|
||||
<ListItemText>{props.children}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
export default SortByOptionCreator;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { MenuList } from '@mui/material';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import SortByOptionCreator, { SortOptionProps } from './optionCreator';
|
||||
|
||||
export default function CollectionSortOptions(props: SortOptionProps) {
|
||||
const SortByOption = SortByOptionCreator(props);
|
||||
|
||||
return (
|
||||
<MenuList>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
||||
{constants.SORT_BY_NAME}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_DESCENDING}>
|
||||
{constants.SORT_BY_CREATION_TIME_DESCENDING}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.CREATION_TIME_ASCENDING}>
|
||||
{constants.SORT_BY_CREATION_TIME_ASCENDING}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING}>
|
||||
{constants.SORT_BY_UPDATION_TIME_DESCENDING}
|
||||
</SortByOption>
|
||||
</MenuList>
|
||||
);
|
||||
}
|
30
src/components/Collections/AllCollections/content.tsx
Normal file
30
src/components/Collections/AllCollections/content.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import React from 'react';
|
||||
import { DialogContent } from '@mui/material';
|
||||
import { FlexWrapper } from 'components/Container';
|
||||
import AllCollectionCard from './CollectionCard';
|
||||
|
||||
export default function AllCollectionContent({
|
||||
sortedCollectionSummaries,
|
||||
onCollectionClick,
|
||||
}) {
|
||||
return (
|
||||
<DialogContent>
|
||||
<FlexWrapper
|
||||
style={{
|
||||
flexWrap: 'wrap',
|
||||
}}>
|
||||
{sortedCollectionSummaries.map(
|
||||
({ latestFile, collectionAttributes, fileCount }) => (
|
||||
<AllCollectionCard
|
||||
onCollectionClick={onCollectionClick}
|
||||
collectionAttributes={collectionAttributes}
|
||||
key={collectionAttributes.id}
|
||||
latestFile={latestFile}
|
||||
fileCount={fileCount}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
</FlexWrapper>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
46
src/components/Collections/AllCollections/header.tsx
Normal file
46
src/components/Collections/AllCollections/header.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { DialogTitle, IconButton, Typography } from '@mui/material';
|
||||
import { TwoScreenSpacedOptions } from 'components/Container';
|
||||
import CollectionSort from 'components/Collections/AllCollections/CollectionSort';
|
||||
import constants from 'utils/strings/constants';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
|
||||
export default function AllCollectionsHeader({
|
||||
onClose,
|
||||
collectionCount,
|
||||
collectionSortBy,
|
||||
setCollectionSortBy,
|
||||
}) {
|
||||
return (
|
||||
<DialogTitle>
|
||||
<TwoScreenSpacedOptions>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 36px;
|
||||
`}>
|
||||
{constants.ALL_ALBUMS}
|
||||
</Typography>
|
||||
<IconButton onClick={onClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</TwoScreenSpacedOptions>
|
||||
<TwoScreenSpacedOptions>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 36px;
|
||||
`}
|
||||
color={'text.secondary'}>
|
||||
{`${collectionCount} ${constants.ALBUMS}`}
|
||||
</Typography>
|
||||
<CollectionSort
|
||||
activeSortBy={collectionSortBy}
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
/>
|
||||
</TwoScreenSpacedOptions>
|
||||
</DialogTitle>
|
||||
);
|
||||
}
|
62
src/components/Collections/AllCollections/index.tsx
Normal file
62
src/components/Collections/AllCollections/index.tsx
Normal file
|
@ -0,0 +1,62 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import { sortCollectionSummaries } from 'services/collectionService';
|
||||
import { Transition, FloatingSidebar } from 'components/FloatingSidebar';
|
||||
import { useLocalState } from 'hooks/useLocalState';
|
||||
import { LS_KEYS } from 'utils/storage/localStorage';
|
||||
import AllCollectionsHeader from './header';
|
||||
import { CollectionSummaries } from 'types/collection';
|
||||
import AllCollectionContent from './content';
|
||||
|
||||
interface Iprops {
|
||||
isOpen: boolean;
|
||||
close: () => void;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
setActiveCollection: (id?: number) => void;
|
||||
}
|
||||
|
||||
const LeftSlideTransition = Transition('up');
|
||||
|
||||
export default function AllCollections(props: Iprops) {
|
||||
const { collectionSummaries, isOpen, close, setActiveCollection } = props;
|
||||
|
||||
const [collectionSortBy, setCollectionSortBy] =
|
||||
useLocalState<COLLECTION_SORT_BY>(
|
||||
LS_KEYS.COLLECTION_SORT_BY,
|
||||
COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING
|
||||
);
|
||||
|
||||
const sortedCollectionSummaries = useMemo(
|
||||
() =>
|
||||
sortCollectionSummaries(
|
||||
[...collectionSummaries.values()],
|
||||
collectionSortBy
|
||||
),
|
||||
[collectionSortBy, collectionSummaries]
|
||||
);
|
||||
|
||||
const onCollectionClick = (collectionID: number) => {
|
||||
setActiveCollection(collectionID);
|
||||
close();
|
||||
};
|
||||
|
||||
return (
|
||||
<FloatingSidebar
|
||||
TransitionComponent={LeftSlideTransition}
|
||||
onClose={close}
|
||||
open={isOpen}>
|
||||
<AllCollectionsHeader
|
||||
onClose={close}
|
||||
collectionCount={props.collectionSummaries.size}
|
||||
collectionSortBy={collectionSortBy}
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
/>
|
||||
<Divider />
|
||||
<AllCollectionContent
|
||||
sortedCollectionSummaries={sortedCollectionSummaries}
|
||||
onCollectionClick={onCollectionClick}
|
||||
/>
|
||||
</FloatingSidebar>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CollectionTileWrapper, ActiveIndicator } from '../styledComponents';
|
||||
import CollectionCard from '../CollectionCard';
|
||||
|
||||
const CollectionCardWithActiveIndicator = React.forwardRef(
|
||||
(
|
||||
props: {
|
||||
children;
|
||||
active: boolean;
|
||||
latestFile: EnteFile;
|
||||
onClick: () => void;
|
||||
},
|
||||
ref: any
|
||||
) => {
|
||||
const { active, ...others } = props;
|
||||
|
||||
return (
|
||||
<CollectionTileWrapper ref={ref}>
|
||||
<CollectionCard {...others} />
|
||||
{active && <ActiveIndicator />}
|
||||
</CollectionTileWrapper>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default CollectionCardWithActiveIndicator;
|
|
@ -0,0 +1,12 @@
|
|||
import React from 'react';
|
||||
import constants from 'utils/strings/englishConstants';
|
||||
import { CollectionTitleWithDashedBorder } from '../styledComponents';
|
||||
|
||||
export const CreateNewCollectionTile = (props) => {
|
||||
return (
|
||||
<CollectionTitleWithDashedBorder {...props}>
|
||||
<div>{constants.NEW} </div>
|
||||
<div>{'+'}</div>
|
||||
</CollectionTitleWithDashedBorder>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,56 @@
|
|||
import React from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import NavigateNextIcon from '@mui/icons-material/NavigateNext';
|
||||
|
||||
export enum SCROLL_DIRECTION {
|
||||
LEFT = -1,
|
||||
RIGHT = +1,
|
||||
}
|
||||
|
||||
const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
height: 50px;
|
||||
width: 50px;
|
||||
border: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
|
||||
border-radius: 50%;
|
||||
background-color: ${({ theme }) => theme.palette.background.paper};
|
||||
|
||||
color: ${({ theme }) => theme.palette.text.primary};
|
||||
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT
|
||||
? css`
|
||||
left: 0;
|
||||
text-align: right;
|
||||
transform: translate(-50%, 0%);
|
||||
`
|
||||
: css`
|
||||
right: 0;
|
||||
text-align: left;
|
||||
transform: translate(50%, 0%);
|
||||
`}
|
||||
|
||||
& > svg {
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT &&
|
||||
'transform:rotate(180deg);'}
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationButton = ({ scrollDirection, ...rest }) => (
|
||||
<Wrapper direction={scrollDirection} {...rest}>
|
||||
<NavigateNextIcon />
|
||||
</Wrapper>
|
||||
);
|
||||
export default NavigationButton;
|
112
src/components/Collections/CollectionBar/index.tsx
Normal file
112
src/components/Collections/CollectionBar/index.tsx
Normal file
|
@ -0,0 +1,112 @@
|
|||
import NavigationButton, {
|
||||
SCROLL_DIRECTION,
|
||||
} from 'components/Collections/CollectionBar/NavigationButton';
|
||||
import React, { useEffect } from 'react';
|
||||
import { Collection, CollectionSummaries } from 'types/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { ALL_SECTION } from 'constants/collection';
|
||||
import { Link, Typography } from '@mui/material';
|
||||
import {
|
||||
Hider,
|
||||
CollectionBarWrapper,
|
||||
ScrollContainer,
|
||||
TwoScreenSpacedOptionsWithBodyPadding,
|
||||
} from 'components/Collections/styledComponents';
|
||||
import CollectionCardWithActiveIndicator from 'components/Collections/CollectionBar/CollectionCardWithActiveIndicator';
|
||||
import useComponentScroll from 'hooks/useComponentScroll';
|
||||
import useWindowSize from 'hooks/useWindowSize';
|
||||
|
||||
interface IProps {
|
||||
collections: Collection[];
|
||||
activeCollection?: number;
|
||||
setActiveCollection: (id?: number) => void;
|
||||
isInSearchMode: boolean;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
showAllCollections: () => void;
|
||||
}
|
||||
|
||||
export default function CollectionBar(props: IProps) {
|
||||
const {
|
||||
activeCollection,
|
||||
collections,
|
||||
setActiveCollection,
|
||||
collectionSummaries,
|
||||
showAllCollections,
|
||||
} = props;
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const {
|
||||
componentRef,
|
||||
scrollComponent,
|
||||
hasScrollBar,
|
||||
onFarLeft,
|
||||
onFarRight,
|
||||
} = useComponentScroll({
|
||||
dependencies: [windowSize, collections],
|
||||
});
|
||||
|
||||
const collectionChipsRef = props.collections.reduce(
|
||||
(refMap, collection) => {
|
||||
refMap[collection.id] = React.createRef();
|
||||
return refMap;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
collectionChipsRef[activeCollection]?.current.scrollIntoView({
|
||||
inline: 'center',
|
||||
});
|
||||
}, [activeCollection]);
|
||||
|
||||
const clickHandler = (collectionID?: number) => () => {
|
||||
setActiveCollection(collectionID ?? ALL_SECTION);
|
||||
};
|
||||
|
||||
return (
|
||||
<Hider hide={props.isInSearchMode}>
|
||||
<TwoScreenSpacedOptionsWithBodyPadding>
|
||||
<Typography>{constants.ALBUMS}</Typography>
|
||||
{hasScrollBar && (
|
||||
<Link component="button" onClick={showAllCollections}>
|
||||
{constants.VIEW_ALL_ALBUMS}
|
||||
</Link>
|
||||
)}
|
||||
</TwoScreenSpacedOptionsWithBodyPadding>
|
||||
<CollectionBarWrapper>
|
||||
{!onFarLeft && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollComponent(SCROLL_DIRECTION.LEFT)}
|
||||
/>
|
||||
)}
|
||||
<ScrollContainer ref={componentRef}>
|
||||
<CollectionCardWithActiveIndicator
|
||||
latestFile={null}
|
||||
active={activeCollection === ALL_SECTION}
|
||||
onClick={clickHandler(ALL_SECTION)}>
|
||||
{constants.ALL_SECTION_NAME}
|
||||
</CollectionCardWithActiveIndicator>
|
||||
{collections.map((item) => (
|
||||
<CollectionCardWithActiveIndicator
|
||||
key={item.id}
|
||||
latestFile={
|
||||
collectionSummaries.get(item.id)?.latestFile
|
||||
}
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}>
|
||||
{item.name}
|
||||
</CollectionCardWithActiveIndicator>
|
||||
))}
|
||||
</ScrollContainer>
|
||||
{!onFarRight && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollComponent(SCROLL_DIRECTION.RIGHT)}
|
||||
/>
|
||||
)}
|
||||
</CollectionBarWrapper>
|
||||
</Hider>
|
||||
);
|
||||
}
|
37
src/components/Collections/CollectionCard.tsx
Normal file
37
src/components/Collections/CollectionCard.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { useState, useContext, useEffect } from 'react';
|
||||
import downloadManager from 'services/downloadManager';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { CollectionTile, LargerCollectionTile } from './styledComponents';
|
||||
|
||||
export default function CollectionCard(props: {
|
||||
children?: any;
|
||||
latestFile: EnteFile;
|
||||
onClick: () => void;
|
||||
large?: boolean;
|
||||
}) {
|
||||
const { latestFile: file, onClick, children, large } = props;
|
||||
|
||||
const [coverImageURL, setCoverImageURL] = useState(null);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
if (!galleryContext.thumbs.has(file.id)) {
|
||||
const url = await downloadManager.getThumbnail(file);
|
||||
galleryContext.thumbs.set(file.id, url);
|
||||
}
|
||||
setCoverImageURL(galleryContext.thumbs.get(file.id));
|
||||
};
|
||||
main();
|
||||
}, [file]);
|
||||
const UsedCollectionTile = large ? LargerCollectionTile : CollectionTile;
|
||||
return (
|
||||
<UsedCollectionTile coverImgURL={coverImageURL} onClick={onClick}>
|
||||
{children}
|
||||
</UsedCollectionTile>
|
||||
);
|
||||
}
|
46
src/components/Collections/CollectionInfo.tsx
Normal file
46
src/components/Collections/CollectionInfo.tsx
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Collection, CollectionSummary } from 'types/collection';
|
||||
import { TwoScreenSpacedOptionsWithBodyPadding } from 'components/Collections/styledComponents';
|
||||
import CollectionOptions from 'components/Collections/CollectionOptions';
|
||||
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
|
||||
|
||||
interface Iprops {
|
||||
activeCollection: Collection;
|
||||
collectionSummary: CollectionSummary;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
showCollectionShareModal: () => void;
|
||||
redirectToAll: () => void;
|
||||
}
|
||||
export default function collectionInfo(props: Iprops) {
|
||||
if (!props.collectionSummary) {
|
||||
return <></>;
|
||||
}
|
||||
const {
|
||||
collectionSummary: { collectionAttributes, fileCount },
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<TwoScreenSpacedOptionsWithBodyPadding>
|
||||
<div>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
line-height: 36px;
|
||||
`}>
|
||||
{collectionAttributes.name}
|
||||
</Typography>
|
||||
<Typography
|
||||
css={`
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`}>
|
||||
{fileCount} {constants.PHOTOS}
|
||||
</Typography>
|
||||
</div>
|
||||
<CollectionOptions {...props} />
|
||||
</TwoScreenSpacedOptionsWithBodyPadding>
|
||||
);
|
||||
}
|
101
src/components/Collections/CollectionNamer.tsx
Normal file
101
src/components/Collections/CollectionNamer.tsx
Normal file
|
@ -0,0 +1,101 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
IconButton,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import constants from 'utils/strings/constants';
|
||||
import SubmitButton from 'components/SubmitButton';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import { TwoScreenSpacedOptions } from 'components/Container';
|
||||
import Close from '@mui/icons-material/Close';
|
||||
|
||||
export interface CollectionNamerAttributes {
|
||||
callback: (name) => void;
|
||||
title: string;
|
||||
autoFilledName: string;
|
||||
buttonText: string;
|
||||
}
|
||||
|
||||
export type SetCollectionNamerAttributes = React.Dispatch<
|
||||
React.SetStateAction<CollectionNamerAttributes>
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
attributes: CollectionNamerAttributes;
|
||||
}
|
||||
interface formValues {
|
||||
albumName: string;
|
||||
}
|
||||
|
||||
export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||
if (!attributes) {
|
||||
return <></>;
|
||||
}
|
||||
const onSubmit = ({ albumName }: formValues) => {
|
||||
attributes.callback(albumName);
|
||||
props.onHide();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={props.show} onClose={props.onHide} maxWidth="xs">
|
||||
<DialogTitle>
|
||||
<TwoScreenSpacedOptions>
|
||||
{attributes?.title}
|
||||
<IconButton onClick={props.onHide}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
</TwoScreenSpacedOptions>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Formik<formValues>
|
||||
initialValues={{
|
||||
albumName: attributes.autoFilledName ?? '',
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
albumName: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({
|
||||
values,
|
||||
touched,
|
||||
errors,
|
||||
handleChange,
|
||||
handleSubmit,
|
||||
}) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<TextField
|
||||
margin="normal"
|
||||
fullWidth
|
||||
type="text"
|
||||
label={constants.ENTER_ALBUM_NAME}
|
||||
value={values.albumName}
|
||||
onChange={handleChange('albumName')}
|
||||
autoFocus
|
||||
required
|
||||
error={
|
||||
touched.albumName &&
|
||||
Boolean(errors.albumName)
|
||||
}
|
||||
helperText={
|
||||
touched.albumName && errors.albumName
|
||||
}
|
||||
/>
|
||||
<SubmitButton
|
||||
buttonText={attributes.buttonText}
|
||||
loading={false}
|
||||
/>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
231
src/components/Collections/CollectionOptions.tsx
Normal file
231
src/components/Collections/CollectionOptions.tsx
Normal file
|
@ -0,0 +1,231 @@
|
|||
import React, { useContext, useState } from 'react';
|
||||
import * as CollectionAPI from 'services/collectionService';
|
||||
import {
|
||||
changeCollectionVisibility,
|
||||
downloadAllCollectionFiles,
|
||||
} from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import { Collection } from 'types/collection';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
import { InvertedIconButton } from 'components/Container';
|
||||
import OptionIcon from 'components/icons/OptionIcon-2';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import MenuList from '@mui/material/MenuList';
|
||||
import { ListItem, Menu, MenuItem } from '@mui/material';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
|
||||
interface CollectionOptionsProps {
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
activeCollection: Collection;
|
||||
showCollectionShareModal: () => void;
|
||||
redirectToAll: () => void;
|
||||
}
|
||||
|
||||
enum CollectionActions {
|
||||
RENAME,
|
||||
DOWNLOAD,
|
||||
ARCHIVE,
|
||||
UNARCHIVE,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||
const {
|
||||
activeCollection,
|
||||
redirectToAll,
|
||||
setCollectionNamerAttributes,
|
||||
showCollectionShareModal,
|
||||
} = props;
|
||||
const { startLoading, finishLoading, setDialogMessage, syncWithRemote } =
|
||||
useContext(GalleryContext);
|
||||
|
||||
const [optionEl, setOptionEl] = useState(null);
|
||||
const handleClose = () => setOptionEl(null);
|
||||
|
||||
const handleCollectionAction = (action: CollectionActions) => {
|
||||
let callback;
|
||||
switch (action) {
|
||||
case CollectionActions.RENAME:
|
||||
callback = renameCollection;
|
||||
break;
|
||||
case CollectionActions.DOWNLOAD:
|
||||
callback = downloadCollection;
|
||||
break;
|
||||
case CollectionActions.ARCHIVE:
|
||||
callback = archiveCollection;
|
||||
break;
|
||||
case CollectionActions.UNARCHIVE:
|
||||
callback = unArchiveCollection;
|
||||
break;
|
||||
case CollectionActions.DELETE:
|
||||
callback = deleteCollection;
|
||||
break;
|
||||
default:
|
||||
logError(
|
||||
Error('invalid collection action '),
|
||||
'handleCollectionAction failed'
|
||||
);
|
||||
{
|
||||
action;
|
||||
}
|
||||
}
|
||||
return async (...args) => {
|
||||
startLoading();
|
||||
try {
|
||||
await callback(...args);
|
||||
handleClose();
|
||||
} catch (e) {
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
close: { variant: 'danger' },
|
||||
});
|
||||
}
|
||||
|
||||
syncWithRemote();
|
||||
finishLoading();
|
||||
};
|
||||
};
|
||||
|
||||
const renameCollection = (newName: string) => {
|
||||
if (activeCollection.name !== newName) {
|
||||
CollectionAPI.renameCollection(activeCollection, newName);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteCollection = async () => {
|
||||
await CollectionAPI.deleteCollection(activeCollection.id);
|
||||
redirectToAll();
|
||||
};
|
||||
|
||||
const archiveCollection = () => {
|
||||
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.ARCHIVED);
|
||||
};
|
||||
|
||||
const unArchiveCollection = () => {
|
||||
changeCollectionVisibility(activeCollection, VISIBILITY_STATE.VISIBLE);
|
||||
};
|
||||
|
||||
const downloadCollection = () => {
|
||||
downloadAllCollectionFiles(activeCollection.id);
|
||||
};
|
||||
|
||||
const showRenameCollectionModal = () => {
|
||||
setCollectionNamerAttributes({
|
||||
title: constants.RENAME_COLLECTION,
|
||||
buttonText: constants.RENAME,
|
||||
autoFilledName: activeCollection.name,
|
||||
callback: handleCollectionAction(CollectionActions.RENAME),
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDeleteCollection = () => {
|
||||
setDialogMessage({
|
||||
title: constants.CONFIRM_DELETE_COLLECTION,
|
||||
content: constants.DELETE_COLLECTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.DELETE_COLLECTION,
|
||||
action: handleCollectionAction(CollectionActions.DELETE),
|
||||
variant: 'danger',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDownloadCollection = () => {
|
||||
setDialogMessage({
|
||||
title: constants.CONFIRM_DOWNLOAD_COLLECTION,
|
||||
content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.DOWNLOAD,
|
||||
action: handleCollectionAction(CollectionActions.DOWNLOAD),
|
||||
variant: 'success',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<InvertedIconButton
|
||||
style={{
|
||||
transform: 'rotate(90deg)',
|
||||
}}
|
||||
onClick={(event) => setOptionEl(event.currentTarget)}
|
||||
aria-controls={optionEl ? 'collection-options' : undefined}
|
||||
aria-haspopup="true"
|
||||
aria-expanded={optionEl ? 'true' : undefined}>
|
||||
<OptionIcon />
|
||||
</InvertedIconButton>
|
||||
<Menu
|
||||
id="collection-options"
|
||||
anchorEl={optionEl}
|
||||
open={Boolean(optionEl)}
|
||||
onClose={handleClose}
|
||||
MenuListProps={{
|
||||
disablePadding: true,
|
||||
'aria-labelledby': 'collection-options',
|
||||
}}>
|
||||
<Paper sx={{ borderRadius: '10px' }}>
|
||||
<MenuList
|
||||
sx={{
|
||||
padding: 0,
|
||||
border: 'none',
|
||||
borderRadius: '8px',
|
||||
}}>
|
||||
<MenuItem>
|
||||
<ListItem onClick={showRenameCollectionModal}>
|
||||
{constants.RENAME}
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItem onClick={showCollectionShareModal}>
|
||||
{constants.SHARE}
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItem onClick={confirmDownloadCollection}>
|
||||
{constants.DOWNLOAD}
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
{IsArchived(activeCollection) ? (
|
||||
<ListItem
|
||||
onClick={handleCollectionAction(
|
||||
CollectionActions.UNARCHIVE
|
||||
)}>
|
||||
{constants.UNARCHIVE}
|
||||
</ListItem>
|
||||
) : (
|
||||
<ListItem
|
||||
onClick={handleCollectionAction(
|
||||
CollectionActions.ARCHIVE
|
||||
)}>
|
||||
{constants.ARCHIVE}
|
||||
</ListItem>
|
||||
)}
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<ListItem
|
||||
color="danger"
|
||||
onClick={confirmDeleteCollection}>
|
||||
{constants.DELETE}
|
||||
</ListItem>
|
||||
</MenuItem>
|
||||
</MenuList>
|
||||
</Paper>
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionOptions;
|
|
@ -16,31 +16,30 @@ import {
|
|||
updateShareableURL,
|
||||
} from 'services/collectionService';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import SubmitButton from './SubmitButton';
|
||||
import MessageDialog from './MessageDialog';
|
||||
import SubmitButton from '../SubmitButton';
|
||||
import MessageDialog from '../MessageDialog';
|
||||
import { Collection, PublicURL, UpdatePublicURL } from 'types/collection';
|
||||
import {
|
||||
appendCollectionKeyToShareURL,
|
||||
selectIntOptions,
|
||||
shareExpiryOptions,
|
||||
} from 'utils/collection';
|
||||
import { FlexWrapper, Label, Row, Value } from './Container';
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { ButtonVariant, getVariantColor } from './pages/gallery/LinkButton';
|
||||
import { FlexWrapper, Label, Row, Value } from '../Container';
|
||||
import { CodeBlock } from '../CodeBlock';
|
||||
import { ButtonVariant, getVariantColor } from '../pages/gallery/LinkButton';
|
||||
import { handleSharingErrors } from 'utils/error';
|
||||
import { sleep } from 'utils/common';
|
||||
import { SelectStyles } from './Search/styles';
|
||||
import { SelectStyles } from '../Search/styles';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { dateStringWithMMH } from 'utils/time';
|
||||
import styled from 'styled-components';
|
||||
import SingleInputForm from './SingleInputForm';
|
||||
import SingleInputForm from '../SingleInputForm';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
collection: Collection;
|
||||
syncWithRemote: () => Promise<void>;
|
||||
}
|
||||
interface formValues {
|
||||
email: string;
|
79
src/components/Collections/index.tsx
Normal file
79
src/components/Collections/index.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { Collection, CollectionSummaries } from 'types/collection';
|
||||
import CollectionBar from 'components/Collections/CollectionBar';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import AllCollections from 'components/Collections/AllCollections';
|
||||
import CollectionInfo from 'components/Collections/CollectionInfo';
|
||||
import { ALL_SECTION } from 'constants/collection';
|
||||
import CollectionShare from 'components/Collections/CollectionShare';
|
||||
import { SetCollectionNamerAttributes } from 'components/Collections/CollectionNamer';
|
||||
interface Iprops {
|
||||
collections: Collection[];
|
||||
activeCollectionID?: number;
|
||||
setActiveCollectionID: (id?: number) => void;
|
||||
isInSearchMode: boolean;
|
||||
collectionSummaries: CollectionSummaries;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
}
|
||||
|
||||
export default function Collections(props: Iprops) {
|
||||
const {
|
||||
collections,
|
||||
isInSearchMode,
|
||||
activeCollectionID,
|
||||
setActiveCollectionID,
|
||||
collectionSummaries,
|
||||
setCollectionNamerAttributes,
|
||||
} = props;
|
||||
|
||||
const [allCollectionView, setAllCollectionView] = useState(false);
|
||||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
useState(false);
|
||||
const collectionsMap = useRef<Map<number, Collection>>(new Map());
|
||||
const activeCollection = useRef<Collection>(null);
|
||||
|
||||
useEffect(() => {
|
||||
collectionsMap.current = new Map(
|
||||
props.collections.map((collection) => [collection.id, collection])
|
||||
);
|
||||
}, [collections]);
|
||||
|
||||
useEffect(() => {
|
||||
activeCollection.current =
|
||||
collectionsMap.current.get(activeCollectionID);
|
||||
}, [activeCollectionID, collections]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CollectionBar
|
||||
collections={collections}
|
||||
isInSearchMode={isInSearchMode}
|
||||
activeCollection={activeCollectionID}
|
||||
setActiveCollection={setActiveCollectionID}
|
||||
collectionSummaries={collectionSummaries}
|
||||
showAllCollections={() => setAllCollectionView(true)}
|
||||
/>
|
||||
|
||||
<AllCollections
|
||||
isOpen={allCollectionView}
|
||||
close={() => setAllCollectionView(false)}
|
||||
collectionSummaries={collectionSummaries}
|
||||
setActiveCollection={setActiveCollectionID}
|
||||
/>
|
||||
|
||||
<CollectionInfo
|
||||
collectionSummary={collectionSummaries.get(activeCollectionID)}
|
||||
activeCollection={activeCollection.current}
|
||||
setCollectionNamerAttributes={setCollectionNamerAttributes}
|
||||
redirectToAll={() => setActiveCollectionID(ALL_SECTION)}
|
||||
showCollectionShareModal={() =>
|
||||
setCollectionShareModalView(true)
|
||||
}
|
||||
/>
|
||||
<CollectionShare
|
||||
show={collectionShareModalView}
|
||||
onHide={() => setCollectionShareModalView(false)}
|
||||
collection={activeCollection.current}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
83
src/components/Collections/styledComponents.ts
Normal file
83
src/components/Collections/styledComponents.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
import { TwoScreenSpacedOptions } from 'components/Container';
|
||||
import { IMAGE_CONTAINER_MAX_WIDTH } from 'constants/gallery';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export const CollectionBarWrapper = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
height: 86px;
|
||||
width: 100%;
|
||||
margin: 10px auto;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
border-bottom: 1px solid ${({ theme }) => theme.palette.grey.A200};
|
||||
`;
|
||||
|
||||
export const TwoScreenSpacedOptionsWithBodyPadding = styled(
|
||||
TwoScreenSpacedOptions
|
||||
)`
|
||||
margin-bottom: 8px;
|
||||
margin-top: 16px;
|
||||
padding: 0 24px;
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ScrollContainer = styled.div`
|
||||
width: 100%;
|
||||
height: 100px;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
export const CollectionTile = styled.div<{
|
||||
coverImgURL?: string;
|
||||
}>`
|
||||
display: flex;
|
||||
width: 80px;
|
||||
height: 64px;
|
||||
border-radius: 4px;
|
||||
padding: 4px 6px;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
background-image: url(${({ coverImgURL }) => coverImgURL});
|
||||
background-size: cover;
|
||||
border: 1px solid ${({ theme }) => theme.palette.grey.A200};
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
`;
|
||||
|
||||
export const CollectionTileWrapper = styled.div`
|
||||
margin-right: 4px;
|
||||
`;
|
||||
|
||||
export const ActiveIndicator = styled.div`
|
||||
height: 3px;
|
||||
background-color: ${({ theme }) => theme.palette.text.primary};
|
||||
margin-top: 18px;
|
||||
border-radius: 2px;
|
||||
`;
|
||||
|
||||
export const Hider = styled.div<{ hide: boolean }>`
|
||||
opacity: ${(props) => (props.hide ? '0' : '100')};
|
||||
height: ${(props) => (props.hide ? '0' : 'auto')};
|
||||
`;
|
||||
|
||||
export const LargerCollectionTile = styled(CollectionTile)`
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
align-items: flex-start;
|
||||
margin: 2px;
|
||||
`;
|
||||
|
||||
export const CollectionTitleWithDashedBorder = styled(CollectionTile)`
|
||||
border: 1px dashed ${({ theme }) => theme.palette.grey.A200};
|
||||
`;
|
|
@ -55,10 +55,8 @@ export const Value = styled.div<{ width?: string }>`
|
|||
`;
|
||||
|
||||
export const FlexWrapper = styled.div`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
align-item: center;
|
||||
`;
|
||||
|
||||
export const FreeFlowText = styled.div`
|
||||
|
@ -66,3 +64,20 @@ export const FreeFlowText = styled.div`
|
|||
min-width: 30%;
|
||||
text-align: left;
|
||||
`;
|
||||
|
||||
export const TwoScreenSpacedOptions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
export const InvertedIconButton = styled(IconButton)`
|
||||
background-color: ${({ theme }) => theme.palette.primary.main};
|
||||
color: ${({ theme }) => theme.palette.background.default};
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.palette.grey.A100};
|
||||
}
|
||||
&:focus {
|
||||
background-color: ${({ theme }) => theme.palette.primary.main};
|
||||
}
|
||||
`;
|
||||
|
|
30
src/components/FloatingSidebar.tsx
Normal file
30
src/components/FloatingSidebar.tsx
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Dialog, Slide, styled } from '@mui/material';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const FloatingSidebar = styled(Dialog)(({ theme }) => ({
|
||||
'& .MuiDialog-container': {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
'& .MuiPaper-root': {
|
||||
maxWidth: '498px',
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
padding: theme.spacing(3, 2),
|
||||
},
|
||||
'& .MuiDialogContent-root': {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
FloatingSidebar.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const Transition = (direction: 'left' | 'right' | 'up') =>
|
||||
React.forwardRef(
|
||||
(props: { children: React.ReactElement<any, any> }, ref) => {
|
||||
return <Slide direction={direction} ref={ref} {...props} />;
|
||||
}
|
||||
);
|
26
src/components/MessageDialog/TitleWithCloseButton.tsx
Normal file
26
src/components/MessageDialog/TitleWithCloseButton.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import { DialogTitle, IconButton } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
import { TwoScreenSpacedOptions } from 'components/Container';
|
||||
|
||||
const DialogTitleWithCloseButton = (props) => {
|
||||
const { children, onClose, ...other } = props;
|
||||
|
||||
return (
|
||||
<DialogTitle {...other}>
|
||||
<TwoScreenSpacedOptions>
|
||||
{children}
|
||||
{onClose && (
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
onClick={onClose}
|
||||
sx={{ float: 'right' }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
)}
|
||||
</TwoScreenSpacedOptions>
|
||||
</DialogTitle>
|
||||
);
|
||||
};
|
||||
|
||||
export default DialogTitleWithCloseButton;
|
|
@ -25,6 +25,7 @@ type Props = React.PropsWithChildren<{
|
|||
attributes: MessageAttributes;
|
||||
size?: 'sm' | 'lg' | 'xl';
|
||||
}>;
|
||||
|
||||
export default function MessageDialog({
|
||||
attributes,
|
||||
children,
|
|
@ -1,69 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import NavigateNext from './icons/NavigateNext';
|
||||
|
||||
export enum SCROLL_DIRECTION {
|
||||
LEFT = -1,
|
||||
RIGHT = +1,
|
||||
}
|
||||
|
||||
const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
background-color: #191919;
|
||||
border: none;
|
||||
color: #eee;
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT
|
||||
? 'margin-right: 10px;'
|
||||
: 'margin-left: 10px;'}
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
|
||||
|
||||
& > svg {
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT &&
|
||||
'transform:rotate(180deg);'}
|
||||
border-radius: 50%;
|
||||
height: 30px;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
&:hover > svg {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
background: linear-gradient(
|
||||
to
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT
|
||||
? 'right'
|
||||
: 'left'},
|
||||
#191919 5%,
|
||||
rgba(255, 255, 255, 0) 80%
|
||||
);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
${(props) =>
|
||||
props.direction === SCROLL_DIRECTION.LEFT
|
||||
? 'left: 40px;'
|
||||
: 'right: 40px;'}
|
||||
}
|
||||
`;
|
||||
|
||||
const NavigationButton = ({ scrollDirection, ...rest }) => (
|
||||
<Wrapper direction={scrollDirection} {...rest}>
|
||||
<NavigateNext />
|
||||
</Wrapper>
|
||||
);
|
||||
export default NavigationButton;
|
|
@ -38,7 +38,7 @@ import { livePhotoBtnHTML } from 'components/LivePhotoBtn';
|
|||
import { logError } from 'utils/sentry';
|
||||
|
||||
import CloseIcon from 'components/icons/CloseIcon';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
import TickIcon from '@mui/icons-material/Done';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function NavigateNext(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
width="24px"
|
||||
fill="currentColor"
|
||||
{...props}>
|
||||
<path d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
NavigateNext.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function TickIcon(props) {
|
||||
export default function OptionIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -8,12 +8,13 @@ export default function TickIcon(props) {
|
|||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
fill="currentColor">
|
||||
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z" />
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />{' '}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
TickIcon.defaultProps = {
|
||||
OptionIcon.defaultProps = {
|
||||
height: 20,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
|
@ -1,20 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SortIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}>
|
||||
<path d="M0 0h24v24H0V0z" fill="none" />
|
||||
<path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
SortIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -1,94 +0,0 @@
|
|||
import React, { useEffect, useRef } from 'react';
|
||||
import { Form } from 'react-bootstrap';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Formik } from 'formik';
|
||||
import * as Yup from 'yup';
|
||||
import SubmitButton from 'components/SubmitButton';
|
||||
import MessageDialog from 'components/MessageDialog';
|
||||
|
||||
export interface CollectionNamerAttributes {
|
||||
callback: (name) => void;
|
||||
title: string;
|
||||
autoFilledName: string;
|
||||
buttonText: string;
|
||||
}
|
||||
|
||||
export type SetCollectionNamerAttributes = React.Dispatch<
|
||||
React.SetStateAction<CollectionNamerAttributes>
|
||||
>;
|
||||
|
||||
interface Props {
|
||||
show: boolean;
|
||||
onHide: () => void;
|
||||
attributes: CollectionNamerAttributes;
|
||||
}
|
||||
interface formValues {
|
||||
albumName: string;
|
||||
}
|
||||
|
||||
export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||
const collectionNameInputRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (attributes) {
|
||||
setTimeout(() => {
|
||||
collectionNameInputRef.current?.focus();
|
||||
}, 200);
|
||||
}
|
||||
}, [attributes]);
|
||||
if (!attributes) {
|
||||
return (
|
||||
<MessageDialog show={false} onHide={() => null} attributes={{}} />
|
||||
);
|
||||
}
|
||||
const onSubmit = ({ albumName }: formValues) => {
|
||||
attributes.callback(albumName);
|
||||
props.onHide();
|
||||
};
|
||||
return (
|
||||
<MessageDialog
|
||||
show={props.show}
|
||||
onHide={props.onHide}
|
||||
size="sm"
|
||||
attributes={{
|
||||
title: attributes?.title,
|
||||
}}>
|
||||
<Formik<formValues>
|
||||
initialValues={{ albumName: attributes.autoFilledName ?? '' }}
|
||||
validationSchema={Yup.object().shape({
|
||||
albumName: Yup.string().required(constants.REQUIRED),
|
||||
})}
|
||||
validateOnChange={false}
|
||||
validateOnBlur={false}
|
||||
onSubmit={onSubmit}>
|
||||
{({ values, touched, errors, handleChange, handleSubmit }) => (
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
className="text-center"
|
||||
type="text"
|
||||
value={values.albumName}
|
||||
onChange={handleChange('albumName')}
|
||||
isInvalid={Boolean(
|
||||
touched.albumName && errors.albumName
|
||||
)}
|
||||
placeholder={constants.ENTER_ALBUM_NAME}
|
||||
ref={collectionNameInputRef}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback
|
||||
type="invalid"
|
||||
className="text-center">
|
||||
{errors.albumName}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<SubmitButton
|
||||
buttonText={attributes.buttonText}
|
||||
loading={false}
|
||||
/>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</MessageDialog>
|
||||
);
|
||||
}
|
|
@ -1,187 +0,0 @@
|
|||
import React from 'react';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { ListGroup, Popover } from 'react-bootstrap';
|
||||
import { deleteCollection, renameCollection } from 'services/collectionService';
|
||||
import {
|
||||
changeCollectionVisibilityHelper,
|
||||
downloadCollection,
|
||||
getSelectedCollection,
|
||||
} from 'utils/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import LinkButton, { ButtonVariant, LinkButtonProps } from './LinkButton';
|
||||
import { sleep } from 'utils/common';
|
||||
import { Collection } from 'types/collection';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
|
||||
interface CollectionOptionsProps {
|
||||
syncWithRemote: () => Promise<void>;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
collections: Collection[];
|
||||
selectedCollectionID: number;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
startLoading: () => void;
|
||||
finishLoading: () => void;
|
||||
showCollectionShareModal: () => void;
|
||||
redirectToAll: () => void;
|
||||
}
|
||||
|
||||
export const MenuLink = ({ children, ...props }: LinkButtonProps) => (
|
||||
<LinkButton
|
||||
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
|
||||
{...props}>
|
||||
{children}
|
||||
</LinkButton>
|
||||
);
|
||||
|
||||
export const MenuItem = (props: { children: any }) => (
|
||||
<ListGroup.Item
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: '#282828',
|
||||
padding: 0,
|
||||
}}>
|
||||
{props.children}
|
||||
</ListGroup.Item>
|
||||
);
|
||||
|
||||
const CollectionOptions = (props: CollectionOptionsProps) => {
|
||||
const collectionRename = async (
|
||||
selectedCollection: Collection,
|
||||
newName: string
|
||||
) => {
|
||||
if (selectedCollection.name !== newName) {
|
||||
await renameCollection(selectedCollection, newName);
|
||||
props.syncWithRemote();
|
||||
}
|
||||
};
|
||||
const showRenameCollectionModal = () => {
|
||||
props.setCollectionNamerAttributes({
|
||||
title: constants.RENAME_COLLECTION,
|
||||
buttonText: constants.RENAME,
|
||||
autoFilledName: getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections
|
||||
)?.name,
|
||||
callback: (newName) => {
|
||||
props.startLoading();
|
||||
collectionRename(
|
||||
getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections
|
||||
),
|
||||
newName
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
const confirmDeleteCollection = () => {
|
||||
props.setDialogMessage({
|
||||
title: constants.CONFIRM_DELETE_COLLECTION,
|
||||
content: constants.DELETE_COLLECTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.DELETE_COLLECTION,
|
||||
action: () => {
|
||||
props.startLoading();
|
||||
deleteCollection(
|
||||
props.selectedCollectionID,
|
||||
props.syncWithRemote,
|
||||
props.redirectToAll,
|
||||
props.setDialogMessage
|
||||
);
|
||||
},
|
||||
variant: 'danger',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const archiveCollectionHelper = () => {
|
||||
changeCollectionVisibilityHelper(
|
||||
getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections
|
||||
),
|
||||
props.startLoading,
|
||||
props.finishLoading,
|
||||
props.setDialogMessage,
|
||||
props.syncWithRemote
|
||||
);
|
||||
};
|
||||
|
||||
const confirmDownloadCollection = () => {
|
||||
props.setDialogMessage({
|
||||
title: constants.CONFIRM_DOWNLOAD_COLLECTION,
|
||||
content: constants.DOWNLOAD_COLLECTION_MESSAGE(),
|
||||
staticBackdrop: true,
|
||||
proceed: {
|
||||
text: constants.DOWNLOAD,
|
||||
action: downloadCollectionHelper,
|
||||
variant: 'success',
|
||||
},
|
||||
close: {
|
||||
text: constants.CANCEL,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const downloadCollectionHelper = async () => {
|
||||
props.startLoading();
|
||||
await downloadCollection(
|
||||
props.selectedCollectionID,
|
||||
props.setDialogMessage
|
||||
);
|
||||
await sleep(1000);
|
||||
props.finishLoading();
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover id="collection-options" style={{ borderRadius: '10px' }}>
|
||||
<Popover.Content style={{ padding: 0, border: 'none' }}>
|
||||
<ListGroup style={{ borderRadius: '8px' }}>
|
||||
<MenuItem>
|
||||
<MenuLink onClick={showRenameCollectionModal}>
|
||||
{constants.RENAME}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuLink onClick={props.showCollectionShareModal}>
|
||||
{constants.SHARE}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuLink onClick={confirmDownloadCollection}>
|
||||
{constants.DOWNLOAD}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuLink onClick={archiveCollectionHelper}>
|
||||
{IsArchived(
|
||||
getSelectedCollection(
|
||||
props.selectedCollectionID,
|
||||
props.collections
|
||||
)
|
||||
)
|
||||
? constants.UNARCHIVE
|
||||
: constants.ARCHIVE}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<MenuLink
|
||||
variant={ButtonVariant.danger}
|
||||
onClick={confirmDeleteCollection}>
|
||||
{constants.DELETE}
|
||||
</MenuLink>
|
||||
</MenuItem>
|
||||
</ListGroup>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionOptions;
|
|
@ -1,31 +0,0 @@
|
|||
import { IconButton } from 'components/Container';
|
||||
import SortIcon from 'components/icons/SortIcon';
|
||||
import React from 'react';
|
||||
import { OverlayTrigger } from 'react-bootstrap';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import constants from 'utils/strings/constants';
|
||||
import CollectionSortOptions from './CollectionSortOptions';
|
||||
import { IconWithMessage } from 'components/IconWithMessage';
|
||||
|
||||
interface Props {
|
||||
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
|
||||
activeSortBy: COLLECTION_SORT_BY;
|
||||
}
|
||||
export default function CollectionSort(props: Props) {
|
||||
const collectionSortOptions = CollectionSortOptions(props);
|
||||
return (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionSortOptions}>
|
||||
<div>
|
||||
<IconWithMessage message={constants.SORT}>
|
||||
<IconButton style={{ color: '#fff' }}>
|
||||
<SortIcon />
|
||||
</IconButton>
|
||||
</IconWithMessage>
|
||||
</div>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
import { Value } from 'components/Container';
|
||||
import TickIcon from 'components/icons/TickIcon';
|
||||
import React from 'react';
|
||||
import { ListGroup, Popover, Row } from 'react-bootstrap';
|
||||
import { COLLECTION_SORT_BY } from 'constants/collection';
|
||||
import styled from 'styled-components';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { MenuItem, MenuLink } from './CollectionOptions';
|
||||
|
||||
interface OptionProps {
|
||||
activeSortBy: COLLECTION_SORT_BY;
|
||||
setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void;
|
||||
}
|
||||
|
||||
const TickWrapper = styled.span`
|
||||
color: #aaa;
|
||||
margin-left: 5px;
|
||||
`;
|
||||
|
||||
const SortByOptionCreator =
|
||||
({ setCollectionSortBy, activeSortBy }: OptionProps) =>
|
||||
(props: { sortBy: COLLECTION_SORT_BY; children: any }) =>
|
||||
(
|
||||
<MenuItem>
|
||||
<Row>
|
||||
<Value width="20px">
|
||||
{activeSortBy === props.sortBy && (
|
||||
<TickWrapper>
|
||||
<TickIcon />
|
||||
</TickWrapper>
|
||||
)}
|
||||
</Value>
|
||||
<Value width="165px">
|
||||
<MenuLink
|
||||
onClick={() => setCollectionSortBy(props.sortBy)}
|
||||
variant={
|
||||
activeSortBy === props.sortBy && 'success'
|
||||
}>
|
||||
{props.children}
|
||||
</MenuLink>
|
||||
</Value>
|
||||
</Row>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const CollectionSortOptions = (props: OptionProps) => {
|
||||
const SortByOption = SortByOptionCreator(props);
|
||||
|
||||
return (
|
||||
<Popover id="collection-sort-options" style={{ borderRadius: '10px' }}>
|
||||
<Popover.Content
|
||||
style={{ padding: 0, border: 'none', width: '185px' }}>
|
||||
<ListGroup style={{ borderRadius: '8px' }}>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.LATEST_FILE}>
|
||||
{constants.SORT_BY_LATEST_PHOTO}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.MODIFICATION_TIME}>
|
||||
{constants.SORT_BY_MODIFICATION_TIME}
|
||||
</SortByOption>
|
||||
<SortByOption sortBy={COLLECTION_SORT_BY.NAME}>
|
||||
{constants.SORT_BY_COLLECTION_NAME}
|
||||
</SortByOption>
|
||||
</ListGroup>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionSortOptions;
|
|
@ -1,299 +0,0 @@
|
|||
import CollectionShare from 'components/CollectionShare';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import NavigationButton, {
|
||||
SCROLL_DIRECTION,
|
||||
} from 'components/NavigationButton';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
|
||||
import { sortCollections } from 'services/collectionService';
|
||||
import { User } from 'types/user';
|
||||
import styled from 'styled-components';
|
||||
import { IMAGE_CONTAINER_MAX_WIDTH } from 'constants/gallery';
|
||||
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
|
||||
import { getSelectedCollection } from 'utils/collection';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import CollectionOptions from './CollectionOptions';
|
||||
import CollectionSort from './CollectionSort';
|
||||
import OptionIcon, { OptionIconWrapper } from './OptionIcon';
|
||||
import {
|
||||
ALL_SECTION,
|
||||
ARCHIVE_SECTION,
|
||||
CollectionType,
|
||||
COLLECTION_SORT_BY,
|
||||
TRASH_SECTION,
|
||||
} from 'constants/collection';
|
||||
import { IsArchived } from 'utils/magicMetadata';
|
||||
import Archive from 'components/icons/Archive';
|
||||
import { IconWithMessage } from 'components/IconWithMessage';
|
||||
|
||||
interface CollectionProps {
|
||||
collections: Collection[];
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[];
|
||||
activeCollection?: number;
|
||||
setActiveCollection: (id?: number) => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
syncWithRemote: () => Promise<void>;
|
||||
setCollectionNamerAttributes: SetCollectionNamerAttributes;
|
||||
startLoading: () => void;
|
||||
finishLoading: () => void;
|
||||
isInSearchMode: boolean;
|
||||
collectionFilesCount: Map<number, number>;
|
||||
}
|
||||
|
||||
const CollectionContainer = styled.div`
|
||||
overflow-y: hidden;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
width: calc(100% - 80px);
|
||||
position: relative;
|
||||
padding: 0 24px;
|
||||
|
||||
@media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * 4}px) {
|
||||
padding: 0 4px;
|
||||
}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
height: 70px;
|
||||
flex: 1;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
max-width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
`;
|
||||
|
||||
const CollectionBar = styled.div`
|
||||
width: 100%;
|
||||
margin: 10px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
`;
|
||||
|
||||
const Chip = styled.button<{ active: boolean; archived?: boolean }>`
|
||||
border-radius: 8px;
|
||||
padding: 4px;
|
||||
padding-left: 15px;
|
||||
${({ archived }) => !archived && 'padding-left: 24px;'}
|
||||
margin: 3px;
|
||||
border: none;
|
||||
background-color: ${(props) =>
|
||||
props.active ? '#fff' : 'rgba(255, 255, 255, 0.3)'};
|
||||
outline: none !important;
|
||||
&:hover {
|
||||
background-color: ${(props) => !props.active && '#bbbbbb'};
|
||||
}
|
||||
&:hover ${OptionIconWrapper} {
|
||||
opacity: 1;
|
||||
color: #000000;
|
||||
}
|
||||
`;
|
||||
|
||||
const SectionChipCreater =
|
||||
({ activeCollection, clickHandler }) =>
|
||||
({ section, label }) =>
|
||||
(
|
||||
<Chip
|
||||
active={activeCollection === section}
|
||||
onClick={clickHandler(section)}>
|
||||
{label}
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
</Chip>
|
||||
);
|
||||
const Hider = styled.div<{ hide: boolean }>`
|
||||
opacity: ${(props) => (props.hide ? '0' : '100')};
|
||||
height: ${(props) => (props.hide ? '0' : 'auto')};
|
||||
`;
|
||||
|
||||
export default function Collections(props: CollectionProps) {
|
||||
const { activeCollection, collections, setActiveCollection } = props;
|
||||
const [selectedCollectionID, setSelectedCollectionID] =
|
||||
useState<number>(null);
|
||||
const collectionWrapperRef = useRef<HTMLDivElement>(null);
|
||||
const collectionChipsRef = props.collections.reduce(
|
||||
(refMap, collection) => {
|
||||
refMap[collection.id] = React.createRef();
|
||||
return refMap;
|
||||
},
|
||||
{}
|
||||
);
|
||||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
useState(false);
|
||||
const [scrollObj, setScrollObj] = useState<{
|
||||
scrollLeft?: number;
|
||||
scrollWidth?: number;
|
||||
clientWidth?: number;
|
||||
}>({});
|
||||
const [collectionSortBy, setCollectionSortBy] =
|
||||
useState<COLLECTION_SORT_BY>(COLLECTION_SORT_BY.LATEST_FILE);
|
||||
|
||||
const updateScrollObj = () => {
|
||||
if (collectionWrapperRef.current) {
|
||||
const { scrollLeft, scrollWidth, clientWidth } =
|
||||
collectionWrapperRef.current;
|
||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateScrollObj();
|
||||
}, [collectionWrapperRef.current, props.isInSearchMode, collections]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!collectionWrapperRef?.current) {
|
||||
return;
|
||||
}
|
||||
collectionWrapperRef.current.scrollLeft = 0;
|
||||
}, [collections]);
|
||||
|
||||
useEffect(() => {
|
||||
collectionChipsRef[activeCollection]?.current.scrollIntoView({
|
||||
inline: 'center',
|
||||
});
|
||||
}, [activeCollection]);
|
||||
|
||||
const clickHandler = (collectionID?: number) => () => {
|
||||
setSelectedCollectionID(collectionID);
|
||||
setActiveCollection(collectionID ?? ALL_SECTION);
|
||||
};
|
||||
|
||||
const user: User = getData(LS_KEYS.USER);
|
||||
|
||||
const collectionOptions = CollectionOptions({
|
||||
syncWithRemote: props.syncWithRemote,
|
||||
setCollectionNamerAttributes: props.setCollectionNamerAttributes,
|
||||
collections: props.collections,
|
||||
selectedCollectionID,
|
||||
setDialogMessage: props.setDialogMessage,
|
||||
startLoading: props.startLoading,
|
||||
finishLoading: props.finishLoading,
|
||||
showCollectionShareModal: setCollectionShareModalView.bind(null, true),
|
||||
redirectToAll: setActiveCollection.bind(null, ALL_SECTION),
|
||||
});
|
||||
|
||||
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
||||
collectionWrapperRef.current.scrollBy(250 * direction, 0);
|
||||
};
|
||||
const renderTooltip = (collectionID: number) => {
|
||||
const fileCount = props.collectionFilesCount?.get(collectionID) ?? 0;
|
||||
return (
|
||||
<Tooltip id="button-tooltip">
|
||||
{fileCount} {fileCount > 1 ? 'items' : 'item'}
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
const SectionChip = SectionChipCreater({ activeCollection, clickHandler });
|
||||
|
||||
return (
|
||||
<Hider hide={props.isInSearchMode}>
|
||||
<CollectionShare
|
||||
show={collectionShareModalView}
|
||||
onHide={() => setCollectionShareModalView(false)}
|
||||
collection={getSelectedCollection(
|
||||
selectedCollectionID,
|
||||
props.collections
|
||||
)}
|
||||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<CollectionBar>
|
||||
<CollectionContainer>
|
||||
{scrollObj.scrollLeft > 0 && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
/>
|
||||
)}
|
||||
<Wrapper
|
||||
ref={collectionWrapperRef}
|
||||
onScroll={updateScrollObj}>
|
||||
<SectionChip
|
||||
section={ALL_SECTION}
|
||||
label={constants.ALL}
|
||||
/>
|
||||
{sortCollections(
|
||||
collections,
|
||||
props.collectionAndTheirLatestFile,
|
||||
collectionSortBy
|
||||
).map((item) => (
|
||||
<OverlayTrigger
|
||||
key={item.id}
|
||||
placement="top"
|
||||
delay={{ show: 250, hide: 400 }}
|
||||
overlay={renderTooltip(item.id)}>
|
||||
<Chip
|
||||
ref={collectionChipsRef[item.id]}
|
||||
active={activeCollection === item.id}
|
||||
onClick={clickHandler(item.id)}
|
||||
archived={IsArchived(item)}>
|
||||
{IsArchived(item) && (
|
||||
<IconWithMessage
|
||||
message={constants.ARCHIVED_ALBUM}>
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
marginRight: '5px',
|
||||
}}>
|
||||
<Archive />
|
||||
</div>
|
||||
</IconWithMessage>
|
||||
)}
|
||||
{item.name}
|
||||
{item.type !== CollectionType.favorites &&
|
||||
item.owner.id === user?.id ? (
|
||||
<OverlayTrigger
|
||||
rootClose
|
||||
trigger="click"
|
||||
placement="bottom"
|
||||
overlay={collectionOptions}>
|
||||
<OptionIcon
|
||||
onClick={() =>
|
||||
setSelectedCollectionID(
|
||||
item.id
|
||||
)
|
||||
}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
) : (
|
||||
<div
|
||||
style={{
|
||||
display: 'inline-block',
|
||||
width: '24px',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Chip>
|
||||
</OverlayTrigger>
|
||||
))}
|
||||
<SectionChip
|
||||
section={ARCHIVE_SECTION}
|
||||
label={constants.ARCHIVE}
|
||||
/>
|
||||
<SectionChip
|
||||
section={TRASH_SECTION}
|
||||
label={constants.TRASH}
|
||||
/>
|
||||
</Wrapper>
|
||||
{scrollObj.scrollLeft <
|
||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
/>
|
||||
)}
|
||||
</CollectionContainer>
|
||||
<CollectionSort
|
||||
setCollectionSortBy={setCollectionSortBy}
|
||||
activeSortBy={collectionSortBy}
|
||||
/>
|
||||
</CollectionBar>
|
||||
</Hider>
|
||||
);
|
||||
}
|
|
@ -6,7 +6,7 @@ import { SetDialogMessage } from 'components/MessageDialog';
|
|||
import UploadProgress from './UploadProgress';
|
||||
|
||||
import UploadStrategyChoiceModal from './UploadStrategyChoiceModal';
|
||||
import { SetCollectionNamerAttributes } from './CollectionNamer';
|
||||
import { SetCollectionNamerAttributes } from '../../Collections/CollectionNamer';
|
||||
import { SetCollectionSelectorAttributes } from './CollectionSelector';
|
||||
import { GalleryContext } from 'pages/gallery';
|
||||
import { AppContext } from 'pages/_app';
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { Collection } from 'types/collection';
|
||||
|
||||
interface Iprops {
|
||||
collection: Collection;
|
||||
}
|
||||
|
||||
const Info = styled.h5`
|
||||
margin: 20px;
|
||||
`;
|
||||
|
||||
export function CollectionInfo(props: Iprops) {
|
||||
if (!props.collection) {
|
||||
return <></>;
|
||||
}
|
||||
return <Info>{props.collection.name}</Info>;
|
||||
}
|
|
@ -9,9 +9,10 @@ export enum CollectionType {
|
|||
}
|
||||
|
||||
export enum COLLECTION_SORT_BY {
|
||||
LATEST_FILE,
|
||||
MODIFICATION_TIME,
|
||||
NAME,
|
||||
CREATION_TIME_ASCENDING,
|
||||
CREATION_TIME_DESCENDING,
|
||||
UPDATION_TIME_DESCENDING,
|
||||
}
|
||||
|
||||
export const COLLECTION_SHARE_DEFAULT_VALID_DURATION =
|
||||
|
|
|
@ -2,8 +2,32 @@ import { createTheme } from '@mui/material/styles';
|
|||
|
||||
// Create a theme instance.
|
||||
const darkThemeOptions = createTheme({
|
||||
components: {
|
||||
MuiPaper: {
|
||||
styleOverrides: { root: { backgroundImage: 'none' } },
|
||||
},
|
||||
MuiMenu: {
|
||||
styleOverrides: { paper: { margin: '10px' } },
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: {
|
||||
main: '#fff',
|
||||
},
|
||||
text: {
|
||||
primary: '#fff',
|
||||
secondary: '#808080',
|
||||
},
|
||||
background: { default: '#191919', paper: '#303030' },
|
||||
grey: {
|
||||
A100: '#ccc',
|
||||
A200: 'rgba(256, 256, 256, 0.24)',
|
||||
},
|
||||
divider: 'rgba(255, 255, 255, 0.24)',
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 8,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
59
src/hooks/useComponentScroll.tsx
Normal file
59
src/hooks/useComponentScroll.tsx
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { SCROLL_DIRECTION } from 'components/Collections/CollectionBar/NavigationButton';
|
||||
import { useRef, useState, useEffect } from 'react';
|
||||
|
||||
export default function useComponentScroll({
|
||||
dependencies,
|
||||
}: {
|
||||
dependencies: any[];
|
||||
}) {
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [scrollObj, setScrollObj] = useState<{
|
||||
scrollLeft?: number;
|
||||
scrollWidth?: number;
|
||||
clientWidth?: number;
|
||||
}>({});
|
||||
|
||||
const updateScrollObj = () => {
|
||||
if (!componentRef.current) {
|
||||
return;
|
||||
}
|
||||
const { scrollLeft, scrollWidth, clientWidth } = componentRef.current;
|
||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!componentRef.current) {
|
||||
return;
|
||||
}
|
||||
// Add event listener
|
||||
componentRef.current?.addEventListener('scroll', updateScrollObj);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
updateScrollObj();
|
||||
// Remove event listener on cleanup
|
||||
return () =>
|
||||
componentRef.current.removeEventListener('resize', updateScrollObj);
|
||||
}, [componentRef.current]);
|
||||
|
||||
useEffect(() => {
|
||||
updateScrollObj();
|
||||
}, [...dependencies]);
|
||||
|
||||
const scrollComponent = (direction: SCROLL_DIRECTION) => () => {
|
||||
componentRef.current.scrollBy(250 * direction, 0);
|
||||
};
|
||||
|
||||
const hasScrollBar = scrollObj.scrollWidth > scrollObj.clientWidth;
|
||||
const onFarLeft = scrollObj.scrollLeft === 0;
|
||||
const onFarRight =
|
||||
scrollObj.scrollLeft + scrollObj.clientWidth === scrollObj.scrollWidth;
|
||||
|
||||
return {
|
||||
hasScrollBar,
|
||||
onFarLeft,
|
||||
onFarRight,
|
||||
scrollComponent,
|
||||
componentRef: componentRef,
|
||||
};
|
||||
}
|
20
src/hooks/useLocalState.tsx
Normal file
20
src/hooks/useLocalState.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
|
||||
|
||||
export function useLocalState<T>(
|
||||
key: LS_KEYS,
|
||||
initialValue?: T
|
||||
): [T, Dispatch<SetStateAction<T>>] {
|
||||
const [value, setValue] = useState<T>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const { value } = getData(key) ?? {};
|
||||
setValue(value ?? initialValue);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setData(key, { value });
|
||||
}, [value]);
|
||||
|
||||
return [value, setValue];
|
||||
}
|
37
src/hooks/useWindowSize.tsx
Normal file
37
src/hooks/useWindowSize.tsx
Normal file
|
@ -0,0 +1,37 @@
|
|||
// https://usehooks.com/useWindowSize/
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
// Hook
|
||||
export default function useWindowSize() {
|
||||
// Initialize state with undefined width/height so server and client renders match
|
||||
// Learn more here: https://joshwcomeau.com/react/the-perils-of-rehydration/
|
||||
const [windowSize, setWindowSize] = useState<{
|
||||
width: number;
|
||||
height: number;
|
||||
}>({
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// Handler to call on window resize
|
||||
function handleResize() {
|
||||
// Set window width/height to state
|
||||
setWindowSize({
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
}
|
||||
|
||||
// Add event listener
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
// Call handler right away so state gets updated with initial window size
|
||||
handleResize();
|
||||
|
||||
// Remove event listener on cleanup
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, []); // Empty array ensures that effect is only run on mount
|
||||
|
||||
return windowSize;
|
||||
}
|
|
@ -1,14 +1,12 @@
|
|||
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||
import styled, {
|
||||
createGlobalStyle,
|
||||
ThemeProvider as SThemeProvider,
|
||||
} from 'styled-components';
|
||||
import styled, { ThemeProvider as SThemeProvider } from 'styled-components';
|
||||
import Navbar from 'components/Navbar';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { useRouter } from 'next/router';
|
||||
import Container from 'components/Container';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
import 'styles/global.css';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
import { logError } from '../utils/sentry';
|
||||
// import { Workbox } from 'workbox-window';
|
||||
|
@ -27,496 +25,9 @@ import MessageDialog, {
|
|||
} from 'components/MessageDialog';
|
||||
import { ThemeProvider as MThemeProvider } from '@mui/material/styles';
|
||||
import darkThemeOptions from 'darkThemeOptions';
|
||||
|
||||
const GlobalStyles = createGlobalStyle`
|
||||
/* ubuntu-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('/fonts/ubuntu-v15-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/ubuntu-v15-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
|
||||
/* ubuntu-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Ubuntu';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''),
|
||||
url('/fonts/ubuntu-v15-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/fonts/ubuntu-v15-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
html, body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: #191919;
|
||||
color: #aaa;
|
||||
font-family:Ubuntu, Arial, sans-serif !important;
|
||||
}
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: #d7d7d7;
|
||||
}
|
||||
|
||||
#__next {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
vertical-align: middle;
|
||||
margin: 8px;
|
||||
}
|
||||
|
||||
.pswp__item video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pswp-item-container > * {
|
||||
position: absolute;
|
||||
transition: opacity 1s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container > img{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pswp-item-container > video{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.pswp-item-container > div.download-banner {
|
||||
width:100%;
|
||||
height: 16vh;
|
||||
padding:2vh 0;
|
||||
background-color: #151414;
|
||||
color:#ddd;
|
||||
display: flex;
|
||||
flex-direction:column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
.download-banner > a{
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #e26f99,
|
||||
};
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.pswp__img {
|
||||
object-fit: contain;
|
||||
}
|
||||
.pswp__caption{
|
||||
font-size:20px;
|
||||
height:10%;
|
||||
padding-left:5%;
|
||||
color:#eee;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 2000;
|
||||
}
|
||||
.modal-dialog-centered {
|
||||
min-height: -webkit-calc(80% - 3.5rem);
|
||||
min-height: -moz-calc(80% - 3.5rem);
|
||||
min-height: calc(80% - 3.5rem);
|
||||
}
|
||||
.modal .modal-header, .modal .modal-footer , .toast-header{
|
||||
border-color: #444 !important;
|
||||
}
|
||||
.modal .modal-header .close, .toast-header .close {
|
||||
color: #aaa;
|
||||
text-shadow: none;
|
||||
}
|
||||
.modal-backdrop {
|
||||
z-index:2000;
|
||||
opacity:0.8 !important;
|
||||
}
|
||||
|
||||
.toast-header{
|
||||
border-radius:0px !important;
|
||||
}
|
||||
.modal .card , .table , .toast {
|
||||
background-color: #202020;
|
||||
border: none;
|
||||
}
|
||||
.modal .card > div {
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.modal-content ,.toast-header{
|
||||
border-radius:15px;
|
||||
background-color:#202020 !important;
|
||||
}
|
||||
.modal-dialog{
|
||||
margin:5vh auto;
|
||||
width:90%;
|
||||
}
|
||||
.modal-body{
|
||||
max-height:74vh;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.modal-xl{
|
||||
max-width:90% !important;
|
||||
}
|
||||
.modal-lg {
|
||||
max-width: 720px !important;
|
||||
}
|
||||
.plan-selector-modal-content {
|
||||
width:auto;
|
||||
margin:auto;
|
||||
}
|
||||
.pswp-custom {
|
||||
opacity: 0.75;
|
||||
transition: opacity .2s;
|
||||
display: inline-block;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
}
|
||||
.pswp-custom:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
.download-btn{
|
||||
background: url('/download_icon.png') no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
.info-btn{
|
||||
background: url('/info_icon.png') no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
.share-btn{
|
||||
background: url('/share_icon.png') no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
.btn.focus , .btn:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-success {
|
||||
background: #51cd7c;
|
||||
color: #fff;
|
||||
border-color: #51cd7c;
|
||||
}
|
||||
.btn-success:hover .btn-success:focus .btn-success:active {
|
||||
background-color: #29a354;
|
||||
border-color: #51cd7c;
|
||||
color: #242424;
|
||||
}
|
||||
.btn-success:disabled {
|
||||
background-color: #69b383;
|
||||
}
|
||||
.btn-outline-success {
|
||||
background: #51cd7c;
|
||||
color: #fff;
|
||||
border-color: #51cd7c;
|
||||
}
|
||||
.btn-outline-success:hover:enabled {
|
||||
background: #4db76c;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-outline-danger, .btn-outline-secondary, .btn-outline-primary{
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
#go-to-ente{
|
||||
background:none;
|
||||
border-color: #3dbb69;
|
||||
color:#51cd7c;
|
||||
}
|
||||
#go-to-ente:hover, #go-to-ente:focus, #go-to-ente:active {
|
||||
color:#fff;
|
||||
background-color: #44774d;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
a:hover {
|
||||
color: #51cd7c;
|
||||
}
|
||||
.btn-link {
|
||||
color: #fff;
|
||||
}
|
||||
.btn-link:hover {
|
||||
color: #51cd7c;
|
||||
}
|
||||
.btn-link-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
.btn-link-danger:hover {
|
||||
color: #ff495a;
|
||||
}
|
||||
.card {
|
||||
background-color: #242424;
|
||||
color: #d1d1d1;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.jumbotron{
|
||||
background-color: #191919;
|
||||
color: #d1d1d1;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.alert-primary {
|
||||
background-color: rgb(235, 255, 243);
|
||||
color: #000000;
|
||||
}
|
||||
.alert-success {
|
||||
background-color: #c4ffd6;
|
||||
}
|
||||
.bm-burger-button {
|
||||
position: fixed;
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
top:27px;
|
||||
left: 32px;
|
||||
z-index:100 !important;
|
||||
}
|
||||
.bm-burger-bars {
|
||||
background: #bdbdbd;
|
||||
}
|
||||
.bm-menu-wrap {
|
||||
top:0px;
|
||||
}
|
||||
.bm-menu {
|
||||
background: #131313;
|
||||
font-size: 1.15em;
|
||||
color:#d1d1d1;
|
||||
display: flex;
|
||||
}
|
||||
.bm-cross {
|
||||
background: #d1d1d1;
|
||||
}
|
||||
.bm-cross-button {
|
||||
top: 20px !important;
|
||||
}
|
||||
.bm-item-list {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
.bm-item {
|
||||
padding: 20px;
|
||||
}
|
||||
.bm-overlay {
|
||||
top: 0;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
.custom-switch.custom-switch-md{
|
||||
z-index:0;
|
||||
}
|
||||
.custom-switch.custom-switch-md .custom-control-label {
|
||||
padding-left: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-label::before {
|
||||
height: 1.5rem;
|
||||
background-color: #303030;
|
||||
border: none;
|
||||
width: calc(2.5rem + 0.75rem);
|
||||
border-radius: 3rem;
|
||||
}
|
||||
.custom-switch.custom-switch-md:active .custom-control-label::before {
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-label::after {
|
||||
top:2px;
|
||||
background:#c4c4c4;
|
||||
width: calc(2.0rem - 4px);
|
||||
height: calc(2.0rem - 4px);
|
||||
border-radius: calc(2rem - (2.0rem / 2));
|
||||
left: -38px;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after {
|
||||
transform: translateX(calc(2.0rem - 0.25rem));
|
||||
background:#c4c4c4;
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
background-color: #29a354;
|
||||
}
|
||||
|
||||
.bold-text{
|
||||
color: #ECECEC;
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.dropdown-item:active{
|
||||
color: #16181b;
|
||||
text-decoration: none;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
hr{
|
||||
border-top: 1rem solid #444 !important;
|
||||
}
|
||||
.list-group-item:hover{
|
||||
background-color:#343434 !important;
|
||||
}
|
||||
.list-group-item:active , list-group-item:focus{
|
||||
background-color:#000 !important;
|
||||
}
|
||||
#button-tooltip > .arrow::before{
|
||||
border-bottom-color:#282828 !important;
|
||||
}
|
||||
#button-tooltip > .arrow::after{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
|
||||
}
|
||||
.arrow::before{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
}
|
||||
.arrow::after{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
}
|
||||
.carousel-inner {
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
.carousel-indicators li {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.carousel-indicators .active {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
div.otp-input input::placeholder {
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
div.otp-input input:not(:placeholder-shown) , div.otp-input input:focus{
|
||||
border: 2px solid #51cd7c;
|
||||
border-radius:1px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
.flash-message{
|
||||
padding:16px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
@-webkit-keyframes rotation
|
||||
{
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
#button-tooltip{
|
||||
color: #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
padding:0px
|
||||
}
|
||||
.tooltip-inner{
|
||||
background-color: #282828;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.ente-form-group{
|
||||
margin:0;
|
||||
}
|
||||
.form-check-input:hover, .form-check-label :hover{
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.file-type-choice-modal{
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
|
||||
.manageLinkHeader:hover{
|
||||
color:#bbb;
|
||||
}
|
||||
`;
|
||||
import { CssBaseline } from '@mui/material';
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import * as types from 'styled-components/cssprop';
|
||||
|
||||
export const LogoImage = styled.img`
|
||||
max-height: 28px;
|
||||
|
@ -720,9 +231,10 @@ export default function App({ Component, err }) {
|
|||
content="initial-scale=1, width=device-width"
|
||||
/>
|
||||
</Head>
|
||||
<GlobalStyles />
|
||||
|
||||
<MThemeProvider theme={darkThemeOptions}>
|
||||
<SThemeProvider theme={darkThemeOptions}>
|
||||
<CssBaseline />
|
||||
{showNavbar && (
|
||||
<Navbar>
|
||||
<FlexContainer shouldJustifyLeft={isAlbumsDomain}>
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
|
||||
export const config = {
|
||||
api: {
|
||||
bodyParser: false,
|
||||
},
|
||||
};
|
||||
|
||||
const API_ENDPOINT =
|
||||
process.env.NEXT_PUBLIC_ENTE_ENDPOINT || 'https://api.staging.ente.io';
|
||||
|
||||
export default createProxyMiddleware({
|
||||
target: API_ENDPOINT,
|
||||
changeOrigin: true,
|
||||
pathRewrite: { '^/api': '/' },
|
||||
});
|
|
@ -22,6 +22,7 @@ import {
|
|||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
createCollection,
|
||||
getCollectionSummaries,
|
||||
} from 'services/collectionService';
|
||||
import constants from 'utils/strings/constants';
|
||||
import billingService from 'services/billingService';
|
||||
|
@ -48,7 +49,6 @@ import {
|
|||
getSelectedFiles,
|
||||
mergeMetadata,
|
||||
sortFiles,
|
||||
sortFilesIntoCollections,
|
||||
} from 'utils/file';
|
||||
import SearchBar from 'components/Search';
|
||||
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions/GalleryOptions';
|
||||
|
@ -57,7 +57,7 @@ import CollectionSelector, {
|
|||
} from 'components/pages/gallery/CollectionSelector';
|
||||
import CollectionNamer, {
|
||||
CollectionNamerAttributes,
|
||||
} from 'components/pages/gallery/CollectionNamer';
|
||||
} from 'components/Collections/CollectionNamer';
|
||||
import AlertBanner from 'components/pages/gallery/AlertBanner';
|
||||
import UploadButton from 'components/pages/gallery/UploadButton';
|
||||
import PlanSelector from 'components/pages/gallery/PlanSelector';
|
||||
|
@ -93,7 +93,11 @@ import DeleteBtn from 'components/DeleteBtn';
|
|||
import FixCreationTime, {
|
||||
FixCreationTimeAttributes,
|
||||
} from 'components/FixCreationTime';
|
||||
import { Collection, CollectionAndItsLatestFile } from 'types/collection';
|
||||
import {
|
||||
Collection,
|
||||
CollectionAndItsLatestFile,
|
||||
CollectionSummaries,
|
||||
} from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import {
|
||||
GalleryContextType,
|
||||
|
@ -101,11 +105,11 @@ import {
|
|||
Search,
|
||||
NotificationAttributes,
|
||||
} from 'types/gallery';
|
||||
import Collections from 'components/pages/gallery/Collections';
|
||||
import { VISIBILITY_STATE } from 'types/magicMetadata';
|
||||
import ToastNotification from 'components/ToastNotification';
|
||||
import { ElectronFile } from 'types/upload';
|
||||
import importService from 'services/importService';
|
||||
import Collections from 'components/Collections';
|
||||
|
||||
export const DeadCenter = styled.div`
|
||||
flex: 1;
|
||||
|
@ -130,6 +134,9 @@ const defaultGalleryContext: GalleryContextType = {
|
|||
syncWithRemote: () => null,
|
||||
setNotificationAttributes: () => null,
|
||||
setBlockingLoad: () => null,
|
||||
startLoading: () => null,
|
||||
finishLoading: () => null,
|
||||
setDialogMessage: () => null,
|
||||
};
|
||||
|
||||
export const GalleryContext = createContext<GalleryContextType>(
|
||||
|
@ -185,8 +192,8 @@ export default function Gallery() {
|
|||
const [deleted, setDeleted] = useState<number[]>([]);
|
||||
const { startLoading, finishLoading, setDialogMessage, ...appContext } =
|
||||
useContext(AppContext);
|
||||
const [collectionFilesCount, setCollectionFilesCount] =
|
||||
useState<Map<number, number>>();
|
||||
const [collectionSummaries, setCollectionSummaries] =
|
||||
useState<CollectionSummaries>(new Map());
|
||||
const [activeCollection, setActiveCollection] = useState<number>(undefined);
|
||||
const [trash, setTrash] = useState<Trash>([]);
|
||||
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
|
||||
|
@ -303,6 +310,7 @@ export default function Gallery() {
|
|||
const trash = await syncTrash(collections, setFiles, files);
|
||||
setTrash(trash);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
switch (e.message) {
|
||||
case ServerErrorCodes.SESSION_EXPIRED:
|
||||
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
|
||||
|
@ -340,20 +348,21 @@ export default function Gallery() {
|
|||
const favItemIds = await getFavItemIds(files);
|
||||
setFavItemIds(favItemIds);
|
||||
const nonEmptyCollections = getNonEmptyCollections(collections, files);
|
||||
|
||||
setCollections(nonEmptyCollections);
|
||||
const collectionsAndTheirLatestFile = getCollectionsAndTheirLatestFile(
|
||||
nonEmptyCollections,
|
||||
files
|
||||
);
|
||||
setCollectionsAndTheirLatestFile(collectionsAndTheirLatestFile);
|
||||
const collectionWiseFiles = sortFilesIntoCollections(files);
|
||||
const collectionFilesCount = new Map<number, number>();
|
||||
for (const [id, files] of collectionWiseFiles) {
|
||||
collectionFilesCount.set(id, files.length);
|
||||
}
|
||||
setCollectionFilesCount(collectionFilesCount);
|
||||
const collectionSummaries = getCollectionSummaries(
|
||||
nonEmptyCollections,
|
||||
files
|
||||
);
|
||||
|
||||
const archivedCollections = getArchivedCollections(collections);
|
||||
setCollectionSummaries(collectionSummaries);
|
||||
|
||||
const archivedCollections = getArchivedCollections(nonEmptyCollections);
|
||||
setArchivedCollections(new Set(archivedCollections));
|
||||
};
|
||||
|
||||
|
@ -427,24 +436,28 @@ export default function Gallery() {
|
|||
}
|
||||
};
|
||||
|
||||
const showCreateCollectionModal = (ops: COLLECTION_OPS_TYPE) => {
|
||||
const showCreateCollectionModal = (ops?: COLLECTION_OPS_TYPE) => {
|
||||
const callback = async (collectionName: string) => {
|
||||
try {
|
||||
startLoading();
|
||||
const collection = await createCollection(
|
||||
collectionName,
|
||||
CollectionType.album,
|
||||
collections
|
||||
);
|
||||
|
||||
if (ops) {
|
||||
await collectionOpsHelper(ops)(collection);
|
||||
}
|
||||
} catch (e) {
|
||||
logError(e, 'create and collection ops failed');
|
||||
logError(e, 'create and collection ops failed', { ops });
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
finishLoading();
|
||||
}
|
||||
};
|
||||
return () =>
|
||||
|
@ -570,6 +583,9 @@ export default function Gallery() {
|
|||
syncWithRemote,
|
||||
setNotificationAttributes,
|
||||
setBlockingLoad,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
setDialogMessage,
|
||||
}}>
|
||||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
|
@ -607,16 +623,11 @@ export default function Gallery() {
|
|||
/>
|
||||
<Collections
|
||||
collections={collections}
|
||||
collectionAndTheirLatestFile={collectionsAndTheirLatestFile}
|
||||
isInSearchMode={isInSearchMode}
|
||||
activeCollection={activeCollection}
|
||||
setActiveCollection={setActiveCollection}
|
||||
syncWithRemote={syncWithRemote}
|
||||
setDialogMessage={setDialogMessage}
|
||||
activeCollectionID={activeCollection}
|
||||
setActiveCollectionID={setActiveCollection}
|
||||
collectionSummaries={collectionSummaries}
|
||||
setCollectionNamerAttributes={setCollectionNamerAttributes}
|
||||
startLoading={startLoading}
|
||||
finishLoading={finishLoading}
|
||||
collectionFilesCount={collectionFilesCount}
|
||||
/>
|
||||
<CollectionNamer
|
||||
show={collectionNamerView}
|
||||
|
|
|
@ -13,11 +13,10 @@ import {
|
|||
syncPublicFiles,
|
||||
verifyPublicCollectionPassword,
|
||||
} from 'services/publicCollectionService';
|
||||
import { Collection } from 'types/collection';
|
||||
import { Collection, CollectionSummaries } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { mergeMetadata, sortFiles } from 'utils/file';
|
||||
import { AppContext } from 'pages/_app';
|
||||
import { CollectionInfo } from 'components/pages/sharedAlbum/CollectionInfo';
|
||||
import { AbuseReportForm } from 'components/pages/sharedAlbum/AbuseReportForm';
|
||||
import {
|
||||
defaultPublicCollectionGalleryContext,
|
||||
|
@ -59,6 +58,8 @@ export default function PublicCollectionGallery() {
|
|||
const router = useRouter();
|
||||
const [isPasswordProtected, setIsPasswordProtected] =
|
||||
useState<boolean>(false);
|
||||
const [collectionSummaries, setCollectionSummaries] =
|
||||
useState<CollectionSummaries>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
appContext.showNavBar(true);
|
||||
|
@ -132,7 +133,7 @@ export default function PublicCollectionGallery() {
|
|||
collection?.publicURLs?.[0]?.passwordEnabled;
|
||||
setIsPasswordProtected(isPasswordProtected);
|
||||
setErrorMessage(null);
|
||||
|
||||
let files = [];
|
||||
// remove outdated password, sharer has disabled the password
|
||||
if (!isPasswordProtected && passwordJWTToken.current) {
|
||||
passwordJWTToken.current = null;
|
||||
|
@ -143,7 +144,7 @@ export default function PublicCollectionGallery() {
|
|||
(isPasswordProtected && passwordJWTToken.current)
|
||||
) {
|
||||
try {
|
||||
await syncPublicFiles(
|
||||
files = await syncPublicFiles(
|
||||
token.current,
|
||||
passwordJWTToken.current,
|
||||
collection,
|
||||
|
@ -161,6 +162,17 @@ export default function PublicCollectionGallery() {
|
|||
if (isPasswordProtected && !passwordJWTToken.current) {
|
||||
await removePublicFiles(collectionUID);
|
||||
}
|
||||
collectionSummaries.set(collection.id, {
|
||||
collectionAttributes: {
|
||||
name: collection.name,
|
||||
type: collection.type,
|
||||
id: collection.id,
|
||||
updationTime: collection.updationTime,
|
||||
},
|
||||
latestFile: files[0],
|
||||
fileCount: files.length,
|
||||
});
|
||||
setCollectionSummaries(collectionSummaries);
|
||||
} catch (e) {
|
||||
const parsedError = parseSharingErrorCodes(e);
|
||||
if (
|
||||
|
@ -277,7 +289,6 @@ export default function PublicCollectionGallery() {
|
|||
accessedThroughSharedURL: true,
|
||||
openReportForm,
|
||||
}}>
|
||||
<CollectionInfo collection={publicCollection} />
|
||||
<PhotoFrame
|
||||
files={publicFiles}
|
||||
setFiles={setPublicFiles}
|
||||
|
|
|
@ -4,15 +4,13 @@ import localForage from 'utils/storage/localForage';
|
|||
|
||||
import { getActualKey, getToken } from 'utils/common/key';
|
||||
import CryptoWorker from 'utils/crypto';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { getPublicKey } from './userService';
|
||||
import { B64EncryptionResult } from 'utils/crypto';
|
||||
import HTTPService from './HTTPService';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { logError } from 'utils/sentry';
|
||||
import { CustomError } from 'utils/error';
|
||||
import { sortFiles } from 'utils/file';
|
||||
import { sortFiles, sortFilesIntoCollections } from 'utils/file';
|
||||
import {
|
||||
Collection,
|
||||
CollectionAndItsLatestFile,
|
||||
|
@ -23,6 +21,8 @@ import {
|
|||
CreatePublicAccessTokenRequest,
|
||||
PublicURL,
|
||||
UpdatePublicURL,
|
||||
CollectionSummaries,
|
||||
CollectionSummary,
|
||||
} from 'types/collection';
|
||||
import { COLLECTION_SORT_BY, CollectionType } from 'constants/collection';
|
||||
import { UpdateMagicMetadataRequest } from 'types/magicMetadata';
|
||||
|
@ -156,7 +156,7 @@ export const syncCollections = async () => {
|
|||
}
|
||||
});
|
||||
|
||||
let collections: Collection[] = [];
|
||||
const collections: Collection[] = [];
|
||||
let updationTime = await localForage.getItem<number>(
|
||||
COLLECTION_UPDATION_TIME
|
||||
);
|
||||
|
@ -167,11 +167,7 @@ export const syncCollections = async () => {
|
|||
updationTime = Math.max(updationTime, collection.updationTime);
|
||||
}
|
||||
}
|
||||
collections = sortCollections(
|
||||
collections,
|
||||
[],
|
||||
COLLECTION_SORT_BY.MODIFICATION_TIME
|
||||
);
|
||||
|
||||
await localForage.setItem(COLLECTION_TABLE, collections);
|
||||
await localForage.setItem(COLLECTION_UPDATION_TIME, updationTime);
|
||||
return collections;
|
||||
|
@ -478,12 +474,7 @@ export const removeFromCollection = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const deleteCollection = async (
|
||||
collectionID: number,
|
||||
syncWithRemote: () => Promise<void>,
|
||||
redirectToAll: () => void,
|
||||
setDialogMessage: SetDialogMessage
|
||||
) => {
|
||||
export const deleteCollection = async (collectionID: number) => {
|
||||
try {
|
||||
const token = getToken();
|
||||
|
||||
|
@ -493,15 +484,9 @@ export const deleteCollection = async (
|
|||
null,
|
||||
{ 'X-Auth-Token': token }
|
||||
);
|
||||
await syncWithRemote();
|
||||
redirectToAll();
|
||||
} catch (e) {
|
||||
logError(e, 'delete collection failed ');
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
content: constants.DELETE_COLLECTION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
});
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -719,79 +704,95 @@ export const getNonEmptyCollections = (
|
|||
);
|
||||
};
|
||||
|
||||
export function sortCollections(
|
||||
collections: Collection[],
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
export function sortCollectionSummaries(
|
||||
collectionSummaries: CollectionSummary[],
|
||||
sortBy: COLLECTION_SORT_BY
|
||||
) {
|
||||
return moveFavCollectionToFront(
|
||||
collections.sort((collectionA, collectionB) => {
|
||||
collectionSummaries.sort((a, b) => {
|
||||
switch (sortBy) {
|
||||
case COLLECTION_SORT_BY.LATEST_FILE:
|
||||
case COLLECTION_SORT_BY.CREATION_TIME_DESCENDING:
|
||||
return compareCollectionsLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionA,
|
||||
collectionB
|
||||
b.latestFile,
|
||||
a.latestFile
|
||||
);
|
||||
case COLLECTION_SORT_BY.CREATION_TIME_ASCENDING:
|
||||
return (
|
||||
-1 *
|
||||
compareCollectionsLatestFile(b.latestFile, a.latestFile)
|
||||
);
|
||||
case COLLECTION_SORT_BY.UPDATION_TIME_DESCENDING:
|
||||
return (
|
||||
b.collectionAttributes.updationTime -
|
||||
a.collectionAttributes.updationTime
|
||||
);
|
||||
case COLLECTION_SORT_BY.MODIFICATION_TIME:
|
||||
return collectionB.updationTime - collectionA.updationTime;
|
||||
case COLLECTION_SORT_BY.NAME:
|
||||
return collectionA.name.localeCompare(collectionB.name);
|
||||
return a.collectionAttributes.name.localeCompare(
|
||||
b.collectionAttributes.name
|
||||
);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function compareCollectionsLatestFile(
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
collectionA: Collection,
|
||||
collectionB: Collection
|
||||
) {
|
||||
if (!collectionAndTheirLatestFile?.length) {
|
||||
return 0;
|
||||
}
|
||||
const CollectionALatestFile = getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionA
|
||||
);
|
||||
const CollectionBLatestFile = getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile,
|
||||
collectionB
|
||||
);
|
||||
if (!CollectionALatestFile || !CollectionBLatestFile) {
|
||||
return 0;
|
||||
} else {
|
||||
const sortedFiles = sortFiles([
|
||||
CollectionALatestFile,
|
||||
CollectionBLatestFile,
|
||||
]);
|
||||
if (sortedFiles[0].id !== CollectionALatestFile.id) {
|
||||
function compareCollectionsLatestFile(first: EnteFile, second: EnteFile) {
|
||||
const sortedFiles = sortFiles([first, second]);
|
||||
if (sortedFiles[0].id !== first.id) {
|
||||
return 1;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCollectionLatestFile(
|
||||
collectionAndTheirLatestFile: CollectionAndItsLatestFile[],
|
||||
collection: Collection
|
||||
) {
|
||||
const collectionAndItsLatestFile = collectionAndTheirLatestFile.filter(
|
||||
(collectionAndItsLatestFile) =>
|
||||
collectionAndItsLatestFile.collection.id === collection.id
|
||||
);
|
||||
if (collectionAndItsLatestFile.length === 1) {
|
||||
return collectionAndItsLatestFile[0].file;
|
||||
}
|
||||
}
|
||||
|
||||
function moveFavCollectionToFront(collections: Collection[]) {
|
||||
return collections.sort((collectionA, collectionB) =>
|
||||
collectionA.type === CollectionType.favorites
|
||||
function moveFavCollectionToFront(collectionSummaries: CollectionSummary[]) {
|
||||
return collectionSummaries.sort((a, b) =>
|
||||
a.collectionAttributes.type === CollectionType.favorites
|
||||
? -1
|
||||
: collectionB.type === CollectionType.favorites
|
||||
: b.collectionAttributes.type === CollectionType.favorites
|
||||
? 1
|
||||
: 0
|
||||
);
|
||||
}
|
||||
|
||||
export function getCollectionSummaries(
|
||||
collections: Collection[],
|
||||
files: EnteFile[]
|
||||
): CollectionSummaries {
|
||||
const CollectionSummaries: CollectionSummaries = new Map();
|
||||
const collectionAndTheirLatestFile = getCollectionsAndTheirLatestFile(
|
||||
collections,
|
||||
files
|
||||
);
|
||||
const collectionAndTheirLatestFileMap = new Map();
|
||||
for (const collectionAndItsLatestFile of collectionAndTheirLatestFile) {
|
||||
collectionAndTheirLatestFileMap.set(
|
||||
collectionAndItsLatestFile.collection.id,
|
||||
collectionAndItsLatestFile.file
|
||||
);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const collectionFilesCount = getCollectionsFileCount(files);
|
||||
|
||||
for (const collection of collections) {
|
||||
CollectionSummaries.set(collection.id, {
|
||||
collectionAttributes: {
|
||||
id: collection.id,
|
||||
name: collection.name,
|
||||
type: collection.type,
|
||||
updationTime: collection.updationTime,
|
||||
},
|
||||
latestFile: collectionAndTheirLatestFileMap.get(collection.id),
|
||||
fileCount: collectionFilesCount.get(collection.id),
|
||||
});
|
||||
}
|
||||
return CollectionSummaries;
|
||||
}
|
||||
|
||||
function getCollectionsFileCount(files: EnteFile[]) {
|
||||
const collectionWiseFiles = sortFilesIntoCollections(files);
|
||||
const collectionFilesCount = new Map<number, number>();
|
||||
for (const [id, files] of collectionWiseFiles) {
|
||||
collectionFilesCount.set(id, files.length);
|
||||
}
|
||||
return collectionFilesCount;
|
||||
}
|
||||
|
|
499
src/styles/global.css
Normal file
499
src/styles/global.css
Normal file
|
@ -0,0 +1,499 @@
|
|||
/* Inter-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local(''),
|
||||
url('/fonts/Inter-Regular.ttf') format('truetype');
|
||||
}
|
||||
|
||||
/* Inter-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Inter';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local(''),
|
||||
url('/fonts/Inter-Bold.ttf') format('truetype');
|
||||
}
|
||||
html, body {
|
||||
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-family:Inter, Arial, sans-serif !important;
|
||||
}
|
||||
:is(h1, h2, h3, h4, h5, h6) {
|
||||
color: #d7d7d7;
|
||||
}
|
||||
|
||||
#__next {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.download-btn{
|
||||
background: url('/download_icon.png') no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
.info-btn{
|
||||
background: url('/info_icon.png') no-repeat;
|
||||
background-size: 20px 20px;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
|
||||
.pswp__item video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.pswp-item-container > * {
|
||||
position: absolute;
|
||||
transition: opacity 1s ease;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.pswp-item-container > img{
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pswp-item-container > video{
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
|
||||
.pswp-item-container > div.download-banner {
|
||||
width:100%;
|
||||
height: 16vh;
|
||||
padding:2vh 0;
|
||||
background-color: #151414;
|
||||
color:#ddd;
|
||||
display: flex;
|
||||
flex-direction:column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
opacity: 0.8;
|
||||
font-size:20px;
|
||||
}
|
||||
|
||||
.download-banner > a{
|
||||
width: 130px;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary: #e26f99,
|
||||
};
|
||||
|
||||
svg {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.pswp__img {
|
||||
object-fit: contain;
|
||||
}
|
||||
.pswp__caption{
|
||||
font-size:20px;
|
||||
height:10%;
|
||||
padding-left:5%;
|
||||
color:#eee;
|
||||
}
|
||||
|
||||
.modal {
|
||||
z-index: 2000;
|
||||
}
|
||||
.modal-dialog-centered {
|
||||
min-height: -webkit-calc(80% - 3.5rem);
|
||||
min-height: -moz-calc(80% - 3.5rem);
|
||||
min-height: calc(80% - 3.5rem);
|
||||
}
|
||||
.modal .modal-header, .modal .modal-footer , .toast-header{
|
||||
border-color: #444 !important;
|
||||
}
|
||||
.modal .modal-header .close, .toast-header .close {
|
||||
color: #aaa;
|
||||
text-shadow: none;
|
||||
}
|
||||
.modal-backdrop {
|
||||
z-index:2000;
|
||||
opacity:0.8 !important;
|
||||
}
|
||||
|
||||
.toast-header{
|
||||
border-radius:0px !important;
|
||||
}
|
||||
.modal .card , .table , .toast {
|
||||
background-color: #202020;
|
||||
border: none;
|
||||
}
|
||||
.modal .card > div {
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
margin: 0 0 5px 0;
|
||||
}
|
||||
.modal-content ,.toast-header{
|
||||
border-radius:15px;
|
||||
background-color:#202020 !important;
|
||||
}
|
||||
.modal-dialog{
|
||||
margin:5vh auto;
|
||||
width:90%;
|
||||
}
|
||||
.modal-body{
|
||||
max-height:74vh;
|
||||
overflow-y:auto;
|
||||
}
|
||||
.modal-xl{
|
||||
max-width:90% !important;
|
||||
}
|
||||
.modal-lg {
|
||||
max-width: 720px !important;
|
||||
}
|
||||
.plan-selector-modal-content {
|
||||
width:auto;
|
||||
margin:auto;
|
||||
}
|
||||
.pswp-custom {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
opacity: 0.75;
|
||||
transition: opacity .2s;
|
||||
padding-top: 4px;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
height: 44px;
|
||||
width: 44px;
|
||||
}
|
||||
.pswp-custom:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--left::before, .pswp__button--arrow--right::before {
|
||||
background-color: #333333;
|
||||
border-radius: 50%;
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
top:0;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--left::before {
|
||||
background-position: -128px -32px;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--left{
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.pswp__button--arrow--right{
|
||||
margin-right: 20px;
|
||||
}
|
||||
.pswp__button--arrow--right::before {
|
||||
background-position: -83px -30px;
|
||||
}
|
||||
.btn.focus , .btn:focus{
|
||||
box-shadow: none;
|
||||
}
|
||||
.btn-success {
|
||||
background: #51cd7c;
|
||||
color: #fff;
|
||||
border-color: #51cd7c;
|
||||
}
|
||||
.btn-success:hover .btn-success:focus .btn-success:active {
|
||||
background-color: #29a354;
|
||||
border-color: #51cd7c;
|
||||
color: #242424;
|
||||
}
|
||||
.btn-success:disabled {
|
||||
background-color: #69b383;
|
||||
}
|
||||
.btn-outline-success {
|
||||
background: #51cd7c;
|
||||
color: #fff;
|
||||
border-color: #51cd7c;
|
||||
}
|
||||
.btn-outline-success:hover:enabled {
|
||||
background: #4db76c;
|
||||
color: #fff;
|
||||
}
|
||||
.btn-outline-danger, .btn-outline-secondary, .btn-outline-primary{
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
#go-to-ente{
|
||||
background:none;
|
||||
border-color: #3dbb69;
|
||||
color:#51cd7c;
|
||||
}
|
||||
#go-to-ente:hover, #go-to-ente:focus, #go-to-ente:active {
|
||||
color:#fff;
|
||||
background-color: #44774d;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
}
|
||||
a:hover {
|
||||
color: #51cd7c;
|
||||
}
|
||||
.btn-link {
|
||||
color: #fff;
|
||||
}
|
||||
.btn-link:hover {
|
||||
color: #51cd7c;
|
||||
}
|
||||
.btn-link-danger {
|
||||
color: #dc3545;
|
||||
}
|
||||
.btn-link-danger:hover {
|
||||
color: #ff495a;
|
||||
}
|
||||
.card {
|
||||
background-color: #242424;
|
||||
color: #d1d1d1;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.jumbotron{
|
||||
background-color: #191919;
|
||||
color: #d1d1d1;
|
||||
text-align: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
.alert-primary {
|
||||
background-color: rgb(235, 255, 243);
|
||||
color: #000000;
|
||||
}
|
||||
.alert-success {
|
||||
background-color: #c4ffd6;
|
||||
}
|
||||
.bm-burger-button {
|
||||
position: fixed;
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
top:27px;
|
||||
left: 32px;
|
||||
z-index:100 !important;
|
||||
}
|
||||
.bm-burger-bars {
|
||||
background: #bdbdbd;
|
||||
}
|
||||
.bm-menu-wrap {
|
||||
top:0px;
|
||||
}
|
||||
.bm-menu {
|
||||
background: #131313;
|
||||
font-size: 1.15em;
|
||||
color:#d1d1d1;
|
||||
display: flex;
|
||||
}
|
||||
.bm-cross {
|
||||
background: #d1d1d1;
|
||||
}
|
||||
.bm-cross-button {
|
||||
top: 20px !important;
|
||||
}
|
||||
.bm-item-list {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
max-height: 100%;
|
||||
flex: 1;
|
||||
}
|
||||
.bm-item {
|
||||
padding: 20px;
|
||||
}
|
||||
.bm-overlay {
|
||||
top: 0;
|
||||
background: rgba(0, 0, 0, 0.8) !important;
|
||||
}
|
||||
.bg-upload-progress-bar {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
.custom-switch.custom-switch-md{
|
||||
z-index:0;
|
||||
}
|
||||
.custom-switch.custom-switch-md .custom-control-label {
|
||||
padding-left: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-label::before {
|
||||
height: 1.5rem;
|
||||
background-color: #303030;
|
||||
border: none;
|
||||
width: calc(2.5rem + 0.75rem);
|
||||
border-radius: 3rem;
|
||||
}
|
||||
.custom-switch.custom-switch-md:active .custom-control-label::before {
|
||||
background-color: #303030;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-label::after {
|
||||
top:2px;
|
||||
background:#c4c4c4;
|
||||
width: calc(2.0rem - 4px);
|
||||
height: calc(2.0rem - 4px);
|
||||
border-radius: calc(2rem - (2.0rem / 2));
|
||||
left: -38px;
|
||||
}
|
||||
|
||||
.custom-switch.custom-switch-md .custom-control-input:checked ~ .custom-control-label::after {
|
||||
transform: translateX(calc(2.0rem - 0.25rem));
|
||||
background:#c4c4c4;
|
||||
}
|
||||
|
||||
.custom-control-input:checked ~ .custom-control-label::before {
|
||||
background-color: #29a354;
|
||||
}
|
||||
|
||||
.bold-text{
|
||||
color: #ECECEC;
|
||||
line-height: 24px;
|
||||
font-size: 24px;
|
||||
}
|
||||
.dropdown-item:active{
|
||||
color: #16181b;
|
||||
text-decoration: none;
|
||||
background-color: #e9ecef;
|
||||
}
|
||||
|
||||
.list-group-item:hover{
|
||||
background-color:#343434 !important;
|
||||
}
|
||||
.list-group-item:active , list-group-item:focus{
|
||||
background-color:#000 !important;
|
||||
}
|
||||
#button-tooltip > .arrow::before{
|
||||
border-bottom-color:#282828 !important;
|
||||
}
|
||||
#button-tooltip > .arrow::after{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
|
||||
}
|
||||
.arrow::before{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
}
|
||||
.arrow::after{
|
||||
border-bottom-color:#282828 !important;
|
||||
border-top-color:#282828 !important;
|
||||
}
|
||||
.carousel-inner {
|
||||
padding-bottom: 50px !important;
|
||||
}
|
||||
.carousel-indicators li {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
margin-right: 12px;
|
||||
}
|
||||
.carousel-indicators .active {
|
||||
background-color: #51cd7c;
|
||||
}
|
||||
div.otp-input input {
|
||||
width: 36px !important;
|
||||
height: 36px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
div.otp-input input::placeholder {
|
||||
opacity:0;
|
||||
}
|
||||
|
||||
div.otp-input input:not(:placeholder-shown) , div.otp-input input:focus{
|
||||
border: 2px solid #51cd7c;
|
||||
border-radius:1px;
|
||||
-webkit-transition: 0.5s;
|
||||
transition: 0.5s;
|
||||
outline: none;
|
||||
}
|
||||
.flash-message{
|
||||
padding:16px;
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
@-webkit-keyframes rotation
|
||||
{
|
||||
from {
|
||||
-webkit-transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
-webkit-transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
#button-tooltip{
|
||||
color: #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 12px;
|
||||
padding:0px
|
||||
}
|
||||
.tooltip-inner{
|
||||
background-color: #282828;
|
||||
}
|
||||
.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;
|
||||
}
|
||||
.ente-form-group{
|
||||
margin:0;
|
||||
}
|
||||
.form-check-input:hover, .form-check-label :hover{
|
||||
cursor:pointer;
|
||||
}
|
||||
|
||||
@media (min-width: 450px) {
|
||||
.file-type-choice-modal{
|
||||
width: 25em;
|
||||
}
|
||||
}
|
||||
|
||||
.manageLinkHeader:hover{
|
||||
color:#bbb;
|
||||
}
|
|
@ -91,3 +91,15 @@ export interface CollectionMagicMetadata
|
|||
extends Omit<MagicMetadataCore, 'data'> {
|
||||
data: CollectionMagicMetadataProps;
|
||||
}
|
||||
export interface CollectionSummary {
|
||||
collectionAttributes: {
|
||||
id: number;
|
||||
name: string;
|
||||
type: CollectionType;
|
||||
updationTime: number;
|
||||
};
|
||||
latestFile: EnteFile;
|
||||
fileCount: number;
|
||||
}
|
||||
|
||||
export type CollectionSummaries = Map<number, CollectionSummary>;
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { Collection } from 'types/collection';
|
||||
import { EnteFile } from 'types/file';
|
||||
import { DateValue, Bbox } from 'types/search';
|
||||
|
@ -30,6 +31,9 @@ export type GalleryContextType = {
|
|||
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
|
||||
setNotificationAttributes: (attributes: NotificationAttributes) => void;
|
||||
setBlockingLoad: (value: boolean) => void;
|
||||
startLoading: () => void;
|
||||
finishLoading: () => void;
|
||||
setDialogMessage: SetDialogMessage;
|
||||
};
|
||||
|
||||
export interface NotificationAttributes {
|
||||
|
|
|
@ -41,6 +41,9 @@ export interface User {
|
|||
encryptedToken: string;
|
||||
isTwoFactorEnabled: boolean;
|
||||
twoFactorSessionID: string;
|
||||
usage: number;
|
||||
fileCount: number;
|
||||
sharedCollectionCount: number;
|
||||
}
|
||||
export interface EmailVerificationResponse {
|
||||
id: number;
|
||||
|
|
|
@ -12,7 +12,6 @@ import { CustomError, ServerErrorCodes } from 'utils/error';
|
|||
import { SelectedState } from 'types/gallery';
|
||||
import { User } from 'types/user';
|
||||
import { getData, LS_KEYS } from 'utils/storage/localStorage';
|
||||
import { SetDialogMessage } from 'components/MessageDialog';
|
||||
import { logError } from 'utils/sentry';
|
||||
import constants from 'utils/strings/constants';
|
||||
import { Collection, CollectionMagicMetadataProps } from 'types/collection';
|
||||
|
@ -96,10 +95,7 @@ export function isFavoriteCollection(
|
|||
}
|
||||
}
|
||||
|
||||
export async function downloadCollection(
|
||||
collectionID: number,
|
||||
setDialogMessage: SetDialogMessage
|
||||
) {
|
||||
export async function downloadAllCollectionFiles(collectionID: number) {
|
||||
try {
|
||||
const allFiles = await getLocalFiles();
|
||||
const collectionFiles = allFiles.filter(
|
||||
|
@ -108,11 +104,6 @@ export async function downloadCollection(
|
|||
await downloadFiles(collectionFiles);
|
||||
} catch (e) {
|
||||
logError(e, 'download collection failed ');
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
content: constants.DELETE_COLLECTION_FAILED,
|
||||
close: { variant: 'danger' },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,19 +159,13 @@ export const shareExpiryOptions = [
|
|||
},
|
||||
];
|
||||
|
||||
export const changeCollectionVisibilityHelper = async (
|
||||
export const changeCollectionVisibility = async (
|
||||
collection: Collection,
|
||||
startLoading: () => void,
|
||||
finishLoading: () => void,
|
||||
setDialogMessage: SetDialogMessage,
|
||||
syncWithRemote: () => Promise<void>
|
||||
visibility: VISIBILITY_STATE
|
||||
) => {
|
||||
startLoading();
|
||||
try {
|
||||
const updatedMagicMetadataProps: CollectionMagicMetadataProps = {
|
||||
visibility: collection.magicMetadata?.data.visibility
|
||||
? VISIBILITY_STATE.VISIBLE
|
||||
: VISIBILITY_STATE.ARCHIVED,
|
||||
visibility,
|
||||
};
|
||||
|
||||
const updatedCollection = {
|
||||
|
@ -197,23 +182,9 @@ export const changeCollectionVisibilityHelper = async (
|
|||
logError(e, 'change file visibility failed');
|
||||
switch (e.status?.toString()) {
|
||||
case ServerErrorCodes.FORBIDDEN:
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.NOT_FILE_OWNER,
|
||||
});
|
||||
return;
|
||||
throw Error(constants.NOT_FILE_OWNER);
|
||||
}
|
||||
setDialogMessage({
|
||||
title: constants.ERROR,
|
||||
staticBackdrop: true,
|
||||
close: { variant: 'danger' },
|
||||
content: constants.UNKNOWN_ERROR,
|
||||
});
|
||||
} finally {
|
||||
await syncWithRemote();
|
||||
finishLoading();
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ export enum LS_KEYS {
|
|||
LIVE_PHOTO_INFO_SHOWN_COUNT = 'livePhotoInfoShownCount',
|
||||
FAILED_UPLOADS = 'failedUploads',
|
||||
LOGS = 'logs',
|
||||
COLLECTION_SORT_BY = 'collectionSortBy',
|
||||
}
|
||||
|
||||
export const setData = (key: LS_KEYS, value: object) => {
|
||||
|
|
|
@ -554,7 +554,7 @@ const englishConstants = {
|
|||
'these files were not uploaded as they exceed our maximum file size limit',
|
||||
UPLOAD_TO_COLLECTION: 'upload to album',
|
||||
ARCHIVE: 'archive',
|
||||
ALL: 'all',
|
||||
ALL_SECTION_NAME: 'All Photos',
|
||||
MOVE_TO_COLLECTION: 'move to album',
|
||||
UNARCHIVE: 'un-archive',
|
||||
MOVE: 'move',
|
||||
|
@ -586,9 +586,11 @@ const englishConstants = {
|
|||
</p>
|
||||
</>
|
||||
),
|
||||
SORT_BY_LATEST_PHOTO: 'recent photo',
|
||||
SORT_BY_MODIFICATION_TIME: 'last updated',
|
||||
SORT_BY_COLLECTION_NAME: 'album name',
|
||||
|
||||
SORT_BY_CREATION_TIME_ASCENDING: 'Oldest',
|
||||
SORT_BY_CREATION_TIME_DESCENDING: 'Newest',
|
||||
SORT_BY_UPDATION_TIME_DESCENDING: 'Last updated',
|
||||
SORT_BY_NAME: 'Name',
|
||||
FIX_LARGE_THUMBNAILS: 'compress thumbnails',
|
||||
THUMBNAIL_REPLACED: 'thumbnails compressed',
|
||||
FIX_THUMBNAIL: 'compress',
|
||||
|
@ -712,6 +714,12 @@ const englishConstants = {
|
|||
'are you sure that you want to stop all the uploads in progress?',
|
||||
STOP_UPLOADS_HEADER: 'stop uploads?',
|
||||
YES_STOP_UPLOADS: 'yes, stop uploads',
|
||||
ALBUMS: 'Albums',
|
||||
NEW: 'New',
|
||||
VIEW_ALL_ALBUMS: 'View all Albums',
|
||||
ALL_ALBUMS: 'All Albums',
|
||||
PHOTOS: 'Photos',
|
||||
ENTE: 'ente',
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
16
yarn.lock
16
yarn.lock
|
@ -1065,6 +1065,13 @@
|
|||
prop-types "^15.7.2"
|
||||
react-is "^17.0.2"
|
||||
|
||||
"@mui/icons-material@^5.6.2":
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.6.2.tgz#239c40fc5841dc7c6af7d00e4e988550de170fcd"
|
||||
integrity sha512-9QdI7axKuBAyaGz4mtdi7Uy1j73/thqFmEuxpJHxNC7O8ADEK1Da3t2veK2tgmsXsUlAHcAG63gg+GvWWeQNqQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.2"
|
||||
|
||||
"@mui/material@^5.6.2":
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.6.2.tgz#bcd0ecdb265aca1bfee11773f6561aeedaa25b45"
|
||||
|
@ -1093,6 +1100,7 @@
|
|||
prop-types "^15.7.2"
|
||||
|
||||
"@mui/styled-engine-sc@^5.6.1", "@mui/styled-engine@^5.6.1", "@mui/styled-engine@npm:@mui/styled-engine-sc@latest":
|
||||
name "@mui/styled-engine"
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine-sc/-/styled-engine-sc-5.6.1.tgz#d9505c005eeefa5c7d7ef9c03e63324db889ee09"
|
||||
integrity sha512-BMY5Pb8YgOxvvwg9s6mPeDgzha4244/KgRmHvTVPBmvqypbiLPApR6SNyYON+1/3kLHt3TpmxrlRY3jPiZF2QQ==
|
||||
|
@ -1626,10 +1634,10 @@
|
|||
resolved "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz"
|
||||
integrity sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==
|
||||
|
||||
"@types/styled-components@^5.1.3":
|
||||
version "5.1.14"
|
||||
resolved "https://registry.npmjs.org/@types/styled-components/-/styled-components-5.1.14.tgz"
|
||||
integrity sha512-d6P1/tyNytqKwam3cQXq7a9uPtovc/mdAs7dBiz1YbDdNIT3X4WmuFU78YdSYh84TXVuhOwezZ3EeKuNBhwsHQ==
|
||||
"@types/styled-components@^5.1.25":
|
||||
version "5.1.25"
|
||||
resolved "https://registry.yarnpkg.com/@types/styled-components/-/styled-components-5.1.25.tgz#0177c4ab5fa7c6ed0565d36f597393dae3f380ad"
|
||||
integrity sha512-fgwl+0Pa8pdkwXRoVPP9JbqF0Ivo9llnmsm+7TCI330kbPIFd9qv1Lrhr37shf4tnxCOSu+/IgqM7uJXLWZZNQ==
|
||||
dependencies:
|
||||
"@types/hoist-non-react-statics" "*"
|
||||
"@types/react" "*"
|
||||
|
|
Loading…
Reference in a new issue