Merge pull request #745 from ae-utbm/picture-zip

Add image download progress bar and fix output name of pictures
This commit is contained in:
thomas girod 2024-07-31 10:19:50 +02:00 committed by GitHub
commit a321bd79ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 160 additions and 115 deletions

View File

@ -28,3 +28,5 @@ $twitblue: hsl(206, 82%, 63%);
$shadow-color: rgb(223, 223, 223); $shadow-color: rgb(223, 223, 223);
$background-button-color: hsl(0, 0%, 95%); $background-button-color: hsl(0, 0%, 95%);
$deepblue: #354a5f;

View File

@ -1,7 +1,8 @@
@import "colors";
$hovered-text-color: #c2c2c2; $hovered-text-color: #c2c2c2;
$text-color: white; $text-color: white;
$background-color: #354a5f;
$background-color-hovered: #283747; $background-color-hovered: #283747;
$red-text-color: #eb2f06; $red-text-color: #eb2f06;
@ -9,7 +10,7 @@ $hovered-red-text-color: #ff4d4d;
.header { .header {
box-sizing: border-box; box-sizing: border-box;
background-color: $background-color; background-color: $deepblue;
box-shadow: 3px 3px 3px 0 #dfdfdf; box-shadow: 3px 3px 3px 0 #dfdfdf;
border-radius: 0; border-radius: 0;
width: 100%; width: 100%;
@ -98,7 +99,7 @@ $hovered-red-text-color: #ff4d4d;
border-radius: 0; border-radius: 0;
margin: 0; margin: 0;
box-sizing: border-box; box-sizing: border-box;
background-color: $background-color; background-color: $deepblue;
width: 45px; width: 45px;
height: 25px; height: 25px;
padding: 0; padding: 0;
@ -213,7 +214,7 @@ $hovered-red-text-color: #ff4d4d;
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
background-color: $background-color; background-color: $deepblue;
} }
>.options { >.options {

View File

@ -1 +0,0 @@
!function(e){"object"==typeof exports?module.exports=e():"function"==typeof define&&define.amd?define(e):"undefined"!=typeof window?window.JSZipUtils=e():"undefined"!=typeof global?global.JSZipUtils=e():"undefined"!=typeof self&&(self.JSZipUtils=e())}(function(){return function o(i,f,u){function s(n,e){if(!f[n]){if(!i[n]){var t="function"==typeof require&&require;if(!e&&t)return t(n,!0);if(a)return a(n,!0);throw new Error("Cannot find module '"+n+"'")}var r=f[n]={exports:{}};i[n][0].call(r.exports,function(e){var t=i[n][1][e];return s(t||e)},r,r.exports,o,i,f,u)}return f[n].exports}for(var a="function"==typeof require&&require,e=0;e<u.length;e++)s(u[e]);return s}({1:[function(e,t,n){"use strict";var u={};function r(){try{return new window.XMLHttpRequest}catch(e){}}u._getBinaryFromXHR=function(e){return e.response||e.responseText};var s="undefined"!=typeof window&&window.ActiveXObject?function(){return r()||function(){try{return new window.ActiveXObject("Microsoft.XMLHTTP")}catch(e){}}()}:r;u.getBinaryContent=function(t,n){var e,r,o,i;"function"==typeof(n=n||{})?(i=n,n={}):"function"==typeof n.callback&&(i=n.callback),i||"undefined"==typeof Promise?(r=function(e){i(null,e)},o=function(e){i(e,null)}):e=new Promise(function(e,t){r=e,o=t});try{var f=s();f.open("GET",t,!0),"responseType"in f&&(f.responseType="arraybuffer"),f.overrideMimeType&&f.overrideMimeType("text/plain; charset=x-user-defined"),f.onreadystatechange=function(e){if(4===f.readyState)if(200===f.status||0===f.status)try{r(u._getBinaryFromXHR(f))}catch(e){o(new Error(e))}else o(new Error("Ajax error for "+t+" : "+this.status+" "+this.statusText))},n.progress&&(f.onprogress=function(e){n.progress({path:t,originalEvent:e,percent:e.loaded/e.total*100,loaded:e.loaded,total:e.total})}),f.send()}catch(e){o(new Error(e),null)}return e},t.exports=u},{}]},{},[1])(1)});

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2023, Gildas Lormeau
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,28 @@
# Built scripts of zip.js
**Warning**: These files are not compatible with ES modules, i.e. they cannot be imported with `import`. Instead, import `index.js` in the root folder of the project or one of the files prefixed with `zip-` in the [`/lib`](../lib) folder (e.g. `/lib/zip-no-worker-inflate.js`).
- for production (minified):
| | [`ZipReader`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipReader.html) API | [`ZipWriter`](https://gildas-lormeau.github.io/zip.js/api/classes/ZipWriter.html) API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers | Usage |
|--------------------------------|-----------------|-----------------|--------------|-------------|----------------|-------------------------------------------------------|
| `zip.min.js` | x | x | | x | | compression/decompression with web workers |
| `zip-no-worker.min.js` | x | x | | | x | compression/decompression without web workers |
| `zip-no-worker-inflate.min.js` | x | | | | x | decompression without web workers |
| `zip-no-worker-deflate.min.js` | | x | | | x | compression without web workers |
| `zip-full.min.js` | x | x | | x | x | compression/decompression with or without web workers |
| `zip-fs.min.js` | x | x | x | x | | compression/decompression with web workers |
| `zip-fs-full.min.js` | x | x | x | x | x | compression/decompression with or without web workers |
- for development/debugging:
| | `zip` API | [`zip.fs`](https://gildas-lormeau.github.io/zip.js/api/classes/FS.html) API | Web Workers | No Web Workers |
|-----------------------|-----------|--------------|-------------|----------------|
| `zip.js` | x | | x | |
| `zip-full.js` | x | | x | x |
| `zip-fs.js` | x | x | x | |
| `zip-fs-full.js` | x | x | x | x |
- `z-worker.js` can be used as a web worker script if the [Content Security Policy](https://developer.mozilla.org/docs/Web/HTTP/CSP) blocks scripts loaded with a `blob:` scheme
- `z-worker-fflate.js` is the web worker script for using [fflate](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-fflate)
- `z-worker-pako.js` is the web worker script for using [pako](https://gildas-lormeau.github.io/zip.js/core-api.html#alternative-codec-pako)

File diff suppressed because one or more lines are too long

View File

@ -105,7 +105,7 @@ a:not(.button) {
.collapse-header { .collapse-header {
color: white; color: white;
background-color: #354a5f; background-color: $deepblue;
padding: 5px 10px; padding: 5px 10px;
display: flex; display: flex;
align-items: center; align-items: center;
@ -206,34 +206,36 @@ a:not(.button) {
width: 90%; width: 90%;
margin: 20px auto 0; margin: 20px auto 0;
/*---------------------------------NAV---------------------------------*/ /*---------------------------------NAV---------------------------------*/
.btn { .btn {
font-size: 15px; font-size: 15px;
font-weight: normal; font-weight: normal;
color: white; color: white;
min-width: 60px; padding: 9px 13px;
padding: 5px 10px;
border: none; border: none;
text-decoration: none; text-decoration: none;
&.btn-blue { &.btn-blue {
background-color: #354a5f; background-color: $deepblue;
} &:not(:disabled):hover {
background-color: darken($deepblue, 10%);
&.btn-blue:disabled { }
background-color: rgba(70, 90, 126, 0.4); &:disabled {
} background-color: rgba(70, 90, 126, 0.4);
}
&.btn-blue.clickable:not(:disabled):hover {
background-color: #2c3646;
} }
&.btn-grey { &.btn-grey {
background-color: grey; background-color: grey;
&:not(:disabled):hover {
background-color: darken(gray, 15%);
}
&:disabled {
background-color: lighten(gray, 15%);
}
} }
&.btn-grey.clickable:not(:disabled):hover { i {
background-color: hsl(210, 5%, 30%); margin-right: 4px;
} }
} }
@ -977,7 +979,7 @@ thead td {
} }
thead { thead {
background-color: #354a5f; background-color: $deepblue;
color: white; color: white;
} }

View File

@ -5,12 +5,12 @@
{%- endblock -%} {%- endblock -%}
{% block additional_js %} {% block additional_js %}
<script defer src="{{ static('core/js/jszip/jszip.min.js') }}"></script>
<script defer src="{{ static('core/js/jszip/jszip-utils.min.js') }}"></script>
<script defer type="module"> <script defer type="module">
import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}"; import { showSaveFilePicker } from "{{ static('core/js/native-file-system-adapter/mod.js') }}";
window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */ window.showSaveFilePicker = showSaveFilePicker; /* Export function to normal javascript */
</script> </script>
<script defer type="text/javascript" src="{{ static('core/js/zipjs/zip-fs-full.min.js') }}"></script>
<script defer src="{{ static("core/js/alpinejs.min.js") }}"></script>
{% endblock %} {% endblock %}
{% block title %} {% block title %}
@ -19,11 +19,22 @@
{% block content %} {% block content %}
<main> <main>
{% if can_edit(profile, user) %} {% if user.id == object.id and albums|length > 0 %}
<button disabled id="download" onclick="download('{{ url('api:pictures') }}?users_identified={{ object.id }}')">{% trans %}Download all my pictures{% endtrans %}</button> <div x-data="picture_download" x-cloak>
<button
:disabled="in_progress"
class="btn btn-blue"
@click="download('{{ url("api:pictures") }}?users_identified={{ object.id }}')"
>
<i class="fa fa-download"></i>
{% trans %}Download all my pictures{% endtrans %}
</button>
<progress x-ref="progress" x-show="in_progress"></progress>
</div>
{% endif %} {% endif %}
{% for album, pictures in albums|items %} {% for album, pictures in albums|items %}
<h4>{{ album }}</h4> <h4>{{ album }}</h4>
<br />
<div class="photos"> <div class="photos">
{% for picture in pictures %} {% for picture in pictures %}
{% if picture.can_be_viewed_by(user) %} {% if picture.can_be_viewed_by(user) %}
@ -51,52 +62,61 @@
</div> </div>
<br> <br>
{% endfor %} {% endfor %}
<script>
document.addEventListener("DOMContentLoaded", () => {
/* Enable button once everything is loaded and if JSZip is supported */
document.getElementById("download").disabled = !JSZip.support.blob;
});
async function download(url) {
let zip = new JSZip();
let size = 0;
let pictures = await (await fetch(url)).json();
pictures.forEach(async (picture) => {
size += picture.size;
zip.file(
"IMG_" + picture.date + picture.name.slice(picture.name.lastIndexOf(".")),
new Promise(function (resolve, reject) {
JSZipUtils.getBinaryContent(picture.full_size_url, (err, data) => {
if (err) {
reject(err);
return;
}
resolve(data);
})
}),
{ binary: true }
);
});
let fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
let writeStream = await fileHandle.createWritable();
await zip.generateInternalStream({
type: "uint8array",
streamFiles: true,
compression: "DEFLATE",
compressionOptions: { level: 9 }
})
.on("data", (data) => writeStream.write(data))
.on("error", (err) => console.error(err))
.on("end", () => writeStream.close())
.resume();
}
</script>
</main> </main>
{% endblock %} {% endblock content %}
{% block script %}
{{ super() }}
{% if user.id == object.id %}
<script>
/**
* @typedef Picture
* @property {number} id
* @property {string} name
* @property {number} size
* @property {string} date
* @property {Object} author
* @property {string} full_size_url
* @property {string} compressed_url
* @property {string} thumb_url
*/
document.addEventListener("alpine:init", () => {
Alpine.data("picture_download", () => ({
in_progress: false,
async download(url) {
this.in_progress = true;
const bar = this.$refs.progress;
bar.value = 0;
/** @type Picture[] */
const pictures = await (await fetch(url)).json();
bar.max = pictures.length;
const fileHandle = await window.showSaveFilePicker({
_preferPolyfill: false,
suggestedName: "{%- trans -%} pictures {%- endtrans -%}.zip",
types: {},
excludeAcceptAllOption: false,
})
const zipWriter = new zip.ZipWriter(await fileHandle.createWritable());
await Promise.all(pictures.map(p => {
const img_name = "IMG_" + p.date.replaceAll(/[:\-]/g, "_") + p.name.slice(p.name.lastIndexOf("."));
return zipWriter.add(
img_name,
new zip.HttpReader(p.full_size_url),
{level: 9, lastModDate: new Date(p.date), onstart: () => bar.value += 1}
);
}));
await zipWriter.close();
this.in_progress = false;
}
}))
});
</script>
{% endif %}
{% endblock script %}

View File

@ -136,7 +136,7 @@
right: 5px; right: 5px;
padding: 5px; padding: 5px;
border-radius: 50%; border-radius: 50%;
box-shadow: 0px 0px 12px 2px rgb(0 0 0 / 14%); box-shadow: 0 0 12px 2px rgb(0 0 0 / 14%);
background-color: white; background-color: white;
width: 20px; width: 20px;
height: 20px; height: 20px;
@ -186,29 +186,7 @@
} }
#eboutic .catalog-buttons button { #eboutic .catalog-buttons button {
font-size: 15px!important;
font-weight: normal;
color: white;
min-width: 60px; min-width: 60px;
padding: 10px 15px;
}
#eboutic .catalog-buttons .validate {
background-color: #354a5f;
}
#eboutic .catalog-buttons .clear {
background-color: gray;
}
#eboutic .catalog-buttons button i {
margin-right: 4px;
}
#eboutic .catalog-buttons button.validate:hover {
background-color: #2c3646;
}
#eboutic .catalog-buttons button.clear:hover {
background-color:hsl(210,5%,30%);
} }
#eboutic .catalog-buttons form { #eboutic .catalog-buttons form {
@ -252,7 +230,7 @@
} }
#eboutic .product-image { #eboutic .product-image {
margin-bottom: 0px; margin-bottom: 0;
max-width: 70px; max-width: 70px;
} }
} }

View File

@ -62,13 +62,13 @@
</li> </li>
</ul> </ul>
<div class="catalog-buttons"> <div class="catalog-buttons">
<button @click="clear_basket()" class="clear"> <button @click="clear_basket()" class="btn btn-grey">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{% trans %}Clear{% endtrans %} {% trans %}Clear{% endtrans %}
</button> </button>
<form method="get" action="{{ url('eboutic:command') }}"> <form method="get" action="{{ url('eboutic:command') }}">
{% csrf_token %} {% csrf_token %}
<button class="validate"> <button class="btn btn-blue">
<i class="fa fa-check"></i> <i class="fa fa-check"></i>
<input type="submit" value="{% trans %}Validate{% endtrans %}"/> <input type="submit" value="{% trans %}Validate{% endtrans %}"/>
</button> </button>

View File

@ -724,8 +724,7 @@ if SENTRY_DSN:
) )
SITH_FRONT_DEP_VERSIONS = { SITH_FRONT_DEP_VERSIONS = {
"https://github.com/Stuk/jszip-utils": "0.1.0", "https://github.com/gildas-lormeau/zip.js": "2.7.47",
"https://github.com/Stuk/jszip": "3.10.1",
"https://github.com/jimmywarting/native-file-system-adapter": "3.0.1", "https://github.com/jimmywarting/native-file-system-adapter": "3.0.1",
"https://github.com/chartjs/Chart.js/": "2.6.0", "https://github.com/chartjs/Chart.js/": "2.6.0",
"https://github.com/Ionaru/easy-markdown-editor/": "2.18.0", "https://github.com/Ionaru/easy-markdown-editor/": "2.18.0",