mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
4a72dea37e
commit
eac62492aa
10 changed files with 196 additions and 117 deletions
|
|
@ -1 +1 @@
|
||||||
{"version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags"],"language":"en","flagWords":[]}
|
{"language":"en","flagWords":[],"version":"0.2","words":["Punct","KEYMAP","splitn","crossterm","YAZI","unar","peekable","ratatui","syntect","pbpaste","pbcopy","ffmpegthumbnailer","oneshot","Posix","Lsar","XADDOS","zoxide","cands","Deque","precache","imageops","IFBLK","IFCHR","IFDIR","IFIFO","IFLNK","IFMT","IFSOCK","IRGRP","IROTH","IRUSR","ISGID","ISUID","ISVTX","IWGRP","IWOTH","IWUSR","IXGRP","IXOTH","IXUSR","libc","winsize","TIOCGWINSZ","xpixel","ypixel","ioerr","appender","Catppuccin","macchiato","gitmodules","Dotfiles","bashprofile","vimrc","flac","webp","exiftool","mediainfo","ripgrep","nvim","indexmap","indexmap","unwatch","canonicalize","serde","fsevent","Ueberzug","iterm","wezterm","sixel","chafa","ueberzugpp","️ Überzug","️ Überzug","Konsole","Alacritty","Überzug","pkgs","paru","unarchiver","pdftoppm","poppler","prebuild","singlefile","jpegopt","EXIF","rustfmt","mktemp","nanos","xclip","xsel","natord","Mintty","nixos","nixpkgs","SIGTSTP","SIGCONT","SIGCONT","mlua","nonstatic","userdata","metatable","natsort","backstack","luajit","Succ","Succ","cand","fileencoding","foldmethod","lightgreen","darkgray","lightred","lightyellow","lightcyan","nushell","msvc","aarch","linemode","sxyazi","rsplit","ZELLIJ","bitflags","bitflags","USERPROFILE"]}
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,31 @@ use yazi_shared::{expand_path, Debounce, InputError, Url};
|
||||||
|
|
||||||
use crate::{emit, input::InputOpt, tab::Tab};
|
use crate::{emit, input::InputOpt, tab::Tab};
|
||||||
|
|
||||||
|
pub struct Opt {
|
||||||
|
target: Url,
|
||||||
|
interactive: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(e: &Exec) -> Self {
|
||||||
|
Self {
|
||||||
|
target: e.args.first().map(Url::from).unwrap_or_default(),
|
||||||
|
interactive: e.named.contains_key("interactive"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl From<Url> for Opt {
|
||||||
|
fn from(target: Url) -> Self { Self { target, interactive: false } }
|
||||||
|
}
|
||||||
|
|
||||||
impl Tab {
|
impl Tab {
|
||||||
pub fn cd(&mut self, target: Url) -> bool {
|
pub fn cd(&mut self, opt: impl Into<Opt>) -> bool {
|
||||||
if self.current.cwd == target {
|
let opt = opt.into() as Opt;
|
||||||
|
if opt.interactive {
|
||||||
|
return self.cd_interactive(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.current.cwd == opt.target {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -19,30 +41,34 @@ impl Tab {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Current
|
// Current
|
||||||
let rep = self.history_new(&target);
|
let rep = self.history_new(&opt.target);
|
||||||
let rep = mem::replace(&mut self.current, rep);
|
let rep = mem::replace(&mut self.current, rep);
|
||||||
if rep.cwd.is_regular() {
|
if rep.cwd.is_regular() {
|
||||||
self.history.insert(rep.cwd.clone(), rep);
|
self.history.insert(rep.cwd.clone(), rep);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parent
|
// Parent
|
||||||
if let Some(parent) = target.parent_url() {
|
if let Some(parent) = opt.target.parent_url() {
|
||||||
self.parent = Some(self.history_new(&parent));
|
self.parent = Some(self.history_new(&parent));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backstack
|
// Backstack
|
||||||
if target.is_regular() {
|
if opt.target.is_regular() {
|
||||||
self.backstack.push(target.clone());
|
self.backstack.push(opt.target.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
emit!(Refresh);
|
emit!(Refresh);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cd_interactive(&mut self, target: Url) -> bool {
|
pub fn cd_interactive(&mut self, opt: impl Into<Opt>) -> bool {
|
||||||
|
let opt = opt.into() as Opt;
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let rx = emit!(Input(
|
let rx = emit!(Input(
|
||||||
InputOpt::top("Change directory:").with_value(target.to_string_lossy()).with_completion()
|
InputOpt::top("Change directory:")
|
||||||
|
.with_value(opt.target.to_string_lossy())
|
||||||
|
.with_completion()
|
||||||
));
|
));
|
||||||
|
|
||||||
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
|
let rx = Debounce::new(UnboundedReceiverStream::new(rx), Duration::from_millis(50));
|
||||||
|
|
|
||||||
|
|
@ -39,13 +39,13 @@ impl Tab {
|
||||||
let _defer = Defer::new(|| Event::Stop(false, None).emit());
|
let _defer = Defer::new(|| Event::Stop(false, None).emit());
|
||||||
emit!(Stop(true)).await;
|
emit!(Stop(true)).await;
|
||||||
|
|
||||||
let rx = if opt.type_ == OptType::Fzf {
|
let url = if opt.type_ == OptType::Fzf {
|
||||||
external::fzf(FzfOpt { cwd })
|
external::fzf(FzfOpt { cwd }).await
|
||||||
} else {
|
} else {
|
||||||
external::zoxide(ZoxideOpt { cwd })
|
external::zoxide(ZoxideOpt { cwd }).await
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
let op = if global && !ends_with_slash(&url) { "reveal" } else { "cd" };
|
let op = if opt.type_ == OptType::Fzf && !ends_with_slash(&url) { "reveal" } else { "cd" };
|
||||||
emit!(Call(Exec::call(op, vec![url.to_string()]).vec(), KeymapLayer::Manager));
|
emit!(Call(Exec::call(op, vec![url.to_string()]).vec(), KeymapLayer::Manager));
|
||||||
Ok::<(), anyhow::Error>(())
|
Ok::<(), anyhow::Error>(())
|
||||||
});
|
});
|
||||||
|
|
|
||||||
36
yazi-core/src/tasks/commands/arrow.rs
Normal file
36
yazi-core/src/tasks/commands/arrow.rs
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
|
||||||
|
use crate::tasks::Tasks;
|
||||||
|
|
||||||
|
pub struct Opt {
|
||||||
|
step: isize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(e: &Exec) -> Self {
|
||||||
|
Self { step: e.args.first().and_then(|s| s.parse().ok()).unwrap_or(0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tasks {
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
fn next(&mut self) -> bool {
|
||||||
|
let limit = Self::limit().min(self.len());
|
||||||
|
|
||||||
|
let old = self.cursor;
|
||||||
|
self.cursor = limit.saturating_sub(1).min(self.cursor + 1);
|
||||||
|
|
||||||
|
old != self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prev(&mut self) -> bool {
|
||||||
|
let old = self.cursor;
|
||||||
|
self.cursor = self.cursor.saturating_sub(1);
|
||||||
|
old != self.cursor
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn arrow(&mut self, opt: impl Into<Opt>) -> bool {
|
||||||
|
let opt = opt.into() as Opt;
|
||||||
|
if opt.step > 0 { self.next() } else { self.prev() }
|
||||||
|
}
|
||||||
|
}
|
||||||
22
yazi-core/src/tasks/commands/cancel.rs
Normal file
22
yazi-core/src/tasks/commands/cancel.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
|
||||||
|
use crate::tasks::Tasks;
|
||||||
|
|
||||||
|
pub struct Opt;
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(_: &Exec) -> Self { Self }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tasks {
|
||||||
|
pub fn cancel(&mut self, _: impl Into<Opt>) -> bool {
|
||||||
|
let id = self.scheduler.running.read().get_id(self.cursor);
|
||||||
|
if id.map(|id| self.scheduler.cancel(id)) != Some(true) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let len = self.scheduler.running.read().len();
|
||||||
|
self.cursor = self.cursor.min(len.saturating_sub(1));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
77
yazi-core/src/tasks/commands/inspect.rs
Normal file
77
yazi-core/src/tasks/commands/inspect.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
use std::io::{stdout, Write};
|
||||||
|
|
||||||
|
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
||||||
|
use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time};
|
||||||
|
use yazi_config::keymap::Exec;
|
||||||
|
use yazi_shared::{Defer, Term};
|
||||||
|
|
||||||
|
use crate::{emit, tasks::Tasks, Event, BLOCKER};
|
||||||
|
|
||||||
|
pub struct Opt;
|
||||||
|
|
||||||
|
impl From<&Exec> for Opt {
|
||||||
|
fn from(_: &Exec) -> Self { Self }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tasks {
|
||||||
|
pub fn inspect(&self, _: impl Into<Opt>) -> bool {
|
||||||
|
let Some(id) = self.scheduler.running.read().get_id(self.cursor) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let scheduler = self.scheduler.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _guard = BLOCKER.acquire().await.unwrap();
|
||||||
|
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||||
|
|
||||||
|
let buffered = {
|
||||||
|
let mut running = scheduler.running.write();
|
||||||
|
let Some(task) = running.get_mut(id) else { return };
|
||||||
|
|
||||||
|
task.logger = Some(tx);
|
||||||
|
task.logs.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
emit!(Stop(true)).await;
|
||||||
|
let _defer = Defer::new(|| {
|
||||||
|
disable_raw_mode().ok();
|
||||||
|
Event::Stop(false, None).emit();
|
||||||
|
});
|
||||||
|
|
||||||
|
Term::clear(&mut stdout()).ok();
|
||||||
|
stdout().write_all(buffered.as_bytes()).ok();
|
||||||
|
enable_raw_mode().ok();
|
||||||
|
|
||||||
|
let mut stdin = stdin();
|
||||||
|
let mut quit = [0; 10];
|
||||||
|
loop {
|
||||||
|
select! {
|
||||||
|
Some(line) = rx.recv() => {
|
||||||
|
let mut stdout = stdout().lock();
|
||||||
|
stdout.write_all(line.as_bytes()).ok();
|
||||||
|
stdout.write_all(b"\r\n").ok();
|
||||||
|
}
|
||||||
|
_ = time::sleep(time::Duration::from_millis(100)) => {
|
||||||
|
if scheduler.running.read().get(id).is_none() {
|
||||||
|
stdout().write_all(b"Task finished, press `q` to quit\r\n").ok();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Ok(_) = stdin.read(&mut quit) => {
|
||||||
|
if quit[0] == b'q' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(task) = scheduler.running.write().get_mut(id) {
|
||||||
|
task.logger = None;
|
||||||
|
}
|
||||||
|
while quit[0] != b'q' {
|
||||||
|
stdin.read(&mut quit).await.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
3
yazi-core/src/tasks/commands/mod.rs
Normal file
3
yazi-core/src/tasks/commands/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
mod arrow;
|
||||||
|
mod cancel;
|
||||||
|
mod inspect;
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod commands;
|
||||||
mod running;
|
mod running;
|
||||||
mod scheduler;
|
mod scheduler;
|
||||||
mod task;
|
mod task;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,15 @@
|
||||||
use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, io::{stdout, Write}, path::Path, sync::Arc};
|
use std::{collections::{BTreeMap, HashMap, HashSet}, ffi::OsStr, path::Path, sync::Arc};
|
||||||
|
|
||||||
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tokio::{io::{stdin, AsyncReadExt}, select, sync::mpsc, time};
|
|
||||||
use tracing::debug;
|
use tracing::debug;
|
||||||
use yazi_config::{manager::SortBy, open::Opener, OPEN};
|
use yazi_config::{manager::SortBy, open::Opener, OPEN};
|
||||||
use yazi_shared::{Defer, MimeKind, Term, Url};
|
use yazi_shared::{MimeKind, Term, Url};
|
||||||
|
|
||||||
use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT};
|
use super::{running::Running, task::TaskSummary, Scheduler, TASKS_PADDING, TASKS_PERCENT};
|
||||||
use crate::{emit, files::{File, Files}, input::InputOpt, Event, BLOCKER};
|
use crate::{emit, files::{File, Files}, input::InputOpt};
|
||||||
|
|
||||||
pub struct Tasks {
|
pub struct Tasks {
|
||||||
scheduler: Arc<Scheduler>,
|
pub(super) scheduler: Arc<Scheduler>,
|
||||||
|
|
||||||
pub visible: bool,
|
pub visible: bool,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
|
|
@ -39,99 +37,11 @@ impl Tasks {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::should_implement_trait)]
|
|
||||||
pub fn next(&mut self) -> bool {
|
|
||||||
let limit = Self::limit().min(self.len());
|
|
||||||
|
|
||||||
let old = self.cursor;
|
|
||||||
self.cursor = limit.saturating_sub(1).min(self.cursor + 1);
|
|
||||||
|
|
||||||
old != self.cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prev(&mut self) -> bool {
|
|
||||||
let old = self.cursor;
|
|
||||||
self.cursor = self.cursor.saturating_sub(1);
|
|
||||||
old != self.cursor
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn paginate(&self) -> Vec<TaskSummary> {
|
pub fn paginate(&self) -> Vec<TaskSummary> {
|
||||||
let running = self.scheduler.running.read();
|
let running = self.scheduler.running.read();
|
||||||
running.values().take(Self::limit()).map(Into::into).collect()
|
running.values().take(Self::limit()).map(Into::into).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn inspect(&self) -> bool {
|
|
||||||
let Some(id) = self.scheduler.running.read().get_id(self.cursor) else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
let scheduler = self.scheduler.clone();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _guard = BLOCKER.acquire().await.unwrap();
|
|
||||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
let buffered = {
|
|
||||||
let mut running = scheduler.running.write();
|
|
||||||
let Some(task) = running.get_mut(id) else { return };
|
|
||||||
|
|
||||||
task.logger = Some(tx);
|
|
||||||
task.logs.clone()
|
|
||||||
};
|
|
||||||
|
|
||||||
emit!(Stop(true)).await;
|
|
||||||
let _defer = Defer::new(|| {
|
|
||||||
disable_raw_mode().ok();
|
|
||||||
Event::Stop(false, None).emit();
|
|
||||||
});
|
|
||||||
|
|
||||||
Term::clear(&mut stdout()).ok();
|
|
||||||
stdout().write_all(buffered.as_bytes()).ok();
|
|
||||||
enable_raw_mode().ok();
|
|
||||||
|
|
||||||
let mut stdin = stdin();
|
|
||||||
let mut quit = [0; 10];
|
|
||||||
loop {
|
|
||||||
select! {
|
|
||||||
Some(line) = rx.recv() => {
|
|
||||||
let mut stdout = stdout().lock();
|
|
||||||
stdout.write_all(line.as_bytes()).ok();
|
|
||||||
stdout.write_all(b"\r\n").ok();
|
|
||||||
}
|
|
||||||
_ = time::sleep(time::Duration::from_millis(100)) => {
|
|
||||||
if scheduler.running.read().get(id).is_none() {
|
|
||||||
stdout().write_all(b"Task finished, press `q` to quit\r\n").ok();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Ok(_) = stdin.read(&mut quit) => {
|
|
||||||
if quit[0] == b'q' {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(task) = scheduler.running.write().get_mut(id) {
|
|
||||||
task.logger = None;
|
|
||||||
}
|
|
||||||
while quit[0] != b'q' {
|
|
||||||
stdin.read(&mut quit).await.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cancel(&mut self) -> bool {
|
|
||||||
let id = self.scheduler.running.read().get_id(self.cursor);
|
|
||||||
if id.map(|id| self.scheduler.cancel(id)) != Some(true) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let len = self.scheduler.running.read().len();
|
|
||||||
self.cursor = self.cursor.min(len.saturating_sub(1));
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn file_open(&self, targets: &[(impl AsRef<Path>, impl AsRef<str>)]) -> bool {
|
pub fn file_open(&self, targets: &[(impl AsRef<Path>, impl AsRef<str>)]) -> bool {
|
||||||
let mut openers = BTreeMap::new();
|
let mut openers = BTreeMap::new();
|
||||||
for (path, mime) in targets {
|
for (path, mime) in targets {
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,8 @@ impl<'a> Executor<'a> {
|
||||||
on!(ACTIVE, enter);
|
on!(ACTIVE, enter);
|
||||||
on!(ACTIVE, back);
|
on!(ACTIVE, back);
|
||||||
on!(ACTIVE, forward);
|
on!(ACTIVE, forward);
|
||||||
// on!(A, cd);
|
on!(ACTIVE, cd);
|
||||||
|
on!(ACTIVE, reveal);
|
||||||
|
|
||||||
// Selection
|
// Selection
|
||||||
on!(ACTIVE, select);
|
on!(ACTIVE, select);
|
||||||
|
|
@ -145,17 +146,20 @@ impl<'a> Executor<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tasks(&mut self, exec: &Exec) -> bool {
|
fn tasks(&mut self, exec: &Exec) -> bool {
|
||||||
match exec.cmd.as_str() {
|
macro_rules! on {
|
||||||
"close" => self.cx.tasks.toggle(),
|
($name:ident) => {
|
||||||
|
if exec.cmd == stringify!($name) {
|
||||||
"arrow" => {
|
return self.cx.tasks.$name(exec);
|
||||||
let step = exec.args.first().and_then(|s| s.parse().ok()).unwrap_or(0);
|
}
|
||||||
if step > 0 { self.cx.tasks.next() } else { self.cx.tasks.prev() }
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
"inspect" => self.cx.tasks.inspect(),
|
on!(arrow);
|
||||||
"cancel" => self.cx.tasks.cancel(),
|
on!(inspect);
|
||||||
|
on!(cancel);
|
||||||
|
|
||||||
|
match exec.cmd.as_str() {
|
||||||
|
"close" => self.cx.tasks.toggle(),
|
||||||
"help" => self.cx.help.toggle(KeymapLayer::Tasks),
|
"help" => self.cx.help.toggle(KeymapLayer::Tasks),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue