mirror of
https://github.com/piskelapp/piskel.git
synced 2023-08-10 21:12:52 +03:00
Merged changes
This commit is contained in:
@ -11,6 +11,8 @@ var Constants = {
|
|||||||
MAX_HEIGHT : 1024,
|
MAX_HEIGHT : 1024,
|
||||||
MAX_WIDTH : 1024,
|
MAX_WIDTH : 1024,
|
||||||
|
|
||||||
|
MAX_CURRENT_COLORS_DISPLAYED : 100,
|
||||||
|
|
||||||
MINIMUM_ZOOM : 1,
|
MINIMUM_ZOOM : 1,
|
||||||
|
|
||||||
PREVIEW_FILM_SIZE : 96,
|
PREVIEW_FILM_SIZE : 96,
|
||||||
|
@ -10,7 +10,6 @@
|
|||||||
// I apologize to my future self for this one.
|
// I apologize to my future self for this one.
|
||||||
var NO_SCROLL_MAX_COLORS = 20;
|
var NO_SCROLL_MAX_COLORS = 20;
|
||||||
|
|
||||||
var MAX_COLORS = 100;
|
|
||||||
|
|
||||||
ns.PalettesListController = function (paletteController, usedColorService) {
|
ns.PalettesListController = function (paletteController, usedColorService) {
|
||||||
this.usedColorService = usedColorService;
|
this.usedColorService = usedColorService;
|
||||||
@ -80,8 +79,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (colors.length > MAX_COLORS) {
|
if (colors.length > Constants.MAX_CURRENT_COLORS_DISPLAYED) {
|
||||||
colors = colors.slice(0, MAX_COLORS);
|
colors = colors.slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return colors;
|
return colors;
|
||||||
|
@ -15,6 +15,10 @@
|
|||||||
this.refreshZoom_();
|
this.refreshZoom_();
|
||||||
|
|
||||||
this.redrawFlag = true;
|
this.redrawFlag = true;
|
||||||
|
|
||||||
|
this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor();
|
||||||
|
this.cachedFrameProcessor.setFrameProcessor(this.frameToPreviewCanvas_.bind(this));
|
||||||
|
this.cachedFrameProcessor.setOutputCloner(this.clonePreviewCanvas_.bind(this));
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.PreviewFilmController.prototype.init = function() {
|
ns.PreviewFilmController.prototype.init = function() {
|
||||||
@ -39,7 +43,6 @@
|
|||||||
|
|
||||||
ns.PreviewFilmController.prototype.render = function () {
|
ns.PreviewFilmController.prototype.render = function () {
|
||||||
if (this.redrawFlag) {
|
if (this.redrawFlag) {
|
||||||
// TODO(vincz): Full redraw on any drawing modification, optimize.
|
|
||||||
this.createPreviews_();
|
this.createPreviews_();
|
||||||
this.redrawFlag = false;
|
this.redrawFlag = false;
|
||||||
}
|
}
|
||||||
@ -175,11 +178,8 @@
|
|||||||
cloneFrameButton.className = "tile-overlay duplicate-frame-action";
|
cloneFrameButton.className = "tile-overlay duplicate-frame-action";
|
||||||
previewTileRoot.appendChild(cloneFrameButton);
|
previewTileRoot.appendChild(cloneFrameButton);
|
||||||
|
|
||||||
var canvasRenderer = new pskl.rendering.CanvasRenderer(currentFrame, this.zoom);
|
|
||||||
canvasRenderer.drawTransparentAs(Constants.TRANSPARENT_COLOR);
|
canvasContainer.appendChild(this.getCanvasForFrame(currentFrame));
|
||||||
var canvas = canvasRenderer.render();
|
|
||||||
canvas.classList.add('tile-view', 'canvas');
|
|
||||||
canvasContainer.appendChild(canvas);
|
|
||||||
previewTileRoot.appendChild(canvasContainer);
|
previewTileRoot.appendChild(canvasContainer);
|
||||||
|
|
||||||
if(tileNumber > 0 || this.piskelController.getFrameCount() > 1) {
|
if(tileNumber > 0 || this.piskelController.getFrameCount() > 1) {
|
||||||
@ -206,6 +206,25 @@
|
|||||||
return previewTileRoot;
|
return previewTileRoot;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ns.PreviewFilmController.prototype.getCanvasForFrame = function (frame) {
|
||||||
|
var canvas = this.cachedFrameProcessor.get(frame, this.zoom);
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.PreviewFilmController.prototype.frameToPreviewCanvas_ = function (frame) {
|
||||||
|
var canvasRenderer = new pskl.rendering.CanvasRenderer(frame, this.zoom);
|
||||||
|
canvasRenderer.drawTransparentAs(Constants.TRANSPARENT_COLOR);
|
||||||
|
var canvas = canvasRenderer.render();
|
||||||
|
canvas.classList.add('tile-view', 'canvas');
|
||||||
|
return canvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.PreviewFilmController.prototype.clonePreviewCanvas_ = function (canvas) {
|
||||||
|
var clone = pskl.CanvasUtils.clone(canvas);
|
||||||
|
clone.classList.add('tile-view', 'canvas');
|
||||||
|
return clone;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate the preview zoom depending on the piskel size
|
* Calculate the preview zoom depending on the piskel size
|
||||||
*/
|
*/
|
||||||
|
@ -77,10 +77,16 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ns.PiskelController.prototype.getFrameAt = function (index) {
|
ns.PiskelController.prototype.getFrameAt = function (index) {
|
||||||
|
var hash = [];
|
||||||
var frames = this.getLayers().map(function (l) {
|
var frames = this.getLayers().map(function (l) {
|
||||||
return l.getFrameAt(index);
|
var frame = l.getFrameAt(index);
|
||||||
|
hash.push(frame.getHash());
|
||||||
|
return frame;
|
||||||
});
|
});
|
||||||
return pskl.utils.FrameUtils.merge(frames);
|
var mergedFrame = pskl.utils.FrameUtils.merge(frames);
|
||||||
|
mergedFrame.id = hash.join('-');
|
||||||
|
mergedFrame.version = 0;
|
||||||
|
return mergedFrame;
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.PiskelController.prototype.hasFrameAt = function (index) {
|
ns.PiskelController.prototype.hasFrameAt = function (index) {
|
||||||
|
@ -62,12 +62,26 @@
|
|||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
evt.stopPropagation();
|
evt.stopPropagation();
|
||||||
|
|
||||||
this.beforeSaving_();
|
var name = this.getName();
|
||||||
pskl.app.storageService.store({
|
|
||||||
success : this.onSaveSuccess_.bind(this),
|
if (!name) {
|
||||||
error : this.onSaveError_.bind(this),
|
name = window.prompt('Please specify a name', 'New piskel');
|
||||||
after : this.afterSaving_.bind(this)
|
}
|
||||||
});
|
|
||||||
|
if (name) {
|
||||||
|
var description = this.getDescription();
|
||||||
|
var isPublic = !!this.isPublicCheckbox.prop('checked');
|
||||||
|
|
||||||
|
var descriptor = new pskl.model.piskel.Descriptor(name, description, isPublic);
|
||||||
|
this.piskelController.getPiskel().setDescriptor(descriptor);
|
||||||
|
|
||||||
|
this.beforeSaving_();
|
||||||
|
pskl.app.storageService.store({
|
||||||
|
success : this.onSaveSuccess_.bind(this),
|
||||||
|
error : this.onSaveError_.bind(this),
|
||||||
|
after : this.afterSaving_.bind(this)
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.SaveController.prototype.onSaveLocalClick_ = function (evt) {
|
ns.SaveController.prototype.onSaveLocalClick_ = function (evt) {
|
||||||
|
@ -27,7 +27,9 @@
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
ns.Lighten.prototype.applyToolAt = function(col, row, color, frame, overlay, event, mouseButton) {
|
ns.Lighten.prototype.applyToolAt = function(col, row, color, frame, overlay, event, mouseButton) {
|
||||||
var pixelColor = frame.getPixel(col, row);
|
var overlayColor = overlay.getPixel(col, row);
|
||||||
|
var frameColor = frame.getPixel(col, row);
|
||||||
|
var pixelColor = overlayColor === Constants.TRANSPARENT_COLOR ? frameColor : overlayColor;
|
||||||
|
|
||||||
var isDarken = event.ctrlKey || event.cmdKey;
|
var isDarken = event.ctrlKey || event.cmdKey;
|
||||||
var isSinglePass = event.shiftKey;
|
var isSinglePass = event.shiftKey;
|
||||||
@ -55,7 +57,11 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
ns.Lighten.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) {
|
ns.Lighten.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) {
|
||||||
|
// apply on real frame
|
||||||
|
this.setPixelsToFrame_(frame, this.pixels);
|
||||||
|
|
||||||
this.resetUsedPixels_();
|
this.resetUsedPixels_();
|
||||||
|
|
||||||
$.publish(Events.PISKEL_SAVE_STATE, {
|
$.publish(Events.PISKEL_SAVE_STATE, {
|
||||||
type : pskl.service.HistoryService.SNAPSHOT
|
type : pskl.service.HistoryService.SNAPSHOT
|
||||||
});
|
});
|
||||||
|
@ -22,12 +22,17 @@
|
|||||||
* @override
|
* @override
|
||||||
*/
|
*/
|
||||||
ns.SimplePen.prototype.applyToolAt = function(col, row, color, frame, overlay, event) {
|
ns.SimplePen.prototype.applyToolAt = function(col, row, color, frame, overlay, event) {
|
||||||
frame.setPixel(col, row, color);
|
overlay.setPixel(col, row, color);
|
||||||
|
|
||||||
|
if (color === Constants.TRANSPARENT_COLOR) {
|
||||||
|
frame.setPixel(col, row, color);
|
||||||
|
}
|
||||||
this.previousCol = col;
|
this.previousCol = col;
|
||||||
this.previousRow = row;
|
this.previousRow = row;
|
||||||
this.pixels.push({
|
this.pixels.push({
|
||||||
col : col,
|
col : col,
|
||||||
row : row
|
row : row,
|
||||||
|
color : color
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -55,17 +60,26 @@
|
|||||||
|
|
||||||
|
|
||||||
ns.SimplePen.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) {
|
ns.SimplePen.prototype.releaseToolAt = function(col, row, color, frame, overlay, event) {
|
||||||
|
// apply on real frame
|
||||||
|
this.setPixelsToFrame_(frame, this.pixels);
|
||||||
|
|
||||||
|
// save state
|
||||||
this.raiseSaveStateEvent({
|
this.raiseSaveStateEvent({
|
||||||
pixels : this.pixels.slice(0),
|
pixels : this.pixels.slice(0),
|
||||||
color : color
|
color : color
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// reset
|
||||||
this.pixels = [];
|
this.pixels = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.SimplePen.prototype.replay = function (frame, replayData) {
|
ns.SimplePen.prototype.replay = function (frame, replayData) {
|
||||||
var pixels = replayData.pixels;
|
this.setPixelsToFrame_(frame, replayData.pixels, replayData.color);
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.SimplePen.prototype.setPixelsToFrame_ = function (frame, pixels, color) {
|
||||||
pixels.forEach(function (pixel) {
|
pixels.forEach(function (pixel) {
|
||||||
frame.setPixel(pixel.col, pixel.row, replayData.color);
|
frame.setPixel(pixel.col, pixel.row, pixel.color);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
79
src/js/model/frame/CachedFrameProcessor.js
Normal file
79
src/js/model/frame/CachedFrameProcessor.js
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
(function () {
|
||||||
|
var ns = $.namespace('pskl.model.frame');
|
||||||
|
|
||||||
|
|
||||||
|
var DEFAULT_CLEAR_INTERVAL = 10 * 60 *1000;
|
||||||
|
|
||||||
|
var DEFAULT_FRAME_PROCESSOR = function (frame) {
|
||||||
|
return pskl.utils.FrameUtils.toImage(frame);
|
||||||
|
};
|
||||||
|
|
||||||
|
var DEFAULT_OUTPUT_CLONER = function (o) {return o;};
|
||||||
|
|
||||||
|
var DEFAULT_NAMESPACE = '__cache_default__';
|
||||||
|
|
||||||
|
ns.CachedFrameProcessor = function (cacheResetInterval) {
|
||||||
|
this.cache_ = {};
|
||||||
|
this.cacheResetInterval = cacheResetInterval || DEFAULT_CLEAR_INTERVAL;
|
||||||
|
this.frameProcessor = DEFAULT_FRAME_PROCESSOR;
|
||||||
|
this.outputCloner = DEFAULT_OUTPUT_CLONER;
|
||||||
|
|
||||||
|
window.setInterval(this.clear.bind(this), this.cacheResetInterval);
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.CachedFrameProcessor.prototype.clear = function () {
|
||||||
|
this.cache_ = {};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the processor function that will be called when there is a cache miss
|
||||||
|
* Function with 1 argument : pskl.model.Frame
|
||||||
|
* @param {Function} frameProcessor
|
||||||
|
*/
|
||||||
|
ns.CachedFrameProcessor.prototype.setFrameProcessor = function (frameProcessor) {
|
||||||
|
this.frameProcessor = frameProcessor;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the cloner that will be called when there is a miss on the 1st level cache
|
||||||
|
* but a hit on the 2nd level cache.
|
||||||
|
* Function with 2 arguments : cached value, frame
|
||||||
|
* @param {Function} outputCloner
|
||||||
|
*/
|
||||||
|
ns.CachedFrameProcessor.prototype.setOutputCloner = function (outputCloner) {
|
||||||
|
this.outputCloner = outputCloner;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the processed frame from the cache, in the (optional) namespace
|
||||||
|
* If the first level cache is empty, attempt to clone it from 2nd level cache. If second level cache is empty process the frame.
|
||||||
|
* @param {pskl.model.Frame} frame
|
||||||
|
* @param {String} namespace
|
||||||
|
* @return {Object} the processed frame
|
||||||
|
*/
|
||||||
|
ns.CachedFrameProcessor.prototype.get = function (frame, namespace) {
|
||||||
|
var processedFrame = null;
|
||||||
|
namespace = namespace || DEFAULT_NAMESPACE;
|
||||||
|
|
||||||
|
if (!this.cache_[namespace]) {
|
||||||
|
this.cache_[namespace] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
var cache = this.cache_[namespace];
|
||||||
|
|
||||||
|
var cacheKey = frame.getHash();
|
||||||
|
if (this.cache_[cacheKey]) {
|
||||||
|
processedFrame = this.cache_[cacheKey];
|
||||||
|
} else {
|
||||||
|
var frameAsString = JSON.stringify(frame.getPixels());
|
||||||
|
if (this.cache_[frameAsString]) {
|
||||||
|
processedFrame = this.outputCloner(this.cache_[frameAsString], frame);
|
||||||
|
} else {
|
||||||
|
processedFrame = this.frameProcessor(frame);
|
||||||
|
this.cache_[frameAsString] = processedFrame;
|
||||||
|
}
|
||||||
|
this.cache_[cacheKey] = processedFrame;
|
||||||
|
}
|
||||||
|
return processedFrame;
|
||||||
|
};
|
||||||
|
})();
|
@ -8,11 +8,19 @@
|
|||||||
this.displayContainer = document.createElement('div');
|
this.displayContainer = document.createElement('div');
|
||||||
this.displayContainer.classList.add('tiled-frame-container');
|
this.displayContainer.classList.add('tiled-frame-container');
|
||||||
container.get(0).appendChild(this.displayContainer);
|
container.get(0).appendChild(this.displayContainer);
|
||||||
|
|
||||||
|
this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor();
|
||||||
|
this.cachedFrameProcessor.setFrameProcessor(this.frameToDataUrl_.bind(this));
|
||||||
|
};
|
||||||
|
|
||||||
|
ns.TiledFrameRenderer.prototype.frameToDataUrl_ = function (frame) {
|
||||||
|
var canvas = new pskl.utils.FrameUtils.toImage(frame, this.zoom);
|
||||||
|
return canvas.toDataURL('image/png');
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.TiledFrameRenderer.prototype.render = function (frame) {
|
ns.TiledFrameRenderer.prototype.render = function (frame) {
|
||||||
var canvas = new pskl.utils.FrameUtils.toImage(frame, this.zoom);
|
var imageSrc = this.cachedFrameProcessor.get(frame, this.zoom);
|
||||||
this.displayContainer.style.backgroundImage = 'url(' + canvas.toDataURL('image/png') + ')';
|
this.displayContainer.style.backgroundImage = 'url(' + imageSrc + ')';
|
||||||
};
|
};
|
||||||
|
|
||||||
ns.TiledFrameRenderer.prototype.show = function () {
|
ns.TiledFrameRenderer.prototype.show = function () {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
(function () {
|
(function () {
|
||||||
var ns = $.namespace('pskl.service');
|
var ns = $.namespace('pskl.service');
|
||||||
var BACKUP_INTERVAL = 1000 * 30;
|
var BACKUP_INTERVAL = 1000 * 60;
|
||||||
|
|
||||||
ns.BackupService = function (piskelController) {
|
ns.BackupService = function (piskelController) {
|
||||||
this.piskelController = piskelController;
|
this.piskelController = piskelController;
|
||||||
|
@ -4,6 +4,9 @@
|
|||||||
ns.CurrentColorsService = function (piskelController) {
|
ns.CurrentColorsService = function (piskelController) {
|
||||||
this.piskelController = piskelController;
|
this.piskelController = piskelController;
|
||||||
this.currentColors = [];
|
this.currentColors = [];
|
||||||
|
this.cachedFrameProcessor = new pskl.model.frame.CachedFrameProcessor();
|
||||||
|
this.cachedFrameProcessor.setFrameProcessor(this.frameToColors_.bind(this));
|
||||||
|
|
||||||
this.framesColorsCache_ = {};
|
this.framesColorsCache_ = {};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -16,33 +19,35 @@
|
|||||||
return this.currentColors;
|
return this.currentColors;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ns.CurrentColorsService.prototype.frameToColors_ = function (frame) {
|
||||||
|
var frameColors = {};
|
||||||
|
frame.forEachPixel(function (color, x, y) {
|
||||||
|
frameColors[color] = (frameColors[color] || 0) + 1;
|
||||||
|
});
|
||||||
|
return frameColors;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
ns.CurrentColorsService.prototype.onPiskelUpdated_ = function (evt) {
|
ns.CurrentColorsService.prototype.onPiskelUpdated_ = function (evt) {
|
||||||
var layers = this.piskelController.getLayers();
|
var layers = this.piskelController.getLayers();
|
||||||
var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);});
|
var frames = layers.map(function (l) {return l.getFrames();}).reduce(function (p, n) {return p.concat(n);});
|
||||||
var colors = {};
|
var colors = {};
|
||||||
frames.forEach(function (f) {
|
frames.forEach(function (f) {
|
||||||
var frameHash = f.getHash();
|
var frameColors = this.cachedFrameProcessor.get(f);
|
||||||
if (!this.framesColorsCache_[frameHash]) {
|
Object.keys(frameColors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED).forEach(function (color) {
|
||||||
var frameColors = {};
|
colors[color] = (colors[color] || 0) + frameColors[color];
|
||||||
f.forEachPixel(function (color, x, y) {
|
|
||||||
frameColors[color] = true;
|
|
||||||
});
|
|
||||||
this.framesColorsCache_[frameHash] = frameColors;
|
|
||||||
}
|
|
||||||
Object.keys(this.framesColorsCache_[frameHash]).forEach(function (color) {
|
|
||||||
colors[color] = true;
|
|
||||||
});
|
});
|
||||||
}.bind(this));
|
}.bind(this));
|
||||||
|
|
||||||
|
// Remove transparent color from used colors
|
||||||
delete colors[Constants.TRANSPARENT_COLOR];
|
delete colors[Constants.TRANSPARENT_COLOR];
|
||||||
this.currentColors = Object.keys(colors);
|
|
||||||
|
// limit the array to the max colors to display
|
||||||
|
this.currentColors = Object.keys(colors).slice(0, Constants.MAX_CURRENT_COLORS_DISPLAYED);
|
||||||
|
|
||||||
|
// sort by most frequent color
|
||||||
this.currentColors = this.currentColors.sort(function (c1, c2) {
|
this.currentColors = this.currentColors.sort(function (c1, c2) {
|
||||||
if (c1 < c2) {
|
return colors[c2] - colors[c1];
|
||||||
return -1;
|
|
||||||
} else if (c1 > c2) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO : only fire if there was a change
|
// TODO : only fire if there was a change
|
||||||
|
@ -48,6 +48,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
clone : function (canvas) {
|
||||||
|
var clone = pskl.CanvasUtils.createCanvas(canvas.width, canvas.height);
|
||||||
|
|
||||||
|
//apply the old canvas to the new one
|
||||||
|
clone.getContext('2d').drawImage(canvas, 0, 0);
|
||||||
|
|
||||||
|
//return the new canvas
|
||||||
|
return clone;
|
||||||
|
},
|
||||||
|
|
||||||
getImageDataFromCanvas : function (canvas) {
|
getImageDataFromCanvas : function (canvas) {
|
||||||
var sourceContext = canvas.getContext('2d');
|
var sourceContext = canvas.getContext('2d');
|
||||||
return sourceContext.getImageData(0, 0, canvas.width, canvas.height).data;
|
return sourceContext.getImageData(0, 0, canvas.width, canvas.height).data;
|
||||||
|
@ -48,6 +48,7 @@
|
|||||||
"js/model/Frame.js",
|
"js/model/Frame.js",
|
||||||
"js/model/Layer.js",
|
"js/model/Layer.js",
|
||||||
"js/model/piskel/Descriptor.js",
|
"js/model/piskel/Descriptor.js",
|
||||||
|
"js/model/frame/CachedFrameProcessor.js",
|
||||||
"js/model/Piskel.js",
|
"js/model/Piskel.js",
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
|
Reference in New Issue
Block a user