Merge branch 'master' into release
This commit is contained in:
commit
c3f284cc0e
|
@ -17,7 +17,9 @@
|
||||||
"@stripe/stripe-js": "^1.13.2",
|
"@stripe/stripe-js": "^1.13.2",
|
||||||
"axios": "^0.20.0",
|
"axios": "^0.20.0",
|
||||||
"bootstrap": "^4.5.2",
|
"bootstrap": "^4.5.2",
|
||||||
|
"chrono-node": "^2.2.6",
|
||||||
"comlink": "^4.3.0",
|
"comlink": "^4.3.0",
|
||||||
|
"debounce-promise": "^3.1.2",
|
||||||
"exif-js": "^2.3.0",
|
"exif-js": "^2.3.0",
|
||||||
"formik": "^2.1.5",
|
"formik": "^2.1.5",
|
||||||
"heic2any": "^0.0.3",
|
"heic2any": "^0.0.3",
|
||||||
|
@ -26,6 +28,7 @@
|
||||||
"libsodium-wrappers": "^0.7.8",
|
"libsodium-wrappers": "^0.7.8",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
"next": "9.5.3",
|
"next": "9.5.3",
|
||||||
|
"next-on-netlify": "^3.0.1",
|
||||||
"next-with-workbox": "^2.0.1",
|
"next-with-workbox": "^2.0.1",
|
||||||
"node-forge": "^0.10.0",
|
"node-forge": "^0.10.0",
|
||||||
"photoswipe": "file:./thirdparty/photoswipe",
|
"photoswipe": "file:./thirdparty/photoswipe",
|
||||||
|
@ -34,6 +37,7 @@
|
||||||
"react-burger-menu": "^3.0.4",
|
"react-burger-menu": "^3.0.4",
|
||||||
"react-dom": "16.13.1",
|
"react-dom": "16.13.1",
|
||||||
"react-dropzone": "^11.2.4",
|
"react-dropzone": "^11.2.4",
|
||||||
|
"react-select": "^4.3.1",
|
||||||
"react-top-loading-bar": "^2.0.1",
|
"react-top-loading-bar": "^2.0.1",
|
||||||
"react-virtualized-auto-sizer": "^1.0.2",
|
"react-virtualized-auto-sizer": "^1.0.2",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
|
@ -50,17 +54,18 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "^9.5.3",
|
"@next/bundle-analyzer": "^9.5.3",
|
||||||
|
"@types/debounce-promise": "^3.1.3",
|
||||||
"@types/libsodium-wrappers": "^0.7.8",
|
"@types/libsodium-wrappers": "^0.7.8",
|
||||||
"@types/localforage": "^0.0.34",
|
"@types/localforage": "^0.0.34",
|
||||||
"@types/node": "^14.6.4",
|
"@types/node": "^14.6.4",
|
||||||
"@types/photoswipe": "^4.1.1",
|
"@types/photoswipe": "^4.1.1",
|
||||||
"@types/react": "^16.9.49",
|
"@types/react": "^16.9.49",
|
||||||
|
"@types/react-select": "^4.0.15",
|
||||||
"@types/react-window": "^1.8.2",
|
"@types/react-window": "^1.8.2",
|
||||||
"@types/react-window-infinite-loader": "^1.0.3",
|
"@types/react-window-infinite-loader": "^1.0.3",
|
||||||
"@types/styled-components": "^5.1.3",
|
"@types/styled-components": "^5.1.3",
|
||||||
"@types/yup": "^0.29.7",
|
"@types/yup": "^0.29.7",
|
||||||
"babel-plugin-styled-components": "^1.11.1",
|
"babel-plugin-styled-components": "^1.11.1",
|
||||||
"next-on-netlify": "^2.4.0",
|
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
"worker-plugin": "^5.0.0"
|
"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",
|
"src": "/images/ente-192.png",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"sizes": "192x192"
|
"sizes": "192x192"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/images/ente-256.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "256x256"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/images/ente-512.png",
|
||||||
|
"type": "image/png",
|
||||||
|
"sizes": "512x512"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"start_url": "/",
|
"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',
|
||||||
|
};
|
|
@ -80,7 +80,7 @@ const EmptyScreen = styled.div`
|
||||||
color: #2dc262;
|
color: #2dc262;
|
||||||
|
|
||||||
& > svg {
|
& > 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;
|
isFirstLoad;
|
||||||
openFileUploader;
|
openFileUploader;
|
||||||
loadingBar;
|
loadingBar;
|
||||||
|
searchMode: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PhotoFrame = ({
|
const PhotoFrame = ({
|
||||||
|
@ -114,6 +115,7 @@ const PhotoFrame = ({
|
||||||
isFirstLoad,
|
isFirstLoad,
|
||||||
openFileUploader,
|
openFileUploader,
|
||||||
loadingBar,
|
loadingBar,
|
||||||
|
searchMode,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
|
@ -274,7 +276,7 @@ const PhotoFrame = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isFirstLoad && files.length == 0 ? (
|
{!isFirstLoad && files.length == 0 && !searchMode ? (
|
||||||
<EmptyScreen>
|
<EmptyScreen>
|
||||||
<CloudUpload width={150} height={150} />
|
<CloudUpload width={150} height={150} />
|
||||||
<Button
|
<Button
|
||||||
|
@ -365,6 +367,7 @@ const PhotoFrame = ({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
files.length < 30 &&
|
files.length < 30 &&
|
||||||
|
!searchMode &&
|
||||||
timeStampList.push({
|
timeStampList.push({
|
||||||
itemType: ITEM_TYPE.BANNER,
|
itemType: ITEM_TYPE.BANNER,
|
||||||
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 { downloadApp } from 'utils/common';
|
||||||
import { logoutUser } from 'services/userService';
|
import { logoutUser } from 'services/userService';
|
||||||
import { SetDialogMessage } from './MessageDialog';
|
import { SetDialogMessage } from './MessageDialog';
|
||||||
|
import { LogoImage } from 'pages/_app';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
files: File[];
|
files: File[];
|
||||||
|
@ -102,9 +103,15 @@ export default function Sidebar(props: Props) {
|
||||||
onStateChange={(state) => setIsOpen(state.isOpen)}
|
onStateChange={(state) => setIsOpen(state.isOpen)}
|
||||||
itemListElement="div"
|
itemListElement="div"
|
||||||
>
|
>
|
||||||
|
<div style={{ display: 'flex', textAlign: 'center' }}>
|
||||||
|
<LogoImage
|
||||||
|
style={{ height: '24px', padding: '3px' }}
|
||||||
|
alt="logo"
|
||||||
|
src="/icon.svg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '8px',
|
|
||||||
outline: 'none',
|
outline: 'none',
|
||||||
color: 'rgb(45, 194, 98)',
|
color: 'rgb(45, 194, 98)',
|
||||||
fontSize: '16px',
|
fontSize: '16px',
|
||||||
|
@ -112,7 +119,7 @@ export default function Sidebar(props: Props) {
|
||||||
>
|
>
|
||||||
{user?.email}
|
{user?.email}
|
||||||
</div>
|
</div>
|
||||||
<div style={{ flex: 1, overflow: 'auto' }}>
|
<div style={{ flex: 1, overflow: 'auto', paddingTop: '0' }}>
|
||||||
<div style={{ outline: 'none' }}>
|
<div style={{ outline: 'none' }}>
|
||||||
<div style={{ display: 'flex' }}>
|
<div style={{ display: 'flex' }}>
|
||||||
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
<h5 style={{ margin: '4px 0 12px 2px' }}>
|
||||||
|
|
|
@ -7,7 +7,7 @@ import Container from 'components/Container';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||||
import 'photoswipe/dist/photoswipe.css';
|
import 'photoswipe/dist/photoswipe.css';
|
||||||
import { Workbox } from "workbox-window";
|
import { Workbox } from 'workbox-window';
|
||||||
import { sentryInit } from '../utils/sentry';
|
import { sentryInit } from '../utils/sentry';
|
||||||
import EnteSpinner from 'components/EnteSpinner';
|
import EnteSpinner from 'components/EnteSpinner';
|
||||||
|
|
||||||
|
@ -183,8 +183,9 @@ const GlobalStyles = createGlobalStyle`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
top:25px;
|
top:27px;
|
||||||
left: 32px;
|
left: 32px;
|
||||||
|
z-index:100 !important;
|
||||||
}
|
}
|
||||||
.bm-burger-bars {
|
.bm-burger-bars {
|
||||||
background: #bdbdbd;
|
background: #bdbdbd;
|
||||||
|
@ -281,7 +282,7 @@ const GlobalStyles = createGlobalStyle`
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Image = styled.img`
|
export const LogoImage = styled.img`
|
||||||
max-height: 28px;
|
max-height: 28px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
`;
|
`;
|
||||||
|
@ -308,17 +309,19 @@ sentryInit();
|
||||||
export default function App({ Component, pageProps, err }) {
|
export default function App({ Component, pageProps, err }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loading, setLoading] = useState(false);
|
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(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
!("serviceWorker" in navigator) ||
|
!('serviceWorker' in navigator) ||
|
||||||
process.env.NODE_ENV !== "production"
|
process.env.NODE_ENV !== 'production'
|
||||||
) {
|
) {
|
||||||
console.warn("Progressive Web App support is disabled");
|
console.warn('Progressive Web App support is disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const wb = new Workbox("sw.js", { scope: "/" });
|
const wb = new Workbox('sw.js', { scope: '/' });
|
||||||
wb.register();
|
wb.register();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
@ -348,8 +351,7 @@ export default function App({ Component, pageProps, err }) {
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener('online', setUserOnline);
|
window.removeEventListener('online', setUserOnline);
|
||||||
window.removeEventListener('offline', setUserOffline);
|
window.removeEventListener('offline', setUserOffline);
|
||||||
}
|
};
|
||||||
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -367,14 +369,16 @@ export default function App({ Component, pageProps, err }) {
|
||||||
<GlobalStyles />
|
<GlobalStyles />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<FlexContainer>
|
<FlexContainer>
|
||||||
<Image
|
<LogoImage
|
||||||
style={{ height: '24px', padding: '3px' }}
|
style={{ height: '24px', padding: '3px' }}
|
||||||
alt="logo"
|
alt="logo"
|
||||||
src="/icon.svg"
|
src="/icon.svg"
|
||||||
/>
|
/>
|
||||||
</FlexContainer>
|
</FlexContainer>
|
||||||
</Navbar>
|
</Navbar>
|
||||||
{offline && <OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>}
|
{offline && (
|
||||||
|
<OfflineContainer>{constants.OFFLINE_MSG}</OfflineContainer>
|
||||||
|
)}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<Container>
|
<Container>
|
||||||
<EnteSpinner>
|
<EnteSpinner>
|
||||||
|
|
|
@ -36,8 +36,13 @@ export default class MyDocument extends Document {
|
||||||
name="description"
|
name="description"
|
||||||
content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
|
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="icon" href="/images/favicon.png" type="image/png" />
|
||||||
<link rel="manifest" href="manifest.json"></link>
|
<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>
|
</Head>
|
||||||
<body>
|
<body>
|
||||||
<Main />
|
<Main />
|
||||||
|
|
|
@ -25,7 +25,7 @@ interface CollectionProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
margin: 0 auto;
|
margin: 10px auto;
|
||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -80,15 +80,18 @@ export default function Collections(props: CollectionProps) {
|
||||||
const [collectionShareModalView, setCollectionShareModalView] =
|
const [collectionShareModalView, setCollectionShareModalView] =
|
||||||
useState(false);
|
useState(false);
|
||||||
const [scrollObj, setScrollObj] = useState<{
|
const [scrollObj, setScrollObj] = useState<{
|
||||||
scrollLeft?: number, scrollWidth?: number, clientWidth?: number
|
scrollLeft?: number;
|
||||||
|
scrollWidth?: number;
|
||||||
|
clientWidth?: number;
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
const updateScrollObj = () => {
|
const updateScrollObj = () => {
|
||||||
if (collectionRef.current) {
|
if (collectionRef.current) {
|
||||||
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
|
const { scrollLeft, scrollWidth, clientWidth } =
|
||||||
|
collectionRef.current;
|
||||||
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateScrollObj();
|
updateScrollObj();
|
||||||
|
@ -125,7 +128,7 @@ export default function Collections(props: CollectionProps) {
|
||||||
|
|
||||||
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
const scrollCollection = (direction: SCROLL_DIRECTION) => () => {
|
||||||
collectionRef.current.scrollBy(250 * direction, 0);
|
collectionRef.current.scrollBy(250 * direction, 0);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -139,10 +142,12 @@ export default function Collections(props: CollectionProps) {
|
||||||
syncWithRemote={props.syncWithRemote}
|
syncWithRemote={props.syncWithRemote}
|
||||||
/>
|
/>
|
||||||
<Container>
|
<Container>
|
||||||
{scrollObj.scrollLeft > 0 && <NavigationButton
|
{scrollObj.scrollLeft > 0 && (
|
||||||
|
<NavigationButton
|
||||||
scrollDirection={SCROLL_DIRECTION.LEFT}
|
scrollDirection={SCROLL_DIRECTION.LEFT}
|
||||||
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
onClick={scrollCollection(SCROLL_DIRECTION.LEFT)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
|
<Wrapper ref={collectionRef} onScroll={updateScrollObj}>
|
||||||
<Chip active={!selected} onClick={clickHandler()}>
|
<Chip active={!selected} onClick={clickHandler()}>
|
||||||
All
|
All
|
||||||
|
@ -182,10 +187,13 @@ export default function Collections(props: CollectionProps) {
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
{scrollObj.scrollLeft < (scrollObj.scrollWidth - scrollObj.clientWidth) && <NavigationButton
|
{scrollObj.scrollLeft <
|
||||||
|
scrollObj.scrollWidth - scrollObj.clientWidth && (
|
||||||
|
<NavigationButton
|
||||||
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
scrollDirection={SCROLL_DIRECTION.RIGHT}
|
||||||
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
|
||||||
/>}
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,17 +1,19 @@
|
||||||
import React from 'react';
|
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 }) {
|
function UploadButton({ openFileUploader }) {
|
||||||
return (
|
return (
|
||||||
<div
|
<Wrapper onClick={openFileUploader}>
|
||||||
onClick={openFileUploader}
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
right: '30px',
|
|
||||||
top: '20px',
|
|
||||||
zIndex: 100,
|
|
||||||
cursor: 'pointer',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 24 24"
|
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"
|
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>
|
</svg>
|
||||||
</div>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,7 @@ import {
|
||||||
setIsFirstLogin,
|
setIsFirstLogin,
|
||||||
setJustSignedUp,
|
setJustSignedUp,
|
||||||
} from 'utils/storage';
|
} from 'utils/storage';
|
||||||
import { logoutUser } from 'services/userService';
|
import { isTokenValid, logoutUser } from 'services/userService';
|
||||||
import AlertBanner from './components/AlertBanner';
|
import AlertBanner from './components/AlertBanner';
|
||||||
import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
|
import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
|
||||||
import { useDropzone } from 'react-dropzone';
|
import { useDropzone } from 'react-dropzone';
|
||||||
|
@ -53,6 +53,7 @@ import { getSelectedFileIds } from 'utils/file';
|
||||||
import { addFilesToCollection } from 'utils/collection';
|
import { addFilesToCollection } from 'utils/collection';
|
||||||
import SelectedFileOptions from './components/SelectedFileOptions';
|
import SelectedFileOptions from './components/SelectedFileOptions';
|
||||||
import { errorCodes } from 'utils/common/errorUtil';
|
import { errorCodes } from 'utils/common/errorUtil';
|
||||||
|
import SearchBar from 'components/SearchBar';
|
||||||
|
|
||||||
export enum FILE_TYPE {
|
export enum FILE_TYPE {
|
||||||
IMAGE,
|
IMAGE,
|
||||||
|
@ -74,6 +75,8 @@ export type selectedState = {
|
||||||
[k: number]: boolean;
|
[k: number]: boolean;
|
||||||
count: number;
|
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 type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
|
||||||
|
|
||||||
export default function Gallery() {
|
export default function Gallery() {
|
||||||
|
@ -110,6 +113,7 @@ export default function Gallery() {
|
||||||
});
|
});
|
||||||
|
|
||||||
const loadingBar = useRef(null);
|
const loadingBar = useRef(null);
|
||||||
|
const [searchMode, setSearchMode] = useState(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
|
@ -156,6 +160,9 @@ export default function Gallery() {
|
||||||
const syncWithRemote = async () => {
|
const syncWithRemote = async () => {
|
||||||
try {
|
try {
|
||||||
checkConnectivity();
|
checkConnectivity();
|
||||||
|
if (!(await isTokenValid())) {
|
||||||
|
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
|
||||||
|
}
|
||||||
loadingBar.current?.continuousStart();
|
loadingBar.current?.continuousStart();
|
||||||
await billingService.updatePlans();
|
await billingService.updatePlans();
|
||||||
await billingService.syncSubscription();
|
await billingService.syncSubscription();
|
||||||
|
@ -194,6 +201,9 @@ export default function Gallery() {
|
||||||
nonClosable: true,
|
nonClosable: true,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case errorCodes.ERR_NO_INTERNET_CONNECTION:
|
||||||
|
setBannerMessage(constants.NO_INTERNET_CONNECTION);
|
||||||
|
break;
|
||||||
case errorCodes.ERR_KEY_MISSING:
|
case errorCodes.ERR_KEY_MISSING:
|
||||||
clearKeys();
|
clearKeys();
|
||||||
router.push('/credentials');
|
router.push('/credentials');
|
||||||
|
@ -250,6 +260,11 @@ export default function Gallery() {
|
||||||
syncWithRemote
|
syncWithRemote
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const updateFiles = (files: File[]) => {
|
||||||
|
setFiles(files);
|
||||||
|
setSinceTime(new Date().getTime());
|
||||||
|
selectCollection(null);
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<FullScreenDropZone
|
<FullScreenDropZone
|
||||||
getRootProps={getRootProps}
|
getRootProps={getRootProps}
|
||||||
|
@ -282,6 +297,14 @@ export default function Gallery() {
|
||||||
onHide={() => setDialogView(false)}
|
onHide={() => setDialogView(false)}
|
||||||
attributes={dialogMessage}
|
attributes={dialogMessage}
|
||||||
/>
|
/>
|
||||||
|
<SearchBar
|
||||||
|
isOpen={searchMode}
|
||||||
|
setOpen={setSearchMode}
|
||||||
|
loadingBar={loadingBar}
|
||||||
|
isFirstLoad={isFirstLoad}
|
||||||
|
setFiles={updateFiles}
|
||||||
|
setCollections={setCollections}
|
||||||
|
/>
|
||||||
<Collections
|
<Collections
|
||||||
collections={collections}
|
collections={collections}
|
||||||
selected={Number(router.query.collection)}
|
selected={Number(router.query.collection)}
|
||||||
|
@ -339,6 +362,7 @@ export default function Gallery() {
|
||||||
isFirstLoad={isFirstLoad}
|
isFirstLoad={isFirstLoad}
|
||||||
openFileUploader={openFileUploader}
|
openFileUploader={openFileUploader}
|
||||||
loadingBar={loadingBar}
|
loadingBar={loadingBar}
|
||||||
|
searchMode={searchMode}
|
||||||
/>
|
/>
|
||||||
{selected.count > 0 && (
|
{selected.count > 0 && (
|
||||||
<SelectedFileOptions
|
<SelectedFileOptions
|
||||||
|
|
|
@ -13,23 +13,6 @@ interface IQueryPrams {
|
||||||
* Service to manage all HTTP calls.
|
* Service to manage all HTTP calls.
|
||||||
*/
|
*/
|
||||||
class HTTPService {
|
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.
|
* header object to be append to all api calls.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -125,7 +125,7 @@ const getCollections = async (
|
||||||
return await Promise.all(promises);
|
return await Promise.all(promises);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('getCollections failed- ', 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(
|
await Promise.all(
|
||||||
files.map(async (file) => {
|
files.map(async (file) => {
|
||||||
file.collectionID = collection.id;
|
file.collectionID = collection.id;
|
||||||
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
|
const newEncryptedKey: B64EncryptionResult =
|
||||||
file.key,
|
await worker.encryptToB64(file.key, collection.key);
|
||||||
collection.key
|
|
||||||
);
|
|
||||||
file.encryptedKey = newEncryptedKey.encryptedData;
|
file.encryptedKey = newEncryptedKey.encryptedData;
|
||||||
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
file.keyDecryptionNonce = newEncryptedKey.nonce;
|
||||||
if (params['files'] == undefined) {
|
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_BAD_REQUEST_ERROR: 'sharing album not allowed',
|
||||||
SHARING_DISABLED_FOR_FREE_ACCOUNTS: 'sharing is disabled for free accounts',
|
SHARING_DISABLED_FOR_FREE_ACCOUNTS: 'sharing is disabled for free accounts',
|
||||||
CREATE_ALBUM_FAILED: 'failed to create album , please try again',
|
CREATE_ALBUM_FAILED: 'failed to create album , please try again',
|
||||||
|
SEARCH_HINT: () => <span>New York, April 14, Christmas...</span>,
|
||||||
TERMS_AND_CONDITIONS: () => (
|
TERMS_AND_CONDITIONS: () => (
|
||||||
<p>
|
<p>
|
||||||
I agree to the{' '}
|
I agree to the{' '}
|
||||||
|
@ -323,6 +324,13 @@ const englishConstants = {
|
||||||
with ente
|
with ente
|
||||||
</p>
|
</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;
|
export default englishConstants;
|
||||||
|
|
Loading…
Reference in a new issue