mirror of
https://github.com/sxyazi/yazi.git
synced 2026-07-25 08:41:05 +00:00
..
This commit is contained in:
parent
528e4a6130
commit
fa2a9cea2c
10 changed files with 209 additions and 102 deletions
|
|
@ -1 +1 @@
|
||||||
{"flagWords":[],"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"],"language":"en","version":"0.2"}
|
{"language":"en","flagWords":[],"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"],"version":"0.2"}
|
||||||
|
|
|
||||||
|
|
@ -13,39 +13,44 @@ function Status.mode()
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status.size()
|
function Status.size()
|
||||||
local hovered = cx.manager.current_hovered
|
-- cx.manager.current.files
|
||||||
if hovered == nil then
|
|
||||||
return ui.Span("")
|
-- yazi.print(#cx.manager.current.files)
|
||||||
|
|
||||||
|
for key, value in pairs(cx.manager.current.files) do
|
||||||
|
yazi.print(key, value)
|
||||||
|
break
|
||||||
end
|
end
|
||||||
|
|
||||||
return ui.Line(
|
return ui.Span("fff")
|
||||||
ui.Span(" " .. hovered.length .. " "):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg),
|
-- return ui.Line(
|
||||||
ui.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg)
|
-- ui.Span(" " .. utils.readable_size(h.length) .. " "):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg),
|
||||||
)
|
-- ui.Span(THEME.status.separator.closing):fg(THEME.status.fancy.bg)
|
||||||
|
-- )
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status.name()
|
function Status.name()
|
||||||
local hovered = cx.manager.current_hovered
|
local h = cx.manager.current.hovered
|
||||||
if hovered == nil then
|
if h == nil then
|
||||||
return ui.Span("")
|
return ui.Span("")
|
||||||
end
|
end
|
||||||
|
|
||||||
return ui.Span(" " .. utils.basename(hovered.url))
|
return ui.Span(" " .. utils.basename(h.url))
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status.permissions()
|
function Status.permissions()
|
||||||
local hovered = cx.manager.current_hovered
|
local h = cx.manager.current.hovered
|
||||||
if hovered == nil then
|
if h == nil then
|
||||||
return ui.Span("")
|
return ui.Span("")
|
||||||
end
|
end
|
||||||
|
|
||||||
if hovered.permissions == nil then
|
if h.permissions == nil then
|
||||||
return ui.Span("")
|
return ui.Span("")
|
||||||
end
|
end
|
||||||
|
|
||||||
local spans = {}
|
local spans = {}
|
||||||
for i = 1, #hovered.permissions do
|
for i = 1, #h.permissions do
|
||||||
local c = hovered.permissions:sub(i, i)
|
local c = h.permissions:sub(i, i)
|
||||||
local style = THEME.status.permissions_t
|
local style = THEME.status.permissions_t
|
||||||
if c == "r" then
|
if c == "r" then
|
||||||
style = THEME.status.permissions_r
|
style = THEME.status.permissions_r
|
||||||
|
|
@ -61,8 +66,8 @@ end
|
||||||
|
|
||||||
function Status.percentage()
|
function Status.percentage()
|
||||||
local percent = 0
|
local percent = 0
|
||||||
local cursor = cx.manager.current_cursor
|
local cursor = cx.manager.current.cursor
|
||||||
local length = cx.manager.current_length
|
local length = cx.manager.current.len()
|
||||||
if cursor ~= 0 and length ~= 0 then
|
if cursor ~= 0 and length ~= 0 then
|
||||||
percent = math.floor((cursor + 1) * 100 / length)
|
percent = math.floor((cursor + 1) * 100 / length)
|
||||||
end
|
end
|
||||||
|
|
@ -74,15 +79,19 @@ function Status.percentage()
|
||||||
end
|
end
|
||||||
|
|
||||||
return ui.Line(
|
return ui.Line(
|
||||||
ui.Span(THEME.status.separator.opening):fg(THEME.status.fancy.bg),
|
ui.Span(" " .. THEME.status.separator.opening):fg(THEME.status.fancy.bg),
|
||||||
ui.Span(percent):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg)
|
ui.Span(percent):fg(THEME.status.mode_normal.bg):bg(THEME.status.fancy.bg)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status.position()
|
function Status.position()
|
||||||
local cursor = cx.manager.current_cursor
|
local cursor = cx.manager.current.cursor
|
||||||
local length = cx.manager.current_length
|
local length = cx.manager.current.length
|
||||||
return ui.Span(string.format(" %d/%d ", cursor + 1, length))
|
|
||||||
|
return ui.Line(
|
||||||
|
ui.Span(string.format(" %2d/%-2d ", cursor + 1, length)):style(THEME.status.mode_normal),
|
||||||
|
ui.Span(THEME.status.separator.closing):fg(THEME.status.mode_normal.bg)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
function Status:render(area)
|
function Status:render(area)
|
||||||
|
|
@ -91,10 +100,12 @@ function Status:render(area)
|
||||||
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
|
:constraints({ ui.Constraint.Percentage(50), ui.Constraint.Percentage(50) })
|
||||||
:split(area)
|
:split(area)
|
||||||
|
|
||||||
local left = ui.Line(self.mode(), self.size(), self.name())
|
-- local left = ui.Line(self.mode(), self.size(), self.name())
|
||||||
local right = ui.Line(self.permissions(), self.percentage(), self.position())
|
-- local right = ui.Line(self.permissions(), self.percentage(), self.position())
|
||||||
|
|
||||||
|
local left = ui.Line(self.mode(), self.size())
|
||||||
return ui.Paragraph.render(
|
return ui.Paragraph.render(
|
||||||
ui.Paragraph(left):area(chunks[1]),
|
ui.Paragraph(left):area(chunks[1])
|
||||||
ui.Paragraph(right):align(ui.Alignment.RIGHT):area(chunks[2])
|
-- ui.Paragraph(right):align(ui.Alignment.RIGHT):area(chunks[2])
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,13 @@
|
||||||
utils = utils or {}
|
utils = utils or {}
|
||||||
|
|
||||||
function utils.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end
|
function utils.basename(str) return string.gsub(str, "(.*[/\\])(.*)", "%2") end
|
||||||
|
|
||||||
|
function utils.readable_size(size)
|
||||||
|
local units = { "B", "KB", "MB", "GB", "TB", "PB", "EB" }
|
||||||
|
local i = 1
|
||||||
|
while size > 1024.0 and i < #units do
|
||||||
|
size = size / 1024.0
|
||||||
|
i = i + 1
|
||||||
|
end
|
||||||
|
return string.format("%.1f %s", size, units[i])
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
use mlua::{LuaSerdeExt, UserData};
|
|
||||||
|
|
||||||
// Manager
|
|
||||||
|
|
||||||
pub struct Manager<'a>(&'a core::manager::Manager);
|
|
||||||
|
|
||||||
impl<'a> Manager<'a> {
|
|
||||||
pub fn new(manager: &'a core::manager::Manager) -> Self { Self(manager) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> UserData for Manager<'a> {
|
|
||||||
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("mode", |_, me| Ok(me.0.active().mode().to_string()));
|
|
||||||
|
|
||||||
fields.add_field_method_get("current_cursor", |_, me| Ok(me.0.current().cursor()));
|
|
||||||
fields.add_field_method_get("current_length", |_, me| Ok(me.0.current().files.len()));
|
|
||||||
fields.add_field_method_get("current_hovered", |_, me| {
|
|
||||||
Ok(me.0.current().hovered.as_ref().map(File::from))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tasks
|
|
||||||
|
|
||||||
pub struct Tasks<'a>(&'a core::tasks::Tasks);
|
|
||||||
|
|
||||||
impl<'a> Tasks<'a> {
|
|
||||||
pub fn new(tasks: &'a core::tasks::Tasks) -> Self { Self(tasks) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> UserData for Tasks<'a> {
|
|
||||||
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("progress", |lua, me| lua.to_value(&me.0.progress))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// File
|
|
||||||
|
|
||||||
pub struct File(core::files::File);
|
|
||||||
|
|
||||||
impl From<&core::files::File> for File {
|
|
||||||
fn from(value: &core::files::File) -> Self { Self(value.clone()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
impl UserData for File {
|
|
||||||
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
|
|
||||||
fields.add_field_method_get("url", |_, me| Ok(me.0.url().to_string_lossy().to_string()));
|
|
||||||
fields.add_field_method_get("length", |_, me| Ok(me.0.length()));
|
|
||||||
fields.add_field_method_get("link_to", |_, me| {
|
|
||||||
Ok(me.0.link_to().map(|l| l.to_string_lossy().to_string()))
|
|
||||||
});
|
|
||||||
fields.add_field_method_get("is_link", |_, me| Ok(me.0.is_link()));
|
|
||||||
fields.add_field_method_get("is_hidden", |_, me| Ok(me.0.is_hidden()));
|
|
||||||
|
|
||||||
// Meta
|
|
||||||
fields.add_field_method_get("permissions", |_, me| {
|
|
||||||
Ok(shared::permissions(me.0.meta().permissions()))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
122
plugin/src/bindings/manager.rs
Normal file
122
plugin/src/bindings/manager.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
use mlua::{AnyUserData, IntoLua, MetaMethod, UserData, UserDataFields, UserDataMethods, UserDataRef, Value};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
use crate::{Url, LUA};
|
||||||
|
|
||||||
|
// Manager
|
||||||
|
pub struct Manager;
|
||||||
|
|
||||||
|
impl Manager {
|
||||||
|
pub fn make<'a>(
|
||||||
|
scope: &mlua::Scope<'a, 'a>,
|
||||||
|
inner: &'a core::manager::Manager,
|
||||||
|
) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
LUA.register_userdata_type::<core::manager::Manager>(|reg| {
|
||||||
|
reg.add_field_method_get("mode", |_, me| Ok(me.active().mode().to_string()));
|
||||||
|
reg.add_field_function_get("current", |_, me| me.named_user_value::<AnyUserData>("current"));
|
||||||
|
reg.add_field_function_get("parent", |_, me| me.named_user_value::<AnyUserData>("parent"));
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ud = scope.create_any_userdata_ref(inner)?;
|
||||||
|
ud.set_named_user_value("current", Folder::make(scope, inner.current())?)?;
|
||||||
|
ud.set_named_user_value("parent", inner.parent().and_then(|p| Folder::make(scope, p).ok()))?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder
|
||||||
|
pub struct Folder;
|
||||||
|
|
||||||
|
impl Folder {
|
||||||
|
fn make<'a>(
|
||||||
|
scope: &mlua::Scope<'a, 'a>,
|
||||||
|
inner: &'a core::manager::Folder,
|
||||||
|
) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
LUA.register_userdata_type::<core::manager::Folder>(|reg| {
|
||||||
|
reg.add_field_method_get("cwd", |_, me| Ok(Url::from(&me.cwd)));
|
||||||
|
reg.add_field_method_get("offset", |_, me| Ok(me.offset()));
|
||||||
|
reg.add_field_method_get("cursor", |_, me| Ok(me.cursor()));
|
||||||
|
|
||||||
|
reg.add_field_function_get("files", |_, me| me.named_user_value::<AnyUserData>("files"));
|
||||||
|
reg.add_field_function_get("hovered", |_, me| me.named_user_value::<AnyUserData>("hovered"));
|
||||||
|
// reg.add_field_method_get("window", |_, me| {
|
||||||
|
// LUA.scope(|scope| scope.create_nonstatic_userdata(Files { files:
|
||||||
|
// &me.0.files }))
|
||||||
|
// });
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ud = scope.create_any_userdata_ref(inner)?;
|
||||||
|
ud.set_named_user_value("files", Files::make(scope, &inner.files)?)?;
|
||||||
|
ud.set_named_user_value(
|
||||||
|
"hovered",
|
||||||
|
inner.hovered.as_ref().and_then(|h| File::make(scope, h).ok()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files
|
||||||
|
pub struct Files;
|
||||||
|
|
||||||
|
impl Files {
|
||||||
|
fn make<'a>(
|
||||||
|
scope: &mlua::Scope<'a, 'a>,
|
||||||
|
inner: &'a core::files::Files,
|
||||||
|
) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
LUA.register_userdata_type::<core::files::Files>(|reg| {
|
||||||
|
reg.add_meta_method(MetaMethod::Len, |_, me, ()| Ok(me.len()));
|
||||||
|
|
||||||
|
reg.add_meta_function(MetaMethod::Pairs, |lua, me: AnyUserData| {
|
||||||
|
let iter = lua.create_function(|lua, (me, i): (AnyUserData, usize)| {
|
||||||
|
let items: Vec<AnyUserData> = me.named_user_value("items")?;
|
||||||
|
|
||||||
|
let i = i + 1;
|
||||||
|
Ok(if i > items.len() {
|
||||||
|
mlua::Variadic::new()
|
||||||
|
} else {
|
||||||
|
let item = items[i - 1].clone().into_lua(lua)?;
|
||||||
|
mlua::Variadic::from_iter([i.into_lua(lua)?, item])
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
Ok((iter, me, 0))
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ud = scope.create_any_userdata_ref(inner)?;
|
||||||
|
ud.set_named_user_value(
|
||||||
|
"items",
|
||||||
|
inner.iter().filter_map(|f| File::make(scope, f).ok()).collect::<Vec<_>>(),
|
||||||
|
)?;
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// File
|
||||||
|
pub struct File;
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
fn make<'a>(
|
||||||
|
scope: &mlua::Scope<'a, 'a>,
|
||||||
|
inner: &'a core::files::File,
|
||||||
|
) -> mlua::Result<AnyUserData<'a>> {
|
||||||
|
LUA.register_userdata_type::<core::files::File>(|reg| {
|
||||||
|
reg.add_field_method_get("url", |_, me| Ok(Url::from(me.url())));
|
||||||
|
reg.add_field_method_get("length", |_, me| Ok(me.length()));
|
||||||
|
reg.add_field_method_get("link_to", |_, me| {
|
||||||
|
Ok(me.link_to().map(|l| l.to_string_lossy().to_string()))
|
||||||
|
});
|
||||||
|
reg.add_field_method_get("is_link", |_, me| Ok(me.is_link()));
|
||||||
|
reg.add_field_method_get("is_hidden", |_, me| Ok(me.is_hidden()));
|
||||||
|
|
||||||
|
// Meta
|
||||||
|
reg.add_field_method_get("permissions", |_, me| {
|
||||||
|
Ok(shared::permissions(me.meta().permissions()))
|
||||||
|
});
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let ud = scope.create_any_userdata_ref(inner)?;
|
||||||
|
Ok(ud)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
plugin/src/bindings/mod.rs
Normal file
7
plugin/src/bindings/mod.rs
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
mod manager;
|
||||||
|
mod shared;
|
||||||
|
mod tasks;
|
||||||
|
|
||||||
|
pub use manager::*;
|
||||||
|
pub use shared::*;
|
||||||
|
pub use tasks::*;
|
||||||
11
plugin/src/bindings/shared.rs
Normal file
11
plugin/src/bindings/shared.rs
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
use mlua::UserData;
|
||||||
|
|
||||||
|
// Url
|
||||||
|
pub struct Url(shared::Url);
|
||||||
|
|
||||||
|
impl From<&shared::Url> for Url {
|
||||||
|
fn from(value: &shared::Url) -> Self { Self(value.clone()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
impl UserData for Url {}
|
||||||
14
plugin/src/bindings/tasks.rs
Normal file
14
plugin/src/bindings/tasks.rs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
use mlua::{LuaSerdeExt, UserData};
|
||||||
|
|
||||||
|
// Tasks
|
||||||
|
pub struct Tasks<'a>(&'a core::tasks::Tasks);
|
||||||
|
|
||||||
|
impl<'a> Tasks<'a> {
|
||||||
|
pub fn new(tasks: &'a core::tasks::Tasks) -> Self { Self(tasks) }
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> UserData for Tasks<'a> {
|
||||||
|
fn add_fields<'lua, F: mlua::UserDataFields<'lua, Self>>(fields: &mut F) {
|
||||||
|
fields.add_field_method_get("progress", |lua, me| lua.to_value(&me.0.progress))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,14 +9,17 @@ pub struct Status;
|
||||||
|
|
||||||
impl Status {
|
impl Status {
|
||||||
fn scope<T, F: FnOnce() -> Result<T>>(cx: &Ctx, f: F) -> Result<T> {
|
fn scope<T, F: FnOnce() -> Result<T>>(cx: &Ctx, f: F) -> Result<T> {
|
||||||
|
// crate::Manager::register()?;
|
||||||
|
|
||||||
LUA.scope(|scope| {
|
LUA.scope(|scope| {
|
||||||
let manager = scope.create_nonstatic_userdata(bindings::Manager::new(&cx.manager))?;
|
let manager = crate::Manager::make(scope, &cx.manager)?;
|
||||||
let tasks = scope.create_nonstatic_userdata(bindings::Tasks::new(&cx.tasks))?;
|
let tasks = scope.create_nonstatic_userdata(bindings::Tasks::new(&cx.tasks))?;
|
||||||
|
|
||||||
let cx = LUA.create_table()?;
|
let cx3 = LUA.create_table()?;
|
||||||
cx.set("manager", manager)?;
|
cx3.set("manager", manager)?;
|
||||||
cx.set("tasks", tasks)?;
|
cx3.set("tasks", tasks)?;
|
||||||
GLOBALS.set("cx", cx)?;
|
|
||||||
|
GLOBALS.set("cx", cx3)?;
|
||||||
|
|
||||||
f()
|
f()
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -76,17 +76,6 @@ pub fn readable_path(p: &Path) -> String {
|
||||||
p.display().to_string()
|
p.display().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn readable_size(size: u64) -> String {
|
|
||||||
let units = ["B", "KB", "MB", "GB", "TB", "PB", "EB"];
|
|
||||||
let mut size = size as f64;
|
|
||||||
let mut i = 0;
|
|
||||||
while size > 1024.0 && i < units.len() - 1 {
|
|
||||||
size /= 1024.0;
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
format!("{:.1} {}", size, units[i])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn unique_path(mut p: Url) -> Url {
|
pub async fn unique_path(mut p: Url) -> Url {
|
||||||
let Some(name) = p.file_name().map(|n| n.to_owned()) else {
|
let Some(name) = p.file_name().map(|n| n.to_owned()) else {
|
||||||
return p;
|
return p;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue