diff --git a/Trunk.toml b/Trunk.toml index 12e2bfca..c9a88b74 100644 --- a/Trunk.toml +++ b/Trunk.toml @@ -5,6 +5,5 @@ target = "./index.html" ignore = ["./src-tauri"] [serve] -addresses = ["127.0.0.1"] port = 1420 open = false diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 70f2b28b..4d0b4419 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -272,21 +272,30 @@ fn browse(service_type: String, window: Window, state: State) { } } -const METRIC_SEND_INTERVAL: Duration = Duration::from_secs(3); +const METRIC_CHECK_INTERVAL: Duration = Duration::from_secs(1); #[tauri::command] fn send_metrics(window: Window, state: State) { if let Ok(mdns) = state.daemon.lock() { let mdns_for_thread = mdns.clone(); + let mut old_metrics = HashMap::new(); std::thread::spawn(move || loop { + std::thread::sleep(METRIC_CHECK_INTERVAL); if let Ok(metrics_receiver) = mdns_for_thread.get_metrics() { if let Ok(metrics) = metrics_receiver.recv() { - window - .emit("metrics", &MetricsEvent { metrics }) - .expect("To emit"); + if old_metrics != metrics { + window + .emit( + "metrics", + &MetricsEvent { + metrics: metrics.clone(), + }, + ) + .expect("To emit"); + old_metrics = metrics; + } } } - std::thread::sleep(METRIC_SEND_INTERVAL); }); } } @@ -533,9 +542,12 @@ pub fn run() { let splashscreen_window = app.get_webview_window("splashscreen").unwrap(); let main_window = app.get_webview_window("main").unwrap(); tauri::async_runtime::spawn(async move { + #[cfg(not(debug_assertions))] tokio::time::sleep(Duration::from_millis(2000)).await; splashscreen_window.close().unwrap(); main_window.show().unwrap(); + #[cfg(debug_assertions)] + main_window.open_devtools(); }); Ok(()) }) diff --git a/src/app.rs b/src/app.rs index 1642530c..e04c5893 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,24 +1,23 @@ -use gloo_timers::future::TimeoutFuture; +use chrono::{DateTime, Local}; +use error::Error; +use futures::{select, StreamExt}; use leptos::*; - +use leptos_meta::provide_meta_context; +use leptos_meta::Style; +use serde::{Deserialize, Serialize}; use std::{ collections::{HashMap, HashSet}, fmt::Display, net::IpAddr, }; - -use chrono::{DateTime, Local}; -use futures::{select, StreamExt}; -use leptos_meta::provide_meta_context; -use leptos_meta::Style; -use serde::{Deserialize, Serialize}; use strsim::jaro_winkler; use tauri_sys::core::invoke; use tauri_sys::event::listen; use thaw::{ - AutoComplete, AutoCompleteOption, AutoCompleteSuffix, Button, ButtonSize, ButtonVariant, Card, - CardFooter, CardHeaderExtra, Collapse, CollapseItem, GlobalStyle, Grid, GridItem, Icon, Layout, - Modal, Space, SpaceAlign, Table, Tag, TagVariant, Text, Theme, ThemeProvider, + AutoComplete, AutoCompleteOption, AutoCompleteRef, AutoCompleteSuffix, Button, ButtonSize, + ButtonVariant, Card, CardFooter, CardHeaderExtra, Collapse, CollapseItem, ComponentRef, + GlobalStyle, Grid, GridItem, Icon, Layout, Modal, Space, SpaceAlign, Table, Tag, TagVariant, + Text, Theme, ThemeProvider, }; use thaw_utils::Model; @@ -70,19 +69,27 @@ pub struct MetricsEventRes { metrics: HashMap, } -async fn invoke_unit(cmd: &str) { +async fn invoke_no_args(cmd: &str) { + log::debug!("Invoke no args `{cmd}`"); let _ = invoke::<()>(cmd, &()).await; } async fn listen_on_metrics_event(event_writer: WriteSignal>) { - let mut events = listen::("metrics").await.unwrap(); - invoke_unit("send_metrics").await; - while let Some(event) = events.next().await { - event_writer.update(|evts| { - evts.clear(); - evts.extend(event.payload.metrics.into_iter().collect::>()); - evts.sort_by(|a, b| a.0.cmp(&b.0)); - }); + log::debug!("Listen on metrics"); + match listen::("metrics").await { + Ok(mut event) => { + invoke_no_args("send_metrics").await; + while let Some(event) = event.next().await { + log::debug!("Received metrics {:#?}", event); + event_writer.update(|evts| { + *evts = event.payload.metrics.into_iter().collect::>(); + evts.sort_by(|a, b| a.0.cmp(&b.0)); + }); + } + } + Err(error) => { + log::error!("Failed to listen on metrics: {}", error); + } } } @@ -90,21 +97,17 @@ async fn listen_on_metrics_event(event_writer: WriteSignal>) pub struct ServiceTypeFoundEventRes { service_type: String, } - type ServiceTypeRemovedEventRes = ServiceTypeFoundEventRes; -async fn listen_on_service_type_events(event_writer: WriteSignal) { - let found = listen::("service-type-found") - .await - .unwrap(); - let removed = listen::("service-type-removed") - .await - .unwrap(); +async fn listen_on_service_type_event_result( + event_writer: WriteSignal, +) -> Result<(), Error> { + let found = listen::("service-type-found").await?; + let removed = listen::("service-type-removed").await?; let mut found_fused = found.fuse(); let mut removed_fused = removed.fuse(); - - invoke_unit("browse_types").await; + invoke_no_args("browse_types").await; loop { select! { @@ -131,6 +134,16 @@ async fn listen_on_service_type_events(event_writer: WriteSignal) complete => break, } } + Ok(()) +} + +async fn listen_on_service_type_events(event_writer: WriteSignal) { + log::debug!("listen on service type events"); + let result = listen_on_service_type_event_result(event_writer).await; + match result { + Ok(_) => log::debug!("Listen on service type events succeeded"), + Err(e) => log::error!("Listening on service type events failed with: {e}"), + } } #[derive(Deserialize, Clone, Debug)] @@ -144,13 +157,12 @@ pub struct ServiceRemovedEventRes { at_ms: u64, } -async fn listen_on_browse_events(event_writer: WriteSignal) { - let resolved = listen::("service-resolved") - .await - .unwrap(); - let removed = listen::("service-removed") - .await - .unwrap(); +async fn listen_on_resolve_events_result( + event_writer: WriteSignal, +) -> Result<(), Error> { + log::debug!("listen on resolve events with result"); + let resolved = listen::("service-resolved").await?; + let removed = listen::("service-removed").await?; let mut resolved_fused = resolved.fuse(); let mut removed_fused = removed.fuse(); @@ -181,6 +193,15 @@ async fn listen_on_browse_events(event_writer: WriteSignal) { complete => break, } } + Ok(()) +} + +async fn listen_on_resolve_events(event_writer: WriteSignal) { + let result = listen_on_resolve_events_result(event_writer).await; + match result { + Ok(_) => log::debug!("Listening on resolve events succeeded"), + Err(e) => log::error!("Listening on resolve events failed with: {e}"), + } } #[derive(Serialize, Deserialize)] @@ -212,6 +233,7 @@ async fn stop_browse(service_type: String) { /// Component to render a string vector into a table #[component] fn ValuesTable(values: Vec, #[prop(into)] title: String) -> impl IntoView { + log::debug!("ValuesTable"); if values.is_empty() { view! {

}.into_view() } else { @@ -294,8 +316,25 @@ fn AutoCompleteServiceType( #[prop(optional, into)] disabled: MaybeSignal, #[prop(optional, into)] invalid: MaybeSignal, ) -> impl IntoView { + log::debug!("AutoCompleteServiceType"); let (service_types, set_service_types) = create_signal(ServiceTypes::new()); - create_local_resource(move || set_service_types, listen_on_service_type_events); + create_resource(move || set_service_types, listen_on_service_type_events); + + let comp_ref = ComponentRef::::new(); + + create_effect(move |_| { + spawn_local(async move { + set_timeout( + move || { + if let Some(comp) = comp_ref.get_untracked() { + comp.focus(); + } + }, + std::time::Duration::from_millis(3000), + ); + }); + }); + let service_type_options = create_memo(move |_| { service_types .get() @@ -315,6 +354,7 @@ fn AutoCompleteServiceType( }) .collect() }); + view! { @@ -353,7 +393,8 @@ fn ToClipBoardCopyable( text: Option, #[prop(optional, into)] disabled: MaybeSignal, ) -> impl IntoView { - let (text_to_copy, _) = create_signal(text.clone().unwrap_or(String::from(""))); + log::debug!("ToClipBoardCopyable"); + let (text_to_copy, _) = create_signal(text.clone().unwrap_or_default()); let copy_to_clipboard_action = create_action(|input: &String| { let input = input.clone(); async move { copy_to_clipboard(input.clone()).await } @@ -379,6 +420,7 @@ fn ToClipBoardCopyable( /// Component that shows a service as a card #[component] fn ResolvedServiceGridItem(resolved_service: ResolvedService) -> impl IntoView { + log::debug!("ResolvedServiceGridItem"); let mut hostname = resolved_service.hostname; hostname.pop(); // remove the trailing dot let updated_at = @@ -627,11 +669,21 @@ mod tests { /// Component that allows for mdns browsing using events #[component] fn Browse() -> impl IntoView { + log::debug!("Browse"); let (resolved, set_resolved) = create_signal(ResolvedServices::new()); - create_local_resource(move || set_resolved, listen_on_browse_events); + create_resource(move || set_resolved, listen_on_resolve_events); - let service_type = use_context::().unwrap().0; let is_desktop = use_context::().unwrap().0; + let browsing = create_rw_signal(false); + let service_type = create_rw_signal(String::new()); + let not_browsing = Signal::derive(move || !browsing.get()); + let service_type_invalid = create_memo(move |_| { + // TODO: report a meaningful error to the user + check_service_type_fully_qualified(service_type.get().clone().as_str()).is_err() + }); + + let browsing_or_service_type_invalid = + Signal::derive(move || browsing.get() || service_type_invalid.get()); let auto_complete_class = Signal::derive(move || { if is_desktop.get() { @@ -641,15 +693,6 @@ fn Browse() -> impl IntoView { } }); - let service_type_invalid = Signal::derive(move || { - // TODO: report a meaningful error to the user - check_service_type_fully_qualified(service_type.get().clone().as_str()).is_err() - }); - let browsing = use_context::().unwrap().0; - let not_browsing = Signal::derive(move || !browsing.get()); - let browsing_or_service_type_invalid = - Signal::derive(move || browsing.get() || service_type_invalid.get()); - let browse_action = create_action(|input: &String| { let input = input.clone(); async move { browse(input.clone()).await } @@ -657,7 +700,7 @@ fn Browse() -> impl IntoView { let on_browse_click = move |_| { browsing.set(true); - let value = service_type.get(); + let value = service_type.get_untracked(); browse_action.dispatch(value); }; @@ -669,7 +712,7 @@ fn Browse() -> impl IntoView { let on_stop_click = move |_| { browsing.set(false); set_resolved.set(Vec::new()); - let value = service_type.get(); + let value = service_type.get_untracked(); stop_browse_action.dispatch(value); }; @@ -740,12 +783,13 @@ pub struct UpdateMetadata { version: String, current_version: String, } + async fn fetch_update() -> Option { invoke::>("fetch_update", &()).await } async fn install_update() { - invoke_unit("install_update").await; + invoke_no_args("install_update").await; } #[derive(Serialize, Deserialize)] @@ -754,17 +798,19 @@ struct OpenArgs<'a> { } async fn open(url: &str) { + log::debug!("Opening {url}"); let _ = invoke::<()>("open", &OpenArgs { url }).await; } async fn get_version(writer: WriteSignal) { let ver = invoke::("version", &()).await; - log::debug!("Got version {}", ver); + log::debug!("Got version {ver}"); writer.update(|v| *v = ver); } async fn get_can_auto_update(writer: WriteSignal) { let can_auto_update = invoke::("can_auto_update", &()).await; + log::debug!("Got can_auto_update {can_auto_update}"); writer.update(|v| *v = can_auto_update); } @@ -773,11 +819,12 @@ const GITHUB_BASE_URL: &str = "https://github.com/hrzlgnm/mdns-browser"; /// Component for info about the app #[component] pub fn About() -> impl IntoView { + log::debug!("About"); let (version, set_version) = create_signal(String::new()); let (update, set_update) = create_signal(None); let (can_auto_update, set_can_auto_update) = create_signal(false); - create_local_resource(move || set_version, get_version); - create_local_resource(move || set_can_auto_update, get_can_auto_update); + create_resource(move || set_version, get_version); + create_resource(move || set_can_auto_update, get_can_auto_update); let show_no_update = create_rw_signal(false); let show_no_update_with_timeout = move || { @@ -792,7 +839,7 @@ pub fn About() -> impl IntoView { let fetch_update_action = create_action(move |_: &()| async move { let update = fetch_update().await; - log::debug!("got update: {:?}", update); + log::debug!("Got update: {:?}", update); if update.is_none() { show_no_update_with_timeout(); } @@ -815,7 +862,6 @@ pub fn About() -> impl IntoView { let github_action = create_action(|action: &String| { let action = action.clone(); - log::debug!("Opening {}", action); async move { open(action.clone().as_str()).await } }); @@ -843,7 +889,7 @@ pub fn About() -> impl IntoView { let on_check_update_click = move |_| { fetch_update_action.dispatch(()); }; - + log::debug!("About view"); view! { @@ -884,7 +930,7 @@ pub fn About() -> impl IntoView { view! { @@ -932,8 +978,10 @@ pub fn About() -> impl IntoView { /// Component for metrics #[component] pub fn Metrics() -> impl IntoView { + log::debug!("Metrics"); let (metrics, set_metrics) = create_signal(Vec::new()); - create_local_resource(move || set_metrics, listen_on_metrics_event); + create_resource(move || set_metrics, listen_on_metrics_event); + log::debug!("Metrics view"); view! { @@ -970,19 +1018,12 @@ pub fn Metrics() -> impl IntoView { } } -#[derive(Clone, Debug)] -pub struct ServiceTypesSignal(RwSignal); - -#[derive(Clone, Debug)] -pub struct BrowsingSignal(RwSignal); - #[derive(Clone, Debug)] pub struct IsDesktopSignal(RwSignal); async fn get_is_desktop(writer: RwSignal) { - // Workaround for is_dekstop request sometimes failing and resulting in a blank window - TimeoutFuture::new(50).await; let is_desktop = invoke::("is_desktop", &()).await; + log::debug!("Got is_desktop {is_desktop}"); writer.update(|v| *v = is_desktop); } @@ -991,21 +1032,19 @@ async fn get_is_desktop(writer: RwSignal) { pub fn App() -> impl IntoView { provide_meta_context(); let theme = create_rw_signal(Theme::dark()); - let browsing = create_rw_signal(false); let is_desktop = create_rw_signal(false); - create_local_resource(move || is_desktop, get_is_desktop); - let service_type = create_rw_signal(String::new()); - provide_context(BrowsingSignal(browsing)); - provide_context(ServiceTypesSignal(service_type)); + create_resource(move || is_desktop, get_is_desktop); provide_context(IsDesktopSignal(is_desktop)); view! { - - }> - - - - + "Loading" }> + + }> + + + + + } }