use std::path::PathBuf;
use freya::{
prelude::*,
radio::{
use_radio,
use_radio_station,
},
router::*,
};
use rfd::AsyncFileDialog;
use crate::state::{
AppChannel,
AppState,
spawn_thinking_watcher,
};
#[derive(PartialEq)]
pub struct InstanceSidebar;
impl Component for InstanceSidebar {
fn render(&self) -> impl IntoElement {
let mut radio = use_radio::<AppState, AppChannel>(AppChannel::App);
let on_open_project = move |_: Event<PressEventData>| {
spawn(async move {
let folder = AsyncFileDialog::new().pick_folder().await;
if let Some(folder) = folder {
let path = folder.path().to_path_buf();
let name = path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "Project".to_string());
radio.write().add_project(name, path);
}
});
};
rect()
.height(Size::fill())
.content(Content::Flex)
.padding(6.)
.child(
ScrollView::new()
.height(Size::flex(1.))
.spacing(4.)
.children(radio.read().projects.iter().map(|project| {
ProjectGroup {
project_id: project.id,
project_name: project.name.clone(),
project_path: project.path.clone(),
}
.into()
})),
)
.child(
Button::new()
.width(Size::fill())
.on_press(on_open_project)
.rounded_xl()
.child("Open Project"),
)
}
}
#[derive(PartialEq)]
struct ProjectGroup {
project_id: usize,
project_name: String,
project_path: PathBuf,
}
impl Component for ProjectGroup {
fn render(&self) -> impl IntoElement {
let mut radio = use_radio::<AppState, AppChannel>(AppChannel::App);
let project_id = self.project_id;
let project_name = self.project_name.clone();
let project_path = self.project_path.clone();
let instances: Vec<_> = radio
.read()
.instances
.iter()
.filter(|i| i.project_id == project_id)
.map(|i| (i.id, i.name.clone()))
.collect();
let on_secondary_press = {
let project_path = project_path.clone();
move |_: Event<PressEventData>| {
let subdirs =
crate::state::Project::new(project_id, String::new(), project_path.clone())
.list_subdirectories();
let router = RouterContext::get();
let mut menu = ScrollView::new()
.width(Size::px(300.))
.height(Size::auto())
.max_height(Size::px(400.));
for dir in subdirs {
let dir_name = dir
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_default();
let dir_path = dir.clone();
let name_for_label = dir_name.clone();
menu = menu.child(
MenuButton::new()
.on_press(move |_| {
ContextMenu::close();
let instance_id = radio.write().add_instance(
project_id,
dir_name.clone(),
dir_path.clone(),
);
router.push(crate::Route::Instance { id: instance_id });
})
.child(name_for_label),
);
}
menu = menu.child(
rect()
.height(Size::px(1.))
.width(Size::fill())
.background((35, 35, 35)),
);
menu = menu.child(
MenuButton::new()
.on_press(move |_| {
ContextMenu::close();
radio.write().remove_project(project_id);
router.replace(crate::Route::Home);
})
.child("Remove Project"),
);
ContextMenu::open(Menu::new().child(menu));
}
};
rect()
.width(Size::fill())
.spacing(4.)
.child(
rect()
.width(Size::fill())
.padding((6., 8.))
.on_secondary_press(on_secondary_press)
.child(
label()
.font_size(12.)
.color((170, 170, 170))
.text(project_name.to_uppercase()),
),
)
.children(instances.iter().map(|(id, name)| {
InstanceSidebarItem {
id: *id,
name: name.clone(),
}
.into()
}))
}
}
#[derive(PartialEq)]
struct InstanceSidebarItem {
id: usize,
name: String,
}
impl Component for InstanceSidebarItem {
fn render(&self) -> impl IntoElement {
let mut radio = use_radio::<AppState, AppChannel>(AppChannel::Instance(self.id));
let station = use_radio_station::<AppState, AppChannel>();
let instance_id = self.id;
let name = self.name.clone();
let mut renaming = use_state(|| false);
let mut rename_value = use_state(String::new);
let focus = use_focus();
let focus_status = use_focus_status(focus);
use_hook(|| {
let handle = radio
.read()
.instances
.iter()
.find(|i| i.id == instance_id)
.and_then(|inst| inst.terminals.first())
.and_then(|term| term.handle.clone());
if let Some(handle) = handle {
spawn_thinking_watcher(station, instance_id, 0, handle);
}
});
let is_thinking = {
let state = radio.read();
state
.instances
.iter()
.find(|i| i.id == instance_id)
.and_then(|inst| inst.terminals.first())
.map(|term| term.is_thinking)
.unwrap_or(false)
};
use_side_effect(move || {
if focus_status() == FocusStatus::Not {
renaming.set(false);
}
});
let on_secondary_press = {
let name = name.clone();
move |_: Event<PressEventData>| {
let name = name.clone();
let router = RouterContext::get();
ContextMenu::open(
Menu::new()
.child(
MenuButton::new()
.on_press(move |e: Event<PressEventData>| {
e.prevent_default();
e.stop_propagation();
ContextMenu::close();
*rename_value.write() = name.clone();
renaming.set(true);
})
.child("Rename"),
)
.child(
MenuButton::new()
.on_press(move |_| {
ContextMenu::close();
radio
.write_channel(AppChannel::App)
.remove_instance(instance_id);
router.replace(crate::Route::Home);
})
.child("Close"),
),
);
}
};
let on_rename_submit = move |submitted: String| {
if let Some(inst) = radio
.write_channel(AppChannel::App)
.instances
.iter_mut()
.find(|i| i.id == instance_id)
{
inst.name = submitted;
}
renaming.set(false);
};
let route = crate::Route::Instance { id: instance_id };
let sidebar_item = SideBarItem::new()
.active_background((75, 75, 75))
.background((45, 45, 45))
.padding(if renaming() { (4., 6.) } else { (8., 12.) })
.child(if renaming() {
Input::new(rename_value)
.a11y_id(focus.a11y_id())
.on_submit(on_rename_submit)
.flat()
.compact()
.background(Color::TRANSPARENT)
.hover_background(Color::TRANSPARENT)
.auto_focus(true)
.width(Size::fill())
.into_element()
} else {
rect()
.horizontal()
.width(Size::fill())
.content(Content::Flex)
.child(
label()
.width(Size::flex(1.))
.max_lines(1)
.text_overflow(TextOverflow::Ellipsis)
.text(name.clone()),
)
.child(
rect()
.width(Size::px(16.))
.height(Size::px(16.))
.maybe(is_thinking, |el| {
el.child(
CircularLoader::new()
.size(16.)
.primary_color((220, 220, 220)),
)
})
.into_element(),
)
.into_element()
});
rect()
.maybe(!renaming(), |el| el.on_secondary_press(on_secondary_press))
.child(ActivableRoute::new(
crate::Route::Instance { id: instance_id },
Link::new(route).child(
TooltipContainer::new(Tooltip::new(name.clone()))
.position(TooltipPosition::Besides)
.child(sidebar_item),
),
))
.into_element()
}
}