mirror of
https://github.com/publiclab/image-sequencer.git
synced 2025-12-11 19:00:00 +01:00
Expand out offline app features for smoother cache clearing and version tracking (#1384)
* Add version number to bottom * Get latest version number from GitHub * Create versionManagement.js Fetching the latest and local version number is now done through versionManagement.js * Add popup to prompt for refresh when a new version is available A new version is available whenever the sw.js file is changed. * Add version number fixed in top right corner * Fix Codeclimate issues * Update versionManagement.js * Update versionManagement.js * Make update prompt appear at front of page Changed z-index * Delete unecessary code * Create task to automatically update sw.js Used grunt-text-replace * Uninstall semver * Add replace task to serve and production tasks * Update demo.js Make version statements more descriptive. * Update versionManagement.js Remove unused versionCompare function * Change URL for getting latest version Changed the URL for getting the latest NPM version to be based on the package.json file's attribute for "homepage". * Update index.html * Update demo.css * Added explanatory comments * Update versionManagement.js * Update versionManagement.js * Updates for readability Changed single-line comments to multiline comments. * Update versionManagement.js * Update versionManagement.js Co-authored-by: Harsh Khandeparkar <34770591+HarshKhandeparkar@users.noreply.github.com> Co-authored-by: Jeffrey Warren <jeff@unterbahn.com>
This commit is contained in:
committed by
Jeffrey Warren
parent
ab793bcf4e
commit
00ed0f148d
18
Gruntfile.js
18
Gruntfile.js
@@ -2,6 +2,7 @@ module.exports = function(grunt) {
|
||||
grunt.loadNpmTasks('grunt-browserify');
|
||||
grunt.loadNpmTasks('grunt-contrib-uglify-es');
|
||||
grunt.loadNpmTasks('grunt-browser-sync');
|
||||
grunt.loadNpmTasks('grunt-text-replace');
|
||||
|
||||
require('matchdep')
|
||||
.filterDev('grunt-*')
|
||||
@@ -48,6 +49,17 @@ module.exports = function(grunt) {
|
||||
}
|
||||
},
|
||||
|
||||
replace: {
|
||||
version: {
|
||||
src: ['examples/sw.js'],
|
||||
overwrite: true,
|
||||
replacements: [{
|
||||
from: /image-sequencer-static-v.*/g,
|
||||
to: "image-sequencer-static-v<%= pkg.version %>';"
|
||||
}]
|
||||
}
|
||||
},
|
||||
|
||||
uglify: {
|
||||
core: {
|
||||
src: ['./dist/image-sequencer.js'],
|
||||
@@ -78,10 +90,10 @@ module.exports = function(grunt) {
|
||||
|
||||
/* Default (development): Watch files and build on change. */
|
||||
grunt.registerTask('default', ['watch']);
|
||||
grunt.registerTask('build', ['browserify:core', 'browserify:ui', 'uglify:core', 'uglify:ui']);
|
||||
grunt.registerTask('serve', ['browserify:core', 'browserify:ui', 'browserSync', 'watch']);
|
||||
grunt.registerTask('build', ['browserify:core', 'browserify:ui', 'replace:version', 'uglify:core', 'uglify:ui']);
|
||||
grunt.registerTask('serve', ['browserify:core', 'browserify:ui', 'replace:version', 'browserSync', 'watch']);
|
||||
grunt.registerTask('compile', ['browserify:core', 'browserify:ui']);
|
||||
grunt.registerTask('production', ['browserify:prodcore', 'browserify:produi', 'uglify:prodcore', 'uglify:produi']);
|
||||
grunt.registerTask('production', ['browserify:prodcore', 'browserify:produi', 'replace:version', 'uglify:prodcore', 'uglify:produi']);
|
||||
|
||||
grunt.registerTask('tests', ['browserify:tests']);
|
||||
};
|
||||
|
||||
@@ -315,8 +315,55 @@ a.name-header{
|
||||
color: #444;
|
||||
}
|
||||
|
||||
#version-number-text {
|
||||
text-align: center;
|
||||
padding-top: 100px;
|
||||
color: gray;
|
||||
}
|
||||
#version-number-top-right {
|
||||
position: fixed;
|
||||
right: 2%;
|
||||
top: 5%;
|
||||
color: lightgray;
|
||||
}
|
||||
/* Non float rightward alignment*/
|
||||
.right {
|
||||
margin-left: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#update-prompt-modal {
|
||||
visibility: hidden;
|
||||
min-width: 250px;
|
||||
margin-left: -125px;
|
||||
background-color: #333;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 10%;
|
||||
top: 30px;
|
||||
}
|
||||
#update-prompt-modal.show {
|
||||
visibility: visible;
|
||||
-webkit-animation: fadein 0.5s;
|
||||
animation: fadein 0.5s;
|
||||
}
|
||||
@-webkit-keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes fadein {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ var defaultHtmlSequencerUi = require('./lib/defaultHtmlSequencerUi.js'),
|
||||
intermediateHtmlStepUi = require('./lib/intermediateHtmlStepUi.js'),
|
||||
DefaultHtmlStepUi = require('./lib/defaultHtmlStepUi.js'),
|
||||
urlHash = require('./lib/urlHash.js'),
|
||||
insertPreview = require('./lib/insertPreview.js');
|
||||
insertPreview = require('./lib/insertPreview.js'),
|
||||
versionManagement = require('./lib/versionManagement.js');
|
||||
|
||||
|
||||
window.onload = function () {
|
||||
sequencer = ImageSequencer(); // Set the global sequencer variable
|
||||
@@ -28,6 +30,17 @@ window.onload = function () {
|
||||
}
|
||||
};
|
||||
|
||||
versionManagement.getLatestVersionNumber(function(versionNumber) {
|
||||
console.log("The latest NPM version number for Image Sequencer (from GitHub) is v" + versionNumber);
|
||||
});
|
||||
console.log("The local version number for Image Sequencer is v" + versionManagement.getLocalVersionNumber());
|
||||
|
||||
function displayVersionNumber() {
|
||||
$('#version-number-text').text("Image Sequencer v" + versionManagement.getLocalVersionNumber());
|
||||
$('#version-number-top-right').text("v" + versionManagement.getLocalVersionNumber());
|
||||
}
|
||||
displayVersionNumber();
|
||||
|
||||
function refreshOptions(options) {
|
||||
// Default options if parameter is empty.
|
||||
if (options == undefined) options = { sortField: 'text' };
|
||||
@@ -310,7 +323,7 @@ window.onload = function () {
|
||||
step.options.step.imgElement.src = reader.result;
|
||||
else
|
||||
step.imgElement.src = reader.result;
|
||||
|
||||
|
||||
insertPreview.updatePreviews(reader.result, document.querySelector('#addStep'));
|
||||
DefaultHtmlStepUi(sequencer).updateDimensions(step);
|
||||
},
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<!-- jspdf to enable save image as pdf -->
|
||||
<script src="../node_modules/jspdf/dist/jspdf.min.js" type="text/javascript" ></script>
|
||||
|
||||
<script src="lib/scrollToTop.js"></script>
|
||||
<!-- <script src="lib/scrollToTop.js"></script> -->
|
||||
<script src="../node_modules/selectize/dist/js/standalone/selectize.min.js"></script>
|
||||
</head>
|
||||
|
||||
@@ -56,6 +56,8 @@
|
||||
<link href="./selectize.default.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
|
||||
<div id="update-prompt-modal">A new version of image sequencer is available. Click <a href="#" id="reload">here</a> to update.</div>
|
||||
|
||||
<div class="container-fluid">
|
||||
|
||||
<header class="text-center" style="min-width: 450px">
|
||||
@@ -73,6 +75,7 @@
|
||||
</a>
|
||||
by <a href="https://publiclab.org" title="Publiclab Website"><i class="fa fa-globe"></i> Publiclab</a>
|
||||
</p>
|
||||
<span id="version-number-top-right"></span>
|
||||
</header>
|
||||
|
||||
<div id="dropzone" class="dropzone">
|
||||
@@ -228,6 +231,9 @@
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<p id="version-number-text">Unable to load version number</p>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<button id="move-up"><i class="fa fa-arrow-circle-o-up"></i></button>
|
||||
@@ -241,4 +247,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
var setupCache = function() {
|
||||
let newWorker; // When sw.js is changed, this is the new service worker generated.
|
||||
|
||||
// Toggle a CSS class to display a popup prompting the user to fetch a new version.
|
||||
function showUpdateModal() {
|
||||
$('#update-prompt-modal').addClass('show');
|
||||
}
|
||||
|
||||
/**
|
||||
* When a new service worker has been loaded, the button in the update prompt
|
||||
* modal should trigger the skipWaiting event to replace the current
|
||||
* service worker with the new one.
|
||||
*/
|
||||
$('#reload').on('click', function() {
|
||||
newWorker.postMessage({ action: 'skipWaiting' });
|
||||
});
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
// Register the service worker.
|
||||
navigator.serviceWorker.register('sw.js', { scope: '/examples/' })
|
||||
.then(function(registration) {
|
||||
registration.addEventListener('updatefound', () => {
|
||||
// When sw.js has been changed, get a reference to the new service worker.
|
||||
newWorker = registration.installing;
|
||||
newWorker.addEventListener('statechange', () => {
|
||||
// Check if service worker state has changed.
|
||||
switch(newWorker.state) {
|
||||
case 'installed':
|
||||
if(navigator.serviceWorker.controller) {
|
||||
// New service worker available; prompt the user to update.
|
||||
showUpdateModal();
|
||||
}
|
||||
// No updates available; do nothing.
|
||||
break;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const installingWorker = registration.installing;
|
||||
installingWorker.onstatechange = () => {
|
||||
console.log(installingWorker);
|
||||
@@ -14,6 +48,17 @@ var setupCache = function() {
|
||||
.catch(function(error) {
|
||||
console.log('Service worker registration failed, error:', error);
|
||||
});
|
||||
|
||||
/**
|
||||
* This is the event listener for when the service worker updates.
|
||||
* When the service worker updates, reload the page.
|
||||
*/
|
||||
let refreshing;
|
||||
navigator.serviceWorker.addEventListener('controllerchange', function() {
|
||||
if(refreshing) return;
|
||||
window.location.reload();
|
||||
refreshing = true;
|
||||
});
|
||||
}
|
||||
|
||||
if ('serviceWorker' in navigator) {
|
||||
@@ -34,6 +79,11 @@ var setupCache = function() {
|
||||
}
|
||||
location.reload();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
};
|
||||
|
||||
module.exports = setupCache;
|
||||
|
||||
43
examples/lib/versionManagement.js
Normal file
43
examples/lib/versionManagement.js
Normal file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Functions for getting version information.
|
||||
* Note: these functions are not used by the service worker to check for updates;
|
||||
* the service worker updates whenever sw.js has changed.
|
||||
* sw.js is changed when grunt replace:version is run. This task is run during
|
||||
* grunt build, serve, and productions tasks.
|
||||
*/
|
||||
|
||||
const package = require('../../package.json');
|
||||
|
||||
/**
|
||||
* Get the current version number from package.json on the homepage.
|
||||
* @param {function} callback The function that uses the version number.
|
||||
*/
|
||||
function getLatestVersionNumber(callback) {
|
||||
// Get the homepage reference from the local package.json.
|
||||
var homepage = package.homepage;
|
||||
var request = new XMLHttpRequest();
|
||||
request.onreadystatechange = function() {
|
||||
if (request.readyState == 4 && request.status == 200) {
|
||||
var response = JSON.parse(this.responseText);
|
||||
var latestVersionNumber = response.version;
|
||||
|
||||
// Do something with the version number using a callback function.
|
||||
if (callback)
|
||||
callback(latestVersionNumber);
|
||||
}
|
||||
}
|
||||
|
||||
// Get the package.json file from online using a GET request.
|
||||
request.open("GET", homepage + "/package.json", true);
|
||||
request.send();
|
||||
}
|
||||
|
||||
// Get the version number from the local package.json file.
|
||||
function getLocalVersionNumber() {
|
||||
return package.version;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getLatestVersionNumber,
|
||||
getLocalVersionNumber
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
const staticCacheName = 'image-sequencer-static-v3';
|
||||
|
||||
const staticCacheName = 'image-sequencer-static-v3.5.1';
|
||||
self.addEventListener('install', event => {
|
||||
console.log('Attempting to install service worker');
|
||||
});
|
||||
@@ -33,3 +32,10 @@ self.addEventListener('fetch', function(event) {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// When the update modal sends a 'skipWaiting' message, call the skipWaiting method.
|
||||
self.addEventListener('message', function(event) {
|
||||
if(event.data.action === 'skipWaiting') {
|
||||
self.skipWaiting();
|
||||
}
|
||||
});
|
||||
|
||||
90
package-lock.json
generated
90
package-lock.json
generated
@@ -152,6 +152,12 @@
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1714,6 +1720,13 @@
|
||||
"bin-version": "^3.0.0",
|
||||
"semver": "^5.6.0",
|
||||
"semver-truncate": "^1.1.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"bin-wrapper": {
|
||||
@@ -3348,6 +3361,13 @@
|
||||
"semver": "^5.5.0",
|
||||
"shebang-command": "^1.2.0",
|
||||
"which": "^1.2.9"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"crypto-browserify": {
|
||||
@@ -4149,6 +4169,12 @@
|
||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz",
|
||||
"integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=",
|
||||
"dev": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6995,6 +7021,12 @@
|
||||
"which": "~1.3.0"
|
||||
}
|
||||
},
|
||||
"grunt-text-replace": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/grunt-text-replace/-/grunt-text-replace-0.4.0.tgz",
|
||||
"integrity": "sha1-252c5Z4v5J2id+nbwZXD4Rz7FsI=",
|
||||
"dev": true
|
||||
},
|
||||
"gzip-size": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-1.0.0.tgz",
|
||||
@@ -7500,6 +7532,14 @@
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
@@ -8404,6 +8444,14 @@
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"pify": {
|
||||
@@ -8467,6 +8515,14 @@
|
||||
"requires": {
|
||||
"pify": "^4.0.1",
|
||||
"semver": "^5.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
@@ -11070,6 +11126,13 @@
|
||||
"integrity": "sha512-9HrZGFVTR5SOu3PZAnAY2hLO36aW1wmA+FDsVkr85BTST32TLCA1H/AEcatVRAsWLyXS3bqUDYCAjq5/QGuSTA==",
|
||||
"requires": {
|
||||
"semver": "^5.4.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-bitmap": {
|
||||
@@ -11125,6 +11188,14 @@
|
||||
"semver": "^5.5.0",
|
||||
"shellwords": "^0.1.1",
|
||||
"which": "^1.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node-png": {
|
||||
@@ -11154,6 +11225,13 @@
|
||||
"is-builtin-module": "^1.0.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
@@ -13220,11 +13298,6 @@
|
||||
"sifter": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.6.0",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.6.0.tgz",
|
||||
"integrity": "sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg=="
|
||||
},
|
||||
"semver-compare": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz",
|
||||
@@ -13242,6 +13315,13 @@
|
||||
"integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=",
|
||||
"requires": {
|
||||
"semver": "^5.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
"integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"send": {
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
"grunt-contrib-concat": "^1.0.1",
|
||||
"grunt-contrib-uglify-es": "^3.3.0",
|
||||
"grunt-contrib-watch": "^1.1.0",
|
||||
"grunt-text-replace": "^0.4.0",
|
||||
"husky": "^3.0.5",
|
||||
"image-filter-core": "~2.0.2",
|
||||
"image-filter-threshold": "~2.0.1",
|
||||
|
||||
Reference in New Issue
Block a user