legacy code buh bye
This commit is contained in:
parent
f194f4c9d4
commit
48b441e25c
5 changed files with 3 additions and 389 deletions
|
@ -153,26 +153,6 @@ export class Store {
|
||||||
'gain': [-0.34, 2.49, 0.23, -0.49, 0.23, -0.12, 0.32, -0.29, 0.33, 0.19, -0.18, -1.27, -0.11, 0.25, -0.18, -0.53, 0.34, 1.32, 1.78, 0.41, -0.28]
|
'gain': [-0.34, 2.49, 0.23, -0.49, 0.23, -0.12, 0.32, -0.29, 0.33, 0.19, -0.18, -1.27, -0.11, 0.25, -0.18, -0.53, 0.34, 1.32, 1.78, 0.41, -0.28]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"spatial": false,
|
|
||||||
"spatial_properties": {
|
|
||||||
"presets": [],
|
|
||||||
"gain": 0.8,
|
|
||||||
"listener_position": [0, 0, 0],
|
|
||||||
"audio_position": [0, 0, 0],
|
|
||||||
"room_dimensions": {
|
|
||||||
"width": 32,
|
|
||||||
"height": 12,
|
|
||||||
"depth": 32
|
|
||||||
},
|
|
||||||
"room_materials": {
|
|
||||||
"left": 'metal',
|
|
||||||
"right": 'metal',
|
|
||||||
"front": 'brick-bare',
|
|
||||||
"back": 'brick-bare',
|
|
||||||
"down": 'acoustic-ceiling-tiles',
|
|
||||||
"up": 'acoustic-ceiling-tiles',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"equalizer": {
|
"equalizer": {
|
||||||
'preset': "default",
|
'preset': "default",
|
||||||
'frequencies': [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
|
'frequencies': [32, 63, 125, 250, 500, 1000, 2000, 4000, 8000, 16000],
|
||||||
|
|
|
@ -158,9 +158,6 @@ const CiderAudio = {
|
||||||
CiderAudio.audioNodes.spatialNode.buffer = await CiderAudio.context.decodeAudioData(bufferedImpulse);
|
CiderAudio.audioNodes.spatialNode.buffer = await CiderAudio.context.decodeAudioData(bufferedImpulse);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
spatialOff: function () {
|
|
||||||
CiderAudio.hierarchical_loading();
|
|
||||||
},
|
|
||||||
intelliGainComp_h0_0: function () {
|
intelliGainComp_h0_0: function () {
|
||||||
let filters = []; const precisionHz = 12;
|
let filters = []; const precisionHz = 12;
|
||||||
if (CiderAudio.audioNodes.audioBands !== null) { filters = filters.concat(CiderAudio.audioNodes.audioBands) }
|
if (CiderAudio.audioNodes.audioBands !== null) { filters = filters.concat(CiderAudio.audioNodes.audioBands) }
|
||||||
|
|
|
@ -1,355 +0,0 @@
|
||||||
<script type="text/x-template" id="spatial-properties">
|
|
||||||
<div class="modal-fullscreen spatialproperties-panel" @click.self="close()" @contextmenu.self="close()">
|
|
||||||
<div class="modal-window" v-if="ready">
|
|
||||||
<div class="modal-header">
|
|
||||||
<div class="modal-title">{{$root.getLz('spatial.spatialProperties')}}</div>
|
|
||||||
<button class="close-btn" @click="close()" :aria-label="$root.getLz('action.close')"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-content">
|
|
||||||
<template v-if="roomEditType == 'dimensions'">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><h3>{{$root.getLz('spatial.roomDimensions')}}</h3></div>
|
|
||||||
<div class="col-auto flex-center">
|
|
||||||
<button class="md-btn" @click="roomEditType = 'positions'">{{$root.getLz('spatial.setPositions')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
{{$root.getLz('spatial.width')}}
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="room_dimensions.width" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="room_dimensions.width" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
{{$root.getLz('spatial.height')}}
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="room_dimensions.height" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="room_dimensions.height" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
{{$root.getLz('spatial.depth')}}
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="room_dimensions.depth" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="room_dimensions.depth" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label v-if="!app.cfg.audio.normalization">
|
|
||||||
{{$root.getLz('spatial.gain')}}
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="app.cfg.audio.spatial_properties.gain" step="0.1"/>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col visual-container">
|
|
||||||
<div class="visual" :style="objectContainerStyle()">
|
|
||||||
<div class="face" :style="[faceStyle()]"></div>
|
|
||||||
<div class="face" :style="[faceStyle(), topFaceStyle()]"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="roomEditType == 'positions'">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><h3>{{$root.getLz('spatial.roomPositions')}}</h3></div>
|
|
||||||
<div class="col-auto flex-center">
|
|
||||||
<button class="md-btn" @click="roomEditType = 'dimensions'">{{$root.getLz('spatial.setDimensions')}}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col">
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
X ({{$root.getLz('spatial.listener')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="listener_position[0]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="listener_position[0]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
Y ({{$root.getLz('spatial.listener')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="listener_position[1]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="listener_position[1]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
Z ({{$root.getLz('spatial.listener')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="listener_position[2]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="listener_position[2]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
X ({{$root.getLz('spatial.audioSource')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="audio_position[0]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="audio_position[0]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
Y ({{$root.getLz('spatial.audioSource')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="audio_position[1]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="audio_position[1]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
Z ({{$root.getLz('spatial.audioSource')}})
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<input type="range" class="md-slider" min="0" max="100" @change="setRoom()" style="width: 100%;"
|
|
||||||
v-model="audio_position[2]" step="1"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-3 flex-center">
|
|
||||||
<input type="number" min="0" @change="setRoom()" style="width: 100%;text-align: center"
|
|
||||||
v-model="audio_position[2]" step="1"/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="col visual-container">
|
|
||||||
<div class="visual">
|
|
||||||
<div class="face" :style="[faceStyle()]"></div>
|
|
||||||
<div class="face" :style="[faceStyle(), topFaceStyle()]"></div>
|
|
||||||
|
|
||||||
<!-- <div class="listener" :style="[listenerStyle()]">L</div> -->
|
|
||||||
<!-- <div class="audiosource" :style="[audioSourceStyle()]">A</div> -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"><h3>{{$root.getLz('spatial.roomMaterials')}}</h3></div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"></div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.up')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.up">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col"></div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col flex-center">
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.left')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.left">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.front')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.front">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.back')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.back">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.right')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.right">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col"></div>
|
|
||||||
<div class="col flex-center">
|
|
||||||
<label>
|
|
||||||
{{$root.getLz('spatial.down')}}
|
|
||||||
<select class="md-select" @change="setRoom()"
|
|
||||||
v-model="room_materials.down">
|
|
||||||
<option v-for="prop in roomProps" :value="prop">{{ prop }}</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
Vue.component('spatial-properties', {
|
|
||||||
template: '#spatial-properties',
|
|
||||||
data: function () {
|
|
||||||
return {
|
|
||||||
app: this.$root,
|
|
||||||
room_dimensions: null,
|
|
||||||
room_materials: null,
|
|
||||||
listener_position: null,
|
|
||||||
audio_position: null,
|
|
||||||
roomEditType: "dimensions",
|
|
||||||
roomProps: [
|
|
||||||
'transparent',
|
|
||||||
'acoustic-ceiling-tiles',
|
|
||||||
'brick-bare',
|
|
||||||
'brick-painted',
|
|
||||||
'concrete-block-coarse',
|
|
||||||
'concrete-block-painted',
|
|
||||||
'curtain-heavy',
|
|
||||||
'fiber-glass-insulation',
|
|
||||||
'glass-thin',
|
|
||||||
'glass-thick',
|
|
||||||
'grass',
|
|
||||||
'linoleum-on-concrete',
|
|
||||||
'marble',
|
|
||||||
'metal',
|
|
||||||
'parquet-on-concrete',
|
|
||||||
'plaster-smooth',
|
|
||||||
'plywood-panel',
|
|
||||||
'polished-concrete-or-tile',
|
|
||||||
'sheetrock',
|
|
||||||
'water-or-ice-surface',
|
|
||||||
'wood-ceiling',
|
|
||||||
'wood-panel',
|
|
||||||
'uniform'
|
|
||||||
],
|
|
||||||
visualMultiplier: 4,
|
|
||||||
ready: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
props: {},
|
|
||||||
mounted() {
|
|
||||||
this.room_dimensions = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.room_dimensions))
|
|
||||||
this.room_materials = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.room_materials))
|
|
||||||
this.audio_position = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.audio_position))
|
|
||||||
this.listener_position = JSON.parse(JSON.stringify(this.$root.cfg.audio.spatial_properties.listener_position))
|
|
||||||
if (typeof this.app.mk.nowPlayingItem != "undefined") {
|
|
||||||
this.setRoom()
|
|
||||||
}
|
|
||||||
this.ready = true
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
listenerStyle() {
|
|
||||||
let style = {
|
|
||||||
transform: `rotateX(60deg) rotateZ(-45deg) translateX(${this.listener_position[0]}px) translateY(${this.listener_position[2]}px) translateZ(${100 + +this.listener_position[1]}px)`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
},
|
|
||||||
audioSourceStyle() {
|
|
||||||
let style = {
|
|
||||||
transform: `rotateX(60deg) rotateZ(-45deg) translateX(${this.audio_position[0]}px) translateY(${this.audio_position[2]}px) translateZ(${100 + +this.audio_position[1]}px)`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
},
|
|
||||||
topFaceStyle() {
|
|
||||||
let style = {
|
|
||||||
transform: `rotateX(60deg) rotateZ(-45deg) translateZ(${this.room_dimensions.height * this.visualMultiplier}px)`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
},
|
|
||||||
objectContainerStyle() {
|
|
||||||
let scale = 1
|
|
||||||
if (this.room_dimensions.width * this.visualMultiplier > 300) {
|
|
||||||
scale = 300 / (this.room_dimensions.width * this.visualMultiplier)
|
|
||||||
}
|
|
||||||
let style = {
|
|
||||||
transform: `scale(${scale})`
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
},
|
|
||||||
faceStyle() {
|
|
||||||
let style = {
|
|
||||||
width: `${this.room_dimensions.width * this.visualMultiplier}px`,
|
|
||||||
height: `${this.room_dimensions.depth * this.visualMultiplier}px`,
|
|
||||||
}
|
|
||||||
return style
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
this.$root.cfg.audio.spatial_properties.room_dimensions = this.room_dimensions
|
|
||||||
this.$root.cfg.audio.spatial_properties.room_materials = this.room_materials
|
|
||||||
this.$root.cfg.audio.spatial_properties.audio_position = this.audio_position
|
|
||||||
this.$root.cfg.audio.spatial_properties.listener_position = this.listener_position
|
|
||||||
app.resetState()
|
|
||||||
},
|
|
||||||
setRoom() {
|
|
||||||
window.CiderAudio.audioNodes.spatialNode.setRoomProperties(this.room_dimensions, this.room_materials);
|
|
||||||
CiderAudio.audioNodes.spatialInput.setPosition(...this.audio_position)
|
|
||||||
CiderAudio.audioNodes.spatialNode.setListenerPosition(...this.listener_position)
|
|
||||||
if (!this.app.cfg.audio.normalization) {
|
|
||||||
window.CiderAudio.audioNodes.gainNode.gain.value = app.cfg.audio.spatial_properties.gain
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
|
@ -198,14 +198,6 @@
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleSpatial: function () {
|
|
||||||
if (app.cfg.audio.maikiwiAudio.spatial) {
|
|
||||||
CiderAudio.spatialOn()
|
|
||||||
CiderAudio.hierarchical_loading();
|
|
||||||
} else {
|
|
||||||
CiderAudio.spatialOff()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toggleMaikiwiSpatial: function () {
|
toggleMaikiwiSpatial: function () {
|
||||||
if (app.cfg.audio.maikiwiAudio.spatial === true) {
|
if (app.cfg.audio.maikiwiAudio.spatial === true) {
|
||||||
CiderAudio.spatialOn()
|
CiderAudio.spatialOn()
|
||||||
|
|
|
@ -329,7 +329,7 @@
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" v-model="app.cfg.audio.normalization"
|
<input type="checkbox" v-model="app.cfg.audio.normalization"
|
||||||
v-on:change="toggleNormalization"
|
v-on:change="toggleNormalization"
|
||||||
:disabled="app.cfg.audio.spatial === true || app.cfg.audio.maikiwiAudio.spatial === true || app.cfg.audio.maikiwiAudio.ciderPPE === true || app.cfg.audio.maikiwiAudio.atmosphereRealizer1 === true || app.cfg.audio.maikiwiAudio.atmosphereRealizer2 === true"
|
:disabled="app.cfg.audio.maikiwiAudio.spatial === true || app.cfg.audio.maikiwiAudio.ciderPPE === true || app.cfg.audio.maikiwiAudio.atmosphereRealizer1 === true || app.cfg.audio.maikiwiAudio.atmosphereRealizer2 === true"
|
||||||
switch/>
|
switch/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1393,7 +1393,7 @@
|
||||||
if (app.cfg.audio.normalization === true) {
|
if (app.cfg.audio.normalization === true) {
|
||||||
CiderAudio.normalizerOn()
|
CiderAudio.normalizerOn()
|
||||||
}
|
}
|
||||||
if (app.cfg.audio.spatial === true) {
|
if (app.cfg.audio.maikiwiAudio.spatial === true) {
|
||||||
CiderAudio.spatialOn()
|
CiderAudio.spatialOn()
|
||||||
CiderAudio.hierarchical_loading();
|
CiderAudio.hierarchical_loading();
|
||||||
}
|
}
|
||||||
|
@ -1404,7 +1404,7 @@
|
||||||
if (app.cfg.audio.normalization === true) {
|
if (app.cfg.audio.normalization === true) {
|
||||||
CiderAudio.normalizerOn()
|
CiderAudio.normalizerOn()
|
||||||
}
|
}
|
||||||
if (app.cfg.audio.spatial === true) {
|
if (app.cfg.audio.maikiwiAudio.spatial === true) {
|
||||||
CiderAudio.spatialOn()
|
CiderAudio.spatialOn()
|
||||||
CiderAudio.hierarchical_loading();
|
CiderAudio.hierarchical_loading();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue