Merge pull request #16 from ente-io/download-button

Download button
This commit is contained in:
Vishnu Mohandas 2021-02-17 15:21:03 +05:30 committed by GitHub
commit 706a544abd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 193 additions and 149 deletions

BIN
public/download_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

View file

@ -17,7 +17,7 @@ export default function FullScreenDropZone({
showModal,
closeModal,
}: Props) {
const closeTimer = useRef<number>();
const closeTimer = useRef<NodeJS.Timeout>();
const clearTimer = () => {
if (closeTimer.current) {

View file

@ -9,17 +9,20 @@ import {
removeFromFavorites,
} from 'services/collectionService';
import { file } from 'services/fileService';
import constants from 'utils/strings/constants';
import DownloadManger from 'services/downloadManager';
interface Iprops {
isOpen: boolean;
items: any[];
options?: Object;
currentIndex?: number;
onClose?: () => void;
gettingData?: (instance: any, index: number, item: file) => void;
id?: string;
className?: string;
favItemIds: Set<number>;
setFavItemIds: (favItemIds: Set<number>) => void;
loadingBar: any;
}
function PhotoSwipe(props: Iprops) {
@ -56,7 +59,12 @@ function PhotoSwipe(props: Iprops) {
}
const openPhotoSwipe = () => {
const { items, options } = props;
const { items, currentIndex } = props;
const options = {
history: false,
maxSpreadZoom: 5,
index: currentIndex,
};
let photoSwipe = new Photoswipe(
pswpElement,
PhotoswipeUIDefault,
@ -122,6 +130,18 @@ function PhotoSwipe(props: Iprops) {
setFavItemIds(favItemIds);
}
};
const downloadFile = async (file) => {
const { loadingBar } = props;
const a = document.createElement('a');
a.style.display = 'none';
loadingBar.current.continuousStart();
a.href = await DownloadManger.getFile(file);
loadingBar.current.complete();
a.download = file.metadata['title'];
document.body.appendChild(a);
a.click();
a.remove();
};
const { id } = props;
let { className } = props;
className = classnames(['pswp', className]).trim();
@ -149,19 +169,22 @@ function PhotoSwipe(props: Iprops) {
<button
className="pswp__button pswp__button--close"
title="Share"
title={constants.CLOSE}
/>
<button
className="pswp__button pswp__button--share"
title="Share"
className="download-btn"
title={constants.DOWNLOAD}
onClick={() => downloadFile(photoSwipe.currItem)}
/>
<button
className="pswp__button pswp__button--fs"
title="Toggle fullscreen"
title={constants.TOGGLE_FULLSCREEN}
/>
<button
className="pswp__button pswp__button--zoom"
title="Zoom in/out"
title={constants.ZOOM_IN_OUT}
/>
<FavButton
size={44}
@ -183,11 +206,11 @@ function PhotoSwipe(props: Iprops) {
</div>
<button
className="pswp__button pswp__button--arrow--left"
title="Previous (arrow left)"
title={constants.PREVIOUS}
/>
<button
className="pswp__button pswp__button--arrow--right"
title="Next (arrow right)"
title={constants.NEXT}
/>
<div className="pswp__caption">
<div className="pswp__caption__center" />

View file

@ -105,6 +105,16 @@ const GlobalStyles = createGlobalStyle`
background-color:#202020 !important;
color:#aaa;
}
.download-btn{
margin-top:10px;
width: 25px;
height: 25px;
float: right;
background: url('/download_icon.png') no-repeat;
cursor: pointer;
background-size: cover;
border: none;
}
.btn-primary {
background: #2dc262;
border-color: #29a354;

View file

@ -126,6 +126,7 @@ export default function Credentials() {
errors.passphrase
)}
disabled={loading}
autoFocus={true}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}

View file

@ -4,7 +4,7 @@ import styled from 'styled-components';
interface CollectionProps {
collections: collection[];
selected?: string;
selected?: number;
selectCollection: (id?: number) => void;
}
@ -64,7 +64,7 @@ export default function Collections(props: CollectionProps) {
{collections?.map((item) => (
<Chip
key={item.id}
active={selected === item.id.toString()}
active={selected === item.id}
onClick={clickHandler(item.id)}
>
{item.name}

View file

@ -1,8 +1,9 @@
import React, { useEffect, useState } from 'react';
import { file, getPreview } from 'services/fileService';
import { file } from 'services/fileService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import styled from 'styled-components';
import PlayCircleOutline from 'components/PlayCircleOutline';
import DownloadManager from 'services/downloadManager';
interface IProps {
data: file;
@ -48,7 +49,7 @@ export default function PreviewCard(props: IProps) {
if (data && !data.msrc) {
const main = async () => {
const token = getData(LS_KEYS.USER).token;
const url = await getPreview(token, data);
const url = await DownloadManager.getPreview(data);
setImgSrc(url);
data.msrc = url;
updateUrl(url);

View file

@ -1,25 +1,17 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { useRouter } from 'next/router';
import Spinner from 'react-bootstrap/Spinner';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import {
file,
getFile,
getPreview,
syncData,
localFiles,
} from 'services/fileService';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { file, syncData, localFiles } from 'services/fileService';
import PreviewCard from './components/PreviewCard';
import { getActualKey, getToken } from 'utils/common/key';
import styled from 'styled-components';
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
import { Options } from 'photoswipe';
import AutoSizer from 'react-virtualized-auto-sizer';
import { VariableSizeList as List } from 'react-window';
import LoadingBar from 'react-top-loading-bar';
import Collections from './components/Collections';
import Upload from './components/Upload';
import DownloadManager from 'services/downloadManager';
import {
collection,
syncCollections,
@ -27,6 +19,7 @@ import {
getCollectionAndItsLatestFile,
getFavItemIds,
getLocalCollections,
getCollectionUpdationTime,
} from 'services/collectionService';
import constants from 'utils/strings/constants';
import ErrorAlert from './components/ErrorAlert';
@ -116,23 +109,21 @@ export default function Gallery(props) {
const [data, setData] = useState<file[]>();
const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [open, setOpen] = useState(false);
const [options, setOptions] = useState<Options>({
history: false,
maxSpreadZoom: 5,
});
const [currentIndex, setCurrentIndex] = useState<number>(0);
const fetching: { [k: number]: boolean } = {};
const [errorCode, setErrorCode] = useState<number>(null);
const [sinceTime, setSinceTime] = useState(0);
const [isFirstLoad, setIsFirstLoad] = useState(false);
const [progress, setProgress] = useState(0);
const loadingBar = useRef(null);
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) {
router.push('/');
return;
}
const main = async () => {
setIsFirstLoad(await getCollectionUpdationTime() == 0);
const data = await localFiles();
const collections = await getLocalCollections();
const collectionAndItsLatestFile = await getCollectionAndItsLatestFile(
@ -145,9 +136,10 @@ export default function Gallery(props) {
const favItemIds = await getFavItemIds(data);
setFavItemIds(favItemIds);
data.length == 0 ? setProgress(20) : setProgress(80);
loadingBar.current.continuousStart();
await syncWithRemote();
setProgress(100);
loadingBar.current.complete();
setIsFirstLoad(false);
};
main();
props.setUploadButtonView(true);
@ -228,10 +220,7 @@ export default function Gallery(props) {
};
const onThumbnailClick = (index: number) => () => {
setOptions({
...options,
index,
});
setCurrentIndex(index);
setOpen(true);
};
@ -247,9 +236,8 @@ export default function Gallery(props) {
};
const getSlideData = async (instance: any, index: number, item: file) => {
const token = getData(LS_KEYS.USER).token;
if (!item.msrc) {
const url = await getPreview(token, item);
const url = await DownloadManager.getPreview(item);
updateUrl(item.dataIndex)(url);
item.msrc = url;
if (!item.src) {
@ -266,7 +254,7 @@ export default function Gallery(props) {
}
if (!fetching[item.dataIndex]) {
fetching[item.dataIndex] = true;
const url = await getFile(token, item);
const url = await DownloadManager.getFile(item);
updateSrcUrl(item.dataIndex, url);
if (item.metadata.fileType === FILE_TYPE.VIDEO) {
item.html = `
@ -293,20 +281,6 @@ export default function Gallery(props) {
if (!data) {
return <div />;
}
if (data.length == 0 && progress != 0) {
return (
<div className="text-center">
<LoadingBar
color="#2dc262"
progress={progress}
onLoaderFinished={() => setProgress(0)}
/>
<Alert variant="primary">
{constants.INITIAL_LOAD_DELAY_WARNING}
</Alert>
</div>
);
}
const selectCollection = (id?: number) => {
const href = `/gallery?collection=${id || ''}`;
@ -343,15 +317,19 @@ export default function Gallery(props) {
return (
<>
<LoadingBar color="#2dc262" ref={loadingBar} />
{isFirstLoad && (
<div className="text-center">
<Alert variant="primary">
{constants.INITIAL_LOAD_DELAY_WARNING}
</Alert>
</div>
)}
<ErrorAlert errorCode={errorCode} />
<LoadingBar
color="#2dc262"
progress={progress}
onLoaderFinished={() => setProgress(0)}
/>
<Collections
collections={collections}
selected={router.query.collection?.toString()}
selected={Number(router.query.collection)}
selectCollection={selectCollection}
/>
<Upload
@ -435,7 +413,7 @@ export default function Gallery(props) {
<List
itemSize={(index) =>
timeStampList[index].itemType ===
ITEM_TYPE.TIME
ITEM_TYPE.TIME
? DATE_CONTAINER_HEIGHT
: IMAGE_CONTAINER_HEIGHT
}
@ -452,14 +430,14 @@ export default function Gallery(props) {
columns={
timeStampList[index]
.itemType ===
ITEM_TYPE.TIME
ITEM_TYPE.TIME
? 1
: columns
}
>
{timeStampList[index]
.itemType ===
ITEM_TYPE.TIME ? (
ITEM_TYPE.TIME ? (
<DateContainer>
{
timeStampList[
@ -478,7 +456,7 @@ export default function Gallery(props) {
index
]
.itemStartIndex +
idx
idx
);
}
)
@ -494,11 +472,12 @@ export default function Gallery(props) {
<PhotoSwipe
isOpen={open}
items={filteredData}
options={options}
currentIndex={currentIndex}
onClose={handleClose}
gettingData={getSlideData}
favItemIds={favItemIds}
setFavItemIds={setFavItemIds}
loadingBar={loadingBar}
/>
</Container>
) : (

View file

@ -1,11 +1,11 @@
import axios, { AxiosRequestConfig } from 'axios';
interface IHTTPHeaders {
[headerKey: string]: string;
[headerKey: string]: any;
}
interface IQueryPrams {
[paramName: string]: string;
[paramName: string]: any;
}
/**

View file

@ -91,7 +91,7 @@ const getCollectionSecrets = async (
const getCollections = async (
token: string,
sinceTime: string,
sinceTime: number,
key: string
): Promise<collection[]> => {
try {
@ -117,10 +117,13 @@ export const getLocalCollections = async (): Promise<collection[]> => {
return collections;
};
export const getCollectionUpdationTime = async (): Promise<number> => {
return await localForage.getItem<number>(COLLECTION_UPDATION_TIME) ?? 0;
}
export const syncCollections = async (token: string, key: string) => {
const localCollections = await getLocalCollections();
const lastCollectionUpdationTime =
(await localForage.getItem<string>(COLLECTION_UPDATION_TIME)) ?? '0';
const lastCollectionUpdationTime = await getCollectionUpdationTime();
const updatedCollections =
(await getCollections(token, lastCollectionUpdationTime, key)) || [];
if (updatedCollections.length == 0) {
@ -163,7 +166,6 @@ export const getCollectionAndItsLatestFile = (
files: file[]
): CollectionAndItsLatestFile[] => {
const latestFile = new Map<number, file>();
const collectionMap = new Map<number, collection>();
files.forEach((file) => {
if (!latestFile.has(file.collectionID)) {

View file

@ -0,0 +1,84 @@
import { getToken } from 'utils/common/key';
import { file } from './fileService';
import HTTPService from './HTTPService';
import { getEndpoint } from 'utils/common/apiUtil';
import * as Comlink from 'comlink';
const ENDPOINT = getEndpoint();
const CryptoWorker: any =
typeof window !== 'undefined' &&
Comlink.wrap(new Worker('worker/crypto.worker.js', { type: 'module' }));
class DownloadManager {
private fileDownloads = new Map<number, Promise<string>>();
private thumbnailDownloads = new Map<number, Promise<string>>();
constructor(private token) {}
public async getPreview(file: file) {
try {
const cache = await caches.open('thumbs');
const cacheResp: Response = await cache.match(file.id.toString());
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
if (!this.thumbnailDownloads.get(file.id)) {
const download = (async () => {
const resp = await HTTPService.get(
`${ENDPOINT}/files/preview/${file.id}`,
null,
{ 'X-Auth-Token': this.token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key
);
try {
await cache.put(
file.id.toString(),
new Response(new Blob([decrypted]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([decrypted]));
})();
this.thumbnailDownloads.set(file.id, download);
}
return await this.thumbnailDownloads.get(file.id);
} catch (e) {
console.log('get preview Failed' + e);
}
}
getFile = async (file: file) => {
if (!this.fileDownloads.get(file.id)) {
const download = (async () => {
try {
const resp = await HTTPService.get(
`${ENDPOINT}/files/download/${file.id}`,
null,
{ 'X-Auth-Token': this.token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key
);
return URL.createObjectURL(new Blob([decrypted]));
} catch (e) {
console.log('get file failed ' + e);
}
})();
this.fileDownloads.set(file.id, download);
}
return await this.fileDownloads.get(file.id);
};
}
export default new DownloadManager(getToken());

View file

@ -123,9 +123,9 @@ export const getFiles = async (
resp = await HTTPService.get(
`${ENDPOINT}/collections/diff`,
{
collectionID: collection.id.toString(),
sinceTime: time.toString(),
limit: limit.toString(),
collectionID: collection.id,
sinceTime: time,
limit: limit,
},
{
'X-Auth-Token': token,
@ -146,7 +146,7 @@ export const getFiles = async (
);
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime.toString();
time = resp.data.diff.slice(-1)[0].updationTime;
}
} while (resp.data.diff.length === limit);
return await Promise.all(promises);
@ -154,58 +154,6 @@ export const getFiles = async (
console.log('Get files failed' + e);
}
};
export const getPreview = async (token: string, file: file) => {
try {
const cache = await caches.open('thumbs');
const cacheResp: Response = await cache.match(file.id.toString());
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const resp = await HTTPService.get(
`${ENDPOINT}/files/preview/${file.id}`,
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key
);
try {
await cache.put(
file.id.toString(),
new Response(new Blob([decrypted]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([decrypted]));
} catch (e) {
console.log('get preview Failed' + e);
}
};
export const getFile = async (token: string, file: file) => {
try {
const resp = await HTTPService.get(
`${ENDPOINT}/files/download/${file.id}`,
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key
);
return URL.createObjectURL(new Blob([decrypted]));
} catch (e) {
console.log('get file failed ' + e);
}
};
const removeDeletedCollectionFiles = async (
collections: collection[],

View file

@ -157,7 +157,7 @@ class UploadService {
encryptedFile.fileKey
);
await this.uploadFile(uploadFile, token);
this.filesCompleted++;
this.changeProgressBarProps();
if (this.filesToBeUploaded.length > 0) {
@ -181,7 +181,6 @@ class UploadService {
total: this.totalFileCount,
});
setPercentComplete(this.filesCompleted * this.perFileProgress);
this.filesCompleted++;
}
private async readFile(recievedFile: File) {
@ -498,8 +497,8 @@ class UploadService {
{
count: Math.min(
50,
(this.filesToBeUploaded.length + 1) * 2
).toString(),
(this.totalFileCount - this.filesCompleted) * 2
),
},
{ 'X-Auth-Token': token }
);
@ -520,7 +519,7 @@ class UploadService {
file: Uint8Array | string
): Promise<string> {
try {
const fileSize = file.length.toString();
const fileSize = file.length;
await HTTPService.put(fileUploadURL.url, file, null, {
contentLengthHeader: fileSize,
});
@ -572,23 +571,15 @@ class UploadService {
let latDegree: number, latMinute: number, latSecond: number;
let lonDegree: number, lonMinute: number, lonSecond: number;
if (exifData.GPSLatitude[0].numerator) {
latDegree = exifData.GPSLatitude[0].numerator;
latMinute = exifData.GPSLatitude[1].numerator;
latSecond = exifData.GPSLatitude[2].numerator;
lonDegree = exifData.GPSLongitude[0].numerator;
lonMinute = exifData.GPSLongitude[1].numerator;
lonSecond = exifData.GPSLongitude[2].numerator;
} else {
latDegree = exifData.GPSLatitude[0];
latMinute = exifData.GPSLatitude[1];
latSecond = exifData.GPSLatitude[2];
latDegree = exifData.GPSLatitude[0];
latMinute = exifData.GPSLatitude[1];
latSecond = exifData.GPSLatitude[2];
lonDegree = exifData.GPSLongitude[0];
lonMinute = exifData.GPSLongitude[1];
lonSecond = exifData.GPSLongitude[2];
lonDegree = exifData.GPSLongitude[0];
lonMinute = exifData.GPSLongitude[1];
lonSecond = exifData.GPSLongitude[2];
}
var latDirection = exifData.GPSLatitudeRef;
var lonDirection = exifData.GPSLongitudeRef;

View file

@ -50,7 +50,7 @@ const englishConstants = {
SELECT_COLLECTION: `select an album to upload to`,
CREATE_COLLECTION: `create album`,
CLOSE: 'close',
NOTHING_HERE: `nothing to see here! 👀`,
NOTHING_HERE: `nothing to see here, yet 👀`,
UPLOAD: {
0: 'preparing to upload',
1: 'reading google metadata files',
@ -77,6 +77,11 @@ const englishConstants = {
NO_ACCOUNT: "don't have an account?",
ALBUM_NAME: 'album name',
CREATE: 'create',
DOWNLOAD: 'download',
TOGGLE_FULLSCREEN: 'Toggle fullscreen',
ZOOM_IN_OUT: 'Zoom in/out',
PREVIOUS: 'Previous (arrow left)',
NEXT: 'Next (arrow right)',
};
export default englishConstants;