mirror of
https://github.com/piskelapp/piskel.git
synced 2023-08-10 21:12:52 +03:00
feature : add keyboard shortcuts
+ decentralized shortcut declaration + each service/controller is now responsible for declaring its shorcuts - documentation (cheatsheet) is still to be maintained manually - init order matters (shortcutService has to be instanciated before everyone else) => should have a standalone KeyboardService singleton which is ready as soon as it is loaded
This commit is contained in:
parent
9d0f41362b
commit
6eabf01ffc
@ -39,12 +39,5 @@ var Events = {
|
||||
HIDE_NOTIFICATION: "HIDE_NOTIFICATION",
|
||||
|
||||
// Events triggered by keyboard
|
||||
UNDO: "UNDO",
|
||||
REDO: "REDO",
|
||||
CUT: "CUT",
|
||||
COPY: "COPY",
|
||||
PASTE: "PASTE",
|
||||
SELECT_TOOL : "SELECT_TOOL",
|
||||
TOGGLE_HELP : "TOGGLE_HELP",
|
||||
SWAP_COLORS : "SWAP_COLORS"
|
||||
SELECT_TOOL : "SELECT_TOOL"
|
||||
};
|
12
js/app.js
12
js/app.js
@ -10,6 +10,9 @@
|
||||
ns.app = {
|
||||
|
||||
init : function () {
|
||||
this.shortcutService = new pskl.service.keyboard.ShortcutService();
|
||||
this.shortcutService.init();
|
||||
|
||||
var size = this.readSizeFromURL_();
|
||||
var piskel = new pskl.model.Piskel(size.width, size.height);
|
||||
|
||||
@ -20,6 +23,7 @@
|
||||
piskel.addLayer(layer);
|
||||
|
||||
this.piskelController = new pskl.controller.PiskelController(piskel);
|
||||
this.piskelController.init();
|
||||
|
||||
this.paletteController = new pskl.controller.PaletteController();
|
||||
this.paletteController.init();
|
||||
@ -39,15 +43,15 @@
|
||||
this.settingsController = new pskl.controller.settings.SettingsController(this.piskelController);
|
||||
this.settingsController.init();
|
||||
|
||||
this.toolController = new pskl.controller.ToolController();
|
||||
this.toolController.init();
|
||||
|
||||
this.selectionManager = new pskl.selection.SelectionManager(this.piskelController);
|
||||
this.selectionManager.init();
|
||||
|
||||
this.historyService = new pskl.service.HistoryService(this.piskelController);
|
||||
this.historyService.init();
|
||||
|
||||
this.keyboardEventService = new pskl.service.KeyboardEventService();
|
||||
this.keyboardEventService.init();
|
||||
|
||||
this.notificationController = new pskl.controller.NotificationController();
|
||||
this.notificationController.init();
|
||||
|
||||
@ -57,8 +61,6 @@
|
||||
this.imageUploadService = new pskl.service.ImageUploadService();
|
||||
this.imageUploadService.init();
|
||||
|
||||
this.toolController = new pskl.controller.ToolController();
|
||||
this.toolController.init();
|
||||
|
||||
this.cheatsheetService = new pskl.service.keyboard.CheatsheetService();
|
||||
this.cheatsheetService.init();
|
||||
|
@ -15,7 +15,9 @@
|
||||
|
||||
$.subscribe(Events.SELECT_PRIMARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:true}));
|
||||
$.subscribe(Events.SELECT_SECONDARY_COLOR, this.onColorSelected_.bind(this, {isPrimary:false}));
|
||||
$.subscribe(Events.SWAP_COLORS, this.onSwapColorsEvent_.bind(this));
|
||||
|
||||
pskl.app.shortcutService.addShortcut('X', this.swapColors.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('D', this.resetColors.bind(this));
|
||||
|
||||
// Initialize colorpickers:
|
||||
var colorPicker = $('#color-picker');
|
||||
@ -72,12 +74,17 @@
|
||||
return this.secondaryColor;
|
||||
};
|
||||
|
||||
ns.PaletteController.prototype.onSwapColorsEvent_ = function () {
|
||||
ns.PaletteController.prototype.swapColors = function () {
|
||||
var primaryColor = this.getPrimaryColor();
|
||||
this.setPrimaryColor(this.getSecondaryColor());
|
||||
this.setSecondaryColor(primaryColor);
|
||||
};
|
||||
|
||||
ns.PaletteController.prototype.resetColors = function () {
|
||||
this.setPrimaryColor(Constants.DEFAULT_PEN_COLOR);
|
||||
this.setSecondaryColor(Constants.TRANSPARENT_COLOR);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
|
@ -15,6 +15,11 @@
|
||||
this.currentFrameIndex = 0;
|
||||
|
||||
this.layerIdCounter = 1;
|
||||
};
|
||||
|
||||
ns.PiskelController.prototype.init = function () {
|
||||
pskl.app.shortcutService.addShortcut('up', this.selectPreviousFrame.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('down', this.selectNextFrame.bind(this));
|
||||
|
||||
$.publish(Events.PISKEL_RESET);
|
||||
$.publish(Events.FRAME_SIZE_CHANGED);
|
||||
@ -116,6 +121,20 @@
|
||||
$.publish(Events.PISKEL_RESET);
|
||||
};
|
||||
|
||||
ns.PiskelController.prototype.selectNextFrame = function () {
|
||||
var nextIndex = this.currentFrameIndex + 1;
|
||||
if (nextIndex < this.getFrameCount()) {
|
||||
this.setCurrentFrameIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
ns.PiskelController.prototype.selectPreviousFrame = function () {
|
||||
var nextIndex = this.currentFrameIndex - 1;
|
||||
if (nextIndex >= 0) {
|
||||
this.setCurrentFrameIndex(nextIndex);
|
||||
}
|
||||
};
|
||||
|
||||
ns.PiskelController.prototype.setCurrentLayerIndex = function (index) {
|
||||
this.currentLayerIndex = index;
|
||||
$.publish(Events.PISKEL_RESET);
|
||||
|
@ -29,15 +29,14 @@
|
||||
* @public
|
||||
*/
|
||||
ns.ToolController.prototype.init = function() {
|
||||
this.createToolMarkup_();
|
||||
this.createToolsDom_();
|
||||
this.addKeyboardShortcuts_();
|
||||
|
||||
// Initialize tool:
|
||||
// Set SimplePen as default selected tool:
|
||||
this.selectTool_(this.tools[0]);
|
||||
// Activate listener on tool panel:
|
||||
$("#tool-section").click($.proxy(this.onToolIconClicked_, this));
|
||||
|
||||
$.subscribe(Events.SELECT_TOOL, $.proxy(this.onKeyboardShortcut_, this));
|
||||
};
|
||||
|
||||
/**
|
||||
@ -85,7 +84,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
ns.ToolController.prototype.onKeyboardShortcut_ = function(evt, charkey) {
|
||||
ns.ToolController.prototype.onKeyboardShortcut_ = function(charkey) {
|
||||
for (var i = 0 ; i < this.tools.length ; i++) {
|
||||
var tool = this.tools[i];
|
||||
if (tool.shortcut.toLowerCase() === charkey.toLowerCase()) {
|
||||
@ -107,20 +106,32 @@
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.ToolController.prototype.createToolMarkup_ = function() {
|
||||
var currentTool, toolMarkup = '', extraClass;
|
||||
|
||||
ns.ToolController.prototype.createToolsDom_ = function() {
|
||||
var toolMarkup = '';
|
||||
for(var i = 0 ; i < this.tools.length ; i++) {
|
||||
var tool = this.tools[i];
|
||||
var instance = tool.instance;
|
||||
|
||||
extraClass = instance.toolId;
|
||||
if (this.currentSelectedTool == tool) {
|
||||
extraClass = extraClass + " selected";
|
||||
}
|
||||
toolMarkup += '<li rel="tooltip" data-placement="right" class="tool-icon ' + extraClass + '" data-tool-id="' + instance.toolId +
|
||||
'" title="' + instance.helpText + '"></li>';
|
||||
toolMarkup += this.getToolMarkup_(this.tools[i]);
|
||||
}
|
||||
$('#tools-container').html(toolMarkup);
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.ToolController.prototype.getToolMarkup_ = function(tool) {
|
||||
var instance = tool.instance;
|
||||
|
||||
var classList = ['tool-icon', instance.toolId];
|
||||
if (this.currentSelectedTool == tool) {
|
||||
classList.push('selected');
|
||||
}
|
||||
|
||||
return '<li rel="tooltip" data-placement="right" class="' + classList.join(' ') + '" data-tool-id="' + instance.toolId +
|
||||
'" title="' + instance.helpText + '"></li>';
|
||||
};
|
||||
|
||||
ns.ToolController.prototype.addKeyboardShortcuts_ = function () {
|
||||
for(var i = 0 ; i < this.tools.length ; i++) {
|
||||
pskl.app.shortcutService.addShortcut(this.tools[i].shortcut, this.onKeyboardShortcut_.bind(this));
|
||||
}
|
||||
};
|
||||
})();
|
@ -13,9 +13,9 @@
|
||||
$.subscribe(Events.SELECTION_DISMISSED, $.proxy(this.onSelectionDismissed_, this));
|
||||
$.subscribe(Events.SELECTION_MOVE_REQUEST, $.proxy(this.onSelectionMoved_, this));
|
||||
|
||||
$.subscribe(Events.PASTE, $.proxy(this.onPaste_, this));
|
||||
$.subscribe(Events.COPY, $.proxy(this.onCopy_, this));
|
||||
$.subscribe(Events.CUT, $.proxy(this.onCut_, this));
|
||||
pskl.app.shortcutService.addShortcut('ctrl+V', this.paste.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('ctrl+X', this.cut.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('ctrl+C', this.copy.bind(this));
|
||||
|
||||
$.subscribe(Events.TOOL_SELECTED, $.proxy(this.onToolSelected_, this));
|
||||
};
|
||||
@ -46,10 +46,7 @@
|
||||
this.cleanSelection_();
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.SelectionManager.prototype.onCut_ = function(evt) {
|
||||
ns.SelectionManager.prototype.cut = function() {
|
||||
if(this.currentSelection) {
|
||||
// Put cut target into the selection:
|
||||
this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame());
|
||||
@ -59,9 +56,8 @@
|
||||
for(var i=0, l=pixels.length; i<l; i++) {
|
||||
try {
|
||||
currentFrame.setPixel(pixels[i].col, pixels[i].row, Constants.TRANSPARENT_COLOR);
|
||||
}
|
||||
catch(e) {
|
||||
// Catchng out of frame's bound pixels without testing
|
||||
} catch(e) {
|
||||
// Catching out of frame's bound pixels without testing
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -70,7 +66,7 @@
|
||||
}
|
||||
};
|
||||
|
||||
ns.SelectionManager.prototype.onPaste_ = function(evt) {
|
||||
ns.SelectionManager.prototype.paste = function() {
|
||||
if(this.currentSelection && this.currentSelection.hasPastedContent) {
|
||||
var pixels = this.currentSelection.pixels;
|
||||
var currentFrame = this.piskelController.getCurrentFrame();
|
||||
@ -79,22 +75,17 @@
|
||||
currentFrame.setPixel(
|
||||
pixels[i].col, pixels[i].row,
|
||||
pixels[i].copiedColor);
|
||||
}
|
||||
catch(e) {
|
||||
// Catchng out of frame's bound pixels without testing
|
||||
} catch(e) {
|
||||
// Catching out of frame's bound pixels without testing
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.SelectionManager.prototype.onCopy_ = function(evt) {
|
||||
ns.SelectionManager.prototype.copy = function() {
|
||||
if(this.currentSelection && this.piskelController.getCurrentFrame()) {
|
||||
this.currentSelection.fillSelectionFromFrame(this.piskelController.getCurrentFrame());
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
throw "Bad state for CUT callback in SelectionManager";
|
||||
}
|
||||
};
|
||||
|
@ -7,8 +7,9 @@
|
||||
ns.HistoryService.prototype.init = function () {
|
||||
|
||||
$.subscribe(Events.TOOL_RELEASED, this.saveState.bind(this));
|
||||
$.subscribe(Events.UNDO, this.undo.bind(this));
|
||||
$.subscribe(Events.REDO, this.redo.bind(this));
|
||||
|
||||
pskl.app.shortcutService.addShortcut('ctrl+Z', this.undo.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('ctrl+Y', this.redo.bind(this));
|
||||
};
|
||||
|
||||
ns.HistoryService.prototype.saveState = function () {
|
||||
|
@ -1,72 +0,0 @@
|
||||
(function () {
|
||||
var ns = $.namespace("pskl.service");
|
||||
|
||||
ns.KeyboardEventService = function () {
|
||||
this.keyboardActions_ = {
|
||||
"ctrl" : {
|
||||
"z" : Events.UNDO,
|
||||
"y" : Events.REDO,
|
||||
"x" : Events.CUT,
|
||||
"c" : Events.COPY,
|
||||
"v" : Events.PASTE
|
||||
},
|
||||
"shift" : {
|
||||
"?" : Events.TOGGLE_HELP
|
||||
},
|
||||
"x" : Events.SWAP_COLORS
|
||||
};
|
||||
|
||||
// See ToolController
|
||||
// TODO : Allow for other classes to register new shortcuts
|
||||
var toolKeys = 'pveblrcmzso'.split('');
|
||||
toolKeys.forEach(function (key) {
|
||||
this.keyboardActions_[key] = Events.SELECT_TOOL;
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
ns.KeyboardEventService.prototype.init = function() {
|
||||
$(document.body).keydown($.proxy(this.onKeyUp_, this));
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.KeyboardEventService.prototype.onKeyUp_ = function(evt) {
|
||||
var eventToTrigger;
|
||||
|
||||
// jquery names FTW ...
|
||||
var keycode = evt.which;
|
||||
var charkey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode);
|
||||
|
||||
if(charkey) {
|
||||
if (this.isCtrlKeyPressed_(evt)) {
|
||||
eventToTrigger = this.keyboardActions_.ctrl[charkey];
|
||||
} else if (this.isShiftKeyPressed_(evt)) {
|
||||
eventToTrigger = this.keyboardActions_.shift[charkey];
|
||||
} else {
|
||||
eventToTrigger = this.keyboardActions_[charkey];
|
||||
}
|
||||
|
||||
if(eventToTrigger) {
|
||||
$.publish(eventToTrigger, charkey);
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ns.KeyboardEventService.prototype.isCtrlKeyPressed_ = function (evt) {
|
||||
return this.isMac_() ? evt.metaKey : evt.ctrlKey;
|
||||
};
|
||||
|
||||
ns.KeyboardEventService.prototype.isShiftKeyPressed_ = function (evt) {
|
||||
return evt.shiftKey;
|
||||
};
|
||||
|
||||
ns.KeyboardEventService.prototype.isMac_ = function () {
|
||||
return navigator.appVersion.indexOf("Mac") != -1;
|
||||
};
|
||||
})();
|
@ -11,7 +11,10 @@
|
||||
throw 'cheatsheetEl_ DOM element could not be retrieved';
|
||||
}
|
||||
this.initMarkup_();
|
||||
pskl.app.shortcutService.addShortcut('shift+?', this.toggleCheatsheet_.bind(this));
|
||||
pskl.app.shortcutService.addShortcut('?', this.toggleCheatsheet_.bind(this));
|
||||
$.subscribe(Events.TOGGLE_HELP, this.toggleCheatsheet_.bind(this));
|
||||
$.subscribe(Events.ESCAPE, this.onEscape_.bind(this));
|
||||
};
|
||||
|
||||
ns.CheatsheetService.prototype.toggleCheatsheet_ = function () {
|
||||
@ -22,13 +25,21 @@
|
||||
}
|
||||
};
|
||||
|
||||
ns.CheatsheetService.prototype.onEscape_ = function () {
|
||||
if (this.isDisplayed_) {
|
||||
this.hideCheatsheet_();
|
||||
}
|
||||
};
|
||||
|
||||
ns.CheatsheetService.prototype.showCheatsheet_ = function () {
|
||||
pskl.app.shortcutService.addShortcut('ESC', this.hideCheatsheet_.bind(this));
|
||||
this.cheatsheetEl_.style.display = 'block';
|
||||
this.isDisplayed_ = true;
|
||||
};
|
||||
|
||||
|
||||
ns.CheatsheetService.prototype.hideCheatsheet_ = function () {
|
||||
pskl.app.shortcutService.removeShortcut('ESC');
|
||||
this.cheatsheetEl_.style.display = 'none';
|
||||
this.isDisplayed_ = false;
|
||||
};
|
||||
@ -65,11 +76,16 @@
|
||||
};
|
||||
var miscKeys = [
|
||||
toDescriptor('X', 'Swap primary/secondary colors'),
|
||||
toDescriptor('D', 'Reset default colors'),
|
||||
toDescriptor('ctrl + X', 'Cut selection'),
|
||||
toDescriptor('ctrl + C', 'Copy selection'),
|
||||
toDescriptor('ctrl + V', 'Paste selection'),
|
||||
toDescriptor('ctrl + Z', 'Undo'),
|
||||
toDescriptor('ctrl + Y', 'Redo')
|
||||
toDescriptor('ctrl + Y', 'Redo'),
|
||||
toDescriptor('↑', 'Select previous frame'), /* ASCII for up-arrow */
|
||||
toDescriptor('↓', 'Select next frame'), /* ASCII for down-arrow */
|
||||
toDescriptor('N', 'Create new frame'),
|
||||
toDescriptor('shift + ?', 'Open/Close this popup')
|
||||
];
|
||||
for (var i = 0 ; i < miscKeys.length ; i++) {
|
||||
var key = miscKeys[i];
|
||||
|
@ -1,17 +0,0 @@
|
||||
(function () {
|
||||
var ns = $.namespace('service.keyboard');
|
||||
|
||||
ns.KeyboardEvent = function (eventName, args, description) {
|
||||
this.eventName = eventName;
|
||||
this.args = args;
|
||||
this.description = description;
|
||||
};
|
||||
|
||||
ns.KeyboardEvent.prototype.fire = function () {
|
||||
$.publish(this.eventName, this.args);
|
||||
};
|
||||
|
||||
ns.KeyboardEvent.prototype.getDescription = function () {
|
||||
return this.description;
|
||||
};
|
||||
})();
|
@ -1,6 +1,9 @@
|
||||
(function () {
|
||||
var specialKeys = {
|
||||
191 : "?"
|
||||
191 : "?",
|
||||
27 : "esc",
|
||||
38 : "up",
|
||||
40 : "down"
|
||||
};
|
||||
|
||||
var ns = $.namespace('pskl.service.keyboard');
|
||||
@ -11,7 +14,7 @@
|
||||
// key is 0-9
|
||||
return (keycode - 48) + "";
|
||||
} else if (keycode >= 65 && keycode <= 90) {
|
||||
// key is a-z, we'll use base 36 to get the string representation
|
||||
// key is a-z, use base 36 to get the string representation
|
||||
return (keycode - 65 + 10).toString(36);
|
||||
} else {
|
||||
return specialKeys[keycode];
|
||||
|
90
js/service/keyboard/ShortcutService.js
Normal file
90
js/service/keyboard/ShortcutService.js
Normal file
@ -0,0 +1,90 @@
|
||||
(function () {
|
||||
var ns = $.namespace('pskl.service.keyboard');
|
||||
|
||||
ns.ShortcutService = function () {
|
||||
this.shortcuts_ = {};
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
ns.ShortcutService.prototype.init = function() {
|
||||
$(document.body).keydown($.proxy(this.onKeyUp_, this));
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.addShortcut = function (rawKey, callback) {
|
||||
var parsedKey = this.parseKey_(rawKey.toLowerCase());
|
||||
|
||||
var key = parsedKey.key,
|
||||
meta = parsedKey.meta;
|
||||
|
||||
this.shortcuts_[key] = this.shortcuts_[key] || {};
|
||||
|
||||
if (this.shortcuts_[key][meta]) {
|
||||
throw 'Shortcut ' + meta + ' + ' + key + ' already registered';
|
||||
} else {
|
||||
this.shortcuts_[key][meta] = callback;
|
||||
}
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.removeShortcut = function (rawKey) {
|
||||
var parsedKey = this.parseKey_(rawKey.toLowerCase());
|
||||
|
||||
var key = parsedKey.key,
|
||||
meta = parsedKey.meta;
|
||||
|
||||
this.shortcuts_[key] = this.shortcuts_[key] || {};
|
||||
|
||||
this.shortcuts_[key][meta] = null;
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.parseKey_ = function (key) {
|
||||
var meta = 'normal';
|
||||
if (key.indexOf('ctrl+') === 0) {
|
||||
meta = 'ctrl';
|
||||
key = key.replace('ctrl+', '');
|
||||
} else if (key.indexOf('shift+') === 0) {
|
||||
meta = 'shift';
|
||||
key = key.replace('shift+', '');
|
||||
}
|
||||
return {meta : meta, key : key};
|
||||
};
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
ns.ShortcutService.prototype.onKeyUp_ = function(evt) {
|
||||
// jquery names FTW ...
|
||||
var keycode = evt.which;
|
||||
var charkey = pskl.service.keyboard.KeycodeTranslator.toChar(keycode);
|
||||
|
||||
var keyShortcuts = this.shortcuts_[charkey];
|
||||
if(keyShortcuts) {
|
||||
var cb;
|
||||
if (this.isCtrlKeyPressed_(evt)) {
|
||||
cb = keyShortcuts.ctrl;
|
||||
} else if (this.isShiftKeyPressed_(evt)) {
|
||||
cb = keyShortcuts.shift;
|
||||
} else {
|
||||
cb = keyShortcuts.normal;
|
||||
}
|
||||
|
||||
if(cb) {
|
||||
cb(charkey);
|
||||
evt.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.isCtrlKeyPressed_ = function (evt) {
|
||||
return this.isMac_() ? evt.metaKey : evt.ctrlKey;
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.isShiftKeyPressed_ = function (evt) {
|
||||
return evt.shiftKey;
|
||||
};
|
||||
|
||||
ns.ShortcutService.prototype.isMac_ = function () {
|
||||
return navigator.appVersion.indexOf("Mac") != -1;
|
||||
};
|
||||
})();
|
@ -62,9 +62,9 @@ exports.scripts = [
|
||||
// Services
|
||||
"js/service/LocalStorageService.js",
|
||||
"js/service/HistoryService.js",
|
||||
"js/service/keyboard/ShortcutService.js",
|
||||
"js/service/keyboard/KeycodeTranslator.js",
|
||||
"js/service/keyboard/CheatsheetService.js",
|
||||
"js/service/KeyboardEventService.js",
|
||||
"js/service/ImageUploadService.js",
|
||||
|
||||
// Tools
|
||||
|
Loading…
Reference in New Issue
Block a user