mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-29 02:10:08 +00:00
reworked core.Tasks, almost ready to remove legacy code...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
d3e54c83ac
commit
e9a3192335
@ -51,6 +51,8 @@
|
||||
// XXX
|
||||
var DEBUG = typeof(DEBUG) != 'undefined' ? DEBUG : true
|
||||
|
||||
var types = require('lib/types')
|
||||
var runner = require('lib/types/runner')
|
||||
var util = require('lib/util')
|
||||
var object = require('lib/object')
|
||||
var actions = require('lib/actions')
|
||||
@ -59,6 +61,7 @@ var toggler = require('lib/toggler')
|
||||
|
||||
|
||||
|
||||
|
||||
/*********************************************************************/
|
||||
|
||||
// NOTE: if no toggler state is set this assumes that the first state
|
||||
@ -964,9 +967,7 @@ var LifeCycleActions = actions.Actions({
|
||||
evt.indexOf('stopped.pre'),
|
||||
evt.indexOf('stopped.post')) >= 0
|
||||
&& this.isStopped()
|
||||
&& func.call(this)
|
||||
}
|
||||
}],
|
||||
&& func.call(this) } }],
|
||||
|
||||
// helpers...
|
||||
restart: ['System/Soft restart',
|
||||
@ -2350,8 +2351,8 @@ module.Workspace = ImageGridFeatures.Feature({
|
||||
// XXX should this be a separate module???
|
||||
//var tasks = require('lib/tasks')
|
||||
|
||||
var task =
|
||||
module.tast =
|
||||
var Task =
|
||||
module.Tast =
|
||||
function(func){
|
||||
func.__task__ = true
|
||||
return func }
|
||||
@ -2393,11 +2394,12 @@ function(func){
|
||||
// - list/sort/prioritize
|
||||
// - remote (peer/worker)
|
||||
// XXX docs...
|
||||
// XXX LEGACY...
|
||||
var abortablePromise =
|
||||
module.abortablePromise =
|
||||
function(title, func){
|
||||
return Object.assign(
|
||||
task(function(...args){
|
||||
Task(function(...args){
|
||||
var that = this
|
||||
|
||||
var abort = object.mixinFlat(
|
||||
@ -2440,96 +2442,33 @@ function(title, func){
|
||||
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
var Task =
|
||||
module.Task =
|
||||
object.Constructor('Task', {
|
||||
})
|
||||
|
||||
|
||||
// XXX
|
||||
var events = require('lib/types/event')
|
||||
|
||||
// Task action action helpers...
|
||||
//
|
||||
// NOTE: for examples see:
|
||||
// features/examples.js:
|
||||
// ExampleActions.exampleTask(..)
|
||||
// ExampleActions.exampleSessionTask(..)
|
||||
|
||||
var taskAction =
|
||||
module.taskAction =
|
||||
function(title, func){
|
||||
var action
|
||||
return (action = Object.assign(
|
||||
task(function(...args){
|
||||
var that = this
|
||||
|
||||
// XXX
|
||||
var ticket = events.EventMixin({
|
||||
// can be:
|
||||
// - ready
|
||||
// - running
|
||||
// - done
|
||||
state: null,
|
||||
|
||||
start: events.Event('start', function(handle, ...args){
|
||||
if(this.state == 'ready'){
|
||||
that.resumeTask(title, action)
|
||||
handle(...args) } }),
|
||||
pause: events.Event('pause', function(handle, ...args){
|
||||
if(this.state == 'running'){
|
||||
that.pauseTask(title, action)
|
||||
handle(...args) } }),
|
||||
abort: events.Event('abort', function(handle, ...args){
|
||||
if(!this.state != 'done'){
|
||||
that.abortTask(title, action)
|
||||
handle(...args) } }),
|
||||
})
|
||||
|
||||
// XXX
|
||||
|
||||
return func.call(this, ticket, ...args) }),
|
||||
return (action = object.mixin(
|
||||
Task(function(...args){
|
||||
return this.tasks.Task(title, func.bind(this), ...args) }),
|
||||
{
|
||||
__task_title__: title,
|
||||
toString: function(){
|
||||
return `core.taskAction('${ title }', \n${ func.toString() })` },
|
||||
return `core.taskAction('${ title }', \n\t${
|
||||
object.normalizeIndent('\t'+func.toString()) })` },
|
||||
})) }
|
||||
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
|
||||
var makeTaskAction =
|
||||
function(name, from, to, callback){
|
||||
return function(title, task='all'){
|
||||
title = title == '*' || title == 'all' ?
|
||||
[...(this.__running_tasks || new Map()).keys()]
|
||||
: title instanceof Array ?
|
||||
title
|
||||
: [title]
|
||||
this.__running_tasks
|
||||
&& title
|
||||
.forEach(function(title){
|
||||
[...(this.__running_tasks || new Map()).get(title) || []]
|
||||
.forEach(function(t){
|
||||
// filter task...
|
||||
;(task == 'all'
|
||||
|| task == '*'
|
||||
|| task === t
|
||||
|| (task instanceof Array
|
||||
&& task.includes(t)))
|
||||
// filter states...
|
||||
&& (from == '*'
|
||||
|| from == 'all'
|
||||
|| t.state == from)
|
||||
// XXX do we retrigger???
|
||||
//&& t.state != to
|
||||
// call handler...
|
||||
&& t[name]
|
||||
&& t[name]() !== false
|
||||
// state...
|
||||
&& to
|
||||
&& (t.state = to) })
|
||||
callback
|
||||
&& callback.call(this, title, task) }.bind(this))
|
||||
// cleanup...
|
||||
this.__running_tasks
|
||||
&& this.__running_tasks.size == 0
|
||||
&& (delete this.__running_tasks) } }
|
||||
|
||||
var sessionTaskAction =
|
||||
module.sessionTaskAction =
|
||||
function(title, func){
|
||||
return object.mixin(
|
||||
taskAction(...arguments),
|
||||
{ __session_task__: true }) }
|
||||
|
||||
|
||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
|
||||
@ -2538,120 +2477,30 @@ var TaskActions = actions.Actions({
|
||||
config: {
|
||||
},
|
||||
|
||||
// Format:
|
||||
// Map({
|
||||
// title: Set([
|
||||
// {
|
||||
// state: ...,
|
||||
// abort: func,
|
||||
// pause: func,
|
||||
// resume: func,
|
||||
// ...
|
||||
// },
|
||||
// ...
|
||||
// ]),
|
||||
// ...
|
||||
// })
|
||||
// actions that generate tasks...
|
||||
//
|
||||
__running_tasks: null,
|
||||
// XXX cache these???
|
||||
get taskActions(){
|
||||
return this.actions
|
||||
.filter(function(action){
|
||||
return !!this.getActionAttr(action, '__task__') }.bind(this)) },
|
||||
get sessionTaskActions(){
|
||||
return this.actions
|
||||
.filter(function(action){
|
||||
return !!this.getActionAttr(action, '__session_task__') }.bind(this)) },
|
||||
|
||||
// XXX should this .resume(..)???
|
||||
// XXX might be a good idea to make this compatible with tasks.Queue(..)
|
||||
// ...and return a queue if not task is given??
|
||||
Task: ['- System/',
|
||||
doc`
|
||||
`,
|
||||
function(title, task){
|
||||
// reserved titles...
|
||||
if(title == 'all' || title == '*'){
|
||||
throw new Error('.abortable(..): can not set reserved title: "'+ title +'".') }
|
||||
|
||||
var tasks = this.__running_tasks = this.__running_tasks || new Map()
|
||||
var set = tasks.get(title) || new Set()
|
||||
tasks.set(title, set)
|
||||
set.add(task)
|
||||
|
||||
task.state = 'running'
|
||||
|
||||
return task }],
|
||||
|
||||
getTasks: ['- System/',
|
||||
function(title='all', state='all'){
|
||||
var normArg = function(arg){
|
||||
return !arg ?
|
||||
'all'
|
||||
: arg == 'all' || arg == '*' ?
|
||||
arg
|
||||
: arg instanceof Array ?
|
||||
new Set(arg)
|
||||
: new Set([arg]) }
|
||||
|
||||
title = normArg(title)
|
||||
state = normArg(state)
|
||||
|
||||
return this.__running_tasks ?
|
||||
[...this.__running_tasks.entries()]
|
||||
.reduce(function(res, [t, set]){
|
||||
if(title != 'all'
|
||||
&& title != '*'
|
||||
&& !title.has(t)){
|
||||
return res }
|
||||
var l = [...set]
|
||||
.filter(function(t){
|
||||
return state == 'all'
|
||||
|| state == '*'
|
||||
|| state.has(t.state) })
|
||||
l.length > 0
|
||||
&& (res[t] = l)
|
||||
return res }, {})
|
||||
: {} }],
|
||||
// XXX should this abort the cleared tasks???
|
||||
// ...if not would be logical to rename this to ._clearTask(..)
|
||||
clearTask: ['- System/',
|
||||
function(title, task='all'){
|
||||
// clear all...
|
||||
if(title == '*' || title == 'all'){
|
||||
delete this.__running_tasks }
|
||||
|
||||
var set = ((this.__running_tasks || new Map()).get(title) || new Set())
|
||||
// clear specific handler...
|
||||
task != '*'
|
||||
&& task != 'all'
|
||||
&& set.delete(task)
|
||||
// cleanup / clear title...
|
||||
;(set.size == 0
|
||||
|| task == '*'
|
||||
|| task == 'all')
|
||||
&& (this.__running_tasks || new Set()).delete(title)
|
||||
// cleanup...
|
||||
this.__running_tasks
|
||||
&& this.__running_tasks.size == 0
|
||||
&& (delete this.__running_tasks) }],
|
||||
|
||||
// XXX cache???
|
||||
// task manager...
|
||||
//
|
||||
__task_manager__: runner.TaskManager,
|
||||
__tasks: null,
|
||||
get tasks(){
|
||||
return this.actions.filter(function(action){
|
||||
return !!this.getActionAttr(action, '__task__') }.bind(this)) },
|
||||
return (this.__tasks =
|
||||
this.__tasks
|
||||
|| this.__task_manager__()) },
|
||||
// session tasks are stopped when the index is cleared...
|
||||
get sessionTasks(){
|
||||
return this.tasks.titled(...this.sessionTaskActions) },
|
||||
|
||||
get tasksActive(){
|
||||
return this.getTasks() },
|
||||
get tasksRunning(){
|
||||
return this.getTasks('all', 'running') },
|
||||
get tasksPaused(){
|
||||
return this.getTasks('all', 'paused') },
|
||||
|
||||
pauseTask: ['- System/',
|
||||
makeTaskAction('pause', 'running', 'paused')],
|
||||
resumeTask: ['- System/',
|
||||
makeTaskAction('resume', 'paused', 'running')],
|
||||
abortTask: ['- System/',
|
||||
makeTaskAction('abort', 'all', null,
|
||||
function(title, task='all'){
|
||||
this.__running_tasks
|
||||
&& (task == 'all'
|
||||
|| task == '*'
|
||||
|| this.__running_tasks.get(title).size == 0)
|
||||
&& this.__running_tasks.delete(title) })],
|
||||
|
||||
|
||||
// XXX LEGACY -- remove after migrating sharp.js and abortablePromise(..)
|
||||
@ -2748,25 +2597,6 @@ var TaskActions = actions.Actions({
|
||||
this.__abortable
|
||||
&& this.__abortable.size == 0
|
||||
&& (delete this.__abortable) }],
|
||||
|
||||
|
||||
/* XXX LEGACY...
|
||||
get jobs(){
|
||||
return this.__jobs },
|
||||
|
||||
getJob: ['- Jobs/',
|
||||
function(name){
|
||||
name = name || this.data.newGID()
|
||||
|
||||
// get/init task dict...
|
||||
var t = this.__jobs = this.__jobs || {}
|
||||
// get/init task...
|
||||
var job = t[name] = t[name] || tasks.Queue()
|
||||
job.name = name
|
||||
|
||||
return job
|
||||
}],
|
||||
//*/
|
||||
})
|
||||
|
||||
|
||||
@ -2781,6 +2611,8 @@ module.Tasks = ImageGridFeatures.Feature({
|
||||
actions: TaskActions,
|
||||
|
||||
handlers: [
|
||||
['clear',
|
||||
'sessionTasks.stop'],
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@ -267,14 +267,41 @@ var ExampleActions = actions.Actions({
|
||||
// XXX inner/outer action...
|
||||
|
||||
|
||||
// NOTE: action name and task name should be the same to avoid
|
||||
// confusion...
|
||||
// XXX it would be quite complicated to support both and
|
||||
// confusing to support either...
|
||||
exampleTask: ['- Test/',
|
||||
core.taskAction('Example task', function(ticket, ...args){
|
||||
core.taskAction('exampleTask',
|
||||
function(ticket, ...args){
|
||||
console.log('###', ticket.title+':', 'START:', ...args,
|
||||
'\n\t\t(supported messages: "stop", "break", "error", ...)')
|
||||
ticket.onmessage(function(msg, ...args){
|
||||
// stop...
|
||||
if(msg == 'stop'){
|
||||
console.log('###', ticket.title+':', 'STOP')
|
||||
ticket.resolve(...args)
|
||||
|
||||
// XXX
|
||||
console.log('>>>>', ticket, ...args)
|
||||
// break...
|
||||
} else if(msg == 'break'){
|
||||
console.log('###', ticket.title+':', 'BREAK')
|
||||
ticket.reject(...args)
|
||||
|
||||
return Promise.cooperative()
|
||||
})],
|
||||
// error...
|
||||
} else if(msg == 'error'){
|
||||
console.log('###', ticket.title+':', 'ERROR')
|
||||
throw new Error('Task error')
|
||||
|
||||
// other...
|
||||
} else {
|
||||
console.log('###', ticket.title+':', 'Got message:', msg, ...args) } }) })],
|
||||
exampleSessionTask: ['- Test/',
|
||||
core.sessionTaskAction('exampleSessionTask',
|
||||
function(ticket, ...args){
|
||||
console.log('###', ticket.title+':', 'START:', ...args)
|
||||
ticket.onmessage('stop', function(){
|
||||
console.log('###', ticket.title+':', 'STOP:', ...args)
|
||||
ticket.resolve(...args) }) })],
|
||||
})
|
||||
|
||||
var Example =
|
||||
|
||||
@ -411,7 +411,11 @@ var SharpActions = actions.Actions({
|
||||
.then(function(){
|
||||
logger
|
||||
&& logger.emit('done', to)
|
||||
return img }) }) }) }) })],
|
||||
return img }) }) }) })
|
||||
.then(function(res){
|
||||
return res == 'aborted' ?
|
||||
Promise.reject('aborted')
|
||||
: res }) })],
|
||||
|
||||
// XXX this does not update image.base_path -- is this correct???
|
||||
// XXX add support for offloading the processing to a thread/worker...
|
||||
@ -544,7 +548,9 @@ var SharpActions = actions.Actions({
|
||||
|
||||
return [gid, size, name] }) }) })
|
||||
.then(function(res){
|
||||
return res.flat() }) })],
|
||||
return res == 'aborted' ?
|
||||
Promise.reject('aborted')
|
||||
: res.flat() }) })],
|
||||
|
||||
// XXX add support for offloading the processing to a thread/worker...
|
||||
// XXX should we use task.Queue()???
|
||||
@ -762,7 +768,11 @@ var SharpActions = actions.Actions({
|
||||
that.ribbons
|
||||
&& that.ribbons.updateImage(gid)
|
||||
|
||||
return done(gid) }) }) })],
|
||||
return done(gid) }) })
|
||||
.then(function(res){
|
||||
return res == 'aborted' ?
|
||||
Promise.reject('aborted')
|
||||
: res }) })],
|
||||
cacheAllMetadata: ['- Sharp|Image/',
|
||||
core.doc`Cache all metadata
|
||||
NOTE: this is a shorthand to .cacheMetadata('all', ..)`,
|
||||
|
||||
@ -90,6 +90,7 @@ var ProgressActions = actions.Actions({
|
||||
// XXX multiple containers...
|
||||
// XXX shorten the nested css class names...
|
||||
// XXX revise styles...
|
||||
// XXX make the "X" bigger -- finger usable...
|
||||
__progress_cache: null,
|
||||
showProgress: ['- Interface/Show progress bar...',
|
||||
core.doc`Progress bar widget...
|
||||
@ -201,7 +202,7 @@ var ProgressActions = actions.Actions({
|
||||
.append($('<progress/>'))
|
||||
// events...
|
||||
.on('progressClose', function(){
|
||||
widget
|
||||
$(this)
|
||||
.fadeOut(that.config['progress-fade-duration'] || 200, function(){
|
||||
var cache = (that.__progress_cache || {})[text]
|
||||
cache.timeout
|
||||
|
||||
40
Viewer/package-lock.json
generated
40
Viewer/package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ImageGrid.Viewer.g4",
|
||||
"version": "4.0.0a",
|
||||
"version": "4.0.0-a",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -549,9 +549,9 @@
|
||||
}
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz",
|
||||
"integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz",
|
||||
"integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
@ -636,9 +636,9 @@
|
||||
}
|
||||
},
|
||||
"electron": {
|
||||
"version": "9.3.4",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.4.tgz",
|
||||
"integrity": "sha512-OHP8qMKgW8D8GtH+altB22WJw/lBOyyVdoz5e8D0/iPBmJU3Jm93vO4z4Eh/9DvdSXlH8bMHUCMLL9PVW6f+tw==",
|
||||
"version": "9.3.5",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-9.3.5.tgz",
|
||||
"integrity": "sha512-EPmDsp7sO0UPtw7nLD1ufse/nBskP+ifXzBgUg9psCUlapkzuwYi6pmLAzKLW/bVjwgyUKwh1OKWILWfOeLGcQ==",
|
||||
"requires": {
|
||||
"@electron/get": "^1.0.1",
|
||||
"@types/node": "^12.0.12",
|
||||
@ -646,9 +646,9 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/node": {
|
||||
"version": "12.19.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.4.tgz",
|
||||
"integrity": "sha512-o3oj1bETk8kBwzz1WlO6JWL/AfAA3Vm6J1B3C9CsdxHYp7XgPiH7OEXPUbZTndHlRaIElrANkQfe6ZmfJb3H2w=="
|
||||
"version": "12.19.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.19.6.tgz",
|
||||
"integrity": "sha512-U2VopDdmBoYBmtm8Rz340mvvSz34VgX/K9+XCuckvcLGMkt3rbMX8soqFOikIPlPBc5lmw8By9NUK7bEFSBFlQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1073,9 +1073,9 @@
|
||||
"integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg=="
|
||||
},
|
||||
"ig-actions": {
|
||||
"version": "3.24.13",
|
||||
"resolved": "https://registry.npmjs.org/ig-actions/-/ig-actions-3.24.13.tgz",
|
||||
"integrity": "sha512-x7+37fJ1wsdSl/VNb4W569IHiUx2SKtEr76PI6LxXOW3fRkv6/9KnDXJK8pL+XVO012oj096WatYK80aPUlLPQ==",
|
||||
"version": "3.24.15",
|
||||
"resolved": "https://registry.npmjs.org/ig-actions/-/ig-actions-3.24.15.tgz",
|
||||
"integrity": "sha512-fhMZ1F34nPm/Hir1Is+3hKPJpshgwbxjnQRJb5M+/fs52FfoG7TMbDYWzbKKe6qtwfyrcVD2rPRidjCl6779zQ==",
|
||||
"requires": {
|
||||
"ig-object": "^5.0.2"
|
||||
}
|
||||
@ -1105,16 +1105,16 @@
|
||||
}
|
||||
},
|
||||
"ig-object": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.4.11.tgz",
|
||||
"integrity": "sha512-WPPQ5C41c6q3tPfa2fBbWE2xcLF7LoGRu2E6Wr/aoA5oxAyl8lAuE7Kqt4TyPwfW9jVI0+ifBztg9e1tR5mG1Q=="
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/ig-object/-/ig-object-5.4.12.tgz",
|
||||
"integrity": "sha512-9kZM80Js9/eTwXN9VXwLDC1wDJ7gIAdYU9GIzb5KJmNcLAMaW+zhgFrwFFMrcSfggUuadgnqSrS41E4XLe8JZw=="
|
||||
},
|
||||
"ig-types": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ig-types/-/ig-types-4.1.2.tgz",
|
||||
"integrity": "sha512-IYnuBWw7m7qvtW21ggoodFA++v+bHwNMqGRvo9w2IwfI3leVPbOORUC5CjUKb4q/ffGlCcT3gC7w+E/aO8BOTw==",
|
||||
"version": "5.0.14",
|
||||
"resolved": "https://registry.npmjs.org/ig-types/-/ig-types-5.0.14.tgz",
|
||||
"integrity": "sha512-j4oyqZP+xasNYMyWllcZz5kT8vscB58P6smehoBEHxNRPlKMqfZTP4MlxQVZpyAgPH8HCCwGjEYZ7c63FokWFA==",
|
||||
"requires": {
|
||||
"ig-object": "^5.4.11",
|
||||
"ig-object": "^5.4.12",
|
||||
"object-run": "^1.0.1"
|
||||
}
|
||||
},
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
"dependencies": {
|
||||
"app-module-path": "^1.0.6",
|
||||
"async-json": "0.0.2",
|
||||
"electron": "^9.3.4",
|
||||
"electron": "^9.3.5",
|
||||
"exif-reader": "^1.0.3",
|
||||
"exiftool": "^0.0.3",
|
||||
"fs-extra": "^7.0.1",
|
||||
@ -28,11 +28,11 @@
|
||||
"generic-walk": "^1.4.0",
|
||||
"glob": "^7.1.6",
|
||||
"guarantee-events": "^1.0.0",
|
||||
"ig-actions": "^3.24.13",
|
||||
"ig-actions": "^3.24.15",
|
||||
"ig-argv": "^2.15.0",
|
||||
"ig-features": "^3.4.2",
|
||||
"ig-object": "^5.4.11",
|
||||
"ig-types": "^4.1.2",
|
||||
"ig-object": "^5.4.12",
|
||||
"ig-types": "^5.0.14",
|
||||
"moment": "^2.29.1",
|
||||
"object-run": "^1.0.1",
|
||||
"requirejs": "^2.3.6",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user