mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
fix (?) Inset; add arranger cursor description
This commit is contained in:
parent
7df9cc930d
commit
3c8d9668fe
4 changed files with 137 additions and 151 deletions
|
|
@ -101,6 +101,12 @@ pub trait Area<N: Number>: Copy {
|
|||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
#[inline] fn push_x (&self, x: N) -> [N;4] { [self.x() + x, self.y(), self.w(), self.h()] }
|
||||
#[inline] fn push_y (&self, y: N) -> [N;4] { [self.x(), self.y() + y, self.w(), self.h()] }
|
||||
#[inline] fn shrink_x (&self, x: N) -> [N;4] { [self.x(), self.y(), self.w() - x, self.h()] }
|
||||
#[inline] fn shrink_y (&self, y: N) -> [N;4] { [self.x(), self.y(), self.w(), self.h() - y] }
|
||||
#[inline] fn set_w (&self, w: N) -> [N;4] { [self.x(), self.y(), w, self.h()] }
|
||||
#[inline] fn set_h (&self, h: N) -> [N;4] { [self.x(), self.y(), self.w(), h] }
|
||||
}
|
||||
|
||||
impl<N: Number> Area<N> for (N, N, N, N) {
|
||||
|
|
@ -610,18 +616,14 @@ impl<N: Number, T: Widget> Outset<N, T> {
|
|||
|
||||
impl<E: Engine, T: Widget<Engine = E>> Widget for Inset<E::Unit, T> {
|
||||
type Engine = E;
|
||||
fn layout (&self, to: E::Size) -> Perhaps<E::Size> {
|
||||
match *self {
|
||||
Self::X(x, ref inner) => Shrink::X(x + x, inner as &dyn Widget<Engine = E>),
|
||||
Self::Y(y, ref inner) => Shrink::Y(y + y, inner as &dyn Widget<Engine = E>),
|
||||
Self::XY(x, y, ref inner) => Shrink::XY(x + x, y + y, inner as &dyn Widget<Engine = E>),
|
||||
}.layout(to)
|
||||
}
|
||||
fn render (&self, to: &mut E::Output) -> Usually<()> {
|
||||
match *self {
|
||||
Self::X(x, ref inner) => Push::X(x, inner as &dyn Widget<Engine = E>),
|
||||
Self::Y(y, ref inner) => Push::Y(y, inner as &dyn Widget<Engine = E>),
|
||||
Self::XY(x, y, ref inner) => Push::XY(x, y, inner as &dyn Widget<Engine = E>),
|
||||
Self::X(x, ref inner) =>
|
||||
Push::X(x, Shrink::X(x, inner as &dyn Widget<Engine = E>)),
|
||||
Self::Y(y, ref inner) =>
|
||||
Push::Y(y, Shrink::Y(y, inner as &dyn Widget<Engine = E>)),
|
||||
Self::XY(x, y, ref inner) =>
|
||||
Push::XY(x, y, Shrink::XY(x, y, inner as &dyn Widget<Engine = E>)),
|
||||
}.render(to)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -464,12 +464,8 @@ pub struct Bordered<S: BorderStyle, W: Widget<Engine = Tui>>(pub S, pub W);
|
|||
impl<S: BorderStyle, W: Widget<Engine = Tui>> Content for Bordered<S, W> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let style = self.0;
|
||||
Layers::new(move|add|{
|
||||
add(&Inset::XY(1, 1, &self.1 as &dyn Widget<Engine = Tui>))?;
|
||||
add(&Border(style))?;
|
||||
Ok(())
|
||||
}).fill_xy()
|
||||
let content: &dyn Widget<Engine = Tui> = &self.1;
|
||||
lay! { content.inset_xy(1, 1), Border(self.0) }.fill_xy()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ impl Content for ArrangerStandalone<Tui> {
|
|||
) {
|
||||
let arranger = &self.arranger as &dyn Widget<Engine = Tui>;
|
||||
let sequencer = sequencer as &dyn Widget<Engine = Tui>;
|
||||
add(&Split::new(direction, 40, arranger, sequencer.min_y(20)))
|
||||
add(&Split::new(direction, 20, arranger, sequencer.min_y(20)))
|
||||
} else {
|
||||
add(&self.arranger)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -306,9 +306,15 @@ impl Handle<Tui> for Arranger<Tui> {
|
|||
impl Content for Arranger<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Layers::new(move |add|match self.mode {
|
||||
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
||||
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
||||
Layers::new(move |add|{
|
||||
match self.mode {
|
||||
ArrangerViewMode::Horizontal => add(&HorizontalArranger(&self)),
|
||||
ArrangerViewMode::Vertical(factor) => add(&VerticalArranger(&self, factor))
|
||||
}?;
|
||||
add(&Align::SE(self.selected.description(
|
||||
&self.tracks,
|
||||
&self.scenes,
|
||||
).as_str()))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -368,6 +374,41 @@ pub enum ArrangerFocus {
|
|||
|
||||
/// Focus identification methods
|
||||
impl ArrangerFocus {
|
||||
pub fn description <E: Engine> (
|
||||
&self,
|
||||
tracks: &Vec<Sequencer<E>>,
|
||||
scenes: &Vec<Scene>,
|
||||
) -> String {
|
||||
format!("Selected: {}", match self {
|
||||
Self::Mix => format!("Everything"),
|
||||
Self::Track(t) => if let Some(track) = tracks.get(*t) {
|
||||
format!("T{t}: {}", &track.name.read().unwrap())
|
||||
} else {
|
||||
format!("T??")
|
||||
},
|
||||
Self::Scene(s) => if let Some(scene) = scenes.get(*s) {
|
||||
format!("S{s}: {}", &scene.name.read().unwrap())
|
||||
} else {
|
||||
format!("S??")
|
||||
},
|
||||
Self::Clip(t, s) => if let (Some(track), Some(scene)) = (
|
||||
tracks.get(*t),
|
||||
scenes.get(*s),
|
||||
) {
|
||||
if let Some(Some(slot)) = scene.clips.get(*t) {
|
||||
if let Some(clip) = track.phrases.get(*slot) {
|
||||
format!("T{t} S{s} C{slot} ({})", &clip.read().unwrap().name.read().unwrap())
|
||||
} else {
|
||||
format!("T{t} S{s}: Empty")
|
||||
}
|
||||
} else {
|
||||
format!("T{t} S{s}: Empty")
|
||||
}
|
||||
} else {
|
||||
format!("T{t} S{s}: Empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn is_mix (&self) -> bool {
|
||||
match self { Self::Mix => true, _ => false }
|
||||
}
|
||||
|
|
@ -505,7 +546,7 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
|||
}).fixed_xy(offset.saturating_sub(1), height))?;
|
||||
for (track, (w, _x)) in cols.iter().enumerate() {
|
||||
add(&Layers::new(move |add|{
|
||||
let mut color = COLOR_BG0;
|
||||
let mut color = Color::Rgb(40, 50, 30);
|
||||
if let (Some(track), Some(Some(clip))) = (
|
||||
tracks.get(track),
|
||||
scene.clips.get(track),
|
||||
|
|
@ -531,10 +572,10 @@ impl<'a> Content for VerticalArranger<'a, Tui> {
|
|||
add(&col!(track_titles, track_clips))?;
|
||||
Ok(())
|
||||
})
|
||||
.bg(Color::Rgb(35,40,25))
|
||||
.bg(Color::Rgb(35, 40, 25))
|
||||
.border(Lozenge(Style::default()
|
||||
.bg(Color::Rgb(40,50,30))
|
||||
.fg(Color::Rgb(70,80,50))))
|
||||
.bg(Color::Rgb(40, 50, 30))
|
||||
.fg(Color::Rgb(70, 80, 50))))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -612,7 +653,7 @@ impl<'a> Widget for VerticalArrangerCursor<'a> {
|
|||
let area = match selected {
|
||||
ArrangerFocus::Mix => {
|
||||
if focused {
|
||||
to.fill_bg(area, COLOR_BG0);
|
||||
to.fill_bg(area, Color::Rgb(40, 50, 30));
|
||||
}
|
||||
area
|
||||
},
|
||||
|
|
@ -632,21 +673,21 @@ impl<'a> Widget for VerticalArrangerCursor<'a> {
|
|||
},
|
||||
};
|
||||
if let Some([x, y, width, height]) = track_area {
|
||||
to.fill_fg([x, y, 1, height], COLOR_BG5);
|
||||
to.fill_fg([x + width, y, 1, height], COLOR_BG5);
|
||||
to.fill_fg([x, y, 1, height], Color::Rgb(70, 80, 50));
|
||||
to.fill_fg([x + width, y, 1, height], Color::Rgb(70, 80, 50));
|
||||
}
|
||||
if let Some([_, y, _, height]) = scene_area {
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], COLOR_BG5);
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], COLOR_BG5);
|
||||
to.fill_ul([area.x(), y - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
||||
to.fill_ul([area.x(), y + height - 1, area.w(), 1], Color::Rgb(70, 80, 50));
|
||||
}
|
||||
if focused {
|
||||
if let Some(clip_area) = clip_area {
|
||||
to.render_in(clip_area, &CORNERS)?;
|
||||
to.fill_bg(clip_area, COLOR_BG0);
|
||||
to.fill_bg(clip_area, Color::Rgb(40, 50, 30));
|
||||
} else if let Some(track_area) = track_area {
|
||||
to.fill_bg(track_area, COLOR_BG0);
|
||||
to.fill_bg(track_area, Color::Rgb(40, 50, 30));
|
||||
} else if let Some(scene_area) = scene_area {
|
||||
to.fill_bg(scene_area, COLOR_BG0);
|
||||
to.fill_bg(scene_area, Color::Rgb(40, 50, 30));
|
||||
}
|
||||
}
|
||||
//Ok(Some(area))
|
||||
|
|
@ -666,7 +707,7 @@ impl<'a> Content for HorizontalArranger<'a, Tui> {
|
|||
let Arranger { tracks, focused, selected, scenes, .. } = self.0;
|
||||
let tracks = tracks.as_slice();
|
||||
Layers::new(|add|{
|
||||
add(&focused.then_some(Background(COLOR_BG0)))?;
|
||||
add(&focused.then_some(Background(Color::Rgb(40, 50, 30))))?;
|
||||
add(&Stack::right(|add|{
|
||||
add(&TrackNameColumn(tracks, *selected))?;
|
||||
add(&TrackMonitorColumn(tracks))?;
|
||||
|
|
@ -1330,38 +1371,17 @@ impl Content for Sequencer<Tui> {
|
|||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
let toolbar = col!(
|
||||
col! { "Name"
|
||||
, self.name.read().unwrap().as_str(),
|
||||
}.min_xy(10, 4),
|
||||
|
||||
col! { "Start: ", " 1.1.1"
|
||||
, "End: ", " 2.1.1",
|
||||
}.min_xy(10, 6),
|
||||
|
||||
col! { "Loop [ ]"
|
||||
, "From: ", " 1.1.1"
|
||||
, "Length: ", " 1.0.0",
|
||||
}.min_xy(10, 7),
|
||||
|
||||
col! { "Notes: "
|
||||
, "C#0-C#9 "
|
||||
, "[ /2 ]"
|
||||
, "[ x2 ]"
|
||||
, "[ Rev ]"
|
||||
, "[ Inv ]"
|
||||
, "[ Dup ]"
|
||||
}.min_xy(10, 9),
|
||||
col! { "Name", self.name.read().unwrap().as_str(), }.min_xy(10, 4),
|
||||
col! { "Start: ", " 1.1.1", "End: ", " 2.1.1", }.min_xy(10, 6),
|
||||
col! { "Loop [ ]", "From: ", " 1.1.1", "Length: ", " 1.0.0", }.min_xy(10, 7),
|
||||
col! { "Notes: ", "C#0-C#9 ", "[ /2 ]", "[ x2 ]"
|
||||
, "[ Rev ]", "[ Inv ]", "[ Dup ]" }.min_xy(10, 9),
|
||||
);
|
||||
let content = lay!(
|
||||
|
||||
// keys
|
||||
CustomWidget::new(|to|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
if area.h() < 2 {
|
||||
return Ok(())
|
||||
}
|
||||
let area = [area.x(), area.y(), 5, area.h() - 2];
|
||||
to.buffer_update(area, &|cell, x, y|{
|
||||
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||
if to.area().h() < 2 { return Ok(()) }
|
||||
to.buffer_update(to.area().set_w(5).shrink_y(2), &|cell, x, y|{
|
||||
let y = y + self.note_axis.start as u16;
|
||||
if x < self.keys.area.width && y < self.keys.area.height {
|
||||
*cell = self.keys.get(x, y).clone()
|
||||
|
|
@ -1369,66 +1389,66 @@ impl Content for Sequencer<Tui> {
|
|||
});
|
||||
Ok(())
|
||||
}).fill_y(),
|
||||
|
||||
self.phrase.as_ref().map(|phrase|SequenceTimer(&self, phrase.clone()).fill_x()),
|
||||
|
||||
// notes
|
||||
CustomWidget::new(|to|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
if area.h() < 2 {
|
||||
return Ok(())//Some(area))
|
||||
// playhead
|
||||
CustomWidget::new(|_|Ok(Some([32,2])), |to: &mut TuiOutput|{
|
||||
if let Some(phrase) = self.phrase.as_ref() {
|
||||
let time_0 = self.time_axis.start;
|
||||
let time_z = self.time_axis.scale;
|
||||
let now = self.now % phrase.read().unwrap().length;
|
||||
let [x, y, width, _] = to.area();
|
||||
let x2 = x as usize + Sequencer::H_KEYS_OFFSET;
|
||||
let x3 = x as usize + width as usize;
|
||||
for x in x2..x3 {
|
||||
let step = (time_0 + x2) * time_z;
|
||||
let next_step = (time_0 + x2 + 1) * time_z;
|
||||
let style = Sequencer::<Tui>::style_timer_step(now, step as usize, next_step as usize);
|
||||
to.blit(&"-", x as u16, y, Some(style));
|
||||
}
|
||||
}
|
||||
let area = [
|
||||
area.x() + Sequencer::H_KEYS_OFFSET as u16,
|
||||
area.y() + 1,
|
||||
area.w().saturating_sub(Sequencer::H_KEYS_OFFSET as u16),
|
||||
area.h().saturating_sub(2),
|
||||
];
|
||||
Ok(())
|
||||
}).fill_x(),
|
||||
// notes
|
||||
CustomWidget::new(|_|Ok(Some([32,4])), |to: &mut TuiOutput|{
|
||||
let offset = Sequencer::H_KEYS_OFFSET as u16;
|
||||
if to.area().h() < 2 || to.area().w() < offset { return Ok(()) }
|
||||
let area = to.area().push_x(offset).shrink_x(offset).shrink_y(2);
|
||||
to.buffer_update(area, &move |cell, x, y|{
|
||||
cell.set_bg(Color::Rgb(20, 20, 20));
|
||||
let src_x = ((x as usize + self.time_axis.start) * self.time_axis.scale) as usize;
|
||||
let src_y = (y as usize + self.note_axis.start) as usize;
|
||||
if src_x < self.buffer.width && src_y < self.buffer.height - 1 {
|
||||
let src = self.buffer.get(src_x, self.buffer.height - src_y);
|
||||
src.map(|src|{
|
||||
cell.set_symbol(src.symbol());
|
||||
cell.set_fg(src.fg);
|
||||
});
|
||||
src.map(|src|{ cell.set_symbol(src.symbol()); cell.set_fg(src.fg); });
|
||||
}
|
||||
});
|
||||
Ok(())//Some(area))
|
||||
Ok(())
|
||||
}).fill_x(),
|
||||
|
||||
// cursor
|
||||
CustomWidget::new(|to|Ok(Some([1,1])), |to: &mut TuiOutput|{
|
||||
// note cursor
|
||||
CustomWidget::new(|_|Ok(Some([1,1])), |to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
if let (Some(time), Some(note)) = (self.time_axis.point, self.note_axis.point) {
|
||||
let x = area.x() + Sequencer::H_KEYS_OFFSET as u16 + time as u16;
|
||||
let y = area.y() + 1 + note as u16 / 2;
|
||||
let c = if note % 2 == 0 { "▀" } else { "▄" };
|
||||
to.blit(&c, x, y, self.style_focus());
|
||||
Ok(())
|
||||
} else {
|
||||
//Ok(Some([0,0,0,0]))
|
||||
Ok(())
|
||||
}
|
||||
}),
|
||||
|
||||
//zoom
|
||||
CustomWidget::new(|to|Ok(Some([10,1])), |to: &mut TuiOutput|{
|
||||
let area = to.area();
|
||||
let quant = ppq_to_name(self.time_axis.scale);
|
||||
let quant_x = area.x() + area.w() - 1 - quant.len() as u16;
|
||||
let quant_y = area.y() + area.h() - 2;
|
||||
to.blit(&quant, quant_x, quant_y, self.style_focus());
|
||||
Ok(())
|
||||
}),
|
||||
|
||||
//zoom
|
||||
CustomWidget::new(|_|Ok(Some([10,1])), |to: &mut TuiOutput|{
|
||||
let [x, y, w, h] = to.area.xywh();
|
||||
let quant = ppq_to_name(self.time_axis.scale);
|
||||
let x = x + w - 1 - quant.len() as u16;
|
||||
let y = y + h - 2;
|
||||
to.blit(&quant, x, y, self.style_focus());
|
||||
Ok(())
|
||||
}),
|
||||
);
|
||||
row!(toolbar, content).fill_x()
|
||||
.bg(Color::Rgb(40,50,30))
|
||||
.bg(Color::Rgb(40, 50, 30))
|
||||
.border(Lozenge(Style::default()
|
||||
.bg(Color::Rgb(40,50,30))
|
||||
.fg(Color::Rgb(70,80,50))))
|
||||
.bg(Color::Rgb(40, 50, 30))
|
||||
.fg(Color::Rgb(70, 80, 50))))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1479,7 +1499,7 @@ impl Handle<Tui> for Sequencer<Tui> {
|
|||
|
||||
fn nth_octave (index: u16) -> &'static str {
|
||||
match index {
|
||||
0 => "-1",
|
||||
0 => "-1",
|
||||
1 => "0",
|
||||
2 => "1",
|
||||
3 => "2",
|
||||
|
|
@ -1496,12 +1516,12 @@ fn nth_octave (index: u16) -> &'static str {
|
|||
|
||||
fn key_colors (index: u16) -> (Color, Color) {
|
||||
match index % 6 {
|
||||
0 => (Color::White, Color::Black),
|
||||
1 => (Color::White, Color::Black),
|
||||
2 => (Color::White, Color::White),
|
||||
3 => (Color::Black, Color::White),
|
||||
4 => (Color::Black, Color::White),
|
||||
5 => (Color::Black, Color::White),
|
||||
0 => (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
1 => (Color::Rgb(255, 255, 255), Color::Rgb(0, 0, 0)),
|
||||
2 => (Color::Rgb(255, 255, 255), Color::Rgb(255, 255, 255)),
|
||||
3 => (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
4 => (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
5 => (Color::Rgb(0, 0, 0), Color::Rgb(255, 255, 255)),
|
||||
_ => unreachable!()
|
||||
}
|
||||
}
|
||||
|
|
@ -1620,34 +1640,6 @@ pub(crate) fn keys_vert () -> Buffer {
|
|||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
struct SequenceTimer<'a>(&'a Sequencer<Tui>, Arc<RwLock<Phrase>>);
|
||||
impl<'a> Widget for SequenceTimer<'a> {
|
||||
type Engine = Tui;
|
||||
fn layout (&self, _to: [u16;2]) -> Perhaps<[u16;2]> {
|
||||
Ok(Some([32,2]))
|
||||
}
|
||||
fn render (&self, to: &mut TuiOutput) -> Usually<()> {
|
||||
let area = to.area();
|
||||
let phrase = self.1.read().unwrap();
|
||||
let (time0, time_z, now) = (
|
||||
self.0.time_axis.start, self.0.time_axis.scale, self.0.now % phrase.length
|
||||
);
|
||||
let [x, _, width, _] = area;
|
||||
let x2 = x as usize + Sequencer::H_KEYS_OFFSET;
|
||||
let x3 = x as usize + width as usize;
|
||||
for x in x2..x3 {
|
||||
let step = (time0 + x2) * time_z;
|
||||
let next_step = (time0 + x2 + 1) * time_z;
|
||||
let style = Sequencer::<Tui>::style_timer_step(now, step as usize, next_step as usize);
|
||||
to.blit(&"-", x as u16, area.y(), Some(style));
|
||||
}
|
||||
//return Ok(Some([area.x(), area.y(), area.w(), 1]))
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/// A collection of phrases to play on each track.
|
||||
#[derive(Default)]
|
||||
pub struct Scene {
|
||||
|
|
@ -2013,21 +2005,17 @@ impl Handle<Tui> for TransportToolbar<Tui> {
|
|||
impl Content for TransportToolbar<Tui> {
|
||||
type Engine = Tui;
|
||||
fn content (&self) -> impl Widget<Engine = Tui> {
|
||||
Stack::right(|add|{
|
||||
let focus_wrap = |focused, component|Layers::new(move |add|{
|
||||
if focused {
|
||||
add(&CORNERS)?;
|
||||
add(&Background(COLOR_BG1))?;
|
||||
}
|
||||
add(component)
|
||||
});
|
||||
add(&focus_wrap(self.focused && self.playing.focused, &self.playing))?;
|
||||
add(&focus_wrap(self.focused && self.bpm.focused, &self.bpm))?;
|
||||
add(&focus_wrap(self.focused && self.quant.focused, &self.quant))?;
|
||||
add(&focus_wrap(self.focused && self.sync.focused, &self.sync))?;
|
||||
add(&focus_wrap(self.focused && self.clock.focused, &self.clock))?;
|
||||
Ok(())
|
||||
})
|
||||
let focus_wrap = |focused, component|Layers::new(move |add|{
|
||||
if focused { add(&CORNERS)?; add(&Background(COLOR_BG1))?; }
|
||||
add(component)
|
||||
});
|
||||
row! {
|
||||
focus_wrap(self.focused && self.playing.focused, &self.playing),
|
||||
focus_wrap(self.focused && self.bpm.focused, &self.bpm),
|
||||
focus_wrap(self.focused && self.quant.focused, &self.quant),
|
||||
focus_wrap(self.focused && self.sync.focused, &self.sync),
|
||||
focus_wrap(self.focused && self.clock.focused, &self.clock),
|
||||
}.fill_x().bg(Color::Rgb(25, 30, 20))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue