Merge branch 'master' into release
This commit is contained in:
commit
c3f284cc0e
|
@ -17,7 +17,9 @@
|
|||
"@stripe/stripe-js": "^1.13.2",
|
||||
"axios": "^0.20.0",
|
||||
"bootstrap": "^4.5.2",
|
||||
"chrono-node": "^2.2.6",
|
||||
"comlink": "^4.3.0",
|
||||
"debounce-promise": "^3.1.2",
|
||||
"exif-js": "^2.3.0",
|
||||
"formik": "^2.1.5",
|
||||
"heic2any": "^0.0.3",
|
||||
|
@ -26,6 +28,7 @@
|
|||
"libsodium-wrappers": "^0.7.8",
|
||||
"localforage": "^1.9.0",
|
||||
"next": "9.5.3",
|
||||
"next-on-netlify": "^3.0.1",
|
||||
"next-with-workbox": "^2.0.1",
|
||||
"node-forge": "^0.10.0",
|
||||
"photoswipe": "file:./thirdparty/photoswipe",
|
||||
|
@ -34,6 +37,7 @@
|
|||
"react-burger-menu": "^3.0.4",
|
||||
"react-dom": "16.13.1",
|
||||
"react-dropzone": "^11.2.4",
|
||||
"react-select": "^4.3.1",
|
||||
"react-top-loading-bar": "^2.0.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.2",
|
||||
"react-window": "^1.8.6",
|
||||
|
@ -50,17 +54,18 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@next/bundle-analyzer": "^9.5.3",
|
||||
"@types/debounce-promise": "^3.1.3",
|
||||
"@types/libsodium-wrappers": "^0.7.8",
|
||||
"@types/localforage": "^0.0.34",
|
||||
"@types/node": "^14.6.4",
|
||||
"@types/photoswipe": "^4.1.1",
|
||||
"@types/react": "^16.9.49",
|
||||
"@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/yup": "^0.29.7",
|
||||
"babel-plugin-styled-components": "^1.11.1",
|
||||
"next-on-netlify": "^2.4.0",
|
||||
"typescript": "^4.1.3",
|
||||
"worker-plugin": "^5.0.0"
|
||||
},
|
||||
|
|
BIN
public/images/ente-256.png
Normal file
BIN
public/images/ente-256.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
BIN
public/images/ente-512.png
Normal file
BIN
public/images/ente-512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.3 KiB |
BIN
public/images/favicon.png
Normal file
BIN
public/images/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 422 B |
|
@ -6,6 +6,16 @@
|
|||
"src": "/images/ente-192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/images/ente-256.png",
|
||||
"type": "image/png",
|
||||
"sizes": "256x256"
|
||||
},
|
||||
{
|
||||
"src": "/images/ente-512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": "/",
|
||||
|
|
20
src/components/CrossIcon.tsx
Normal file
20
src/components/CrossIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function DateIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
DateIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
20
src/components/DateIcon.tsx
Normal file
20
src/components/DateIcon.tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function DateIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-4.5-7a2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 0-5z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
DateIcon.defaultProps = {
|
||||
height: 20,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
22
src/components/LocationIcon.tsx
Normal file
22
src/components/LocationIcon.tsx
Normal file
|
@ -0,0 +1,22 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
export default function LocationIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zM7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9z"></path>
|
||||
<circle cx="12" cy="9" r="2.5"></circle>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
LocationIcon.defaultProps = {
|
||||
height: 20,
|
||||
width: 20,
|
||||
viewBox: '0 0 24 24',
|
||||
};
|
|
@ -78,9 +78,9 @@ const EmptyScreen = styled.div`
|
|||
flex-direction: column;
|
||||
flex: 1;
|
||||
color: #2dc262;
|
||||
|
||||
|
||||
& > svg {
|
||||
filter: drop-shadow(3px 3px 5px rgba(45,194,98,0.5));
|
||||
filter: drop-shadow(3px 3px 5px rgba(45, 194, 98, 0.5));
|
||||
}
|
||||
`;
|
||||
|
||||
|
@ -101,6 +101,7 @@ interface Props {
|
|||
isFirstLoad;
|
||||
openFileUploader;
|
||||
loadingBar;
|
||||
searchMode: boolean;
|
||||
}
|
||||
|
||||
const PhotoFrame = ({
|
||||
|
@ -114,6 +115,7 @@ const PhotoFrame = ({
|
|||
isFirstLoad,
|
||||
openFileUploader,
|
||||
loadingBar,
|
||||
searchMode,
|
||||
}: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||
|
@ -274,7 +276,7 @@ const PhotoFrame = ({
|
|||
|
||||
return (
|
||||
<>
|
||||
{!isFirstLoad && files.length == 0 ? (
|
||||
{!isFirstLoad && files.length == 0 && !searchMode ? (
|
||||
<EmptyScreen>
|
||||
<CloudUpload width={150} height={150} />
|
||||
<Button
|
||||
|
@ -365,6 +367,7 @@ const PhotoFrame = ({
|
|||
}
|
||||
});
|
||||
files.length < 30 &&
|
||||
!searchMode &&
|
||||
timeStampList.push({
|
||||
itemType: ITEM_TYPE.BANNER,
|
||||
banner: (
|
||||
|
|
335
src/components/SearchBar.tsx
Normal file
335
src/components/SearchBar.tsx
Normal file
|
@ -0,0 +1,335 @@
|
|||
import { SetCollections, SetFiles } from 'pages/gallery';
|
||||
import React, { useEffect, useState, useRef } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
import { components } from 'react-select';
|
||||
import debounce from 'debounce-promise';
|
||||
import { File, getLocalFiles } from 'services/fileService';
|
||||
import {
|
||||
Collection,
|
||||
getLocalCollections,
|
||||
getNonEmptyCollections,
|
||||
} from 'services/collectionService';
|
||||
import { Bbox, parseHumanDate, searchLocation } from 'services/searchService';
|
||||
import {
|
||||
getFilesWithCreationDay,
|
||||
getFilesInsideBbox,
|
||||
getFormattedDate,
|
||||
getDefaultSuggestions,
|
||||
} from 'utils/search';
|
||||
import constants from 'utils/strings/constants';
|
||||
import LocationIcon from './LocationIcon';
|
||||
import DateIcon from './DateIcon';
|
||||
import SearchIcon from './SearchIcon';
|
||||
import CrossIcon from './CrossIcon';
|
||||
|
||||
const Wrapper = styled.div<{ width: number }>`
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
top: 0;
|
||||
left: ${(props) => `max(0px, 50% - min(360px,${props.width / 2}px))`};
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 5%;
|
||||
background-color: #111;
|
||||
color: #fff;
|
||||
min-height: 64px;
|
||||
box-shadow: 0 0 5px rgba(0, 0, 0, 0.7);
|
||||
margin-bottom: 10px;
|
||||
`;
|
||||
|
||||
const SearchButton = styled.div`
|
||||
top: 1px;
|
||||
z-index: 100;
|
||||
right: 80px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
min-height: 64px;
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const SearchStats = styled.div`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: #979797;
|
||||
`;
|
||||
|
||||
export enum SuggestionType {
|
||||
DATE,
|
||||
LOCATION,
|
||||
}
|
||||
export interface Suggestion {
|
||||
type: SuggestionType;
|
||||
label: string;
|
||||
value: Bbox | Date;
|
||||
}
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
isFirstLoad: boolean;
|
||||
setOpen: (value) => void;
|
||||
loadingBar: any;
|
||||
setFiles: SetFiles;
|
||||
setCollections: SetCollections;
|
||||
}
|
||||
interface Stats {
|
||||
resultCount: number;
|
||||
timeTaken: number;
|
||||
}
|
||||
export default function SearchBar(props: Props) {
|
||||
const [allFiles, setAllFiles] = useState<File[]>([]);
|
||||
const [allCollections, setAllCollections] = useState<Collection[]>([]);
|
||||
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
|
||||
const [stats, setStats] = useState<Stats>(null);
|
||||
const selectRef = useRef(null);
|
||||
useEffect(() => {
|
||||
if (props.isOpen) {
|
||||
setTimeout(() => {
|
||||
selectRef.current?.focus();
|
||||
}, 250);
|
||||
}
|
||||
if (!props.isOpen && allFiles?.length > 0) {
|
||||
return;
|
||||
}
|
||||
const main = async () => {
|
||||
setAllFiles(await getLocalFiles());
|
||||
setAllCollections(await getLocalCollections());
|
||||
};
|
||||
main();
|
||||
}, [props.isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener('resize', () =>
|
||||
setWindowWidth(window.innerWidth)
|
||||
);
|
||||
});
|
||||
//==========================
|
||||
// Functionality
|
||||
//==========================
|
||||
const getAutoCompleteSuggestions = async (searchPhrase: string) => {
|
||||
let option = getDefaultSuggestions().filter((suggestion) =>
|
||||
suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase())
|
||||
);
|
||||
|
||||
if (!searchPhrase?.length) {
|
||||
return option;
|
||||
}
|
||||
const searchedDate = parseHumanDate(searchPhrase);
|
||||
if (searchedDate != null) {
|
||||
option.push({
|
||||
type: SuggestionType.DATE,
|
||||
value: searchedDate,
|
||||
label: getFormattedDate(searchedDate),
|
||||
});
|
||||
}
|
||||
const searchResults = await searchLocation(searchPhrase);
|
||||
option.push(
|
||||
...searchResults.map(
|
||||
(searchResult) =>
|
||||
({
|
||||
type: SuggestionType.LOCATION,
|
||||
value: searchResult.bbox,
|
||||
label: searchResult.place,
|
||||
} as Suggestion)
|
||||
)
|
||||
);
|
||||
return option;
|
||||
};
|
||||
|
||||
const getOptions = debounce(getAutoCompleteSuggestions, 250);
|
||||
|
||||
const filterFiles = (selectedOption: Suggestion) => {
|
||||
if (!selectedOption) {
|
||||
return;
|
||||
}
|
||||
const startTime = Date.now();
|
||||
props.setOpen(true);
|
||||
let resultFiles: File[] = [];
|
||||
|
||||
switch (selectedOption.type) {
|
||||
case SuggestionType.DATE:
|
||||
const searchedDate = selectedOption.value as Date;
|
||||
const filesWithSameDate = getFilesWithCreationDay(
|
||||
allFiles,
|
||||
searchedDate
|
||||
);
|
||||
resultFiles = filesWithSameDate;
|
||||
break;
|
||||
case SuggestionType.LOCATION:
|
||||
const bbox = selectedOption.value as Bbox;
|
||||
|
||||
const filesTakenAtLocation = getFilesInsideBbox(allFiles, bbox);
|
||||
resultFiles = filesTakenAtLocation;
|
||||
}
|
||||
props.setFiles(resultFiles);
|
||||
props.setCollections(
|
||||
getNonEmptyCollections(allCollections, resultFiles)
|
||||
);
|
||||
const timeTaken = (Date.now() - startTime) / 1000;
|
||||
setStats({
|
||||
timeTaken,
|
||||
resultCount: resultFiles.length,
|
||||
});
|
||||
};
|
||||
const resetSearch = () => {
|
||||
if (props.isOpen) {
|
||||
selectRef.current.select.state.value = null;
|
||||
props.loadingBar.current?.continuousStart();
|
||||
props.setFiles(allFiles);
|
||||
props.setCollections(allCollections);
|
||||
setTimeout(() => {
|
||||
props.loadingBar.current?.complete();
|
||||
}, 10);
|
||||
props.setOpen(false);
|
||||
setStats(null);
|
||||
}
|
||||
};
|
||||
|
||||
//==========================
|
||||
// UI
|
||||
//==========================
|
||||
|
||||
const getIconByType = (type: SuggestionType) =>
|
||||
type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />;
|
||||
|
||||
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<span style={{ paddingRight: '10px', paddingBottom: '4px' }}>
|
||||
{getIconByType(props.type)}
|
||||
</span>
|
||||
<span>{props.label}</span>
|
||||
</div>
|
||||
);
|
||||
const { Option, Control } = components;
|
||||
|
||||
const OptionWithIcon = (props) => (
|
||||
<Option {...props}>
|
||||
<LabelWithIcon type={props.data.type} label={props.data.label} />
|
||||
</Option>
|
||||
);
|
||||
const ControlWithIcon = (props) => (
|
||||
<Control {...props}>
|
||||
<span
|
||||
className={'icon'}
|
||||
style={{
|
||||
paddingLeft: '10px',
|
||||
paddingBottom: '4px',
|
||||
}}
|
||||
>
|
||||
{props.getValue().length == 0 || props.menuIsOpen ? (
|
||||
<SearchIcon />
|
||||
) : props.getValue()[0].type == SuggestionType.DATE ? (
|
||||
<DateIcon />
|
||||
) : (
|
||||
<LocationIcon />
|
||||
)}
|
||||
</span>
|
||||
{props.children}
|
||||
</Control>
|
||||
);
|
||||
|
||||
const customStyles = {
|
||||
control: (style, { isFocused }) => ({
|
||||
...style,
|
||||
backgroundColor: '#282828',
|
||||
color: '#d1d1d1',
|
||||
borderColor: isFocused ? '#2dc262' : '#444',
|
||||
boxShadow: isFocused && '0 0 3px #2dc262',
|
||||
':hover': {
|
||||
borderColor: '#2dc262',
|
||||
cursor: 'text',
|
||||
'&>.icon': { color: '#2dc262' },
|
||||
},
|
||||
}),
|
||||
input: (style) => ({
|
||||
...style,
|
||||
color: '#d1d1d1',
|
||||
}),
|
||||
menu: (style) => ({
|
||||
...style,
|
||||
marginTop: '10px',
|
||||
backgroundColor: '#282828',
|
||||
}),
|
||||
option: (style, { isFocused }) => ({
|
||||
...style,
|
||||
backgroundColor: isFocused && '#343434',
|
||||
}),
|
||||
dropdownIndicator: (style) => ({
|
||||
...style,
|
||||
display: 'none',
|
||||
}),
|
||||
indicatorSeparator: (style) => ({
|
||||
...style,
|
||||
display: 'none',
|
||||
}),
|
||||
clearIndicator: (style) => ({
|
||||
...style,
|
||||
display: 'none',
|
||||
}),
|
||||
singleValue: (style, state) => ({
|
||||
...style,
|
||||
backgroundColor: '#282828',
|
||||
color: '#d1d1d1',
|
||||
display: state.selectProps.menuIsOpen ? 'none' : 'block',
|
||||
}),
|
||||
placeholder: (style) => ({
|
||||
...style,
|
||||
color: '#686868',
|
||||
wordSpacing: '2px',
|
||||
}),
|
||||
};
|
||||
return (
|
||||
!props.isFirstLoad && (
|
||||
<>
|
||||
{windowWidth > 1000 || props.isOpen ? (
|
||||
<Wrapper width={windowWidth}>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: '10px',
|
||||
}}
|
||||
>
|
||||
<AsyncSelect
|
||||
components={{
|
||||
Option: OptionWithIcon,
|
||||
Control: ControlWithIcon,
|
||||
}}
|
||||
ref={selectRef}
|
||||
placeholder={constants.SEARCH_HINT()}
|
||||
loadOptions={getOptions}
|
||||
onChange={filterFiles}
|
||||
isClearable
|
||||
escapeClearsValue
|
||||
styles={customStyles}
|
||||
noOptionsMessage={() => null}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ width: '24px' }}>
|
||||
{props.isOpen && (
|
||||
<div
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={resetSearch}
|
||||
>
|
||||
<CrossIcon />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Wrapper>
|
||||
) : (
|
||||
<SearchButton onClick={() => props.setOpen(true)}>
|
||||
<SearchIcon />
|
||||
</SearchButton>
|
||||
)}
|
||||
{props.isOpen && stats && (
|
||||
<SearchStats>{constants.SEARCH_STATS(stats)}</SearchStats>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
21
src/components/SearchIcon.tsx
Normal file
21
src/components/SearchIcon.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React from 'react';
|
||||
|
||||
export default function SearchIcon(props) {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
height={props.height}
|
||||
viewBox={props.viewBox}
|
||||
width={props.width}
|
||||
>
|
||||
<path d="M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5A6.5 6.5 0 1 0 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z"></path>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
SearchIcon.defaultProps = {
|
||||
height: 24,
|
||||
width: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
open: false,
|
||||
};
|
|
@ -27,6 +27,7 @@ import LinkButton from 'pages/gallery/components/LinkButton';
|
|||
import { downloadApp } from 'utils/common';
|
||||
import { logoutUser } from 'services/userService';
|
||||
import { SetDialogMessage } from './MessageDialog';
|
||||
import { LogoImage } from 'pages/_app';
|
||||
|
||||
interface Props {
|
||||
files: File[];
|
||||
|
@ -102,9 +103,15 @@ export default function Sidebar(props: Props) {
|
|||
onStateChange={(state) => setIsOpen(state.isOpen)}
|
||||
itemListElement="div"
|
||||
>
|
||||
<div style={{ display: 'flex', textAlign: 'center' }}>
|
||||
<LogoImage
|
||||
style={{ height: '24px', padding: '3px' }}
|
||||
alt="logo"
|
||||
src="/icon.svg"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
marginBottom: '8px',
|
||||
outline: 'none',
|
||||
color: 'rgb(45, 194, 98)',
|
||||
fontSize: '16px',
|
||||
|
@ -112,7 +119,7 @@ export default function Sidebar(props: Props) {
|
|||
>
|
||||
{user?.email}
|
||||
</div>
|
||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
||||
<div style={{ flex: 1, overflow: 'auto', paddingTop: '0' }}>
|
||||
<div style={{ outline: 'none' }}>
|
||||
<div style={{ display: 'flex' }}>
|
||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||
|
|
|
@ -7,7 +7,7 @@ import Container from 'components/Container';
|
|||
import Head from 'next/head';
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'photoswipe/dist/photoswipe.css';
|
||||
import { Workbox } from "workbox-window";
|
||||
import { Workbox } from 'workbox-window';
|
||||
import { sentryInit } from '../utils/sentry';
|
||||
import EnteSpinner from 'components/EnteSpinner';
|
||||
|
||||
|
@ -183,8 +183,9 @@ const GlobalStyles = createGlobalStyle`
|
|||
position: fixed;
|
||||
width: 24px;
|
||||
height: 16px;
|
||||
top:25px;
|
||||
top:27px;
|
||||
left: 32px;
|
||||
z-index:100 !important;
|
||||
}
|
||||
.bm-burger-bars {
|
||||
background: #bdbdbd;
|
||||
|
@ -281,7 +282,7 @@ const GlobalStyles = createGlobalStyle`
|
|||
}
|
||||
`;
|
||||
|
||||
const Image = styled.img`
|
||||
export const LogoImage = styled.img`
|
||||
max-height: 28px;
|
||||
margin-right: 5px;
|
||||
`;
|
||||
|
@ -308,23 +309,25 @@ sentryInit();
|
|||
export default function App({ Component, pageProps, err }) {
|
||||
const router = useRouter();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [offline, setOffline] = useState(typeof window !== 'undefined' && !window.navigator.onLine);
|
||||
const [offline, setOffline] = useState(
|
||||
typeof window !== 'undefined' && !window.navigator.onLine
|
||||
);
|
||||
useEffect(() => {
|
||||
if (
|
||||
!("serviceWorker" in navigator) ||
|
||||
process.env.NODE_ENV !== "production"
|
||||
!('serviceWorker' in navigator) ||
|
||||
process.env.NODE_ENV !== 'production'
|
||||
) {
|
||||
console.warn("Progressive Web App support is disabled");
|
||||
return;
|
||||
console.warn('Progressive Web App support is disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const wb = new Workbox("sw.js", { scope: "/" });
|
||||
|
||||
const wb = new Workbox('sw.js', { scope: '/' });
|
||||
wb.register();
|
||||
}, []);
|
||||
|
||||
const setUserOnline = () => setOffline(false);
|
||||
const setUserOffline = () => setOffline(true);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
`%c${constants.CONSOLE_WARNING_STOP}`,
|
||||
|
@ -342,14 +345,13 @@ export default function App({ Component, pageProps, err }) {
|
|||
setLoading(false);
|
||||
});
|
||||
|
||||
window.addEventListener('online', setUserOnline);
|
||||
window.addEventListener('online', setUserOnline);
|
||||
window.addEventListener('offline', setUserOffline);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('online', setUserOnline);
|
||||
window.removeEventListener('offline', setUserOffline);
|
||||
}
|
||||
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
@ -367,14 +369,16 @@ export default function App({ Component, pageProps, err }) {
|
|||
<GlobalStyles />
|
||||
<Navbar>
|
||||
<FlexContainer>
|
||||
<Image
|
||||
<LogoImage
|
||||
style={{ height: '24px', padding: '3px' }}
|
||||
alt="logo"
|
||||
src="/icon.svg"
|
||||
/>
|
||||
</FlexContainer>
|
||||
</Navbar>
|
||||
{offline && <OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>}
|
||||
{offline && (
|
||||
<OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>
|
||||
)}
|
||||
{loading ? (
|
||||
<Container>
|
||||
<EnteSpinner>
|
||||
|
|
|
@ -36,8 +36,13 @@ export default class MyDocument extends Document {
|
|||
name="description"
|
||||
content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
|
||||
/>
|
||||
<link rel="icon" href="/icon.svg" type="image/png" />
|
||||
<link rel="manifest" href="manifest.json"></link>
|
||||
<link rel="icon" href="/images/favicon.png" type="image/png" />
|
||||
<link rel="manifest" href="manifest.json" />
|
||||
<link rel="apple-touch-icon" href="/images/ente-512.png" />
|
||||
<meta name="theme-color" content="#111" />
|
||||
<link rel="icon" type="image/png" href="/images/favicon.png" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
|
||||
</Head>
|
||||
<body>
|
||||
<Main />
|
||||
|
|
|
@ -25,7 +25,7 @@ interface CollectionProps {
|
|||
}
|
||||
|
||||
const Container = styled.div`
|
||||
margin: 0 auto;
|
||||
margin: 10px auto;
|
||||
overflow-y: hidden;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
|
@ -80,15 +80,18 @@ export default function Collections(props: CollectionProps) {
|
|||
const [collectionShareModalView, setCollectionShareModalView] =
|
||||
useState(false);
|
||||
const [scrollObj, setScrollObj] = useState<{
|
||||
scrollLeft?: number, scrollWidth?: number, clientWidth?: number
|
||||
scrollLeft?: number;
|
||||
scrollWidth?: number;
|
||||
clientWidth?: number;
|
||||
}>({});
|
||||
|
||||
const updateScrollObj = () => {
|
||||
if (collectionRef.current) {
|
||||
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
|
||||
const { scrollLeft, scrollWidth, clientWidth } =
|
||||
collectionRef.current;
|
||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
updateScrollObj();
|
||||
|
@ -125,7 +128,7 @@ export default function Collections(props: CollectionProps) {
|
|||
|
||||
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
||||
collectionRef.current.scrollBy(250 * direction, 0);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -139,10 +142,12 @@ export default function Collections(props: CollectionProps) {
|
|||
syncWithRemote={props.syncWithRemote}
|
||||
/>
|
||||
<Container>
|
||||
{scrollObj.scrollLeft > 0 && <NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
/>}
|
||||
{scrollObj.scrollLeft > 0 && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||
/>
|
||||
)}
|
||||
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
|
||||
<Chip active={!selected} onClick={clickHandler()}>
|
||||
All
|
||||
|
@ -182,10 +187,13 @@ export default function Collections(props: CollectionProps) {
|
|||
</Chip>
|
||||
))}
|
||||
</Wrapper>
|
||||
{scrollObj.scrollLeft < (scrollObj.scrollWidth - scrollObj.clientWidth) && <NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
/>}
|
||||
{scrollObj.scrollLeft <
|
||||
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||
<NavigationButton
|
||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
min-height: 64px;
|
||||
right: 32px;
|
||||
`;
|
||||
function UploadButton({ openFileUploader }) {
|
||||
return (
|
||||
<div
|
||||
onClick={openFileUploader}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '30px',
|
||||
top: '20px',
|
||||
zIndex: 100,
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<Wrapper onClick={openFileUploader}>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
|
@ -25,7 +27,7 @@ function UploadButton({ openFileUploader }) {
|
|||
d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10zM8 13.01l1.41 1.41L11 12.84V17h2v-4.16l1.59 1.59L16 13.01 12.01 9 8 13.01z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -36,7 +36,7 @@ import {
|
|||
setIsFirstLogin,
|
||||
setJustSignedUp,
|
||||
} from 'utils/storage';
|
||||
import { logoutUser } from 'services/userService';
|
||||
import { isTokenValid, logoutUser } from 'services/userService';
|
||||
import AlertBanner from './components/AlertBanner';
|
||||
import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
|
@ -53,6 +53,7 @@ import { getSelectedFileIds } from 'utils/file';
|
|||
import { addFilesToCollection } from 'utils/collection';
|
||||
import SelectedFileOptions from './components/SelectedFileOptions';
|
||||
import { errorCodes } from 'utils/common/errorUtil';
|
||||
import SearchBar from 'components/SearchBar';
|
||||
|
||||
export enum FILE_TYPE {
|
||||
IMAGE,
|
||||
|
@ -74,6 +75,8 @@ export type selectedState = {
|
|||
[k: number]: boolean;
|
||||
count: number;
|
||||
};
|
||||
export type SetFiles = React.Dispatch<React.SetStateAction<File[]>>;
|
||||
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
|
||||
export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
|
||||
|
||||
export default function Gallery() {
|
||||
|
@ -110,6 +113,7 @@ export default function Gallery() {
|
|||
});
|
||||
|
||||
const loadingBar = useRef(null);
|
||||
const [searchMode, setSearchMode] = useState(false);
|
||||
useEffect(() => {
|
||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||
if (!key) {
|
||||
|
@ -156,6 +160,9 @@ export default function Gallery() {
|
|||
const syncWithRemote = async () => {
|
||||
try {
|
||||
checkConnectivity();
|
||||
if (!(await isTokenValid())) {
|
||||
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
|
||||
}
|
||||
loadingBar.current?.continuousStart();
|
||||
await billingService.updatePlans();
|
||||
await billingService.syncSubscription();
|
||||
|
@ -194,6 +201,9 @@ export default function Gallery() {
|
|||
nonClosable: true,
|
||||
});
|
||||
break;
|
||||
case errorCodes.ERR_NO_INTERNET_CONNECTION:
|
||||
setBannerMessage(constants.NO_INTERNET_CONNECTION);
|
||||
break;
|
||||
case errorCodes.ERR_KEY_MISSING:
|
||||
clearKeys();
|
||||
router.push('/credentials');
|
||||
|
@ -250,6 +260,11 @@ export default function Gallery() {
|
|||
syncWithRemote
|
||||
);
|
||||
};
|
||||
const updateFiles = (files: File[]) => {
|
||||
setFiles(files);
|
||||
setSinceTime(new Date().getTime());
|
||||
selectCollection(null);
|
||||
};
|
||||
return (
|
||||
<FullScreenDropZone
|
||||
getRootProps={getRootProps}
|
||||
|
@ -282,6 +297,14 @@ export default function Gallery() {
|
|||
onHide={() => setDialogView(false)}
|
||||
attributes={dialogMessage}
|
||||
/>
|
||||
<SearchBar
|
||||
isOpen={searchMode}
|
||||
setOpen={setSearchMode}
|
||||
loadingBar={loadingBar}
|
||||
isFirstLoad={isFirstLoad}
|
||||
setFiles={updateFiles}
|
||||
setCollections={setCollections}
|
||||
/>
|
||||
<Collections
|
||||
collections={collections}
|
||||
selected={Number(router.query.collection)}
|
||||
|
@ -339,6 +362,7 @@ export default function Gallery() {
|
|||
isFirstLoad={isFirstLoad}
|
||||
openFileUploader={openFileUploader}
|
||||
loadingBar={loadingBar}
|
||||
searchMode={searchMode}
|
||||
/>
|
||||
{selected.count > 0 && (
|
||||
<SelectedFileOptions
|
||||
|
|
|
@ -13,23 +13,6 @@ interface IQueryPrams {
|
|||
* Service to manage all HTTP calls.
|
||||
*/
|
||||
class HTTPService {
|
||||
constructor() {
|
||||
axios.interceptors.response.use(
|
||||
(response) => {
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
(err) => {
|
||||
if (!err.response) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
const response = err.response;
|
||||
if (response?.status === 401) {
|
||||
clearData();
|
||||
}
|
||||
return Promise.reject(response);
|
||||
}
|
||||
);
|
||||
}
|
||||
/**
|
||||
* header object to be append to all api calls.
|
||||
*/
|
||||
|
|
|
@ -125,7 +125,7 @@ const getCollections = async (
|
|||
return await Promise.all(promises);
|
||||
} catch (e) {
|
||||
console.error('getCollections failed- ', e);
|
||||
throw new Error(e?.status?.toString());
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -333,10 +333,8 @@ export const addToCollection = async (
|
|||
await Promise.all(
|
||||
files.map(async (file) => {
|
||||
file.collectionID = collection.id;
|
||||
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
||||
file.key,
|
||||
collection.key
|
||||
);
|
||||
const newEncryptedKey: B64EncryptionResult =
|
||||
await worker.encryptToB64(file.key, collection.key);
|
||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||
if (params['files'] == undefined) {
|
||||
|
|
35
src/services/searchService.ts
Normal file
35
src/services/searchService.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import HTTPService from './HTTPService';
|
||||
import * as chrono from 'chrono-node';
|
||||
import { getEndpoint } from 'utils/common/apiUtil';
|
||||
import { getToken } from 'utils/common/key';
|
||||
|
||||
const ENDPOINT = getEndpoint();
|
||||
|
||||
export type Bbox = [number, number, number, number];
|
||||
export interface LocationSearchResponse {
|
||||
place: string;
|
||||
bbox: Bbox;
|
||||
}
|
||||
export const getMapboxToken = () => {
|
||||
return process.env.NEXT_PUBLIC_MAPBOX_TOKEN;
|
||||
};
|
||||
|
||||
export function parseHumanDate(humanDate: string) {
|
||||
return chrono.parseDate(humanDate);
|
||||
}
|
||||
|
||||
export async function searchLocation(
|
||||
searchPhrase: string
|
||||
): Promise<LocationSearchResponse[]> {
|
||||
const resp = await HTTPService.get(
|
||||
`${ENDPOINT}/search/location`,
|
||||
{
|
||||
query: searchPhrase,
|
||||
limit: 4,
|
||||
},
|
||||
{
|
||||
'X-Auth-Token': getToken(),
|
||||
}
|
||||
);
|
||||
return resp.data.results;
|
||||
}
|
72
src/utils/search/index.ts
Normal file
72
src/utils/search/index.ts
Normal file
|
@ -0,0 +1,72 @@
|
|||
import { Suggestion, SuggestionType } from 'components/SearchBar';
|
||||
import { File } from 'services/fileService';
|
||||
|
||||
export function getFilesInsideBbox(
|
||||
files: File[],
|
||||
bbox: [number, number, number, number]
|
||||
) {
|
||||
return files.filter((file) => {
|
||||
if (file.metadata.latitude == null && file.metadata.longitude == null) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
file.metadata.longitude >= bbox[0] &&
|
||||
file.metadata.latitude >= bbox[1] &&
|
||||
file.metadata.longitude <= bbox[2] &&
|
||||
file.metadata.latitude <= bbox[3]
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const isSameDay = (baseDate) => (compareDate) => {
|
||||
return (
|
||||
baseDate.getMonth() === compareDate.getMonth() &&
|
||||
baseDate.getDate() === compareDate.getDate()
|
||||
);
|
||||
};
|
||||
|
||||
export function getFilesWithCreationDay(files: File[], searchedDate: Date) {
|
||||
const isSearchedDate = isSameDay(searchedDate);
|
||||
|
||||
return files.filter((file) =>
|
||||
isSearchedDate(new Date(file.metadata.creationTime / 1000))
|
||||
);
|
||||
}
|
||||
export function getFormattedDate(date: Date) {
|
||||
return new Intl.DateTimeFormat('en-IN', {
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
}).format(date);
|
||||
}
|
||||
|
||||
export function getDefaultSuggestions() {
|
||||
return [
|
||||
{
|
||||
label: 'Christmas',
|
||||
value: new Date(2021, 11, 25),
|
||||
type: SuggestionType.DATE,
|
||||
},
|
||||
{
|
||||
label: 'Christmas Eve',
|
||||
value: new Date(2021, 11, 24),
|
||||
type: SuggestionType.DATE,
|
||||
},
|
||||
{
|
||||
label: 'New Year',
|
||||
value: new Date(2021, 0, 1),
|
||||
type: SuggestionType.DATE,
|
||||
},
|
||||
{
|
||||
label: 'New Year Eve',
|
||||
value: new Date(2021, 11, 31),
|
||||
type: SuggestionType.DATE,
|
||||
},
|
||||
{
|
||||
label: "Valentine's Day",
|
||||
value: new Date(2021, 1, 14),
|
||||
type: SuggestionType.DATE,
|
||||
},
|
||||
] as Suggestion[];
|
||||
}
|
|
@ -301,6 +301,7 @@ const englishConstants = {
|
|||
SHARING_BAD_REQUEST_ERROR: 'sharing album not allowed',
|
||||
SHARING_DISABLED_FOR_FREE_ACCOUNTS: 'sharing is disabled for free accounts',
|
||||
CREATE_ALBUM_FAILED: 'failed to create album , please try again',
|
||||
SEARCH_HINT: () => <span>New York, April 14, Christmas...</span>,
|
||||
TERMS_AND_CONDITIONS: () => (
|
||||
<p>
|
||||
I agree to the{' '}
|
||||
|
@ -323,6 +324,13 @@ const englishConstants = {
|
|||
with ente
|
||||
</p>
|
||||
),
|
||||
SEARCH_STATS: ({ resultCount, timeTaken }) => (
|
||||
<span>
|
||||
found <span style={{ color: '#2dc262' }}>{resultCount}</span>{' '}
|
||||
memories ( <span style={{ color: '#2dc262' }}> {timeTaken}</span>{' '}
|
||||
seconds )
|
||||
</span>
|
||||
),
|
||||
};
|
||||
|
||||
export default englishConstants;
|
||||
|
|
Loading…
Reference in a new issue