use std::path::Path; use freya::prelude::*; use oneclient_java::{JavaRuntime, JavaVendor, is_launcher_managed}; use super::settings_page; use crate::components::{Button, Icon, IconType, JavaInstallManager, OverlayPopup, ScrollArea}; use crate::hooks::{Actions, java_runtimes, use_dispatch, use_java_runtimes}; use crate::invalidate_java_queries; use crate::theme::colors; use crate::ui::border_all_color; use crate::view::app::settings::section_header; #[derive(PartialEq)] pub struct SettingsJava; impl Component for SettingsJava { fn render(&self) -> impl IntoElement { let dispatch = use_dispatch(); let removing_dispatch = dispatch.clone(); let runtimes_query = use_java_runtimes(); let runtimes = java_runtimes(&runtimes_query); let mut show_manager = use_state(|| false); let pending_remove = use_state(|| None::); let mut shell = settings_page() .child(section_header("ADD RUNTIME")) .child(AddRow { show_manager }.into_element()) .child(section_header("INSTALLED RUNTIMES")) .child(runtimes_table(runtimes, pending_remove)); if *show_manager.read() { shell = shell.child( JavaInstallManager::new() .on_install(move |(vendor, major): (JavaVendor, u32)| { dispatch.install_java_runtime(vendor, major); show_manager.set(false); }) .on_close(move |()| show_manager.set(false)) .into_element(), ); } let pending = pending_remove.read().clone(); if let Some(target) = pending { shell = shell.child(confirm_remove_modal( removing_dispatch, pending_remove, target, )) } shell.into_element() } } #[derive(PartialEq)] struct AddRow { show_manager: State, } impl Component for AddRow { fn render(&self) -> impl IntoElement { let dispatch = use_dispatch(); let mut show_manager = self.show_manager; let pick_dispatch = dispatch.clone(); let pick = move |_| { let dispatch = pick_dispatch.clone(); spawn(async move { if let Some(handle) = rfd::AsyncFileDialog::new() .set_title("Select a Java installation folder") .pick_folder() .await { dispatch.add_custom_java_runtime(handle.path().to_path_buf()); } }); }; let refresh_dispatch = dispatch; let refresh = move |_| { let dispatch = refresh_dispatch.clone(); spawn(async move { invalidate_java_queries().await; dispatch .notify("Java runtimes refreshed") .body("The installed runtime list is up to date") .info() .toast_only() .send(); }); }; rect() .horizontal() .width(Size::fill()) .spacing(10.) .child( Button::new() .primary() .on_press(move |_| show_manager.set(true)) .child(Icon::new(IconType::Download01).size(14.)) .text("Install Manager"), ) .child( Button::new() .secondary() .on_press(pick) .child(Icon::new(IconType::Folder).size(14.)) .text("Add from folder"), ) .child( Button::new() .secondary() .on_press(refresh) .child(Icon::new(IconType::RefreshCcw02).size(14.)) .text("Refresh"), ) } } fn runtimes_table( runtimes: Vec, pending_remove: State>, ) -> impl IntoElement { if runtimes.is_empty() { return rect() .width(Size::fill()) .padding(Gaps::new_symmetric(16., 16.)) .corner_radius(CornerRadius::new_all(12.)) .background(colors::page_elevated()) .child( label() .text("No Java runtimes installed yet.") .font_size(12.) .color(colors::fg_secondary()), ) .into_element(); } let mut table = rect() .vertical() .width(Size::fill()) .corner_radius(CornerRadius::new_all(12.)) .background(colors::page_elevated()) .border(border_all_color(1., colors::component_border())) .overflow(Overflow::Clip) .child(table_header()); let count = runtimes.len(); for (idx, runtime) in runtimes.into_iter().enumerate() { table = table.child( RuntimeRow { runtime, last: idx + 1 == count, pending_remove, } .into_element(), ); } table.into_element() } fn table_header() -> impl IntoElement { fn head(text: &'static str, width: Size) -> impl IntoElement { rect() .width(width) .child( label() .text(text) .font_size(11.) .font_weight(FontWeight::MEDIUM) .color(colors::fg_secondary()), ) .into_element() } rect() .horizontal() .width(Size::fill()) .content(Content::Flex) .cross_align(Alignment::Center) .spacing(12.) .padding(Gaps::new_symmetric(8., 14.)) .background(colors::component_bg()) .child(head("VENDOR", Size::px(130.))) .child(head("VERSION", Size::px(90.))) .child( rect().width(Size::flex(1.0)).child( label() .text("PATH") .font_size(11.) .font_weight(FontWeight::MEDIUM) .color(colors::fg_secondary()), ), ) .child(rect().width(Size::px(34.))) .into_element() } #[derive(PartialEq)] struct RuntimeRow { runtime: JavaRuntime, last: bool, pending_remove: State>, } impl Component for RuntimeRow { fn render(&self) -> impl IntoElement { let runtime = &self.runtime; let path = runtime.absolute_path.clone(); // so the scrollarea becomes bigger when horizontal scroll bar is visible (then it won't obscure the file path) let mut viewport_w = use_state(|| 0f32); let content_w = path_content_width(&runtime.absolute_path); let measured_w = *viewport_w.read(); let has_scrollbar = measured_w <= 0. || measured_w < content_w; let path_height = if has_scrollbar { 28. } else { 18. }; fn cell(text: String, width: Size) -> impl IntoElement { rect() .width(width) .child( label() .text(text) .font_size(13.) .color(colors::fg_primary()), ) .into_element() } rect() .horizontal() .width(Size::fill()) .content(Content::Flex) .cross_align(Alignment::Center) .spacing(12.) .padding(Gaps::new_symmetric(8., 14.)) .maybe(!self.last, |el| { el.border( Border::new() .width(BorderWidth { bottom: 1., ..Default::default() }) .fill(colors::component_border()), ) }) .child(cell( format!("{} {}", runtime.vendor, runtime.major), Size::px(130.), )) .child(cell(runtime.version.clone(), Size::px(90.))) .child( rect() .width(Size::flex(1.0)) .height(Size::px(path_height)) .overflow(Overflow::Clip) .on_sized(move |e: Event| { let w = e.area.width(); if (*viewport_w.read() - w).abs() > 0.5 { viewport_w.set(w); } }) .child( ScrollArea::new() .horizontal(content_w) .width(Size::fill()) .height(Size::px(path_height)) .show_scrollbar(false) .child( label() .text(runtime.absolute_path.clone()) .font_size(12.) .max_lines(1) .color(colors::fg_secondary()) .into_element(), ), ), ) .child(remove_button(self.pending_remove, path)) } } fn path_content_width(path: &str) -> f32 { (path.chars().count() as f32 * 7.0).max(1.0) } #[derive(Clone, PartialEq)] struct PendingRemove { path: String, /// Whether the files live in OneClient's own java dir, which is the only /// case where removal takes them off disk managed: bool, } fn confirm_remove_modal( dispatch: Actions, mut pending: State>, target: PendingRemove, ) -> impl IntoElement { let PendingRemove { path, managed } = target; let remove_path = path.clone(); let consequence = if managed { "This runtime is managed by OneClient. Its files WILL be deleted." } else { "This runtime is not managed by OneClient. Its files will not be deleted." }; let confirm = if managed { Button::new().danger().text("Remove and delete files") } else { Button::new().primary().text("Remove from list") }; OverlayPopup::new() .on_close(move |()| pending.set(None)) .child( rect() .width(Size::window_percent(100.)) .height(Size::window_percent(100.)) .center() .child( rect() .vertical() .width(Size::px(440.)) .max_width(Size::window_percent(90.)) .spacing(14.) .padding(Gaps::new_all(20.)) .corner_radius(CornerRadius::new_all(14.)) .background(colors::page_elevated()) .child( label() .text("Remove Java Runtime?") .font_size(16.) .font_weight(FontWeight::SEMI_BOLD) .color(colors::fg_primary()), ) .child( label() .text(consequence) .font_size(12.) .max_lines(4) .width(Size::fill()) .color(colors::fg_secondary()), ) .child( label() .text(path) .font_size(12.) .max_lines(3) .width(Size::fill()) .color(colors::fg_secondary()), ) .child( rect() .horizontal() .width(Size::fill()) .main_align(Alignment::End) .spacing(8.) .child( Button::new() .secondary() .on_press(move |_| pending.set(None)) .text("Cancel"), ) .child(confirm.on_press(move |_| { dispatch.remove_java_runtime(remove_path.clone()); pending.set(None); })), ), ), ) .into_element() } fn remove_button( mut pending_remove: State>, path: String, ) -> impl IntoElement { Button::new() .ghost() .small() .on_press(move |_| { pending_remove.set(Some(PendingRemove { managed: is_launcher_managed(Path::new(&path)), path: path.clone(), })); }) .child( Icon::new(IconType::Trash01) .size(14.) .color(colors::danger()), ) .into_element() }