Skip to content

Commit

Permalink
Add photo album download
Browse files Browse the repository at this point in the history
  • Loading branch information
celinaryholt committed Mar 23, 2022
1 parent 7bf849e commit 1d9f3b5
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 7 deletions.
1 change: 1 addition & 0 deletions app/actions/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export const GalleryPicture = {
EDIT: (generateStatuses('GalleryPicture.EDIT'): AAT),
DELETE: (generateStatuses('GalleryPicture.DELETE'): AAT),
UPLOAD: (generateStatuses('GalleryPicture.UPLOAD'): AAT),
CLEAR: 'GalleryPicture.CLEAR',
};

/**
Expand Down
10 changes: 10 additions & 0 deletions app/actions/GalleryPictureActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,13 @@ export function uploadAndCreateGalleryPicture(
return uploadGalleryPicturesInTurn(files, galleryId, dispatch);
};
}

export function clear(galleryId: number): Thunk<any> {
return (dispatch) =>
dispatch({
type: GalleryPicture.CLEAR,
meta: {
id: galleryId,
},
});
}
18 changes: 18 additions & 0 deletions app/reducers/galleryPictures.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,24 @@ function mutateGalleryPicture(state: any, action: any) {
},
};
}
case GalleryPicture.CLEAR: {
const newById = Object.fromEntries(
// Not using Object.entries() since flow will complain...
Object.keys(state.byId)
.map((key) => [key, state.byId[key]])
.filter(([_, v: GalleryPictureEntity]) => {
return v.gallery !== action.meta.id;
})
);
const newItems = Object.keys(newById).map((id) => parseInt(id));
return {
...state,
byId: newById,
items: newItems,
pagination: {},
paginationNext: {},
};
}
default:
return state;
}
Expand Down
8 changes: 7 additions & 1 deletion app/routes/photos/GalleryDetailRoute.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import loadingIndicator from 'app/utils/loadingIndicator';
import prepare from 'app/utils/prepare';
import {
fetch,
clear,
uploadAndCreateGalleryPicture,
} from 'app/actions/GalleryPictureActions';
import { push } from 'connected-react-router';
Expand Down Expand Up @@ -69,7 +70,12 @@ const propertyGenerator = (props, config) => {
];
};

const mapDispatchToProps = { push, fetch, uploadAndCreateGalleryPicture };
const mapDispatchToProps = {
push,
fetch,
clear,
uploadAndCreateGalleryPicture,
};

function metadataHelper<Props>() {
return (ActualComponent) => {
Expand Down
71 changes: 69 additions & 2 deletions app/routes/photos/components/GalleryDetail.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import type { DropFile } from 'app/components/Upload/ImageUpload';
import type { ID, ActionGrant } from 'app/models';
import type { GalleryPictureEntity } from 'app/reducers/galleryPictures';
import Button from 'app/components/Button';

import JsZip from 'jszip';
import FileSaver from 'file-saver';
import LoadingIndicator from 'app/components/LoadingIndicator';
type Props = {
gallery: Object,
loggedIn: boolean,
Expand All @@ -23,18 +25,21 @@ type Props = {
fetching: boolean,
children: Element<*>,
fetch: (galleryId: Number, args: { next: boolean }) => Promise<*>,
clear: (galleryId: Number) => Promise<*>,
push: (string) => Promise<*>,
uploadAndCreateGalleryPicture: (ID, File | Array<DropFile>) => Promise<*>,
actionGrant: ActionGrant,
};

type State = {
upload: boolean,
downloading: boolean,
};

export default class GalleryDetail extends Component<Props, State> {
state = {
upload: false,
downloading: false,
};

toggleUpload = (response?: File | Array<DropFile>) => {
Expand All @@ -49,6 +54,55 @@ export default class GalleryDetail extends Component<Props, State> {
this.props.push(`/photos/${this.props.gallery.id}/picture/${picture.id}`);
};

downloadGallery = () => {
this.setState({ downloading: true });
// Force re-fetch to avoid expired image urls
this.props.clear(this.props.gallery.id);
const finishDownload = () => this.setState({ downloading: false });
this.downloadNext(0, [])
.then((blobs) => {
const names = this.props.pictures.map((picture) =>
picture.file.split('/').pop()
);
this.zipFiles(this.props.gallery.title, names, blobs).finally(
finishDownload
);
})
.catch(finishDownload);
};

downloadNext = (index: number, blobsAccum: Blob[]) => {
return this.props
.fetch(this.props.gallery.id, { next: true, filters: {} })
.then(() => {
const urls = this.props.pictures
.slice(index)
.map((picture) => picture.rawFile);
return this.downloadFiles(urls).then((blobs) => {
blobsAccum.push(...blobs);
if (this.props.hasMore) {
return this.downloadNext(this.props.pictures.length, blobsAccum);
}
return blobsAccum;
});
});
};

downloadFiles = (urls: string[]) =>
Promise.all(
urls.map(async (url) => await fetch(url).then((res) => res.blob()))
);

zipFiles = (zipTitle: string, fileNames: string[], blobs: Blob[]) => {
const zip = JsZip();
blobs.forEach((blob, i) => {
zip.file(fileNames[i], blob);
});
return zip
.generateAsync({ type: 'blob' })
.then((zipFile) => FileSaver.saveAs(zipFile, `${zipTitle}.zip`));
};

render() {
const {
gallery,
Expand All @@ -68,7 +122,20 @@ export default class GalleryDetail extends Component<Props, State> {
<Helmet title={gallery.title} />
<NavigationTab
title={gallery.title}
details={<GalleryDetailsRow gallery={gallery} showDescription />}
details={
<>
<GalleryDetailsRow gallery={gallery} showDescription />
<div style={{ minHeight: '40px' }}>
{this.state.downloading ? (
<LoadingIndicator loading={true} small margin={0} />
) : (
<Button flat={true} onClick={this.downloadGallery}>
Last ned album
</Button>
)}
</div>
</>
}
>
<NavigationLink
onClick={(e: Event) => {
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,13 @@
"core-js": "^3.21.1",
"crypto-browserify": "^3.12.0",
"es6-promise-pool": "^2.5.0",
"file-saver": "^2.0.5",
"fuzzy": "^0.1.3",
"immer": "^9.0.12",
"isomorphic-fetch": "3.0.0",
"js-cookie": "^2.1.4",
"jsdom": "^19.0.0",
"jszip": "^3.7.1",
"jwt-decode": "3.1.2",
"lodash-es": "^4.17.21",
"mazemap": "file:mazemap",
Expand Down
70 changes: 66 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3229,6 +3229,11 @@ [email protected]:
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7"
integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=

core-util-is@~1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==

cosmiconfig@^7.0.0, cosmiconfig@^7.0.1:
version "7.0.1"
resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-7.0.1.tgz#714d756522cace867867ccb4474c5d01bbae5d6d"
Expand Down Expand Up @@ -4808,6 +4813,11 @@ file-entry-cache@^6.0.1:
dependencies:
flat-cache "^3.0.4"

file-saver@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.5.tgz#d61cfe2ce059f414d899e9dd6d4107ee25670c38"
integrity sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==

file-selector@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
Expand Down Expand Up @@ -5504,7 +5514,7 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"

inherits@2, [email protected], inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.4:
inherits@2, [email protected], inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3, inherits@~2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
Expand Down Expand Up @@ -5817,7 +5827,7 @@ [email protected]:
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
integrity sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=

isarray@^1.0.0:
isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=
Expand Down Expand Up @@ -6481,6 +6491,16 @@ jsprim@^2.0.2:
array-includes "^3.1.3"
object.assign "^4.1.2"

jszip@^3.7.1:
version "3.7.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.7.1.tgz#bd63401221c15625a1228c556ca8a68da6fda3d9"
integrity sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
set-immediate-shim "~1.0.1"

[email protected]:
version "3.1.2"
resolved "https://registry.yarnpkg.com/jwt-decode/-/jwt-decode-3.1.2.tgz#3fb319f3675a2df0c2895c8f5e9fa4b67b04ed59"
Expand Down Expand Up @@ -6556,6 +6576,13 @@ [email protected]:
dependencies:
immediate "~3.0.5"

lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"

lilconfig@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.4.tgz#f4507d043d7058b380b6a8f5cb7bcd4b34cee082"
Expand Down Expand Up @@ -7340,6 +7367,11 @@ p-try@^2.0.0:
resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==

pako@~1.0.2:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==

parent-module@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2"
Expand Down Expand Up @@ -8069,6 +8101,11 @@ pretty-format@^27.5.1:
ansi-styles "^5.0.0"
react-is "^17.0.1"

process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==

process@^0.11.10:
version "0.11.10"
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
Expand Down Expand Up @@ -8652,6 +8689,19 @@ readable-stream@^3.1.1, readable-stream@^3.5.0, readable-stream@^3.6.0:
string_decoder "^1.1.1"
util-deprecate "^1.0.1"

readable-stream@~2.3.6:
version "2.3.7"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.3"
isarray "~1.0.0"
process-nextick-args "~2.0.0"
safe-buffer "~5.1.1"
string_decoder "~1.1.1"
util-deprecate "~1.0.1"

recharts-scale@^0.4.4:
version "0.4.5"
resolved "https://registry.yarnpkg.com/recharts-scale/-/recharts-scale-0.4.5.tgz#0969271f14e732e642fcc5bd4ab270d6e87dd1d9"
Expand Down Expand Up @@ -8960,7 +9010,7 @@ rxjs@^7.5.1:
dependencies:
tslib "^2.1.0"

[email protected], safe-buffer@~5.1.1:
[email protected], safe-buffer@~5.1.0, safe-buffer@~5.1.1:
version "5.1.2"
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
Expand Down Expand Up @@ -9098,6 +9148,11 @@ [email protected], serve-static@^1.14.1:
parseurl "~1.3.3"
send "0.17.2"

set-immediate-shim@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"
integrity sha1-SysbJ+uAip+NzEgaWOXlb1mfP2E=

[email protected]:
version "1.2.0"
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
Expand Down Expand Up @@ -9451,6 +9506,13 @@ string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"

string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
dependencies:
safe-buffer "~5.1.0"

strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
Expand Down Expand Up @@ -10000,7 +10062,7 @@ use-latest@^1.0.0:
dependencies:
use-isomorphic-layout-effect "^1.0.0"

util-deprecate@^1.0.1, util-deprecate@^1.0.2:
util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
Expand Down

0 comments on commit 1d9f3b5

Please sign in to comment.