#![cfg(feature = "client")]
use std::path::Path;
use std::sync::Arc;
use terrazzo::autoclone;
use terrazzo::html;
use terrazzo::prelude::*;
use terrazzo::template;
use wasm_bindgen_futures::spawn_local;
use web_sys::DragEvent;
use web_sys::MouseEvent;
use self::diagnostics::debug;
use self::diagnostics::error;
use super::SideViewList;
use super::SideViewNode;
use super::SvnItem;
use super::SvnProperties;
use super::SvnStatus;
use super::mutation::filter_active_folder_content;
use super::mutation::show_folder_content;
use crate::assets::icons;
use crate::frontend::mousemove::Position;
use crate::text_editor::file_path::FilePath;
use crate::text_editor::fsio::FileMetadata;
use crate::text_editor::fsio::ROOT_BASE_PATH;
use crate::text_editor::fsio::ROOT_FILE_PATH;
use crate::text_editor::fsio::client::list_folder;
use crate::text_editor::manager::TextEditorManager;
use crate::text_editor::ui::RemoveBehavior;
use crate::text_editor::ui::drag::on_move_dragover;
use crate::text_editor::ui::drag::on_move_dragstart;
use crate::text_editor::ui::drag::on_move_drop;
terrazzo_css::import_style!(style, "side.scss");
#[cfg(not(feature = "client-prod"))]
use crate::utils::more_path::MorePath as _;
impl TextEditorManager {
pub fn show_side_view(self: &Ptr<TextEditorManager>) -> XElement {
show_side_view(self.clone(), self.path.base.clone(), self.side_view.clone())
}
}
#[html]
#[template(tag = div, key = "side-view")]
fn show_side_view(
manager: Ptr<TextEditorManager>,
#[signal] base: Arc<Path>,
#[signal] side_view: Option<Arc<SideViewNode>>,
) -> XElement {
debug!(?base, "Loading side view");
let root = base
.file_name()
.map(Path::new)
.unwrap_or_else(|| &ROOT_BASE_PATH);
return tag(
class = style::SIDE,
#[cfg(not(feature = "client-prod"))]
class = "side-view",
style::flex %= side_view_width(manager.side_view_resize_manager.delta.clone()),
side_view.map(|side_view| {
show_side_view_node(
&manager,
&FilePath {
base: base.clone(),
file: ROOT_FILE_PATH.clone(),
},
root,
&side_view,
)
})..,
);
#[template(wrap = true)]
fn side_view_width(#[signal] position: Option<Position>) -> XAttributeValue {
let position = position.unwrap_or_default();
format!("0 0 max(8rem, calc(200px + {}px))", position.x)
}
}
#[html]
fn show_side_view_node(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
name: &Path,
side_view: &SideViewNode,
) -> XElement {
debug!(?path, ?name, "Show side view node");
li(match &side_view.item {
SvnItem::Folder { folder, notify: _ } => {
show_side_view_folder(manager, path, name, &side_view.properties, folder)
}
SvnItem::File { metadata, .. } => {
show_side_view_file(manager, path, &side_view.properties, metadata)
}
})
}
#[autoclone]
#[html]
fn show_side_view_folder(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
name: &Path,
properties: &SvnProperties,
folder: &Arc<SideViewList>,
) -> XElement {
let name_display = name.display();
let dragover_class: XSignal<Option<&'static str>> = XSignal::new("side-view-dragover", None);
#[template(wrap = true)]
fn get_dragover_class(#[signal] dragover_class: Option<&'static str>) -> XAttributeValue {
dragover_class
}
let on_move_drop = on_move_drop(manager, path);
let is_expanded = folder
.values()
.any(|child| child.properties.status == SvnStatus::Show);
div(
key = "folder",
#[cfg(not(feature = "client-prod"))]
class = "side-view-folder",
#[cfg(not(feature = "client-prod"))]
data_folder_path = path.file.to_owned_string(),
div(
class = style::FOLDER,
#[cfg(not(feature = "client-prod"))]
class = "side-view-folder-row",
draggable = (*path.file != *Path::new("")).then_some("true"),
dragstart = on_move_dragstart(path),
dragover = move |event: DragEvent| {
autoclone!(dragover_class);
if !on_move_dragover(event) {
return;
}
dragover_class.set(style::MOVE_DRAGOVER);
},
dragleave = move |_| {
autoclone!(dragover_class);
dragover_class.set(None)
},
drop = move |event| {
dragover_class.set(None);
on_move_drop(event);
},
img(src = icons::folder(), class = style::ICON),
div(
class %= selected_item(manager.path.file.clone(), path.file.clone()),
class %= get_dragover_class(dragover_class.clone()),
click = move |_| {
autoclone!(manager, path);
manager.path.file.set(path.file.clone())
},
dblclick = expand_folder(manager, path),
span("{name_display}", class = name_display_class(properties)),
),
folder_expand_icon(manager, path, is_expanded),
(*path.file != "".as_ref()).then(|| {
close_icon(
manager,
path,
match properties.status {
SvnStatus::Active => RemoveBehavior::Soft,
SvnStatus::Show => RemoveBehavior::Hard,
},
)
})..,
),
div(
class = style::SUB_FOLDER,
ul(folder.sorted_iter().map(|(name, child)| {
show_side_view_node(
manager,
&FilePath {
base: path.base.clone(),
file: path.file.join(name.as_ref()).into(),
},
name,
child,
)
})..),
),
)
}
#[autoclone]
#[html]
fn show_side_view_file(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
properties: &SvnProperties,
metadata: &FileMetadata,
) -> XElement {
let name = &metadata.name;
div(
key = "file",
class = style::FILE,
draggable = true,
dragstart = on_move_dragstart(path),
#[cfg(not(feature = "client-prod"))]
data_file_path = path.file.to_owned_string(),
img(src = icons::file(), class = style::ICON),
div(
class %= selected_item(manager.path.file.clone(), path.file.clone()),
span(class = name_display_class(properties), "{name}"),
click = move |_| {
autoclone!(manager, path);
manager.path.file.force(path.file.clone())
},
),
close_icon(manager, path, RemoveBehavior::Hard),
)
}
#[template(wrap = true)]
fn selected_item(#[signal] file_path: Arc<Path>, path: Arc<Path>) -> XAttributeValue {
if file_path == path {
style::SELECTED_LABEL
} else {
style::LABEL
}
}
#[html]
fn folder_expand_icon(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
is_expanded: bool,
) -> XElement {
if !is_expanded {
img(
src = icons::split_vert(),
class = style::BUTTON_HOVER_ICON,
#[cfg(not(feature = "client-prod"))]
class = "side-view-expand-folder",
click = expand_folder(manager, path),
)
} else {
img(
src = icons::collapse_vert(),
class = style::BUTTON_ICON,
#[cfg(not(feature = "client-prod"))]
class = "side-view-collapse-folder",
click = collapse_folder(manager, path),
)
}
}
#[autoclone]
fn expand_folder(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
) -> impl Fn(MouseEvent) + 'static {
move |_| {
autoclone!(manager, path);
spawn_local(async move {
autoclone!(manager, path);
debug!(?path, "Expand folder view");
let content = list_folder(manager.remote.clone(), path.clone())
.await
.inspect_err(|error| error!("Failed to load folder {path:?}: {error}"));
let Ok(Some(content)) = content else {
debug!(?path, "Folder was not found or not a folder");
return;
};
debug!(?path, "Found {} items", content.len());
manager.side_view.update(|side_view| {
let new_node =
show_folder_content(&manager, side_view.as_deref(), &path, content.as_ref());
new_node.map(|new_node| Some(Arc::new(new_node)))
});
});
}
}
#[autoclone]
fn collapse_folder(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
) -> impl Fn(MouseEvent) + 'static {
move |_| {
autoclone!(manager, path);
manager.side_view.update(|side_view| {
debug!(?path, "Collapse folder view");
let new_node = filter_active_folder_content(&manager, side_view.as_deref(), &path);
new_node.map(|new_node| Some(Arc::new(new_node)))
});
}
}
#[autoclone]
#[html]
fn close_icon(
manager: &Ptr<TextEditorManager>,
path: &FilePath<Arc<Path>>,
behavior: RemoveBehavior,
) -> XElement {
img(
src = icons::close_tab(),
class = style::BUTTON_HOVER_ICON,
#[cfg(not(feature = "client-prod"))]
class = "side-view-close-file",
click = move |_ev| {
autoclone!(manager, path);
debug!(?path, "Remove item from side view");
manager.remove_from_side_view(&path, behavior);
},
)
}
fn name_display_class(properties: &SvnProperties) -> impl Into<XAttributeValue> {
(properties.status == SvnStatus::Show).then_some(style::SHOW_ONLY_ITEM)
}
impl SideViewList {
pub fn sorted_iter(&self) -> impl Iterator<Item = (&Arc<Path>, &Arc<SideViewNode>)> {
let iter = self.iter();
let mut data = iter.collect::<Vec<_>>();
data.sort_by_key(|(path, node)| {
(
!matches!(node.item, SvnItem::Folder { .. }),
path.file_name()
.map(|n| n.to_ascii_lowercase())
.unwrap_or_default(),
)
});
data.into_iter()
}
}