mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
wip
This commit is contained in:
parent
f48f17e9a4
commit
4ebecc2427
7 changed files with 107 additions and 71 deletions
|
|
@ -99,15 +99,16 @@ impl<'a> LauncherGridView<'a> {
|
||||||
if let Some(scene) = self.state.scenes.get(index) {
|
if let Some(scene) = self.state.scenes.get(index) {
|
||||||
let hi = (track + 1 == self.state.cursor.0) &&
|
let hi = (track + 1 == self.state.cursor.0) &&
|
||||||
(index + 1 == self.state.cursor.1);
|
(index + 1 == self.state.cursor.1);
|
||||||
let style = Some(self.highlight(hi));
|
let style = Some(self.highlight(hi));
|
||||||
if let Some(Some(clip)) = scene.clips.get(track) {
|
let clip = scene.clips.get(track);
|
||||||
|
if let Some(Some(clip)) = clip {
|
||||||
if let Some(phrase) = self.state.tracks[track].sequencer.state().sequences.get(*clip) {
|
if let Some(phrase) = self.state.tracks[track].sequencer.state().sequences.get(*clip) {
|
||||||
format!("⯈ {}", phrase.name).blit(self.buf, x, y + y2, style);
|
format!("⯈ {}", phrase.name).blit(self.buf, x, y + y2, style);
|
||||||
} else {
|
} else {
|
||||||
"????".blit(self.buf, x, y + y2, Some(Style::default().dim()))
|
"????".blit(self.buf, x, y + y2, Some(Style::default().dim()))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"....".blit(self.buf, x, y + y2, Some(Style::default().dim()))
|
" ·········".blit(self.buf, x, y + y2, Some(Style::default().dim()))
|
||||||
}
|
}
|
||||||
if hi {
|
if hi {
|
||||||
draw_box_styled(self.buf, Rect {
|
draw_box_styled(self.buf, Rect {
|
||||||
|
|
@ -116,12 +117,25 @@ impl<'a> LauncherGridView<'a> {
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 3
|
height: 3
|
||||||
}, style);
|
}, style);
|
||||||
|
if self.focused {
|
||||||
|
let style = Some(self.highlight(hi).bold().yellow());
|
||||||
|
if let Some(Some(_)) = clip { } else {
|
||||||
|
"+ Add clip".blit(self.buf, x + 1, y + y2, Some(Style::default().dim()));
|
||||||
|
"+".blit(self.buf, x + 1, y + y2, style);
|
||||||
|
}
|
||||||
|
"↑".blit(self.buf, x + 6, y + y2 - 1, style);
|
||||||
|
"↓".blit(self.buf, x + 6, y + y2 + 1, style);
|
||||||
|
",".blit(self.buf, x - 1, y + y2, style);
|
||||||
|
"←".blit(self.buf, x - 2, y + y2, style);
|
||||||
|
".".blit(self.buf, x + 12, y + y2, style);
|
||||||
|
"→".blit(self.buf, x + 13, y + y2, style);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
y2 = y2 + 1;
|
y2 = y2 + 1;
|
||||||
}
|
}
|
||||||
"+Add clip…".blit(self.buf, x, y + y2, Some(Style::default().dim()));
|
" + Add clip".blit(self.buf, x, y + y2, Some(Style::default().dim()));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight (&self, highlight: bool) -> Style {
|
fn highlight (&self, highlight: bool) -> Style {
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ fn add_track (state: &mut Launcher) -> Usually<bool> {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
fn delete_track (state: &mut Launcher) -> Usually<bool> {
|
fn delete_track (state: &mut Launcher) -> Usually<bool> {
|
||||||
if state.cursor.0 >= 1 {
|
if state.tracks.len() > 0 && state.cursor.0 >= 1 {
|
||||||
state.tracks.remove(state.cursor.0 - 1);
|
state.tracks.remove(state.cursor.0 - 1);
|
||||||
state.cursor.0 = state.cursor.0.min(state.tracks.len());
|
state.cursor.0 = state.cursor.0.min(state.tracks.len());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,7 @@ impl Launcher {
|
||||||
recording: false,
|
recording: false,
|
||||||
overdub: true,
|
overdub: true,
|
||||||
transport,
|
transport,
|
||||||
cursor: (1, 1),
|
cursor: (0, 0),
|
||||||
position: 0,
|
position: 0,
|
||||||
scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]),
|
scenes: scenes.unwrap_or_else(||vec![Scene::new(&"Scene 1", &[None])]),
|
||||||
tracks: if let Some(tracks) = tracks { tracks } else { vec![
|
tracks: if let Some(tracks) = tracks { tracks } else { vec![
|
||||||
|
|
@ -229,14 +229,26 @@ fn draw_section_sequencer (state: &Launcher, buf: &mut Buffer, area: Rect) -> Us
|
||||||
};
|
};
|
||||||
if let Some(track) = state.tracks.get(state.col().saturating_sub(1)) {
|
if let Some(track) = state.tracks.get(state.col().saturating_sub(1)) {
|
||||||
let state = track.sequencer.state();
|
let state = track.sequencer.state();
|
||||||
crate::device::sequencer::horizontal::keys(&state, buf, Rect { x, y: y + 1, width, height })?;
|
let keys_area = Rect { x, y: y + 1, width, height };
|
||||||
crate::device::sequencer::horizontal::lanes(&state, buf, x, y + 1, width);
|
crate::device::sequencer::horizontal::keys(buf, keys_area, state.note_axis.1)?;
|
||||||
crate::device::sequencer::horizontal::cursor(
|
if let Some(phrase) = state.phrase() {
|
||||||
&state, buf, x, y + 1, match view {
|
crate::device::sequencer::horizontal::lanes(buf, x, y + 1,
|
||||||
LauncherView::Sequencer => Style::default().green().not_dim(),
|
&phrase,
|
||||||
_ => Style::default().green().dim(),
|
state.timebase.ppq() as u32,
|
||||||
}
|
state.resolution as u32,
|
||||||
);
|
state.time_axis.0 as u32,
|
||||||
|
state.time_axis.1 as u32,
|
||||||
|
state.note_axis.0 as u32,
|
||||||
|
state.note_axis.1 as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let cursor_style = match view {
|
||||||
|
LauncherView::Sequencer => Style::default().green().not_dim(),
|
||||||
|
_ => Style::default().green().dim(),
|
||||||
|
};
|
||||||
|
crate::device::sequencer::horizontal::cursor(buf, x, y + 1, cursor_style,
|
||||||
|
state.time_cursor,
|
||||||
|
state.note_cursor);
|
||||||
}
|
}
|
||||||
Ok(area)
|
Ok(area)
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +258,6 @@ fn draw_highlight (buf: &mut Buffer, highlight: &Option<Rect>, style: Style) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn draw_section_chains (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
fn draw_section_chains (state: &Launcher, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
let Rect { x, y, width, height } = area;
|
|
||||||
let style = Some(Style::default().green().dim());
|
let style = Some(Style::default().green().dim());
|
||||||
let chain = state.active_chain();
|
let chain = state.active_chain();
|
||||||
let plugins = if let Some(chain) = &chain {
|
let plugins = if let Some(chain) = &chain {
|
||||||
|
|
@ -9,11 +9,23 @@ pub fn draw (
|
||||||
) -> Usually<Rect> {
|
) -> Usually<Rect> {
|
||||||
area.x = area.x + 13;
|
area.x = area.x + 13;
|
||||||
let Rect { x, y, width, .. } = area;
|
let Rect { x, y, width, .. } = area;
|
||||||
keys(s, buf, area)?;
|
keys(buf, area, s.note_axis.1)?;
|
||||||
timer(s, buf, x, y, beat);
|
timer(s, buf, x, y, beat);
|
||||||
let height = 32.max(s.note_axis.1 - s.note_axis.0) / 2;
|
let height = 32.max(s.note_axis.1 - s.note_axis.0) / 2;
|
||||||
lanes(s, buf, x, y, width);
|
if let Some(phrase) = s.phrase() {
|
||||||
cursor(s, buf, x, y, Style::default().green().not_dim());
|
lanes(buf, x, y,
|
||||||
|
phrase,
|
||||||
|
s.timebase.ppq() as u32,
|
||||||
|
s.resolution as u32,
|
||||||
|
s.time_axis.0 as u32,
|
||||||
|
s.time_axis.1 as u32,
|
||||||
|
s.note_axis.0 as u32,
|
||||||
|
s.note_axis.1 as u32,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cursor(buf, x, y, Style::default().green().not_dim(),
|
||||||
|
s.time_cursor,
|
||||||
|
s.note_cursor);
|
||||||
footer(s, buf, x, y, width, height);
|
footer(s, buf, x, y, width, height);
|
||||||
Ok(Rect {
|
Ok(Rect {
|
||||||
x: x - 13,
|
x: x - 13,
|
||||||
|
|
@ -24,7 +36,6 @@ pub fn draw (
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn timer (s: &Sequencer, buf: &mut Buffer, x: u16, y: u16, beat: usize) {
|
pub fn timer (s: &Sequencer, buf: &mut Buffer, x: u16, y: u16, beat: usize) {
|
||||||
let bw = Style::default().dim();
|
|
||||||
let (time0, time1) = s.time_axis;
|
let (time0, time1) = s.time_axis;
|
||||||
for step in time0..time1 {
|
for step in time0..time1 {
|
||||||
buf.set_string(x + 6 + step, y - 1, &"-", if beat % s.steps == step as usize {
|
buf.set_string(x + 6 + step, y - 1, &"-", if beat % s.steps == step as usize {
|
||||||
|
|
@ -35,11 +46,9 @@ pub fn timer (s: &Sequencer, buf: &mut Buffer, x: u16, y: u16, beat: usize) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keys (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
pub fn keys (buf: &mut Buffer, area: Rect, note1: u16) -> Usually<Rect> {
|
||||||
let bw = Style::default().dim();
|
let bw = Style::default().dim();
|
||||||
let Rect { x, y, width, height } = area;
|
let Rect { x, y, width, height } = area;
|
||||||
let (note0, note1) = s.note_axis;
|
|
||||||
let (time0, time1) = s.time_axis;
|
|
||||||
let h = 32.max(height.saturating_sub(2)*2)/2;
|
let h = 32.max(height.saturating_sub(2)*2)/2;
|
||||||
for i in 0..h {
|
for i in 0..h {
|
||||||
let y = y + i;
|
let y = y + i;
|
||||||
|
|
@ -55,68 +64,64 @@ pub fn keys (s: &Sequencer, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
Ok(area)
|
Ok(area)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn lanes (s: &Sequencer, buf: &mut Buffer, x: u16, y: u16, width: u16) {
|
pub fn lanes (
|
||||||
|
buf: &mut Buffer,
|
||||||
|
x: u16,
|
||||||
|
y: u16,
|
||||||
|
phrase: &Phrase,
|
||||||
|
ppq: u32,
|
||||||
|
time_zoom: u32,
|
||||||
|
time0: u32,
|
||||||
|
time1: u32,
|
||||||
|
note0: u32,
|
||||||
|
note1: u32,
|
||||||
|
) {
|
||||||
let bg = Style::default();
|
let bg = Style::default();
|
||||||
let bw = bg.dim();
|
let bw = bg.dim();
|
||||||
let wh = bg.white();
|
let wh = bg.white();
|
||||||
let ppq = s.timebase.ppq() as u32;
|
//let (time0, time1) = s.time_axis;
|
||||||
let (time0, time1) = s.time_axis;
|
//let (note0, note1) = s.note_axis;
|
||||||
let (note0, note1) = s.note_axis;
|
//let resolution = s.resolution;
|
||||||
let notes = &s.sequences[s.sequence].notes;
|
|
||||||
for step in time0..time1 {
|
for step in time0..time1 {
|
||||||
let (a, b) = (
|
let x = x as u32 + 5 + step;
|
||||||
(step + 0) as u32 * ppq / s.resolution as u32,
|
let (a, b) = ((step + 0) * ppq / time_zoom, (step + 1) * ppq / time_zoom,);
|
||||||
(step + 1) as u32 * ppq / s.resolution as u32,
|
if step % time_zoom == 0 {
|
||||||
);
|
format!("{}", step + 1).blit(buf, x as u16, y - 1, None);
|
||||||
if step % s.resolution as u16 == 0 {
|
|
||||||
buf.set_string(x + 5 + step, y - 1, &format!("{}", step + 1), Style::default());
|
|
||||||
}
|
}
|
||||||
let h = (note1 - note0)/2;
|
let h = (note1-note0)/2;
|
||||||
for k in 0..h {
|
for k in 0..h {
|
||||||
let (character, style) = match (
|
let (character, style) = match (
|
||||||
contains_note_on(&s.sequences[s.sequence],
|
contains_note_on(phrase, u7::from_int_lossy((note0 + k * 2 + 0) as u8), a, b),
|
||||||
::midly::num::u7::from_int_lossy((note0 + k * 2 + 0) as u8),
|
contains_note_on(phrase, u7::from_int_lossy((note0 + k * 2 + 1) as u8), a, b),
|
||||||
a, b),
|
|
||||||
contains_note_on(&s.sequences[s.sequence],
|
|
||||||
::midly::num::u7::from_int_lossy((note0 + k * 2 + 1) as u8),
|
|
||||||
a, b),
|
|
||||||
) {
|
) {
|
||||||
(true, true) => ("█", wh),
|
(true, true) => ("█", wh),
|
||||||
(true, false) => ("▀", wh),
|
(true, false) => ("▀", wh),
|
||||||
(false, true) => ("▄", wh),
|
(false, true) => ("▄", wh),
|
||||||
(false, false) => ("·", bw),
|
(false, false) => ("·", bw),
|
||||||
};
|
};
|
||||||
//let (character, style) = ("▄", bg);
|
let y = y as u32 + h + k;
|
||||||
buf.set_string(x + 5 + step, y + h - k, character, style);
|
character.blit(buf, x as u16, y as u16, Some(style));
|
||||||
}
|
}
|
||||||
//for (_, (_, events)) in notes.range(time_start..time_end).enumerate() {
|
|
||||||
//if events.len() > 0 {
|
|
||||||
//buf.set_string(x + 6 + step as u16, y, "█", wh);
|
|
||||||
//}
|
|
||||||
//}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor (s: &Sequencer, buf: &mut Buffer, x: u16, y: u16, style: Style) {
|
pub fn cursor (buf: &mut Buffer, x: u16, y: u16, style: Style, time_cursor: u16, note_cursor: u16) {
|
||||||
buf.set_string(
|
buf.set_string(
|
||||||
x + 5 + s.time_cursor,
|
x + 5 + time_cursor,
|
||||||
y + s.note_cursor / 2,
|
y + note_cursor / 2,
|
||||||
if s.note_cursor % 2 == 0 { "▀" } else { "▄" },
|
if note_cursor % 2 == 0 { "▀" } else { "▄" },
|
||||||
style
|
style
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn footer (s: &Sequencer, buf: &mut Buffer, mut x: u16, y: u16, width: u16, height: u16) {
|
pub fn footer (s: &Sequencer, buf: &mut Buffer, mut x: u16, y: u16, width: u16, height: u16) {
|
||||||
let bw = Style::default().dim();
|
|
||||||
let bg = Style::default();
|
|
||||||
let (note0, note1) = s.note_axis;
|
|
||||||
buf.set_string(x, y + height, format!("├{}┤", "-".repeat((width - 2).into())),
|
buf.set_string(x, y + height, format!("├{}┤", "-".repeat((width - 2).into())),
|
||||||
Style::default().dim());
|
Style::default().dim());
|
||||||
buf.set_string(x, y + height + 2, format!("├{}┤", "-".repeat((width - 2).into())),
|
buf.set_string(x, y + height + 2, format!("├{}┤", "-".repeat((width - 2).into())),
|
||||||
Style::default().dim());
|
Style::default().dim());
|
||||||
x = x + 2;
|
x = x + 2;
|
||||||
{
|
{
|
||||||
for (i, [letter, title, value]) in [
|
for (_, [letter, title, value]) in [
|
||||||
["S", &format!("ync"), &format!("<4/4>")],
|
["S", &format!("ync"), &format!("<4/4>")],
|
||||||
["Q", &format!("uant"), &format!("<1/{}>", 4 * s.resolution)],
|
["Q", &format!("uant"), &format!("<1/{}>", 4 * s.resolution)],
|
||||||
["N", &format!("ote"), &format!("{} ({}-{})",
|
["N", &format!("ote"), &format!("{} ({}-{})",
|
||||||
|
|
|
||||||
|
|
@ -33,10 +33,7 @@ pub struct Sequencer {
|
||||||
pub midi_out: Port<MidiOut>,
|
pub midi_out: Port<MidiOut>,
|
||||||
|
|
||||||
/// Holds info about tempo
|
/// Holds info about tempo
|
||||||
timebase: Arc<Timebase>,
|
pub timebase: Arc<Timebase>,
|
||||||
/// Sequencer resolution, e.g. 16 steps per beat.
|
|
||||||
/// FIXME: grid in ppm will simplify calculations
|
|
||||||
resolution: usize,
|
|
||||||
/// Steps in sequence, e.g. 64 16ths = 4 beat loop.
|
/// Steps in sequence, e.g. 64 16ths = 4 beat loop.
|
||||||
/// FIXME: play start / end / loop in ppm
|
/// FIXME: play start / end / loop in ppm
|
||||||
steps: usize,
|
steps: usize,
|
||||||
|
|
@ -59,13 +56,16 @@ pub struct Sequencer {
|
||||||
/// Display mode
|
/// Display mode
|
||||||
mode: SequencerView,
|
mode: SequencerView,
|
||||||
/// Range of notes to display
|
/// Range of notes to display
|
||||||
note_axis: (u16, u16),
|
pub note_axis: (u16, u16),
|
||||||
/// Position of cursor within note range
|
/// Position of cursor within note range
|
||||||
note_cursor: u16,
|
pub note_cursor: u16,
|
||||||
|
/// Sequencer resolution, e.g. 16 steps per beat.
|
||||||
|
/// FIXME: grid in ppm will simplify calculations
|
||||||
|
pub resolution: usize,
|
||||||
/// Range of time steps to display
|
/// Range of time steps to display
|
||||||
time_axis: (u16, u16),
|
pub time_axis: (u16, u16),
|
||||||
/// Position of cursor within time range
|
/// Position of cursor within time range
|
||||||
time_cursor: u16,
|
pub time_cursor: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
|
|
@ -111,6 +111,10 @@ impl Sequencer {
|
||||||
}).activate(client)
|
}).activate(client)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn phrase <'a> (&'a self) -> Option<&'a Phrase> {
|
||||||
|
self.sequences.get(self.sequence)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
pub fn process (&mut self, _: &Client, scope: &ProcessScope) -> Control {
|
||||||
|
|
||||||
// Prepare output buffer
|
// Prepare output buffer
|
||||||
18
src/main.rs
18
src/main.rs
|
|
@ -29,11 +29,11 @@ fn main () -> Result<(), Box<dyn Error>> {
|
||||||
ppq: AtomicUsize::new(96),
|
ppq: AtomicUsize::new(96),
|
||||||
});
|
});
|
||||||
let ppq = timebase.ppq() as u32;
|
let ppq = timebase.ppq() as u32;
|
||||||
run(Launcher::new("Launcher#0", &timebase,
|
let app = Launcher::new("Launcher#0", &timebase,
|
||||||
Some(vec![
|
Some(vec![
|
||||||
|
|
||||||
Track::new("Kick", &timebase, Some(vec![
|
Track::new("Kick", &timebase, Some(vec![
|
||||||
//Sampler::new("Sampler")?.boxed(),
|
//Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
||||||
Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
|
||||||
]), Some(vec![
|
]), Some(vec![
|
||||||
Phrase::new("HelloKick", ppq * 4, Some(BTreeMap::from([
|
Phrase::new("HelloKick", ppq * 4, Some(BTreeMap::from([
|
||||||
( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
( ppq * 0, vec![MidiMessage::NoteOn { key: 36.into(), vel: 100.into() }] ),
|
||||||
|
|
@ -58,17 +58,19 @@ fn main () -> Result<(), Box<dyn Error>> {
|
||||||
])))
|
])))
|
||||||
]))?,
|
]))?,
|
||||||
|
|
||||||
|
//Sampler::new("Sampler")?.boxed(),
|
||||||
//Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
//Plugin::lv2("Kick/ChowKick", "file:///home/user/.lv2/ChowKick.lv2", &[1, 1, 0, 2])?.boxed(),
|
||||||
//Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
//Plugin::lv2("Bass/Helm", "file:///home/user/.lv2/Helm.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||||
//Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(),
|
//Plugin::lv2("Pads/Odin2", "file:///home/user/.lv2/Odin2.lv2", &[1, 0, 0, 2])?.boxed(),
|
||||||
]),
|
]),
|
||||||
Some(vec![
|
Some(vec![
|
||||||
Scene::new(&"Scene#01", &[Some(0), None, None, None]),
|
Scene::new(&"Scene#01", &[Some(0), None, None, None]),
|
||||||
Scene::new(&"Scene#02", &[Some(0), Some(0), None, None]),
|
//Scene::new(&"Scene#02", &[Some(0), Some(0), None, None]),
|
||||||
Scene::new(&"Scene#03", &[None, Some(0), None, None]),
|
//Scene::new(&"Scene#03", &[None, Some(0), None, None]),
|
||||||
Scene::new(&"Scene#04", &[None, None, None, None]),
|
//Scene::new(&"Scene#04", &[None, None, None, None]),
|
||||||
Scene::new(&"Scene#05", &[None, None, None, None]),
|
//Scene::new(&"Scene#05", &[None, None, None, None]),
|
||||||
])
|
])
|
||||||
|
|
||||||
)?.connect(input, &output)?)
|
)?.connect(input, &output)?;
|
||||||
|
run(app)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue