diff --git a/models/src/lib.rs b/models/src/lib.rs index 5fc752f..95f6d09 100644 --- a/models/src/lib.rs +++ b/models/src/lib.rs @@ -22,6 +22,7 @@ impl Display for TxtRecord { #[derive(Deserialize, Serialize, Debug, Clone)] pub struct ResolvedService { pub instance_name: String, + pub service_type: String, pub hostname: String, pub port: u16, pub addresses: Vec, @@ -299,6 +300,7 @@ mod tests { fn test_resolved_service_initialization() { // Arrange let instance_name = "test_service".to_string(); + let service_type = "_banan._tcp.local".to_string(); let hostname = "test.local".to_string(); let port = 8080; let addresses = vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))]; @@ -310,6 +312,7 @@ mod tests { // Act let service = ResolvedService { instance_name: instance_name.clone(), + service_type: service_type.clone(), hostname: hostname.clone(), port, addresses: addresses.clone(), @@ -321,6 +324,7 @@ mod tests { // Assert assert_eq!(service.instance_name, instance_name); + assert_eq!(service.service_type, service_type); assert_eq!(service.hostname, hostname); assert_eq!(service.port, port); assert_eq!(service.addresses, addresses); @@ -335,6 +339,7 @@ mod tests { // Arrange let mut service = ResolvedService { instance_name: "test_service".to_string(), + service_type: "_banan._tcp.local.".to_string(), hostname: "test.local".to_string(), port: 8080, addresses: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], @@ -359,6 +364,7 @@ mod tests { // Arrange let mut service = ResolvedService { instance_name: "test_service".to_string(), + service_type: "_banan._tcp.local.".to_string(), hostname: "test.local".to_string(), port: 8080, addresses: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], @@ -383,6 +389,7 @@ mod tests { // Arrange let mut service = ResolvedService { instance_name: "test_service".to_string(), + service_type: "_banan._tcp.local.".to_string(), hostname: "test.local".to_string(), port: 8080, addresses: vec![IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1))], diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2f0859f..0abb819 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -41,6 +41,31 @@ fn get_shared_daemon() -> SharedServiceDaemon { Arc::new(Mutex::new(daemon)) } +fn from_service_info(info: &ServiceInfo) -> ResolvedService { + let mut sorted_addresses: Vec = info.get_addresses().clone().drain().collect(); + sorted_addresses.sort(); + let mut sorted_txt: Vec = info + .get_properties() + .iter() + .map(|r| TxtRecord { + key: r.key().into(), + val: bytes_option_to_string_option_with_escaping(r.val()), + }) + .collect(); + sorted_txt.sort_by(|a, b| a.key.partial_cmp(&b.key).expect("To be partial comparable")); + ResolvedService { + instance_name: info.get_fullname().into(), + service_type: info.get_type().into(), + hostname: info.get_hostname().into(), + port: info.get_port(), + addresses: sorted_addresses, + subtype: info.get_subtype().clone(), + txt: sorted_txt, + updated_at_ms: timestamp_millis(), + dead: false, + } +} + #[tauri::command] fn browse_types(window: Window, state: State) { if let Ok(mdns) = state.daemon.lock() { @@ -68,7 +93,7 @@ fn browse_types(window: Window, state: State) { .emit( "service-type-removed", &ServiceTypeRemovedEvent { - service_type: full_name, + service_type: full_name.clone(), }, ) .expect("To emit"); @@ -87,17 +112,13 @@ fn browse_types(window: Window, state: State) { } #[tauri::command] -fn stop_browse(service_type: String, state: State) { - if service_type.is_empty() { - return; - } +fn stop_browse(state: State) { if let Ok(mdns) = state.daemon.lock() { if let Ok(mut running_browsers) = state.running_browsers.lock() { - if running_browsers.contains(&service_type) { - mdns.stop_browse(service_type.as_str()) - .expect("To stop browsing"); - running_browsers.retain(|s| s != &service_type); - } + running_browsers.iter().for_each(|ty_domain| { + mdns.stop_browse(ty_domain).expect("To stop browsing"); + }); + running_browsers.clear(); } } } @@ -111,90 +132,72 @@ fn verify(instance_fullname: String, state: State) { } } -fn from_service_info(info: &ServiceInfo) -> ResolvedService { - let mut sorted_addresses: Vec = info.get_addresses().clone().drain().collect(); - sorted_addresses.sort(); - let mut sorted_txt: Vec = info - .get_properties() - .iter() - .map(|r| TxtRecord { - key: r.key().into(), - val: bytes_option_to_string_option_with_escaping(r.val()), - }) - .collect(); - sorted_txt.sort_by(|a, b| a.key.partial_cmp(&b.key).expect("To be partial comparable")); - ResolvedService { - instance_name: info.get_fullname().into(), - hostname: info.get_hostname().into(), - port: info.get_port(), - addresses: sorted_addresses, - subtype: info.get_subtype().clone(), - txt: sorted_txt, - updated_at_ms: timestamp_millis(), - dead: false, - } -} - #[tauri::command] -fn browse(service_type: String, window: Window, state: State) { - if service_type.is_empty() { - return; - } - if let Ok(mdns) = state.daemon.lock() { - if let Ok(mut running_browsers) = state.running_browsers.lock() { - if !running_browsers.contains(&service_type) { - running_browsers.push(service_type.clone()); - let receiver = mdns.browse(service_type.as_str()).expect("To browse"); - std::thread::spawn(move || { - while let Ok(event) = receiver.recv() { - match event { - ServiceEvent::ServiceFound(_service_type, instance_name) => { - window - .emit( - "service-found", - &ServiceFoundEvent { - instance_name, - at_ms: timestamp_millis(), - }, - ) - .expect("To emit"); - } - ServiceEvent::SearchStarted(service_type) => { - window - .emit("search-started", &SearchStartedEvent { service_type }) - .expect("to emit"); - } - ServiceEvent::ServiceResolved(info) => { - window - .emit( - "service-resolved", - &ServiceResolvedEvent { - service: from_service_info(&info), - }, - ) - .expect("To emit"); - } - ServiceEvent::ServiceRemoved(_service_type, instance_name) => { - window - .emit( - "service-removed", - &ServiceRemovedEvent { - instance_name, - at_ms: timestamp_millis(), - }, - ) - .expect("To emit"); - } - ServiceEvent::SearchStopped(service_type) => { - window - .emit("search-stopped", &SearchStoppedEvent { service_type }) - .expect("To emit"); - break; +fn browse_many(service_types: Vec, window: Window, state: State) { + for service_type in service_types { + if let Ok(mdns) = state.daemon.lock() { + if let Ok(mut running_browsers) = state.running_browsers.lock() { + if !running_browsers.contains(&service_type) { + running_browsers.push(service_type.clone()); + let receiver = mdns.browse(service_type.as_str()).expect("To browse"); + let window = window.clone(); + std::thread::spawn(move || { + while let Ok(event) = receiver.recv() { + match event { + ServiceEvent::ServiceFound(_service_type, instance_name) => { + window + .emit( + "service-found", + &ServiceFoundEvent { + instance_name, + at_ms: timestamp_millis(), + }, + ) + .expect("To emit"); + } + ServiceEvent::SearchStarted(service_type) => { + window + .emit( + "search-started", + &SearchStartedEvent { service_type }, + ) + .expect("to emit"); + } + ServiceEvent::ServiceResolved(info) => { + window + .emit( + "service-resolved", + &ServiceResolvedEvent { + service: from_service_info(&info), + }, + ) + .expect("To emit"); + } + ServiceEvent::ServiceRemoved(_service_type, instance_name) => { + window + .emit( + "service-removed", + &ServiceRemovedEvent { + instance_name, + at_ms: timestamp_millis(), + }, + ) + .expect("To emit"); + } + ServiceEvent::SearchStopped(service_type) => { + window + .emit( + "search-stopped", + &SearchStoppedEvent { service_type }, + ) + .expect("To emit"); + break; + } } } - } - log::debug!("Browse thread for {} ending.", &service_type); - }); + log::debug!("Browse thread for {} ending.", &service_type); + }); + } } } } @@ -477,7 +480,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ app_updates::fetch_update, app_updates::install_update, - browse, + browse_many, browse_types, can_auto_update, copy_to_clipboard, @@ -500,7 +503,7 @@ pub fn run_mobile() { .plugin(tauri_plugin_clipboard_manager::init()) .manage(ManagedState::new()) .invoke_handler(tauri::generate_handler![ - browse, + browse_many, browse_types, copy_to_clipboard, is_desktop, diff --git a/src-tauri/tauri.android.conf.json b/src-tauri/tauri.android.conf.json index 58edffd..1fa11ca 100644 --- a/src-tauri/tauri.android.conf.json +++ b/src-tauri/tauri.android.conf.json @@ -9,7 +9,7 @@ "active": false }, "productName": "mDNS Browser", - "version": "0.9.11", + "version": "0.10.0", "identifier": "com.github.hrzlgnm.mdns-browser", "plugins": {}, "app": { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 322a31b..0d9b76e 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -38,7 +38,7 @@ } }, "productName": "mdns-browser", - "version": "0.9.11", + "version": "0.10.0", "identifier": "com.github.hrzlgnm.mdns-browser", "plugins": { "updater": { diff --git a/src/app.rs b/src/app.rs index de3db46..ece2632 100644 --- a/src/app.rs +++ b/src/app.rs @@ -14,11 +14,13 @@ use std::collections::HashSet; use strsim::jaro_winkler; use tauri_sys::core::invoke; use tauri_sys::event::listen; +use thaw::ButtonColor; +use thaw::SpaceJustify; use thaw::{ 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, + GlobalStyle, Grid, GridItem, Icon, Layout, Modal, Space, SpaceAlign, Table, Text, Theme, + ThemeProvider, }; use thaw_utils::Model; @@ -47,7 +49,7 @@ async fn listen_on_metrics_event(event_writer: WriteSignal>) } async fn listen_on_service_type_event_result( - event_writer: WriteSignal, + event_writer: RwSignal, ) -> Result<(), Error> { let found = listen::("service-type-found").await?; let removed = listen::("service-type-removed").await?; @@ -84,7 +86,7 @@ async fn listen_on_service_type_event_result( Ok(()) } -async fn listen_on_service_type_events(event_writer: WriteSignal) { +async fn listen_on_service_type_events(event_writer: RwSignal) { log::debug!("listen on service type events"); let result = listen_on_service_type_event_result(event_writer).await; match result { @@ -142,30 +144,34 @@ async fn listen_on_resolve_events(event_writer: WriteSignal) { #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] -struct BrowseArgs<'a> { - serviceType: &'a str, +struct BrowseManyArgs { + serviceTypes: Vec, } async fn browse(service_type: String) { let _ = invoke::<()>( - "browse", - &BrowseArgs { - serviceType: &service_type, + "browse_many", + &BrowseManyArgs { + serviceTypes: vec![service_type], }, ) .await; } -async fn stop_browse(service_type: String) { +async fn browse_many(service_types: Vec) { let _ = invoke::<()>( - "stop_browse", - &BrowseArgs { - serviceType: &service_type, + "browse_many", + &BrowseManyArgs { + serviceTypes: service_types, }, ) .await; } +async fn stop_browse() { + invoke_no_args("stop_browse").await; +} + #[derive(Serialize, Deserialize)] #[allow(non_snake_case)] struct VerifyArgs<'a> { @@ -270,8 +276,11 @@ fn AutoCompleteServiceType( #[prop(optional, into)] comp_ref: ComponentRef, ) -> impl IntoView { log::debug!("AutoCompleteServiceType"); - let (service_types, set_service_types) = create_signal(ServiceTypes::new()); - create_resource(move || set_service_types, listen_on_service_type_events); + let service_types = use_context::() + .expect("service_tyxpes context to exist") + .0; + + create_resource(move || service_types, listen_on_service_type_events); let service_type_options = create_memo(move |_| { service_types @@ -355,7 +364,11 @@ async fn copy_to_clipboard(contents: String) { .await; } -/// Component that shows a service as a card +fn drop_local(fqn: &str) -> String { + fqn.strip_suffix(".local.").unwrap_or(fqn).to_owned() +} + +/// Component that shows a resolved service as a card #[component] fn ResolvedServiceGridItem(resolved_service: ResolvedService) -> impl IntoView { log::debug!("ResolvedServiceGridItem"); @@ -376,9 +389,28 @@ fn ResolvedServiceGridItem(resolved_service: ResolvedService) -> impl IntoView { VERIFY_TIMEOUT, ) }; + let copy_to_clipboard_action = create_action(|input: &String| { + let input = input.clone(); + async move { copy_to_clipboard(input.clone()).await } + }); + + let hostname = drop_local(&resolved_service.hostname); + let hostname_sig = create_rw_signal(resolved_service.hostname.clone()); + let on_copy_hostname_to_clibboard_click = move |_| { + copy_to_clipboard_action.dispatch(hostname_sig.get_untracked()); + }; + + let port_sig = create_rw_signal(resolved_service.port.to_string()); + let on_copy_port_to_clibboard_click = move |_| { + copy_to_clipboard_action.dispatch(port_sig.get_untracked()); + }; + + let service_type_without_local = drop_local(&resolved_service.service_type); + let service_type_sig = create_rw_signal(resolved_service.service_type.clone()); + let on_copy_service_type_to_clibboard_click = move |_| { + copy_to_clipboard_action.dispatch(service_type_sig.get_untracked()); + }; - let mut hostname = resolved_service.hostname; - hostname.pop(); // remove the trailing dot let updated_at = DateTime::from_timestamp_millis(resolved_service.updated_at_ms as i64) .expect("To get convert"); let as_local_datetime: DateTime = updated_at.with_timezone(&Local); @@ -400,10 +432,10 @@ fn ResolvedServiceGridItem(resolved_service: ResolvedService) -> impl IntoView { let card_title = get_instance_name(resolved_service.instance_name.as_str()); let details_title = card_title.clone(); let show_details = create_rw_signal(false); - let (hostname_variant, port_variant, addrs_footer) = if resolved_service.dead { - (TagVariant::Default, TagVariant::Default, vec![]) + let addrs_footer = if resolved_service.dead { + vec![] } else { - (TagVariant::Success, TagVariant::Warning, addrs.clone()) + addrs.clone() }; view! { @@ -411,28 +443,62 @@ fn ResolvedServiceGridItem(resolved_service: ResolvedService) -> impl IntoView { {as_local_datetime.format("%Y-%m-%d %H:%M:%S").to_string()} - - - - - - - + + + + + + + + + + + + + + + impl IntoView { } } +#[derive(Clone, Debug)] +pub struct ServiceTypesSignal(RwSignal); + /// Component that allows for mdns browsing using events #[component] fn Browse() -> impl IntoView { log::debug!("Browse"); + let service_types = create_rw_signal(ServiceTypes::new()); + provide_context(ServiceTypesSignal(service_types)); + + let browsing_all = create_rw_signal(false); + let (resolved, set_resolved) = create_signal(ResolvedServices::new()); create_resource(move || set_resolved, listen_on_resolve_events); let is_desktop = use_context::() .expect("is_desktop context to exist") .0; + let browsing = create_rw_signal(false); let service_type = create_rw_signal(String::new()); let not_browsing = Signal::derive(move || !browsing.get()); @@ -484,18 +559,27 @@ fn Browse() -> impl IntoView { browse_action.dispatch(value); }; - let stop_browse_action = create_action(|input: &String| { + let browse_many_action = create_action(|input: &ServiceTypes| { let input = input.clone(); - async move { stop_browse(input.clone()).await } + async move { browse_many(input.clone()).await } }); + let on_browse_many_click = move |_| { + browsing_all.set(true); + browsing.set(true); + let value = service_types.get_untracked(); + browse_many_action.dispatch(value); + }; + + let stop_browsing_action = create_action(|_| async move { stop_browse().await }); + let comp_ref = ComponentRef::::new(); - let on_stop_click = move |_| { + let on_stopbrowsing_click = move |_| { browsing.set(false); + browsing_all.set(false); set_resolved.set(Vec::new()); - let value = service_type.get_untracked(); - stop_browse_action.dispatch(value); + stop_browsing_action.dispatch(()); service_type.set(String::new()); if let Some(comp) = comp_ref.get_untracked() { comp.focus(); @@ -529,9 +613,12 @@ fn Browse() -> impl IntoView { - +