Pushing the experimental SoundEvent refactoring
This commit is contained in:
@ -16,6 +16,7 @@ import {
|
|||||||
superdough,
|
superdough,
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
} from "superdough";
|
} from "superdough";
|
||||||
|
import { Sound } from "zifferjs/src/types";
|
||||||
|
|
||||||
export type SoundParams = {
|
export type SoundParams = {
|
||||||
dur: number | number[];
|
dur: number | number[];
|
||||||
@ -35,7 +36,16 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
nudge: number;
|
nudge: number;
|
||||||
sound: any;
|
sound: any;
|
||||||
|
|
||||||
private methodMap = {
|
public updateValue<T>(
|
||||||
|
key: string,
|
||||||
|
value: T | T[] | SoundParams[] | null
|
||||||
|
): this {
|
||||||
|
if (value == null) return this;
|
||||||
|
this.values[key] = value;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static methodMap = {
|
||||||
volume: ["volume", "vol"],
|
volume: ["volume", "vol"],
|
||||||
zrand: ["zrand", "zr"],
|
zrand: ["zrand", "zr"],
|
||||||
curve: ["curve"],
|
curve: ["curve"],
|
||||||
@ -67,17 +77,23 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
phaserDepth: ["phaserDepth", "phasdepth"],
|
phaserDepth: ["phaserDepth", "phasdepth"],
|
||||||
phaserSweep: ["phaserSweep", "phassweep"],
|
phaserSweep: ["phaserSweep", "phassweep"],
|
||||||
phaserCenter: ["phaserCenter", "phascenter"],
|
phaserCenter: ["phaserCenter", "phascenter"],
|
||||||
fmadsr: (a: number, d: number, s: number, r: number) => {
|
fmadsr: function (
|
||||||
this.updateValue("fmattack", a);
|
self: SoundEvent,
|
||||||
this.updateValue("fmdecay", d);
|
a: number,
|
||||||
this.updateValue("fmsustain", s);
|
d: number,
|
||||||
this.updateValue("fmrelease", r);
|
s: number,
|
||||||
return this;
|
r: number
|
||||||
|
) {
|
||||||
|
self.updateValue("fmattack", a);
|
||||||
|
self.updateValue("fmdecay", d);
|
||||||
|
self.updateValue("fmsustain", s);
|
||||||
|
self.updateValue("fmrelease", r);
|
||||||
|
return self;
|
||||||
},
|
},
|
||||||
fmad: (a: number, d: number) => {
|
fmad: function (self: SoundEvent, a: number, d: number) {
|
||||||
this.updateValue("fmattack", a);
|
self.updateValue("fmattack", a);
|
||||||
this.updateValue("fmdecay", d);
|
self.updateValue("fmdecay", d);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
ftype: ["ftype"],
|
ftype: ["ftype"],
|
||||||
fanchor: ["fanchor"],
|
fanchor: ["fanchor"],
|
||||||
@ -85,147 +101,174 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
decay: ["decay", "dec"],
|
decay: ["decay", "dec"],
|
||||||
sustain: ["sustain", "sus"],
|
sustain: ["sustain", "sus"],
|
||||||
release: ["release", "rel"],
|
release: ["release", "rel"],
|
||||||
adsr: (a: number, d: number, s: number, r: number) => {
|
adsr: function (
|
||||||
this.updateValue("attack", a);
|
self: SoundEvent,
|
||||||
this.updateValue("decay", d);
|
a: number,
|
||||||
this.updateValue("sustain", s);
|
d: number,
|
||||||
this.updateValue("release", r);
|
s: number,
|
||||||
return this;
|
r: number
|
||||||
|
) {
|
||||||
|
self.updateValue("attack", a);
|
||||||
|
self.updateValue("decay", d);
|
||||||
|
self.updateValue("sustain", s);
|
||||||
|
self.updateValue("release", r);
|
||||||
|
return self;
|
||||||
},
|
},
|
||||||
ad: (a: number, d: number) => {
|
ad: function (self: SoundEvent, a: number, d: number) {
|
||||||
this.updateValue("attack", a);
|
self.updateValue("attack", a);
|
||||||
this.updateValue("decay", d);
|
self.updateValue("decay", d);
|
||||||
this.updateValue("sustain", 0.0);
|
self.updateValue("sustain", 0.0);
|
||||||
this.updateValue("release", 0.0);
|
self.updateValue("release", 0.0);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
lpenv: ["lpenv", "lpe"],
|
lpenv: ["lpenv", "lpe"],
|
||||||
lpattack: ["lpattack", "lpa"],
|
lpattack: ["lpattack", "lpa"],
|
||||||
lpdecay: ["lpdecay", "lpd"],
|
lpdecay: ["lpdecay", "lpd"],
|
||||||
lpsustain: ["lpsustain", "lps"],
|
lpsustain: ["lpsustain", "lps"],
|
||||||
lprelease: ["lprelease", "lpr"],
|
lprelease: ["lprelease", "lpr"],
|
||||||
cutoff: (value: number, resonance?: number) => {
|
cutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("cutoff", value);
|
self.updateValue("cutoff", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("resonance", resonance);
|
self.updateValue("resonance", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
lpf: (value: number, resonance?: number) => {
|
lpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("cutoff", value);
|
self.updateValue("cutoff", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("resonance", resonance);
|
self.updateValue("resonance", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
resonance: (value: number) => {
|
resonance: function (self: SoundEvent, value: number) {
|
||||||
if (value >= 0 && value <= 1) {
|
if (value >= 0 && value <= 1) {
|
||||||
this.updateValue("resonance", 50 * value);
|
self.updateValue("resonance", 50 * value);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
lpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
|
lpadsr: function (
|
||||||
this.updateValue("lpenv", depth);
|
self: SoundEvent,
|
||||||
this.updateValue("lpattack", a);
|
depth: number,
|
||||||
this.updateValue("lpdecay", d);
|
a: number,
|
||||||
this.updateValue("lpsustain", s);
|
d: number,
|
||||||
this.updateValue("lprelease", r);
|
s: number,
|
||||||
return this;
|
r: number
|
||||||
|
) {
|
||||||
|
self.updateValue("lpenv", depth);
|
||||||
|
self.updateValue("lpattack", a);
|
||||||
|
self.updateValue("lpdecay", d);
|
||||||
|
self.updateValue("lpsustain", s);
|
||||||
|
self.updateValue("lprelease", r);
|
||||||
|
return self;
|
||||||
},
|
},
|
||||||
lpad: (depth: number, a: number, d: number) => {
|
lpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||||
this.updateValue("lpenv", depth);
|
self.updateValue("lpenv", depth);
|
||||||
this.updateValue("lpattack", a);
|
self.updateValue("lpattack", a);
|
||||||
this.updateValue("lpdecay", d);
|
self.updateValue("lpdecay", d);
|
||||||
this.updateValue("lpsustain", 0);
|
self.updateValue("lpsustain", 0);
|
||||||
this.updateValue("lprelease", 0);
|
self.updateValue("lprelease", 0);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
hpenv: ["hpenv", "hpe"],
|
hpenv: ["hpenv", "hpe"],
|
||||||
hpattack: ["hpattack", "hpa"],
|
hpattack: ["hpattack", "hpa"],
|
||||||
hpdecay: ["hpdecay", "hpd"],
|
hpdecay: ["hpdecay", "hpd"],
|
||||||
hpsustain: ["hpsustain", "hpsus"],
|
hpsustain: ["hpsustain", "hpsus"],
|
||||||
hprelease: ["hprelease", "hpr"],
|
hprelease: ["hprelease", "hpr"],
|
||||||
hcutoff: (value: number, resonance?: number) => {
|
hcutoff: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("hcutoff", value);
|
self.updateValue("hcutoff", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("hresonance", resonance);
|
self.updateValue("hresonance", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
hpf: (value: number, resonance?: number) => {
|
hpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("hcutoff", value);
|
self.updateValue("hcutoff", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("hresonance", resonance);
|
self.updateValue("hresonance", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
hpq: (value: number) => {
|
hpq: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("hresonance", value);
|
self.updateValue("hresonance", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
hpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
|
hpadsr: function (
|
||||||
this.updateValue("hpenv", depth);
|
self: SoundEvent,
|
||||||
this.updateValue("hpattack", a);
|
depth: number,
|
||||||
this.updateValue("hpdecay", d);
|
a: number,
|
||||||
this.updateValue("hpsustain", s);
|
d: number,
|
||||||
this.updateValue("hprelease", r);
|
s: number,
|
||||||
return this;
|
r: number
|
||||||
|
) {
|
||||||
|
self.updateValue("hpenv", depth);
|
||||||
|
self.updateValue("hpattack", a);
|
||||||
|
self.updateValue("hpdecay", d);
|
||||||
|
self.updateValue("hpsustain", s);
|
||||||
|
self.updateValue("hprelease", r);
|
||||||
|
return self;
|
||||||
},
|
},
|
||||||
hpad: (depth: number, a: number, d: number) => {
|
hpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||||
this.updateValue("hpenv", depth);
|
self.updateValue("hpenv", depth);
|
||||||
this.updateValue("hpattack", a);
|
self.updateValue("hpattack", a);
|
||||||
this.updateValue("hpdecay", d);
|
self.updateValue("hpdecay", d);
|
||||||
this.updateValue("hpsustain", 0);
|
self.updateValue("hpsustain", 0);
|
||||||
this.updateValue("hprelease", 0);
|
self.updateValue("hprelease", 0);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
bpenv: ["bpenv", "bpe"],
|
bpenv: ["bpenv", "bpe"],
|
||||||
bpattack: ["bpattack", "bpa"],
|
bpattack: ["bpattack", "bpa"],
|
||||||
bpdecay: ["bpdecay", "bpd"],
|
bpdecay: ["bpdecay", "bpd"],
|
||||||
bpsustain: ["bpsustain", "bps"],
|
bpsustain: ["bpsustain", "bps"],
|
||||||
bprelease: ["bprelease", "bpr"],
|
bprelease: ["bprelease", "bpr"],
|
||||||
bandf: (value: number, resonance?: number) => {
|
bandf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("bandf", value);
|
self.updateValue("bandf", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("bandq", resonance);
|
self.updateValue("bandq", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
bpf: (value: number, resonance?: number) => {
|
bpf: function (self: SoundEvent, value: number, resonance?: number) {
|
||||||
this.updateValue("bandf", value);
|
self.updateValue("bandf", value);
|
||||||
if (resonance) {
|
if (resonance) {
|
||||||
this.updateValue("bandq", resonance);
|
self.updateValue("bandq", resonance);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
bandq: ["bandq", "bpq"],
|
bandq: ["bandq", "bpq"],
|
||||||
bpadsr: (depth: number, a: number, d: number, s: number, r: number) => {
|
bpadsr: function (
|
||||||
this.updateValue("bpenv", depth);
|
self: SoundEvent,
|
||||||
this.updateValue("bpattack", a);
|
depth: number,
|
||||||
this.updateValue("bpdecay", d);
|
a: number,
|
||||||
this.updateValue("bpsustain", s);
|
d: number,
|
||||||
this.updateValue("bprelease", r);
|
s: number,
|
||||||
return this;
|
r: number
|
||||||
|
) {
|
||||||
|
self.updateValue("bpenv", depth);
|
||||||
|
self.updateValue("bpattack", a);
|
||||||
|
self.updateValue("bpdecay", d);
|
||||||
|
self.updateValue("bpsustain", s);
|
||||||
|
self.updateValue("bprelease", r);
|
||||||
|
return self;
|
||||||
},
|
},
|
||||||
bpad: (depth: number, a: number, d: number) => {
|
bpad: function (self: SoundEvent, depth: number, a: number, d: number) {
|
||||||
this.updateValue("bpenv", depth);
|
self.updateValue("bpenv", depth);
|
||||||
this.updateValue("bpattack", a);
|
self.updateValue("bpattack", a);
|
||||||
this.updateValue("bpdecay", d);
|
self.updateValue("bpdecay", d);
|
||||||
this.updateValue("bpsustain", 0);
|
self.updateValue("bpsustain", 0);
|
||||||
this.updateValue("bprelease", 0);
|
self.updateValue("bprelease", 0);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
vib: ["vib"],
|
vib: ["vib"],
|
||||||
vibmod: ["vibmod"],
|
vibmod: ["vibmod"],
|
||||||
fm: (value: number | string) => {
|
fm: function (self: SoundEvent, value: number | string) {
|
||||||
if (typeof value === "number") {
|
if (typeof value === "number") {
|
||||||
this.values["fmi"] = value;
|
self.values["fmi"] = value;
|
||||||
} else {
|
} else {
|
||||||
let values = value.split(":");
|
let values = value.split(":");
|
||||||
this.values["fmi"] = parseFloat(values[0]);
|
self.values["fmi"] = parseFloat(values[0]);
|
||||||
if (values.length > 1) this.values["fmh"] = parseFloat(values[1]);
|
if (values.length > 1) self.values["fmh"] = parseFloat(values[1]);
|
||||||
}
|
}
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
loop: ["loop"],
|
loop: ["loop"],
|
||||||
loopBegin: ["loopBegin", "loopb"],
|
loopBegin: ["loopBegin", "loopb"],
|
||||||
@ -233,13 +276,13 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
begin: ["begin"],
|
begin: ["begin"],
|
||||||
end: ["end"],
|
end: ["end"],
|
||||||
gain: ["gain"],
|
gain: ["gain"],
|
||||||
dbgain: (value: number) => {
|
dbgain: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
db: (value: number) => {
|
db: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
self.updateValue("gain", Math.min(Math.pow(10, value / 20), 10));
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
velocity: ["velocity", "vel"],
|
velocity: ["velocity", "vel"],
|
||||||
pan: ["pan"],
|
pan: ["pan"],
|
||||||
@ -260,62 +303,71 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
roomlp: ["roomlp", "rlp"],
|
roomlp: ["roomlp", "rlp"],
|
||||||
roomdim: ["roomdim", "rdim"],
|
roomdim: ["roomdim", "rdim"],
|
||||||
sound: ["s", "sound"],
|
sound: ["s", "sound"],
|
||||||
size: (value: number) => {
|
size: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("roomsize", value);
|
self.updateValue("roomsize", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
sz: (value: number) => {
|
sz: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("roomsize", value);
|
self.updateValue("roomsize", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
comp: ["compressor", "cmp"],
|
comp: ["compressor", "cmp"],
|
||||||
ratio: (value: number) => {
|
ratio: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("compressorRatio", value);
|
self.updateValue("compressorRatio", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
knee: (value: number) => {
|
knee: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("compressorKnee", value);
|
self.updateValue("compressorKnee", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
compAttack: (value: number) => {
|
compAttack: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("compressorAttack", value);
|
self.updateValue("compressorAttack", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
compRelease: (value: number) => {
|
compRelease: function (self: SoundEvent, value: number) {
|
||||||
this.updateValue("compressorRelease", value);
|
self.updateValue("compressorRelease", value);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
stretch: (beat: number) => {
|
stretch: function (self: SoundEvent, beat: number) {
|
||||||
this.updateValue("unit", "c");
|
self.updateValue("unit", "c");
|
||||||
this.updateValue("speed", 1 / beat);
|
self.updateValue("speed", 1 / beat);
|
||||||
this.updateValue("cut", beat);
|
self.updateValue("cut", beat);
|
||||||
return this;
|
return self;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(
|
constructor(sound: string | string[] | SoundParams, public app: Editor) {
|
||||||
sound: string | string[] | SoundParams,
|
|
||||||
public app: Editor,
|
|
||||||
) {
|
|
||||||
super(app);
|
super(app);
|
||||||
this.nudge = app.dough_nudge / 100;
|
this.nudge = app.dough_nudge / 100;
|
||||||
|
|
||||||
for (const [methodName, keys] of Object.entries(this.methodMap)) {
|
for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
|
||||||
if (Symbol.iterator in Object(keys)) {
|
if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
|
||||||
for (const key of keys as string[]) {
|
for (const key of keys as string[]) {
|
||||||
// @ts-ignore
|
// Using arrow function to maintain 'this' context
|
||||||
this[key] = (value: number) => this.updateValue(keys[0], value);
|
this[key] = (value: number) => this.updateValue(keys[0], value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
// this[methodName] = keys.bind(this);
|
||||||
this[methodName] = keys;
|
this[methodName] = (...args) => keys(this, ...args);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// for (const [methodName, keys] of Object.entries(SoundEvent.methodMap)) {
|
||||||
|
// if (typeof keys === "object" && Symbol.iterator in Object(keys)) {
|
||||||
|
// for (const key of keys as string[]) {
|
||||||
|
// // @ts-ignore
|
||||||
|
// this[key] = (value: number) => this.updateValue(this, keys[0], value);
|
||||||
|
// }
|
||||||
|
// } else {
|
||||||
|
// // @ts-ignore
|
||||||
|
// this[methodName] = keys;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
this.values = this.processSound(sound);
|
this.values = this.processSound(sound);
|
||||||
}
|
}
|
||||||
|
|
||||||
private processSound = (
|
private processSound = (
|
||||||
sound: string | string[] | SoundParams | SoundParams[],
|
sound: string | string[] | SoundParams | SoundParams[]
|
||||||
): SoundParams => {
|
): SoundParams => {
|
||||||
if (Array.isArray(sound) && typeof sound[0] === "string") {
|
if (Array.isArray(sound) && typeof sound[0] === "string") {
|
||||||
const s: string[] = [];
|
const s: string[] = [];
|
||||||
@ -357,15 +409,6 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private updateValue<T>(
|
|
||||||
key: string,
|
|
||||||
value: T | T[] | SoundParams[] | null,
|
|
||||||
): this {
|
|
||||||
if (value == null) return this;
|
|
||||||
this.values[key] = value;
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
// AbstactEvent overrides
|
// AbstactEvent overrides
|
||||||
// ================================================================================
|
// ================================================================================
|
||||||
@ -395,7 +438,7 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
(event.key as number) || "C4",
|
(event.key as number) || "C4",
|
||||||
(event.pitch as number) || 0,
|
(event.pitch as number) || 0,
|
||||||
(event.parsedScale as number[]) || event.scale || "MAJOR",
|
(event.parsedScale as number[]) || event.scale || "MAJOR",
|
||||||
(event.octave as number) || 0,
|
(event.octave as number) || 0
|
||||||
);
|
);
|
||||||
event.note = note;
|
event.note = note;
|
||||||
event.freq = midiToFreq(note);
|
event.freq = midiToFreq(note);
|
||||||
@ -415,7 +458,7 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
public invert = (howMany: number = 0) => {
|
public invert = (howMany: number = 0) => {
|
||||||
if (this.values.chord) {
|
if (this.values.chord) {
|
||||||
let notes = this.values.chord.map(
|
let notes = this.values.chord.map(
|
||||||
(obj: { [key: string]: number }) => obj.note,
|
(obj: { [key: string]: number }) => obj.note
|
||||||
);
|
);
|
||||||
notes = howMany < 0 ? [...notes].reverse() : notes;
|
notes = howMany < 0 ? [...notes].reverse() : notes;
|
||||||
for (let i = 0; i < Math.abs(howMany); i++) {
|
for (let i = 0; i < Math.abs(howMany); i++) {
|
||||||
@ -457,7 +500,7 @@ export class SoundEvent extends AudibleEvent {
|
|||||||
superdough(
|
superdough(
|
||||||
filteredEvent,
|
filteredEvent,
|
||||||
this.nudge - this.app.clock.deviation,
|
this.nudge - this.app.clock.deviation,
|
||||||
filteredEvent.dur,
|
filteredEvent.dur
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user