diff --git a/next.config.js b/next.config.js index 230c75536..8d7200f56 100644 --- a/next.config.js +++ b/next.config.js @@ -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 { diff --git a/package.json b/package.json index 35968a513..ebdde2b48 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/fonts/Inter-Bold.ttf b/public/fonts/Inter-Bold.ttf new file mode 100644 index 000000000..76a215ccb Binary files /dev/null and b/public/fonts/Inter-Bold.ttf differ diff --git a/public/fonts/Inter-Regular.ttf b/public/fonts/Inter-Regular.ttf new file mode 100644 index 000000000..cc73944ac Binary files /dev/null and b/public/fonts/Inter-Regular.ttf differ diff --git a/public/fonts/OFL.txt b/public/fonts/OFL.txt new file mode 100644 index 000000000..ad214842c --- /dev/null +++ b/public/fonts/OFL.txt @@ -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. diff --git a/public/fonts/UFL.txt b/public/fonts/UFL.txt deleted file mode 100644 index 6e722c88d..000000000 --- a/public/fonts/UFL.txt +++ /dev/null @@ -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. diff --git a/public/fonts/ubuntu-v15-latin-700.woff b/public/fonts/ubuntu-v15-latin-700.woff deleted file mode 100644 index 8f770546a..000000000 Binary files a/public/fonts/ubuntu-v15-latin-700.woff and /dev/null differ diff --git a/public/fonts/ubuntu-v15-latin-700.woff2 b/public/fonts/ubuntu-v15-latin-700.woff2 deleted file mode 100644 index e10142f55..000000000 Binary files a/public/fonts/ubuntu-v15-latin-700.woff2 and /dev/null differ diff --git a/public/fonts/ubuntu-v15-latin-regular.woff b/public/fonts/ubuntu-v15-latin-regular.woff deleted file mode 100644 index 2fc163ffb..000000000 Binary files a/public/fonts/ubuntu-v15-latin-regular.woff and /dev/null differ diff --git a/public/fonts/ubuntu-v15-latin-regular.woff2 b/public/fonts/ubuntu-v15-latin-regular.woff2 deleted file mode 100644 index a590b8a9e..000000000 Binary files a/public/fonts/ubuntu-v15-latin-regular.woff2 and /dev/null differ diff --git a/src/components/CodeBlock.tsx b/src/components/CodeBlock.tsx index fa6906375..62f1aa801 100644 --- a/src/components/CodeBlock.tsx +++ b/src/components/CodeBlock.tsx @@ -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` diff --git a/src/components/Collections/AllCollections/CollectionCard.tsx b/src/components/Collections/AllCollections/CollectionCard.tsx new file mode 100644 index 000000000..924972b70 --- /dev/null +++ b/src/components/Collections/AllCollections/CollectionCard.tsx @@ -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 ( + onCollectionClick(collectionAttributes.id)}> +
+ + {collectionAttributes.name} + + + {fileCount} {constants.PHOTOS} + +
+
+ ); +} diff --git a/src/components/Collections/AllCollections/CollectionSort/index.tsx b/src/components/Collections/AllCollections/CollectionSort/index.tsx new file mode 100644 index 000000000..5013cd95b --- /dev/null +++ b/src/components/Collections/AllCollections/CollectionSort/index.tsx @@ -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 ( + <> + setSortByEl(event.currentTarget)} + aria-controls={sortByEl ? 'collection-sort' : undefined} + aria-haspopup="true" + aria-expanded={sortByEl ? 'true' : undefined}> + + + + + + + ); +} diff --git a/src/components/Collections/AllCollections/CollectionSort/optionCreator.tsx b/src/components/Collections/AllCollections/CollectionSort/optionCreator.tsx new file mode 100644 index 000000000..795358711 --- /dev/null +++ b/src/components/Collections/AllCollections/CollectionSort/optionCreator.tsx @@ -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 ( + + + {activeSortBy === props.sortBy && ( + + )} + + {props.children} + + ); + }; + +export default SortByOptionCreator; diff --git a/src/components/Collections/AllCollections/CollectionSort/options.tsx b/src/components/Collections/AllCollections/CollectionSort/options.tsx new file mode 100644 index 000000000..cb6e7c5d3 --- /dev/null +++ b/src/components/Collections/AllCollections/CollectionSort/options.tsx @@ -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 ( + + + {constants.SORT_BY_NAME} + + + {constants.SORT_BY_CREATION_TIME_DESCENDING} + + + {constants.SORT_BY_CREATION_TIME_ASCENDING} + + + {constants.SORT_BY_UPDATION_TIME_DESCENDING} + + + ); +} diff --git a/src/components/Collections/AllCollections/content.tsx b/src/components/Collections/AllCollections/content.tsx new file mode 100644 index 000000000..287e01609 --- /dev/null +++ b/src/components/Collections/AllCollections/content.tsx @@ -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 ( + + + {sortedCollectionSummaries.map( + ({ latestFile, collectionAttributes, fileCount }) => ( + + ) + )} + + + ); +} diff --git a/src/components/Collections/AllCollections/header.tsx b/src/components/Collections/AllCollections/header.tsx new file mode 100644 index 000000000..914576662 --- /dev/null +++ b/src/components/Collections/AllCollections/header.tsx @@ -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 ( + + + + {constants.ALL_ALBUMS} + + + + + + + + {`${collectionCount} ${constants.ALBUMS}`} + + + + + ); +} diff --git a/src/components/Collections/AllCollections/index.tsx b/src/components/Collections/AllCollections/index.tsx new file mode 100644 index 000000000..7505d173d --- /dev/null +++ b/src/components/Collections/AllCollections/index.tsx @@ -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( + 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 ( + + + + + + ); +} diff --git a/src/components/Collections/CollectionBar/CollectionCardWithActiveIndicator.tsx b/src/components/Collections/CollectionBar/CollectionCardWithActiveIndicator.tsx new file mode 100644 index 000000000..89238765e --- /dev/null +++ b/src/components/Collections/CollectionBar/CollectionCardWithActiveIndicator.tsx @@ -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 ( + + + {active && } + + ); + } +); + +export default CollectionCardWithActiveIndicator; diff --git a/src/components/Collections/CollectionBar/CreateCollectionTile.tsx b/src/components/Collections/CollectionBar/CreateCollectionTile.tsx new file mode 100644 index 000000000..2c03c26b1 --- /dev/null +++ b/src/components/Collections/CollectionBar/CreateCollectionTile.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import constants from 'utils/strings/englishConstants'; +import { CollectionTitleWithDashedBorder } from '../styledComponents'; + +export const CreateNewCollectionTile = (props) => { + return ( + +
{constants.NEW}
+
{'+'}
+
+ ); +}; diff --git a/src/components/Collections/CollectionBar/NavigationButton.tsx b/src/components/Collections/CollectionBar/NavigationButton.tsx new file mode 100644 index 000000000..3081c4aa5 --- /dev/null +++ b/src/components/Collections/CollectionBar/NavigationButton.tsx @@ -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 }) => ( + + + +); +export default NavigationButton; diff --git a/src/components/Collections/CollectionBar/index.tsx b/src/components/Collections/CollectionBar/index.tsx new file mode 100644 index 000000000..ea823dc06 --- /dev/null +++ b/src/components/Collections/CollectionBar/index.tsx @@ -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 ( + + + {constants.ALBUMS} + {hasScrollBar && ( + + {constants.VIEW_ALL_ALBUMS} + + )} + + + {!onFarLeft && ( + + )} + + + {constants.ALL_SECTION_NAME} + + {collections.map((item) => ( + + {item.name} + + ))} + + {!onFarRight && ( + + )} + + + ); +} diff --git a/src/components/Collections/CollectionCard.tsx b/src/components/Collections/CollectionCard.tsx new file mode 100644 index 000000000..cf9b3a4f2 --- /dev/null +++ b/src/components/Collections/CollectionCard.tsx @@ -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 ( + + {children} + + ); +} diff --git a/src/components/Collections/CollectionInfo.tsx b/src/components/Collections/CollectionInfo.tsx new file mode 100644 index 000000000..ce1a5b612 --- /dev/null +++ b/src/components/Collections/CollectionInfo.tsx @@ -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 ( + +
+ + {collectionAttributes.name} + + + {fileCount} {constants.PHOTOS} + +
+ +
+ ); +} diff --git a/src/components/Collections/CollectionNamer.tsx b/src/components/Collections/CollectionNamer.tsx new file mode 100644 index 000000000..e9ed28f13 --- /dev/null +++ b/src/components/Collections/CollectionNamer.tsx @@ -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 +>; + +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 ( + + + + {attributes?.title} + + + + + + + + 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, + }) => ( +
+ + + + )} + +
+
+ ); +} diff --git a/src/components/Collections/CollectionOptions.tsx b/src/components/Collections/CollectionOptions.tsx new file mode 100644 index 000000000..6ff0db913 --- /dev/null +++ b/src/components/Collections/CollectionOptions.tsx @@ -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 ( + <> + setOptionEl(event.currentTarget)} + aria-controls={optionEl ? 'collection-options' : undefined} + aria-haspopup="true" + aria-expanded={optionEl ? 'true' : undefined}> + + + + + + + + {constants.RENAME} + + + + + {constants.SHARE} + + + + + {constants.DOWNLOAD} + + + + {IsArchived(activeCollection) ? ( + + {constants.UNARCHIVE} + + ) : ( + + {constants.ARCHIVE} + + )} + + + + {constants.DELETE} + + + + + + + ); +}; + +export default CollectionOptions; diff --git a/src/components/CollectionShare.tsx b/src/components/Collections/CollectionShare.tsx similarity index 98% rename from src/components/CollectionShare.tsx rename to src/components/Collections/CollectionShare.tsx index de1408636..779a85f3e 100644 --- a/src/components/CollectionShare.tsx +++ b/src/components/Collections/CollectionShare.tsx @@ -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; } interface formValues { email: string; diff --git a/src/components/Collections/index.tsx b/src/components/Collections/index.tsx new file mode 100644 index 000000000..c254d5d46 --- /dev/null +++ b/src/components/Collections/index.tsx @@ -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>(new Map()); + const activeCollection = useRef(null); + + useEffect(() => { + collectionsMap.current = new Map( + props.collections.map((collection) => [collection.id, collection]) + ); + }, [collections]); + + useEffect(() => { + activeCollection.current = + collectionsMap.current.get(activeCollectionID); + }, [activeCollectionID, collections]); + + return ( + <> + setAllCollectionView(true)} + /> + + setAllCollectionView(false)} + collectionSummaries={collectionSummaries} + setActiveCollection={setActiveCollectionID} + /> + + setActiveCollectionID(ALL_SECTION)} + showCollectionShareModal={() => + setCollectionShareModalView(true) + } + /> + setCollectionShareModalView(false)} + collection={activeCollection.current} + /> + + ); +} diff --git a/src/components/Collections/styledComponents.ts b/src/components/Collections/styledComponents.ts new file mode 100644 index 000000000..8200c3eda --- /dev/null +++ b/src/components/Collections/styledComponents.ts @@ -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}; +`; diff --git a/src/components/Container.ts b/src/components/Container.ts index 8b2be4886..b249f617a 100644 --- a/src/components/Container.ts +++ b/src/components/Container.ts @@ -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}; + } +`; diff --git a/src/components/FloatingSidebar.tsx b/src/components/FloatingSidebar.tsx new file mode 100644 index 000000000..4a1fdc2dd --- /dev/null +++ b/src/components/FloatingSidebar.tsx @@ -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 }, ref) => { + return ; + } + ); diff --git a/src/components/MessageDialog/TitleWithCloseButton.tsx b/src/components/MessageDialog/TitleWithCloseButton.tsx new file mode 100644 index 000000000..295e34bde --- /dev/null +++ b/src/components/MessageDialog/TitleWithCloseButton.tsx @@ -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 ( + + + {children} + {onClose && ( + + + + )} + + + ); +}; + +export default DialogTitleWithCloseButton; diff --git a/src/components/MessageDialog.tsx b/src/components/MessageDialog/index.tsx similarity index 99% rename from src/components/MessageDialog.tsx rename to src/components/MessageDialog/index.tsx index b9d50ffe4..a7d5a532e 100644 --- a/src/components/MessageDialog.tsx +++ b/src/components/MessageDialog/index.tsx @@ -25,6 +25,7 @@ type Props = React.PropsWithChildren<{ attributes: MessageAttributes; size?: 'sm' | 'lg' | 'xl'; }>; + export default function MessageDialog({ attributes, children, diff --git a/src/components/NavigationButton.tsx b/src/components/NavigationButton.tsx deleted file mode 100644 index c8aefcd3b..000000000 --- a/src/components/NavigationButton.tsx +++ /dev/null @@ -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 }) => ( - - - -); -export default NavigationButton; diff --git a/src/components/PhotoSwipe/PhotoSwipe.tsx b/src/components/PhotoSwipe/PhotoSwipe.tsx index d677d4e3a..0bdfcc7dd 100644 --- a/src/components/PhotoSwipe/PhotoSwipe.tsx +++ b/src/components/PhotoSwipe/PhotoSwipe.tsx @@ -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'; diff --git a/src/components/icons/NavigateNext.tsx b/src/components/icons/NavigateNext.tsx deleted file mode 100644 index 66b02a9a8..000000000 --- a/src/components/icons/NavigateNext.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from 'react'; - -export default function NavigateNext(props) { - return ( - - - - - ); -} - -NavigateNext.defaultProps = { - height: 24, - width: 24, - viewBox: '0 0 24 24', -}; diff --git a/src/components/icons/TickIcon.tsx b/src/components/icons/OptionIcon-2.tsx similarity index 51% rename from src/components/icons/TickIcon.tsx rename to src/components/icons/OptionIcon-2.tsx index 633d869a2..44f81e177 100644 --- a/src/components/icons/TickIcon.tsx +++ b/src/components/icons/OptionIcon-2.tsx @@ -1,6 +1,6 @@ import React from 'react'; -export default function TickIcon(props) { +export default function OptionIcon(props) { return ( - + + {' '} ); } -TickIcon.defaultProps = { +OptionIcon.defaultProps = { height: 20, width: 20, viewBox: '0 0 24 24', diff --git a/src/components/icons/SortIcon.tsx b/src/components/icons/SortIcon.tsx deleted file mode 100644 index 07c2c0460..000000000 --- a/src/components/icons/SortIcon.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; - -export default function SortIcon(props) { - return ( - - - - - ); -} - -SortIcon.defaultProps = { - height: 24, - width: 24, - viewBox: '0 0 24 24', -}; diff --git a/src/components/pages/gallery/CollectionNamer.tsx b/src/components/pages/gallery/CollectionNamer.tsx deleted file mode 100644 index 8295394e5..000000000 --- a/src/components/pages/gallery/CollectionNamer.tsx +++ /dev/null @@ -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 ->; - -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 ( - null} attributes={{}} /> - ); - } - const onSubmit = ({ albumName }: formValues) => { - attributes.callback(albumName); - props.onHide(); - }; - return ( - - - 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 }) => ( -
- - - - - {errors.albumName} - - - - - )} - -
- ); -} diff --git a/src/components/pages/gallery/CollectionOptions.tsx b/src/components/pages/gallery/CollectionOptions.tsx deleted file mode 100644 index d54a236da..000000000 --- a/src/components/pages/gallery/CollectionOptions.tsx +++ /dev/null @@ -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; - setCollectionNamerAttributes: SetCollectionNamerAttributes; - collections: Collection[]; - selectedCollectionID: number; - setDialogMessage: SetDialogMessage; - startLoading: () => void; - finishLoading: () => void; - showCollectionShareModal: () => void; - redirectToAll: () => void; -} - -export const MenuLink = ({ children, ...props }: LinkButtonProps) => ( - - {children} - -); - -export const MenuItem = (props: { children: any }) => ( - - {props.children} - -); - -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 ( - - - - - - {constants.RENAME} - - - - - {constants.SHARE} - - - - - {constants.DOWNLOAD} - - - - - {IsArchived( - getSelectedCollection( - props.selectedCollectionID, - props.collections - ) - ) - ? constants.UNARCHIVE - : constants.ARCHIVE} - - - - - {constants.DELETE} - - - - - - ); -}; - -export default CollectionOptions; diff --git a/src/components/pages/gallery/CollectionSort.tsx b/src/components/pages/gallery/CollectionSort.tsx deleted file mode 100644 index bbeea71c4..000000000 --- a/src/components/pages/gallery/CollectionSort.tsx +++ /dev/null @@ -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 ( - -
- - - - - -
-
- ); -} diff --git a/src/components/pages/gallery/CollectionSortOptions.tsx b/src/components/pages/gallery/CollectionSortOptions.tsx deleted file mode 100644 index a209c8998..000000000 --- a/src/components/pages/gallery/CollectionSortOptions.tsx +++ /dev/null @@ -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 }) => - ( - - - - {activeSortBy === props.sortBy && ( - - - - )} - - - setCollectionSortBy(props.sortBy)} - variant={ - activeSortBy === props.sortBy && 'success' - }> - {props.children} - - - - - ); - -const CollectionSortOptions = (props: OptionProps) => { - const SortByOption = SortByOptionCreator(props); - - return ( - - - - - {constants.SORT_BY_LATEST_PHOTO} - - - {constants.SORT_BY_MODIFICATION_TIME} - - - {constants.SORT_BY_COLLECTION_NAME} - - - - - ); -}; - -export default CollectionSortOptions; diff --git a/src/components/pages/gallery/Collections.tsx b/src/components/pages/gallery/Collections.tsx deleted file mode 100644 index f8845b124..000000000 --- a/src/components/pages/gallery/Collections.tsx +++ /dev/null @@ -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; - setCollectionNamerAttributes: SetCollectionNamerAttributes; - startLoading: () => void; - finishLoading: () => void; - isInSearchMode: boolean; - collectionFilesCount: Map; -} - -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 }) => - ( - - {label} -
- - ); -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(null); - const collectionWrapperRef = useRef(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.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 ( - - {fileCount} {fileCount > 1 ? 'items' : 'item'} - - ); - }; - - const SectionChip = SectionChipCreater({ activeCollection, clickHandler }); - - return ( - - setCollectionShareModalView(false)} - collection={getSelectedCollection( - selectedCollectionID, - props.collections - )} - syncWithRemote={props.syncWithRemote} - /> - - - {scrollObj.scrollLeft > 0 && ( - - )} - - - {sortCollections( - collections, - props.collectionAndTheirLatestFile, - collectionSortBy - ).map((item) => ( - - - {IsArchived(item) && ( - -
- -
-
- )} - {item.name} - {item.type !== CollectionType.favorites && - item.owner.id === user?.id ? ( - - - setSelectedCollectionID( - item.id - ) - } - /> - - ) : ( -
- )} - - - ))} - - - - {scrollObj.scrollLeft < - scrollObj.scrollWidth - scrollObj.clientWidth && ( - - )} - - - - - ); -} diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/pages/gallery/Upload.tsx index 72a8226a0..b4e606929 100644 --- a/src/components/pages/gallery/Upload.tsx +++ b/src/components/pages/gallery/Upload.tsx @@ -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'; diff --git a/src/components/pages/sharedAlbum/CollectionInfo.tsx b/src/components/pages/sharedAlbum/CollectionInfo.tsx deleted file mode 100644 index 0502203d5..000000000 --- a/src/components/pages/sharedAlbum/CollectionInfo.tsx +++ /dev/null @@ -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 {props.collection.name}; -} diff --git a/src/constants/collection/index.ts b/src/constants/collection/index.ts index 0e68e9a47..d1a8f62c1 100644 --- a/src/constants/collection/index.ts +++ b/src/constants/collection/index.ts @@ -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 = diff --git a/src/darkThemeOptions.tsx b/src/darkThemeOptions.tsx index abc83a1b4..568d61310 100644 --- a/src/darkThemeOptions.tsx +++ b/src/darkThemeOptions.tsx @@ -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, }, }); diff --git a/src/hooks/useComponentScroll.tsx b/src/hooks/useComponentScroll.tsx new file mode 100644 index 000000000..dae0280ef --- /dev/null +++ b/src/hooks/useComponentScroll.tsx @@ -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(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, + }; +} diff --git a/src/hooks/useLocalState.tsx b/src/hooks/useLocalState.tsx new file mode 100644 index 000000000..a1fbdba47 --- /dev/null +++ b/src/hooks/useLocalState.tsx @@ -0,0 +1,20 @@ +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; +import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; + +export function useLocalState( + key: LS_KEYS, + initialValue?: T +): [T, Dispatch>] { + const [value, setValue] = useState(null); + + useEffect(() => { + const { value } = getData(key) ?? {}; + setValue(value ?? initialValue); + }, []); + + useEffect(() => { + setData(key, { value }); + }, [value]); + + return [value, setValue]; +} diff --git a/src/hooks/useWindowSize.tsx b/src/hooks/useWindowSize.tsx new file mode 100644 index 000000000..e1b514f43 --- /dev/null +++ b/src/hooks/useWindowSize.tsx @@ -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; +} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index bf7d96647..8470e2651 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -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" /> - + + {showNavbar && ( diff --git a/src/pages/api/[...all].ts b/src/pages/api/[...all].ts deleted file mode 100644 index 81715decd..000000000 --- a/src/pages/api/[...all].ts +++ /dev/null @@ -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': '/' }, -}); diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index e93db214d..13a585507 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -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( @@ -185,8 +192,8 @@ export default function Gallery() { const [deleted, setDeleted] = useState([]); const { startLoading, finishLoading, setDialogMessage, ...appContext } = useContext(AppContext); - const [collectionFilesCount, setCollectionFilesCount] = - useState>(); + const [collectionSummaries, setCollectionSummaries] = + useState(new Map()); const [activeCollection, setActiveCollection] = useState(undefined); const [trash, setTrash] = useState([]); 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(); - 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 ); - - await collectionOpsHelper(ops)(collection); + 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, }}> (false); + const [collectionSummaries, setCollectionSummaries] = + useState(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, }}> - { } }); - let collections: Collection[] = []; + const collections: Collection[] = []; let updationTime = await localForage.getItem( 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, - 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; +function compareCollectionsLatestFile(first: EnteFile, second: EnteFile) { + const sortedFiles = sortFiles([first, second]); + if (sortedFiles[0].id !== first.id) { + return 1; } else { - const sortedFiles = sortFiles([ - CollectionALatestFile, - CollectionBLatestFile, - ]); - if (sortedFiles[0].id !== CollectionALatestFile.id) { - return 1; - } else { - return -1; - } + 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(); + for (const [id, files] of collectionWiseFiles) { + collectionFilesCount.set(id, files.length); + } + return collectionFilesCount; +} diff --git a/src/styles/global.css b/src/styles/global.css new file mode 100644 index 000000000..ecc2d3f26 --- /dev/null +++ b/src/styles/global.css @@ -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; +} \ No newline at end of file diff --git a/src/types/collection/index.ts b/src/types/collection/index.ts index 2307b1153..50ab5b6be 100644 --- a/src/types/collection/index.ts +++ b/src/types/collection/index.ts @@ -91,3 +91,15 @@ export interface CollectionMagicMetadata extends Omit { data: CollectionMagicMetadataProps; } +export interface CollectionSummary { + collectionAttributes: { + id: number; + name: string; + type: CollectionType; + updationTime: number; + }; + latestFile: EnteFile; + fileCount: number; +} + +export type CollectionSummaries = Map; diff --git a/src/types/gallery/index.ts b/src/types/gallery/index.ts index 2fc5632bf..49dff2c4d 100644 --- a/src/types/gallery/index.ts +++ b/src/types/gallery/index.ts @@ -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; setNotificationAttributes: (attributes: NotificationAttributes) => void; setBlockingLoad: (value: boolean) => void; + startLoading: () => void; + finishLoading: () => void; + setDialogMessage: SetDialogMessage; }; export interface NotificationAttributes { diff --git a/src/types/user/index.ts b/src/types/user/index.ts index 72ad9ba8f..83e8b15ec 100644 --- a/src/types/user/index.ts +++ b/src/types/user/index.ts @@ -41,6 +41,9 @@ export interface User { encryptedToken: string; isTwoFactorEnabled: boolean; twoFactorSessionID: string; + usage: number; + fileCount: number; + sharedCollectionCount: number; } export interface EmailVerificationResponse { id: number; diff --git a/src/utils/collection/index.ts b/src/utils/collection/index.ts index 4f58d4da9..dc67d40ad 100644 --- a/src/utils/collection/index.ts +++ b/src/utils/collection/index.ts @@ -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 + 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; } }; diff --git a/src/utils/storage/localStorage.ts b/src/utils/storage/localStorage.ts index 63b84ba89..aa44ab15e 100644 --- a/src/utils/storage/localStorage.ts +++ b/src/utils/storage/localStorage.ts @@ -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) => { diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 6d8e081fe..d5fbe63db 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -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 = {

), - 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; diff --git a/yarn.lock b/yarn.lock index b4e043bf5..d3617cf51 100644 --- a/yarn.lock +++ b/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" "*"