mirror of
https://github.com/publiclab/image-sequencer.git
synced 2025-12-10 02:10:00 +01:00
Compare commits
14 Commits
v3.3.0
...
headless-o
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd7be2a5fa | ||
|
|
a3558ade24 | ||
|
|
dd7608283f | ||
|
|
50da69563d | ||
|
|
8b110c241f | ||
|
|
5566152e52 | ||
|
|
269215f48b | ||
|
|
1aa9a0e99c | ||
|
|
ff8c27eac2 | ||
|
|
9eed50153c | ||
|
|
66fc58fccb | ||
|
|
20f2b05076 | ||
|
|
2cdfb9dcd8 | ||
|
|
9609a0af72 |
@@ -2,7 +2,7 @@ Image Sequencer
|
||||
====
|
||||
|
||||
|
||||
[](https://travis-ci.org/publiclab/image-sequencer) [](https://codeclimate.com/github/publiclab/image-sequencer/maintainability) 
|
||||
[](https://travis-ci.org/publiclab/image-sequencer) [](https://codeclimate.com/github/publiclab/image-sequencer/maintainability) [](https://codecov.io/gh/publiclab/image-sequencer)
|
||||
|
||||
- **Latest Stable Demo**: https://sequencer.publiclab.org
|
||||
- **Latest Beta Demo**: https://beta.sequencer.publiclab.org
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<body>
|
||||
|
||||
<link href="../node_modules/bootstrap/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="demo.css">
|
||||
<link href="../node_modules/font-awesome/css/font-awesome.min.css" rel="stylesheet">
|
||||
<link href="../node_modules/selectize/dist/css/selectize.default.css" rel="stylesheet">
|
||||
@@ -77,7 +77,7 @@
|
||||
<center>
|
||||
<input type="file" id="fileInput" value="" accept="image/*"><br />
|
||||
<button type="button" id="take-photo" class="btn btn-primary btn-block btn-lg ">Take a Photo</button>
|
||||
<video id="video" width="400" height="300" style="display:none"></video>
|
||||
<video id="video" width="100%" height="100%" style="display:none"></video>
|
||||
<a href="#" id="capture" style="display:none" class="btn btn-primary btn-md">Click Picture</a>
|
||||
<a href="#" id="close" style="display:none" class="btn btn-default btn-md">Close</a>
|
||||
<canvas id="canvas" width="400" height="300" style="display:none"></canvas>
|
||||
@@ -233,4 +233,4 @@
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -42,11 +42,12 @@ function DefaultHtmlSequencerUi(_sequencer, options) {
|
||||
|
||||
function addStepUi() {
|
||||
if ($(addStepSel + " select").val() == "none") return;
|
||||
var newStepName;
|
||||
if(typeof arguments[0] !== "string")
|
||||
newStepName = $(addStepSel + " select option").html().toLowerCase();
|
||||
else newStepName = arguments[0]
|
||||
|
||||
var newStepName = $(addStepSel + " select").val();
|
||||
|
||||
if(!newStepName) newStepName = arguments[0]
|
||||
|
||||
|
||||
/*
|
||||
* after adding the step we run the sequencer from defined step
|
||||
* and since loadImage is not a part of the drawarray the step lies at current
|
||||
|
||||
12451
package-lock.json
generated
12451
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "image-sequencer",
|
||||
"version": "3.3.0",
|
||||
"version": "3.3.1",
|
||||
"description": "A modular JavaScript image manipulation library modeled on a storyboard.",
|
||||
"main": "src/ImageSequencer.js",
|
||||
"scripts": {
|
||||
@@ -40,7 +40,7 @@
|
||||
"imgareaselect": "git://github.com/jywarren/imgareaselect.git#v1.0.0-rc.2",
|
||||
"istanbul": "^0.4.5",
|
||||
"jquery": "^3.3.1",
|
||||
"jsdom": "^14.0.0",
|
||||
"jsdom": "^15.0.0",
|
||||
"jsqr": "^1.1.1",
|
||||
"lodash": "^4.17.11",
|
||||
"ndarray": "^1.0.18",
|
||||
|
||||
@@ -1,36 +1,28 @@
|
||||
module.exports = exports = function(pixels, options,priorstep){
|
||||
module.exports = exports = function(pixels, options){
|
||||
var defaults = require('./../../util/getDefaults.js')(require('./info.json'));
|
||||
|
||||
options.color = options.color || defaults.color;
|
||||
options.x = options.x || defaults.x;
|
||||
options.y = options.y || defaults.y;
|
||||
options.x = Number(options.x) || defaults.x;
|
||||
options.y = Number(options.y) || defaults.y;
|
||||
color = options.color || defaults.color;
|
||||
color = color.split(" ");
|
||||
|
||||
var img = $(priorstep.imgElement);
|
||||
if(Object.keys(img).length === 0){
|
||||
img = $(priorstep.options.step.imgElement);
|
||||
}
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.width = pixels.shape[0]; //img.width();
|
||||
canvas.height = pixels.shape[1]; //img.height();
|
||||
var ctx = canvas.getContext('2d');
|
||||
ctx.drawImage(img[0], 0, 0);
|
||||
var p=2;
|
||||
function drawBoard(){
|
||||
for (var x = 0; x <= canvas.width; x+=options.x) {
|
||||
ctx.moveTo(0.5 + x + p, p);
|
||||
ctx.lineTo(0.5 + x + p, canvas.height + p);
|
||||
for(var x = 0; x < pixels.shape[0]; x+=options.x){
|
||||
for(var y = 0 ; y < pixels.shape[1]; y++){
|
||||
pixels.set(x, y, 0, color[0]);
|
||||
pixels.set(x, y, 1, color[1]);
|
||||
pixels.set(x, y, 2, color[2]);
|
||||
pixels.set(x, y, 3, color[3]);
|
||||
}
|
||||
for (var y = 0; y <= canvas.height; y+=options.y) {
|
||||
ctx.moveTo(p, 0.5 + y + p);
|
||||
ctx.lineTo(canvas.width + p, 0.5 + y + p);
|
||||
}
|
||||
ctx.strokeStyle = options.color;
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
drawBoard();
|
||||
|
||||
for(var y = 0; y < pixels.shape[1]; y+=options.y){
|
||||
for(var x = 0 ; x < pixels.shape[0]; x++){
|
||||
pixels.set(x, y, 0, color[0]);
|
||||
pixels.set(x, y, 1, color[1]);
|
||||
pixels.set(x, y, 2, color[2]);
|
||||
pixels.set(x, y, 3, color[3]);
|
||||
}
|
||||
}
|
||||
|
||||
var myImageData = ctx.getImageData(0,0,canvas.width,canvas.height);
|
||||
pixels.data = myImageData.data
|
||||
return pixels;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,16 +9,9 @@ module.exports = function GridOverlay(options,UI) {
|
||||
progressObj.overrideFlag = true;
|
||||
|
||||
var step = this;
|
||||
if (!options.step.inBrowser) { // This module is only for browser
|
||||
this.output = input;
|
||||
callback();
|
||||
}
|
||||
else{
|
||||
var priorStep = this.getStep(-1); // get the previous step to add text onto it.
|
||||
|
||||
|
||||
function extraManipulation(pixels) {
|
||||
//if (options.step.inBrowser)
|
||||
pixels = require('./GridOverlay')(pixels, options,priorStep);
|
||||
pixels = require('./GridOverlay')(pixels, options);
|
||||
return pixels
|
||||
}
|
||||
|
||||
@@ -38,7 +31,7 @@ module.exports = function GridOverlay(options,UI) {
|
||||
callback: callback
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -12,20 +12,11 @@
|
||||
"desc": "Y-position (measured from top) from where grid starts",
|
||||
"default": 100
|
||||
},
|
||||
"color": {
|
||||
"type": "select",
|
||||
"desc": "Select the color for the grid.",
|
||||
"default": "black",
|
||||
"values": [
|
||||
"black",
|
||||
"blue",
|
||||
"green",
|
||||
"red",
|
||||
"white",
|
||||
"pink",
|
||||
"orange"
|
||||
]
|
||||
}
|
||||
"color":{
|
||||
"type": "string",
|
||||
"desc": "RGBA values separated by a space",
|
||||
"default": "0 0 0 255"
|
||||
}
|
||||
},
|
||||
"only": "browser"
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ module.exports = function runInBrowserContext(input, callback, step, options) {
|
||||
|
||||
var obj = { input: input, modOptions: minOptions }
|
||||
|
||||
puppeteer.launch().then(function(browser) {
|
||||
puppeteer.launch({headless: true, args:['--no-sandbox', '--disable-setuid-sandbox']}).then(function(browser) {
|
||||
browser.newPage().then(page => {
|
||||
/* Maybe there is a better way to this, loading the page coz localstorage API
|
||||
is not available otherwise */
|
||||
@@ -34,4 +34,4 @@ module.exports = function runInBrowserContext(input, callback, step, options) {
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
41
test/core/modules/gridOverlay.js
Normal file
41
test/core/modules/gridOverlay.js
Normal file
@@ -0,0 +1,41 @@
|
||||
var test = require('tape');
|
||||
var base64Img = require('base64-img');
|
||||
var looksSame = require('looks-same');
|
||||
|
||||
require('../../../src/ImageSequencer.js');
|
||||
|
||||
var sequencer = ImageSequencer({ ui: false });
|
||||
var options = {x : 1};
|
||||
var target = 'test_outputs';
|
||||
var red = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAABlBMVEX+AAD///+KQee0AAAAAWJLR0QB/wIt3gAAAAd0SU1FB+EGHRIVAvrm6EMAAAAMSURBVAjXY2AgDQAAADAAAceqhY4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMTctMDYtMjlUMTg6MjE6MDIrMDI6MDDGD83DAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDE3LTA2LTI5VDE4OjIxOjAyKzAyOjAwt1J1fwAAAABJRU5ErkJggg==";
|
||||
var benchmark = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAklEQVR4AewaftIAAAApSURBVKXBAQEAMAzDIF7/nncRgYcTTDTRRBNNNNFEE0000UQTTTTRRB9NQAEfe+dsMAAAAABJRU5ErkJggg==";
|
||||
|
||||
// Test 1 to check GridOverlay module is getting loaded
|
||||
test('Load Grid-Overlay module', function(t) {
|
||||
sequencer.loadImages(red);
|
||||
sequencer.addSteps('grid-overlay', options);
|
||||
t.equal(sequencer.steps[1].options.name, 'grid-overlay', 'Grid-Overlay module is getting loaded');
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Test 2 to check options are correct
|
||||
test('Check Options', function(t) {
|
||||
t.equal(sequencer.steps[1].options.x, 1, 'Options are correct');
|
||||
t.end();
|
||||
});
|
||||
|
||||
// Test 3 to check brightness module works as expected
|
||||
test('GridOverlay module works correctly', function(t) {
|
||||
sequencer.run({ mode: 'test' }, function(out) {
|
||||
var result = sequencer.steps[1].output.src
|
||||
base64Img.imgSync(result, target, 'result')
|
||||
base64Img.imgSync(benchmark, target, 'benchmark')
|
||||
result = './test_outputs/result.png'
|
||||
benchmark = './test_outputs/benchmark.png'
|
||||
looksSame(result, benchmark, function(err, res) {
|
||||
if (err) console.log(err)
|
||||
t.equal(res.equal, true)
|
||||
t.end()
|
||||
})
|
||||
})
|
||||
})
|
||||
63
yarn.lock
63
yarn.lock
@@ -2501,6 +2501,11 @@ gifshot@^0.4.5:
|
||||
resolved "https://registry.yarnpkg.com/gifshot/-/gifshot-0.4.5.tgz#e3cb570203a3b139ff3069d7578098a29c03b0f8"
|
||||
integrity sha1-48tXAgOjsTn/MGnXV4CYopwDsPg=
|
||||
|
||||
glfx@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.yarnpkg.com/glfx/-/glfx-0.0.4.tgz#1b776dc748c081994624b0af3b5638403de78f4d"
|
||||
integrity sha1-G3dtx0jAgZlGJLCvO1Y4QD3nj00=
|
||||
|
||||
glob-base@^0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
|
||||
@@ -3472,10 +3477,10 @@ jsbn@~0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
|
||||
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
|
||||
|
||||
jsdom@^14.0.0:
|
||||
version "14.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-14.0.0.tgz#c7f1441ebcc57902d08d5fb2f6ba2baf746da7c6"
|
||||
integrity sha512-/VkyPmdtbwqpJSkwDx3YyJ3U1oawYNB/h5z8vTUZGAzjtu2OHTeFRfnJqyMHsJ5Cyes23trOmvUpM1GfHH1leA==
|
||||
jsdom@^15.0.0:
|
||||
version "15.0.0"
|
||||
resolved "https://registry.yarnpkg.com/jsdom/-/jsdom-15.0.0.tgz#60cf177d79c7fa1a498453c9e2534f57e65d151f"
|
||||
integrity sha512-rJnHm7CHyIj4tDyz9VaCt0f0P0nEh/wEmMfwp9mMixy+L/r8OW/BNcgmIlfZuBBnVQS3eRBpvd/qM3R7vr7e3A==
|
||||
dependencies:
|
||||
abab "^2.0.0"
|
||||
acorn "^6.0.4"
|
||||
@@ -3487,16 +3492,16 @@ jsdom@^14.0.0:
|
||||
domexception "^1.0.1"
|
||||
escodegen "^1.11.0"
|
||||
html-encoding-sniffer "^1.0.2"
|
||||
nwsapi "^2.0.9"
|
||||
nwsapi "^2.1.3"
|
||||
parse5 "5.1.0"
|
||||
pn "^1.1.0"
|
||||
request "^2.88.0"
|
||||
request-promise-native "^1.0.5"
|
||||
saxes "^3.1.5"
|
||||
saxes "^3.1.9"
|
||||
symbol-tree "^3.2.2"
|
||||
tough-cookie "^2.5.0"
|
||||
w3c-hr-time "^1.0.1"
|
||||
w3c-xmlserializer "^1.0.1"
|
||||
w3c-xmlserializer "^1.1.2"
|
||||
webidl-conversions "^4.0.2"
|
||||
whatwg-encoding "^1.0.5"
|
||||
whatwg-mimetype "^2.3.0"
|
||||
@@ -4235,10 +4240,10 @@ number-is-nan@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d"
|
||||
integrity sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=
|
||||
|
||||
nwsapi@^2.0.9:
|
||||
version "2.0.9"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.0.9.tgz#77ac0cdfdcad52b6a1151a84e73254edc33ed016"
|
||||
integrity sha512-nlWFSCTYQcHk/6A9FFnfhKc14c3aFhfdNBXgo8Qgi9QTBu/qg3Ww+Uiz9wMzXd1T8GFxPc2QIHB6Qtf2XFryFQ==
|
||||
nwsapi@^2.1.3:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.1.3.tgz#25f3a5cec26c654f7376df6659cdf84b99df9558"
|
||||
integrity sha512-RowAaJGEgYXEZfQ7tvvdtAQUKPyTR6T6wNu0fwlNsGQYr/h3yQc6oI8WnVZh3Y/Sylwc+dtAlvPqfFZjhTyk3A==
|
||||
|
||||
oauth-sign@~0.9.0:
|
||||
version "0.9.0"
|
||||
@@ -4823,9 +4828,9 @@ punycode@^2.1.0, punycode@^2.1.1:
|
||||
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
|
||||
|
||||
puppeteer@^1.14.0:
|
||||
version "1.14.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.14.0.tgz#828c1926b307200d5fc8289b99df4e13e962d339"
|
||||
integrity sha512-SayS2wUX/8LF8Yo2Rkpc5nkAu4Jg3qu+OLTDSOZtisVQMB2Z5vjlY2TdPi/5CgZKiZroYIiyUN3sRX63El9iaw==
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.15.0.tgz#1680fac13e51f609143149a5b7fa99eec392b34f"
|
||||
integrity sha512-D2y5kwA9SsYkNUmcBzu9WZ4V1SGHiQTmgvDZSx6sRYFsgV25IebL4V6FaHjF6MbwLK9C6f3G3pmck9qmwM8H3w==
|
||||
dependencies:
|
||||
debug "^4.1.0"
|
||||
extract-zip "^1.6.6"
|
||||
@@ -5278,10 +5283,10 @@ sax@^1.2.4:
|
||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||
|
||||
saxes@^3.1.5:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.6.tgz#2d948a47b54918516c5a64096f08865deb5bd8cd"
|
||||
integrity sha512-LAYs+lChg1v5uKNzPtsgTxSS5hLo8aIhSMCJt1WMpefAxm3D1RTpMwSpb6ebdL31cubiLTnhokVktBW+cv9Y9w==
|
||||
saxes@^3.1.9:
|
||||
version "3.1.9"
|
||||
resolved "https://registry.yarnpkg.com/saxes/-/saxes-3.1.9.tgz#c1c197cd54956d88c09f960254b999e192d7058b"
|
||||
integrity sha512-FZeKhJglhJHk7eWG5YM0z46VHmI3KJpMBAQm3xa9meDvd+wevB5GuBB0wc0exPInZiBBHqi00DbS8AcvCGCFMw==
|
||||
dependencies:
|
||||
xmlchars "^1.3.1"
|
||||
|
||||
@@ -6480,10 +6485,10 @@ w3c-hr-time@^1.0.1:
|
||||
dependencies:
|
||||
browser-process-hrtime "^0.1.2"
|
||||
|
||||
w3c-xmlserializer@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.0.1.tgz#054cdcd359dc5d1f3ec9be4e272c756af4b21d39"
|
||||
integrity sha512-XZGI1OH/OLQr/NaJhhPmzhngwcAnZDLytsvXnRmlYeRkmbb0I7sqFFA22erq4WQR0sUu17ZSQOAV9mFwCqKRNg==
|
||||
w3c-xmlserializer@^1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz#30485ca7d70a6fd052420a3d12fd90e6339ce794"
|
||||
integrity sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==
|
||||
dependencies:
|
||||
domexception "^1.0.1"
|
||||
webidl-conversions "^4.0.2"
|
||||
@@ -6509,6 +6514,11 @@ wcwidth@^1.0.1:
|
||||
dependencies:
|
||||
defaults "^1.0.3"
|
||||
|
||||
webgl-distort@0.0.2:
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webgl-distort/-/webgl-distort-0.0.2.tgz#80d2f61efc4aece28e7a1ff1375e14aac6d06f36"
|
||||
integrity sha1-gNL2HvxK7OKOeh/xN14UqsbQbzY=
|
||||
|
||||
webidl-conversions@^4.0.2:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"
|
||||
@@ -6615,20 +6625,13 @@ wrappy@1:
|
||||
resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
|
||||
integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
|
||||
|
||||
ws@^6.1.0:
|
||||
ws@^6.1.0, ws@^6.1.2:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
|
||||
integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
ws@^6.1.2:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-6.1.2.tgz#3cc7462e98792f0ac679424148903ded3b9c3ad8"
|
||||
integrity sha512-rfUqzvz0WxmSXtJpPMX2EeASXabOrSMk1ruMOV3JBTBjo4ac2lDjGGsbQSyxj8Odhw5fBib8ZKEjDNvgouNKYw==
|
||||
dependencies:
|
||||
async-limiter "~1.0.0"
|
||||
|
||||
ws@~3.3.1:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/ws/-/ws-3.3.3.tgz#f1cf84fe2d5e901ebce94efaece785f187a228f2"
|
||||
|
||||
Reference in New Issue
Block a user