mirror of
https://codeberg.org/unspeaker/tek.git
synced 2025-12-07 04:06:45 +01:00
arranger/sequencer control
This commit is contained in:
parent
a533951fc6
commit
52e9613d52
6 changed files with 243 additions and 209 deletions
|
|
@ -46,6 +46,7 @@ submod! {
|
||||||
handle
|
handle
|
||||||
render
|
render
|
||||||
render_split
|
render_split
|
||||||
|
render_border
|
||||||
time_base
|
time_base
|
||||||
time_note
|
time_note
|
||||||
time_tick
|
time_tick
|
||||||
|
|
|
||||||
|
|
@ -311,166 +311,6 @@ impl Theme for Nord {
|
||||||
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
const SEPARATOR: Color = Color::Rgb(0, 0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait BorderStyle {
|
|
||||||
const NW: &'static str = "";
|
|
||||||
const N: &'static str = "";
|
|
||||||
const NE: &'static str = "";
|
|
||||||
const E: &'static str = "";
|
|
||||||
const SE: &'static str = "";
|
|
||||||
const S: &'static str = "";
|
|
||||||
const SW: &'static str = "";
|
|
||||||
const W: &'static str = "";
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
|
||||||
self.draw_horizontal(buf, area, None)?;
|
|
||||||
self.draw_vertical(buf, area, None)?;
|
|
||||||
self.draw_corners(buf, area, None)?;
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_horizontal());
|
|
||||||
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
|
||||||
self.draw_north(buf, x, area.y, style)?;
|
|
||||||
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
Self::N.blit(buf, x, y, style)
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
Self::S.blit(buf, x, y, style)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_vertical());
|
|
||||||
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
|
||||||
Self::W.blit(buf, area.x, y, style)?;
|
|
||||||
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
|
||||||
let style = style.or_else(||self.style_corners());
|
|
||||||
Self::NW.blit(buf, area.x, area.y, style)?;
|
|
||||||
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
|
||||||
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
|
||||||
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
|
||||||
Ok(area)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_horizontal (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_vertical (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn style_corners (&self) -> Option<Style> {
|
|
||||||
self.style()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! border {
|
|
||||||
($($T:ty {
|
|
||||||
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
|
||||||
$($x:tt)*
|
|
||||||
}),+) => {
|
|
||||||
$(impl BorderStyle for $T {
|
|
||||||
const NW: &'static str = $nw;
|
|
||||||
const N: &'static str = $n;
|
|
||||||
const NE: &'static str = $ne;
|
|
||||||
const W: &'static str = $w;
|
|
||||||
const E: &'static str = $e;
|
|
||||||
const SW: &'static str = $sw;
|
|
||||||
const S: &'static str = $s;
|
|
||||||
const SE: &'static str = $se;
|
|
||||||
$($x)*
|
|
||||||
})+
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Lozenge(pub Style);
|
|
||||||
pub struct LozengeV(pub Style);
|
|
||||||
pub struct LozengeDotted(pub Style);
|
|
||||||
pub struct Quarter(pub Style);
|
|
||||||
pub struct QuarterV(pub Style);
|
|
||||||
pub struct Chamfer(pub Style);
|
|
||||||
pub struct Corners(pub Style);
|
|
||||||
|
|
||||||
border! {
|
|
||||||
Lozenge {
|
|
||||||
"╭" "─" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "─" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LozengeV {
|
|
||||||
"╭" "" "╮"
|
|
||||||
"│" "│"
|
|
||||||
"╰" "" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
LozengeDotted {
|
|
||||||
"╭" "┅" "╮"
|
|
||||||
"┇" "┇"
|
|
||||||
"╰" "┅" "╯"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Quarter {
|
|
||||||
"▎" "▔" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "▁" "🮇"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
QuarterV {
|
|
||||||
"▎" "" "🮇"
|
|
||||||
"▎" "🮇"
|
|
||||||
"▎" "" "🮇"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Chamfer {
|
|
||||||
"🭂" "▔" "🭍"
|
|
||||||
"▎" "🮇"
|
|
||||||
"🭓" "▁" "🭞"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Corners {
|
|
||||||
"🬆" "" "🬊" // 🬴 🬸
|
|
||||||
"" ""
|
|
||||||
"🬱" "" "🬵"
|
|
||||||
fn style (&self) -> Option<Style> {
|
|
||||||
Some(self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
macro_rules! impl_axis_common { ($A:ident $T:ty) => {
|
||||||
impl $A<$T> {
|
impl $A<$T> {
|
||||||
pub fn start_inc (&mut self) -> $T {
|
pub fn start_inc (&mut self) -> $T {
|
||||||
|
|
|
||||||
163
crates/tek_core/src/render_border.rs
Normal file
163
crates/tek_core/src/render_border.rs
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
use crate::*;
|
||||||
|
|
||||||
|
pub trait BorderStyle {
|
||||||
|
const NW: &'static str = "";
|
||||||
|
const N: &'static str = "";
|
||||||
|
const NE: &'static str = "";
|
||||||
|
const E: &'static str = "";
|
||||||
|
const SE: &'static str = "";
|
||||||
|
const S: &'static str = "";
|
||||||
|
const SW: &'static str = "";
|
||||||
|
const W: &'static str = "";
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw (&self, buf: &mut Buffer, area: Rect) -> Usually<Rect> {
|
||||||
|
self.draw_horizontal(buf, area, None)?;
|
||||||
|
self.draw_vertical(buf, area, None)?;
|
||||||
|
self.draw_corners(buf, area, None)?;
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_horizontal (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_horizontal());
|
||||||
|
for x in area.x..(area.x+area.width).saturating_sub(1) {
|
||||||
|
self.draw_north(buf, x, area.y, style)?;
|
||||||
|
self.draw_south(buf, x, (area.y + area.height).saturating_sub(1), style)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn draw_north (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
Self::N.blit(buf, x, y, style)
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn draw_south (&self, buf: &mut Buffer, x: u16, y: u16, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
Self::S.blit(buf, x, y, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_vertical (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_vertical());
|
||||||
|
for y in area.y..(area.y+area.height).saturating_sub(1) {
|
||||||
|
Self::W.blit(buf, area.x, y, style)?;
|
||||||
|
Self::E.blit(buf, area.x + area.width - 1, y, style)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn draw_corners (&self, buf: &mut Buffer, area: Rect, style: Option<Style>) -> Usually<Rect> {
|
||||||
|
let style = style.or_else(||self.style_corners());
|
||||||
|
if area.width > 0 && area.height > 0 {
|
||||||
|
Self::NW.blit(buf, area.x, area.y, style)?;
|
||||||
|
Self::NE.blit(buf, area.x + area.width - 1, area.y, style)?;
|
||||||
|
Self::SW.blit(buf, area.x, area.y + area.height - 1, style)?;
|
||||||
|
Self::SE.blit(buf, area.x + area.width - 1, area.y + area.height - 1, style)?;
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_horizontal (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_vertical (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
#[inline]
|
||||||
|
fn style_corners (&self) -> Option<Style> {
|
||||||
|
self.style()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! border {
|
||||||
|
($($T:ty {
|
||||||
|
$nw:literal $n:literal $ne:literal $w:literal $e:literal $sw:literal $s:literal $se:literal
|
||||||
|
$($x:tt)*
|
||||||
|
}),+) => {
|
||||||
|
$(impl BorderStyle for $T {
|
||||||
|
const NW: &'static str = $nw;
|
||||||
|
const N: &'static str = $n;
|
||||||
|
const NE: &'static str = $ne;
|
||||||
|
const W: &'static str = $w;
|
||||||
|
const E: &'static str = $e;
|
||||||
|
const SW: &'static str = $sw;
|
||||||
|
const S: &'static str = $s;
|
||||||
|
const SE: &'static str = $se;
|
||||||
|
$($x)*
|
||||||
|
})+
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Lozenge(pub Style);
|
||||||
|
pub struct LozengeV(pub Style);
|
||||||
|
pub struct LozengeDotted(pub Style);
|
||||||
|
pub struct Quarter(pub Style);
|
||||||
|
pub struct QuarterV(pub Style);
|
||||||
|
pub struct Chamfer(pub Style);
|
||||||
|
pub struct Corners(pub Style);
|
||||||
|
|
||||||
|
border! {
|
||||||
|
Lozenge {
|
||||||
|
"╭" "─" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "─" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LozengeV {
|
||||||
|
"╭" "" "╮"
|
||||||
|
"│" "│"
|
||||||
|
"╰" "" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
LozengeDotted {
|
||||||
|
"╭" "┅" "╮"
|
||||||
|
"┇" "┇"
|
||||||
|
"╰" "┅" "╯"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Quarter {
|
||||||
|
"▎" "▔" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "▁" "🮇"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QuarterV {
|
||||||
|
"▎" "" "🮇"
|
||||||
|
"▎" "🮇"
|
||||||
|
"▎" "" "🮇"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Chamfer {
|
||||||
|
"🭂" "▔" "🭍"
|
||||||
|
"▎" "🮇"
|
||||||
|
"🭓" "▁" "🭞"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Corners {
|
||||||
|
"🬆" "" "🬊" // 🬴 🬸
|
||||||
|
"" ""
|
||||||
|
"🬱" "" "🬵"
|
||||||
|
fn style (&self) -> Option<Style> {
|
||||||
|
Some(self.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,63 +6,35 @@ use tek_core::Direction;
|
||||||
/// Represents the tracks and scenes of the composition.
|
/// Represents the tracks and scenes of the composition.
|
||||||
pub struct Arranger {
|
pub struct Arranger {
|
||||||
/// Name of arranger
|
/// Name of arranger
|
||||||
pub name: String,
|
pub name: String,
|
||||||
/// Display mode of arranger
|
/// Display mode of arranger
|
||||||
pub mode: ArrangerViewMode,
|
pub mode: ArrangerViewMode,
|
||||||
/// Currently selected element.
|
/// Currently selected element.
|
||||||
pub selected: ArrangerFocus,
|
pub selected: ArrangerFocus,
|
||||||
/// Collection of tracks.
|
/// Collection of tracks.
|
||||||
pub tracks: Vec<Sequencer>,
|
pub tracks: Vec<Sequencer>,
|
||||||
/// Collection of scenes.
|
/// Collection of scenes.
|
||||||
pub scenes: Vec<Scene>,
|
pub scenes: Vec<Scene>,
|
||||||
pub focused: bool,
|
pub focused: bool,
|
||||||
pub entered: bool,
|
pub entered: bool,
|
||||||
pub transport: Option<Arc<RwLock<TransportToolbar>>>,
|
pub transport: Option<Arc<RwLock<TransportToolbar>>>,
|
||||||
pub show_sequencer: Option<Direction>
|
pub show_sequencer: Option<Direction>,
|
||||||
|
pub focus_sequencer: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
render!(Arranger |self, buf, area| {
|
|
||||||
let arrangement = |buf, area| match self.mode {
|
|
||||||
ArrangerViewMode::Horizontal =>
|
|
||||||
super::arranger_view_h::draw(self, buf, area),
|
|
||||||
ArrangerViewMode::VerticalCompact1 =>
|
|
||||||
super::arranger_view_v::draw_compact_1(self, buf, area),
|
|
||||||
ArrangerViewMode::VerticalCompact2 =>
|
|
||||||
super::arranger_view_v::draw_compact_2(self, buf, area),
|
|
||||||
ArrangerViewMode::VerticalExpanded =>
|
|
||||||
super::arranger_view_v::draw_expanded(self, buf, area),
|
|
||||||
};
|
|
||||||
if let Some(direction) = self.show_sequencer {
|
|
||||||
let used = arrangement(buf, area)?;
|
|
||||||
match direction {
|
|
||||||
Direction::Down => {
|
|
||||||
let area = Rect {
|
|
||||||
y: area.y + used.height,
|
|
||||||
height: area.height - used.height,
|
|
||||||
..area
|
|
||||||
};
|
|
||||||
self.sequencer().map(|sequencer|sequencer.render(buf, area));
|
|
||||||
},
|
|
||||||
_ => unimplemented!()
|
|
||||||
}
|
|
||||||
Ok(area)
|
|
||||||
} else {
|
|
||||||
arrangement(buf, area)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
impl Arranger {
|
impl Arranger {
|
||||||
pub fn new (name: &str) -> Self {
|
pub fn new (name: &str) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
mode: ArrangerViewMode::VerticalCompact2,
|
mode: ArrangerViewMode::VerticalCompact2,
|
||||||
selected: ArrangerFocus::Clip(0, 0),
|
selected: ArrangerFocus::Clip(0, 0),
|
||||||
scenes: vec![],
|
scenes: vec![],
|
||||||
tracks: vec![],
|
tracks: vec![],
|
||||||
entered: true,
|
entered: true,
|
||||||
focused: true,
|
focused: true,
|
||||||
transport: None,
|
transport: None,
|
||||||
show_sequencer: Some(Direction::Down),
|
show_sequencer: Some(Direction::Down),
|
||||||
|
focus_sequencer: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn activate (&mut self) {
|
pub fn activate (&mut self) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,24 @@
|
||||||
use crate::*;
|
use crate::*;
|
||||||
|
|
||||||
handle!(Arranger |self, e| handle_keymap(self, e, KEYMAP_ARRANGER));
|
handle!(Arranger |self, e| {
|
||||||
|
match e {
|
||||||
|
AppEvent::Input(Event::Key(k)) => {
|
||||||
|
if k.code == KeyCode::Tab {
|
||||||
|
self.focus_sequencer = !self.focus_sequencer;
|
||||||
|
Ok(true)
|
||||||
|
} else if self.focus_sequencer {
|
||||||
|
if let Some(sequencer) = self.sequencer_mut() {
|
||||||
|
handle_keymap(sequencer, e, KEYMAP_SEQUENCER)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
handle_keymap(self, e, KEYMAP_ARRANGER)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => Ok(false),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
/// Key bindings for arranger section.
|
/// Key bindings for arranger section.
|
||||||
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
|
pub const KEYMAP_ARRANGER: &'static [KeyBinding<Arranger>] = keymap!(Arranger {
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
use crate::*;
|
||||||
|
use tek_core::Direction;
|
||||||
|
|
||||||
/// Display mode of arranger
|
/// Display mode of arranger
|
||||||
pub enum ArrangerViewMode {
|
pub enum ArrangerViewMode {
|
||||||
VerticalExpanded,
|
VerticalExpanded,
|
||||||
|
|
@ -16,3 +19,40 @@ impl ArrangerViewMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
render!(Arranger |self, buf, area| {
|
||||||
|
let arrangement = |buf, area| match self.mode {
|
||||||
|
ArrangerViewMode::Horizontal =>
|
||||||
|
super::arranger_view_h::draw(self, buf, area),
|
||||||
|
ArrangerViewMode::VerticalCompact1 =>
|
||||||
|
super::arranger_view_v::draw_compact_1(self, buf, area),
|
||||||
|
ArrangerViewMode::VerticalCompact2 =>
|
||||||
|
super::arranger_view_v::draw_compact_2(self, buf, area),
|
||||||
|
ArrangerViewMode::VerticalExpanded =>
|
||||||
|
super::arranger_view_v::draw_expanded(self, buf, area),
|
||||||
|
};
|
||||||
|
if let Some(direction) = self.show_sequencer {
|
||||||
|
let arrangement = arrangement(buf, area)?;
|
||||||
|
match direction {
|
||||||
|
Direction::Down => {
|
||||||
|
let sequencer = if let Some(sequencer) = self.sequencer() {
|
||||||
|
sequencer.render(buf, Rect {
|
||||||
|
y: area.y + arrangement.height,
|
||||||
|
height: area.height - arrangement.height,
|
||||||
|
..area
|
||||||
|
})?
|
||||||
|
} else {
|
||||||
|
Rect::default()
|
||||||
|
};
|
||||||
|
Corners(Style::default().green().not_dim()).draw(buf, match self.focus_sequencer {
|
||||||
|
true => sequencer,
|
||||||
|
false => arrangement,
|
||||||
|
})?;
|
||||||
|
},
|
||||||
|
_ => unimplemented!()
|
||||||
|
}
|
||||||
|
Ok(area)
|
||||||
|
} else {
|
||||||
|
arrangement(buf, area)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue