diff --git a/CHANGELOG.md b/CHANGELOG.md index d534982b3..670e8f991 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ All Sniffnet releases with the relative changes are documented in this file. ## [UNRELEASED] +- Introduced thumbnail mode, enabling users to keep an eye on Sniffnet while doing other tasks ([#484](https://github.com/GyulyVGC/sniffnet/pull/484)) - Added support for ICMP connections and messages ([#417](https://github.com/GyulyVGC/sniffnet/pull/417) — fixes [#288](https://github.com/GyulyVGC/sniffnet/issues/288)) - Added capability to identify 6000+ upper layer services, protocols, trojans, and worms ([#450](https://github.com/GyulyVGC/sniffnet/pull/450) — fixes [#374](https://github.com/GyulyVGC/sniffnet/issues/374)) - Added feature to optionally export the analysis as a PCAP file with a custom path ([#473](https://github.com/GyulyVGC/sniffnet/pull/473) — fixes [#162](https://github.com/GyulyVGC/sniffnet/issues/162) and [#291](https://github.com/GyulyVGC/sniffnet/issues/291)) diff --git a/resources/fonts/subset/icons.ttf b/resources/fonts/subset/icons.ttf index b06c382dc..dbe3cb959 100644 Binary files a/resources/fonts/subset/icons.ttf and b/resources/fonts/subset/icons.ttf differ diff --git a/src/chart/manage_chart_data.rs b/src/chart/manage_chart_data.rs index b0f721d3d..3da6cae04 100644 --- a/src/chart/manage_chart_data.rs +++ b/src/chart/manage_chart_data.rs @@ -172,6 +172,7 @@ mod tests { language: Language::default(), chart_type: ChartType::Packets, style: StyleType::default(), + thumbnail: false, }; let mut runtime_data = RunTimeData { all_bytes: 0, diff --git a/src/chart/types/traffic_chart.rs b/src/chart/types/traffic_chart.rs index db7ed2a89..96147624b 100644 --- a/src/chart/types/traffic_chart.rs +++ b/src/chart/types/traffic_chart.rs @@ -42,6 +42,8 @@ pub struct TrafficChart { pub chart_type: ChartType, /// Style of the chart pub style: StyleType, + /// Whether the chart is for the thumbnail page + pub thumbnail: bool, } impl TrafficChart { @@ -59,6 +61,7 @@ impl TrafficChart { language, chart_type: ChartType::Bytes, style, + thumbnail: false, } } @@ -78,6 +81,24 @@ impl TrafficChart { self.style = style; } + fn set_margins_and_label_areas( + &self, + chart_builder: &mut ChartBuilder, + ) { + if self.thumbnail { + chart_builder.margin_right(0); + chart_builder.margin_left(0); + chart_builder.margin_bottom(0); + chart_builder.margin_top(5); + } else { + chart_builder + .margin_right(25) + .margin_top(6) + .set_label_area_size(LabelAreaPosition::Left, 55) + .set_label_area_size(LabelAreaPosition::Bottom, 40); + } + } + fn x_axis_range(&self) -> Range { let first_time_displayed = if self.ticks > 30 { self.ticks - 30 } else { 0 }; let tot_seconds = self.ticks - 1; @@ -156,22 +177,22 @@ impl Chart for TrafficChart { return; } - chart_builder - .margin_right(25) - .margin_top(6) - .set_label_area_size(LabelAreaPosition::Left, 55) - .set_label_area_size(LabelAreaPosition::Bottom, 40); + self.set_margins_and_label_areas(&mut chart_builder); let x_axis_range = self.x_axis_range(); let y_axis_range = self.y_axis_range(); - let x_labels = if self.ticks == 1 { + let x_labels = if self.ticks == 1 || self.thumbnail { 0 } else { self.ticks as usize }; #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] - let y_labels = 1 + (y_axis_range.end - y_axis_range.start) as usize; + let y_labels = if self.thumbnail { + 0 + } else { + 1 + (y_axis_range.end - y_axis_range.start) as usize + }; let mut chart = chart_builder .build_cartesian_2d(x_axis_range, y_axis_range) @@ -212,14 +233,16 @@ impl Chart for TrafficChart { } // chart legend - chart - .configure_series_labels() - .position(SeriesLabelPosition::UpperRight) - .background_style(buttons_color.mix(0.6)) - .border_style(buttons_color.stroke_width(CHARTS_LINE_BORDER * 2)) - .label_font(self.font(13.5)) - .draw() - .expect("Error drawing graph"); + if !self.thumbnail { + chart + .configure_series_labels() + .position(SeriesLabelPosition::UpperRight) + .background_style(buttons_color.mix(0.6)) + .border_style(buttons_color.stroke_width(CHARTS_LINE_BORDER * 2)) + .label_font(self.font(13.5)) + .draw() + .expect("Error drawing graph"); + } } } diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 34f587f66..4cf4f6426 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -90,6 +90,7 @@ mod tests { window: ConfigWindow { position: (440, 99), size: (452, 870), + thumbnail_position: (20, 20), }, }; // we want to be sure that modified config is different from defaults diff --git a/src/configs/types/config_window.rs b/src/configs/types/config_window.rs index d40123717..d794b506f 100644 --- a/src/configs/types/config_window.rs +++ b/src/configs/types/config_window.rs @@ -9,9 +9,14 @@ use crate::SNIFFNET_LOWERCASE; pub struct ConfigWindow { pub position: (i32, i32), pub size: (u32, u32), + pub thumbnail_position: (i32, i32), } impl ConfigWindow { + pub const DEFAULT_SIZE: (u32, u32) = (1190, 670); + pub const MIN_SIZE: (u32, u32) = (800, 500); + const THUMBNAIL_SIZE: (u32, u32) = (360, 222); + const FILE_NAME: &'static str = "window"; #[cfg(not(test))] pub fn load() -> Self { @@ -28,13 +33,18 @@ impl ConfigWindow { pub fn store(self) { confy::store(SNIFFNET_LOWERCASE, Self::FILE_NAME, self).unwrap_or(()); } + + pub fn thumbnail_size(factor: f64) -> (u32, u32) { + Self::THUMBNAIL_SIZE.scale(factor) + } } impl Default for ConfigWindow { fn default() -> Self { Self { position: (0, 0), - size: (1190, 670), + size: ConfigWindow::DEFAULT_SIZE, + thumbnail_position: (0, 0), } } } @@ -53,6 +63,20 @@ impl ToPosition for (i32, i32) { } } +pub trait ToPoint { + fn to_point(self) -> Point; +} + +impl ToPoint for (i32, i32) { + fn to_point(self) -> Point { + #[allow(clippy::cast_precision_loss)] + Point { + x: self.0 as f32, + y: self.1 as f32, + } + } +} + pub trait ToSize { fn to_size(self) -> Size; } @@ -67,6 +91,28 @@ impl ToSize for (u32, u32) { } } +pub trait Scale { + fn scale(self, factor: f64) -> Self; +} + +impl Scale for (u32, u32) { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + fn scale(self, factor: f64) -> (u32, u32) { + let x = (f64::from(self.0) * factor) as u32; + let y = (f64::from(self.1) * factor) as u32; + (x, y) + } +} + +impl Scale for (i32, i32) { + #[allow(clippy::cast_possible_truncation)] + fn scale(self, factor: f64) -> (i32, i32) { + let x = (f64::from(self.0) * factor) as i32; + let y = (f64::from(self.1) * factor) as i32; + (x, y) + } +} + #[cfg(test)] mod tests { use crate::ConfigWindow; diff --git a/src/countries/country_utils.rs b/src/countries/country_utils.rs index cb1ac2daf..dc8bd62dd 100644 --- a/src/countries/country_utils.rs +++ b/src/countries/country_utils.rs @@ -318,11 +318,16 @@ fn get_flag_from_country( pub fn get_flag_tooltip( country: Country, - width: f32, host_info: &DataInfoHost, language: Language, font: Font, + thumbnail: bool, ) -> Tooltip<'static, Message, StyleType> { + let width = if thumbnail { + FLAGS_WIDTH_SMALL + } else { + FLAGS_WIDTH_BIG + }; let is_local = host_info.is_local; let is_loopback = host_info.is_loopback; let traffic_type = host_info.traffic_type; @@ -335,13 +340,19 @@ pub fn get_flag_tooltip( language, ); + let actual_tooltip = if thumbnail { String::new() } else { tooltip }; + let tooltip_style = if thumbnail { + ContainerType::Standard + } else { + ContainerType::Tooltip + }; let mut tooltip = Tooltip::new( content, - Text::new(tooltip).font(font), + Text::new(actual_tooltip).font(font), Position::FollowCursor, ) .snap_within_viewport(true) - .style(ContainerType::Tooltip); + .style(tooltip_style); if width == FLAGS_WIDTH_SMALL { tooltip = tooltip.padding(3); diff --git a/src/gui/app.rs b/src/gui/app.rs index 6244be14b..2c2c2d08b 100644 --- a/src/gui/app.rs +++ b/src/gui/app.rs @@ -6,6 +6,7 @@ use std::time::Duration; use iced::keyboard::key::Named; use iced::keyboard::{Event, Key, Modifiers}; +use iced::mouse::Event::ButtonPressed; use iced::widget::Column; use iced::window::Id; use iced::Event::{Keyboard, Window}; @@ -23,6 +24,7 @@ use crate::gui::pages::overview_page::overview_page; use crate::gui::pages::settings_general_page::settings_general_page; use crate::gui::pages::settings_notifications_page::settings_notifications_page; use crate::gui::pages::settings_style_page::settings_style_page; +use crate::gui::pages::thumbnail_page::thumbnail_page; use crate::gui::pages::types::running_page::RunningPage; use crate::gui::pages::types::settings_page::SettingsPage; use crate::gui::types::message::Message; @@ -63,22 +65,21 @@ impl Application for Sniffer { let font = style.get_extension().font; let font_headers = style.get_extension().font_headers; - let header = header( - font, - color_gradient, - self.running_page.ne(&RunningPage::Init), - language, - self.last_opened_setting, - ); + let header = header(self); - let body = match self.running_page { - RunningPage::Init => initial_page(self), - RunningPage::Overview => overview_page(self), - RunningPage::Inspect => inspect_page(self), - RunningPage::Notifications => notifications_page(self), + let body = if self.thumbnail { + thumbnail_page(self) + } else { + match self.running_page { + RunningPage::Init => initial_page(self), + RunningPage::Overview => overview_page(self), + RunningPage::Inspect => inspect_page(self), + RunningPage::Notifications => notifications_page(self), + } }; let footer = footer( + self.thumbnail, language, color_gradient, font, @@ -122,7 +123,9 @@ impl Application for Sniffer { fn subscription(&self) -> Subscription { const NO_MODIFIER: Modifiers = Modifiers::empty(); - let window_events_subscription = iced::event::listen_with(|event, _| match event { + + // Window subscription + let window_sub = iced::event::listen_with(|event, _| match event { Window(Id::MAIN, window::Event::Focused) => Some(Message::WindowFocused), Window(Id::MAIN, window::Event::Moved { x, y }) => Some(Message::WindowMoved(x, y)), Window(Id::MAIN, window::Event::Resized { width, height }) => { @@ -131,7 +134,9 @@ impl Application for Sniffer { Window(Id::MAIN, window::Event::CloseRequested) => Some(Message::CloseRequested), _ => None, }); - let hot_keys_subscription = iced::event::listen_with(|event, _| match event { + + // Keyboard subscription + let keyboard_sub = iced::event::listen_with(|event, _| match event { Keyboard(Event::KeyPressed { key, modifiers, .. }) => match modifiers { Modifiers::COMMAND => match key.as_ref() { Key::Character("q") => Some(Message::CloseRequested), @@ -156,17 +161,27 @@ impl Application for Sniffer { }, _ => None, }); - let time_subscription = if self.running_page.eq(&RunningPage::Init) { + + // Mouse subscription + let mouse_sub = iced::event::listen_with(|event, _| match event { + iced::event::Event::Mouse(ButtonPressed(_)) => Some(Message::Drag), + _ => None, + }); + + // Time subscription + let time_sub = if self.running_page.eq(&RunningPage::Init) { iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickInit) } else { iced::time::every(Duration::from_millis(PERIOD_TICK)).map(|_| Message::TickRun) }; - Subscription::batch([ - window_events_subscription, - hot_keys_subscription, - time_subscription, - ]) + let mut subscriptions = Vec::from([window_sub, time_sub]); + if self.thumbnail { + subscriptions.push(mouse_sub); + } else { + subscriptions.push(keyboard_sub); + } + Subscription::batch(subscriptions) } fn theme(&self) -> Self::Theme { diff --git a/src/gui/components/button.rs b/src/gui/components/button.rs index 6179b35f1..c8ece9567 100644 --- a/src/gui/components/button.rs +++ b/src/gui/components/button.rs @@ -48,7 +48,7 @@ pub fn button_open_file( action: fn(String) -> Message, ) -> Tooltip<'static, Message, StyleType> { let mut tooltip_str = ""; - let mut tooltip_style = ContainerType::Neutral; + let mut tooltip_style = ContainerType::Standard; let mut button = button( Icon::File diff --git a/src/gui/components/footer.rs b/src/gui/components/footer.rs index 1c4a79535..8ddb34fa7 100644 --- a/src/gui/components/footer.rs +++ b/src/gui/components/footer.rs @@ -5,8 +5,8 @@ use std::sync::{Arc, Mutex}; use iced::alignment::{Horizontal, Vertical}; use iced::widget::text::LineHeight; use iced::widget::tooltip::Position; -use iced::widget::Space; use iced::widget::{button, Container, Row, Text, Tooltip}; +use iced::widget::{horizontal_space, Space}; use iced::{Alignment, Font, Length}; use crate::gui::components::button::row_open_link_tooltip; @@ -24,12 +24,17 @@ use crate::utils::types::web_page::WebPage; use crate::{Language, SNIFFNET_TITLECASE}; pub fn footer( + thumbnail: bool, language: Language, color_gradient: GradientType, font: Font, font_footer: Font, newer_release_available: &Arc>>, ) -> Container<'static, Message, StyleType> { + if thumbnail { + return thumbnail_footer(); + } + let release_details_row = get_release_details(language, font, font_footer, newer_release_available); @@ -165,3 +170,7 @@ fn get_release_details( } ret_val } + +fn thumbnail_footer() -> Container<'static, Message, StyleType> { + Container::new(horizontal_space()).height(0) +} diff --git a/src/gui/components/header.rs b/src/gui/components/header.rs index 05d29ea86..2ebd17346 100644 --- a/src/gui/components/header.rs +++ b/src/gui/components/header.rs @@ -6,21 +6,45 @@ use iced::widget::tooltip::Position; use iced::widget::{button, horizontal_space, Container, Row, Space, Text, Tooltip}; use iced::{Alignment, Font, Length}; +use crate::configs::types::config_settings::ConfigSettings; +use crate::gui::components::tab::notifications_badge; +use crate::gui::pages::types::running_page::RunningPage; use crate::gui::pages::types::settings_page::SettingsPage; +use crate::gui::styles::button::ButtonType; use crate::gui::styles::container::ContainerType; use crate::gui::styles::types::gradient_type::GradientType; use crate::gui::types::message::Message; +use crate::gui::types::sniffer::Sniffer; use crate::translations::translations::{quit_analysis_translation, settings_translation}; +use crate::translations::translations_3::thumbnail_mode_translation; use crate::utils::types::icon::Icon; -use crate::{Language, StyleType}; +use crate::{Language, StyleType, SNIFFNET_TITLECASE}; + +pub fn header(sniffer: &Sniffer) -> Container<'static, Message, StyleType> { + let thumbnail = sniffer.thumbnail; + let ConfigSettings { + style, + language, + color_gradient, + .. + } = sniffer.configs.lock().unwrap().settings; + let font = style.get_extension().font; + + if thumbnail { + let font_headers = style.get_extension().font_headers; + let unread_notifications = sniffer.unread_notifications; + return thumbnail_header( + font, + font_headers, + language, + color_gradient, + unread_notifications, + ); + } + + let last_opened_setting = sniffer.last_opened_setting; + let is_running = sniffer.running_page.ne(&RunningPage::Init); -pub fn header( - font: Font, - color_gradient: GradientType, - back_button: bool, - language: Language, - last_opened_setting: SettingsPage, -) -> Container<'static, Message, StyleType> { let logo = Icon::Sniffnet .to_text() .vertical_alignment(Vertical::Center) @@ -32,19 +56,23 @@ pub fn header( Row::new() .padding([0, 20]) .align_items(Alignment::Center) - .push(if back_button { + .push(if is_running { Container::new(get_button_reset(font, language)) } else { Container::new(Space::with_width(60)) }) .push(horizontal_space()) + .push(Container::new(Space::with_width(40))) + .push(Space::with_width(20)) .push(logo) + .push(Space::with_width(20)) + .push(if is_running { + Container::new(get_button_minimize(font, language, false)) + } else { + Container::new(Space::with_width(40)) + }) .push(horizontal_space()) - .push(Container::new(get_button_settings( - font, - language, - last_opened_setting, - ))), + .push(get_button_settings(font, language, last_opened_setting)), ) .height(90) .align_y(Vertical::Center) @@ -99,3 +127,75 @@ pub fn get_button_settings( .gap(5) .style(ContainerType::Tooltip) } + +pub fn get_button_minimize( + font: Font, + language: Language, + thumbnail: bool, +) -> Tooltip<'static, Message, StyleType> { + let size = if thumbnail { 20 } else { 26 }; + let button_size = if thumbnail { 30 } else { 40 }; + let icon = if thumbnail { + Icon::ThumbnailClose + } else { + Icon::ThumbnailOpen + }; + let tooltip = if thumbnail { + "" + } else { + thumbnail_mode_translation(language) + }; + let tooltip_style = if thumbnail { + ContainerType::Standard + } else { + ContainerType::Tooltip + }; + + let content = button( + icon.to_text() + .size(size) + .horizontal_alignment(Horizontal::Center) + .vertical_alignment(Vertical::Center), + ) + .padding(0) + .height(button_size) + .width(button_size) + .style(ButtonType::Thumbnail) + .on_press(Message::ToggleThumbnail(false)); + + Tooltip::new(content, Text::new(tooltip).font(font), Position::Right) + .gap(0) + .style(tooltip_style) +} + +fn thumbnail_header( + font: Font, + font_headers: Font, + language: Language, + color_gradient: GradientType, + unread_notifications: usize, +) -> Container<'static, Message, StyleType> { + Container::new( + Row::new() + .align_items(Alignment::Center) + .push(horizontal_space()) + .push(Space::with_width(80)) + .push(Text::new(SNIFFNET_TITLECASE).font(font_headers)) + .push(Space::with_width(10)) + .push(get_button_minimize(font, language, true)) + .push(horizontal_space()) + .push(if unread_notifications > 0 { + Container::new( + notifications_badge(font, unread_notifications) + .style(ContainerType::HighlightedOnHeader), + ) + .width(40) + .align_x(Horizontal::Center) + } else { + Container::new(Space::with_width(40)) + }), + ) + .height(30) + .align_y(Vertical::Center) + .style(ContainerType::Gradient(color_gradient)) +} diff --git a/src/gui/components/tab.rs b/src/gui/components/tab.rs index f35095633..e3a7e481b 100644 --- a/src/gui/components/tab.rs +++ b/src/gui/components/tab.rs @@ -102,17 +102,9 @@ fn new_page_tab( if let Some(num) = unread { if num > 0 { - let notifications_badge = Container::new( - Text::new(num.to_string()) - .font(font_headers) - .size(14) - .line_height(LineHeight::Relative(1.0)), - ) - .align_y(Vertical::Center) - .padding([2, 4]) - .height(20) - .style(ContainerType::Highlighted); - content = content.push(Space::with_width(7)).push(notifications_badge); + content = content + .push(Space::with_width(7)) + .push(notifications_badge(font_headers, num)); } } @@ -177,3 +169,19 @@ fn new_settings_tab( }) .on_press(page.action()) } + +pub fn notifications_badge( + font_headers: Font, + num: usize, +) -> Container<'static, Message, StyleType> { + Container::new( + Text::new(num.to_string()) + .font(font_headers) + .size(14) + .line_height(LineHeight::Relative(1.0)), + ) + .align_y(Vertical::Center) + .padding([2, 4]) + .height(20) + .style(ContainerType::Highlighted) +} diff --git a/src/gui/pages/connection_details_page.rs b/src/gui/pages/connection_details_page.rs index 470e00ed7..d98afd40c 100644 --- a/src/gui/pages/connection_details_page.rs +++ b/src/gui/pages/connection_details_page.rs @@ -8,7 +8,6 @@ use iced::widget::{Column, Container, Row, Text, Tooltip}; use iced::{Alignment, Font, Length}; use crate::countries::country_utils::{get_computer_tooltip, get_flag_tooltip}; -use crate::countries::flags_pictures::FLAGS_WIDTH_BIG; use crate::gui::components::button::button_hide; use crate::gui::styles::container::ContainerType; use crate::gui::styles::scrollbar::ScrollbarType; @@ -107,7 +106,7 @@ fn page_content( if let Some((r_dns, host)) = host_option { host_info_col = get_host_info_col(&r_dns, &host, font, language); let host_info = host_info_option.unwrap_or_default(); - let flag = get_flag_tooltip(host.country, FLAGS_WIDTH_BIG, &host_info, language, font); + let flag = get_flag_tooltip(host.country, &host_info, language, font, false); let computer = get_local_tooltip(sniffer, &address_to_lookup, key); if address_to_lookup.eq(&key.address1) { source_caption = source_caption.push(flag); diff --git a/src/gui/pages/inspect_page.rs b/src/gui/pages/inspect_page.rs index c8cfb98ee..7de1399aa 100644 --- a/src/gui/pages/inspect_page.rs +++ b/src/gui/pages/inspect_page.rs @@ -184,7 +184,7 @@ fn report_header_row( .size(FONT_SIZE_FOOTER), ); let tooltip_style = if tooltip_val.is_empty() { - ContainerType::Neutral + ContainerType::Standard } else { ContainerType::Tooltip }; @@ -446,7 +446,7 @@ fn filter_input( .style(if is_filter_active { ContainerType::Badge } else { - ContainerType::Neutral + ContainerType::Standard }) } diff --git a/src/gui/pages/mod.rs b/src/gui/pages/mod.rs index 0e294a052..befc6f6cd 100644 --- a/src/gui/pages/mod.rs +++ b/src/gui/pages/mod.rs @@ -6,4 +6,5 @@ pub mod overview_page; pub mod settings_general_page; pub mod settings_notifications_page; pub mod settings_style_page; +pub mod thumbnail_page; pub mod types; diff --git a/src/gui/pages/notifications_page.rs b/src/gui/pages/notifications_page.rs index ac138ef20..31d490684 100644 --- a/src/gui/pages/notifications_page.rs +++ b/src/gui/pages/notifications_page.rs @@ -8,7 +8,6 @@ use iced::Length::FillPortion; use iced::{Alignment, Font, Length}; use crate::countries::country_utils::get_flag_tooltip; -use crate::countries::flags_pictures::FLAGS_WIDTH_BIG; use crate::gui::components::header::get_button_settings; use crate::gui::components::tab::get_pages_tabs; use crate::gui::components::types::my_modal::MyModal; @@ -327,10 +326,10 @@ fn favorite_notification_log( .spacing(5) .push(get_flag_tooltip( country, - FLAGS_WIDTH_BIG, &logged_notification.data_info_host, language, font, + false, )) .push(Text::new(domain_asn_str).font(font)); diff --git a/src/gui/pages/overview_page.rs b/src/gui/pages/overview_page.rs index 4575e94a9..f91c8309c 100644 --- a/src/gui/pages/overview_page.rs +++ b/src/gui/pages/overview_page.rs @@ -315,10 +315,10 @@ fn col_host(width: f32, sniffer: &Sniffer) -> Column<'static, Message, StyleType .push(star_button) .push(get_flag_tooltip( host.country, - FLAGS_WIDTH_BIG, data_info_host, language, font, + false, )) .push(host_bar); diff --git a/src/gui/pages/settings_general_page.rs b/src/gui/pages/settings_general_page.rs index 3d3b71d05..aaaef01ea 100644 --- a/src/gui/pages/settings_general_page.rs +++ b/src/gui/pages/settings_general_page.rs @@ -302,5 +302,5 @@ fn button_clear_mmdb( button = button.on_press(message(String::new())); } - Tooltip::new(button, "", Position::Right).style(ContainerType::Neutral) + Tooltip::new(button, "", Position::Right) } diff --git a/src/gui/pages/thumbnail_page.rs b/src/gui/pages/thumbnail_page.rs new file mode 100644 index 000000000..6a0a38d32 --- /dev/null +++ b/src/gui/pages/thumbnail_page.rs @@ -0,0 +1,234 @@ +use crate::chart::types::chart_type::ChartType; +use crate::configs::types::config_settings::ConfigSettings; +use crate::countries::country_utils::get_flag_tooltip; +use crate::gui::styles::style_constants::FONT_SIZE_FOOTER; +use crate::gui::styles::types::style_type::StyleType; +use crate::gui::types::message::Message; +use crate::gui::types::sniffer::Sniffer; +use crate::networking::types::host::Host; +use crate::networking::types::info_traffic::InfoTraffic; +use crate::report::get_report_entries::{get_host_entries, get_service_entries}; +use crate::report::types::sort_type::SortType; +use crate::translations::types::language::Language; +use iced::alignment::Horizontal; +use iced::widget::{lazy, vertical_space, Column, Container, Row, Rule, Space, Text}; +use iced::{Alignment, Font, Length}; +use std::cmp::min; +use std::net::IpAddr; +use std::sync::{Arc, Mutex}; + +const MAX_ENTRIES: usize = 4; +const MAX_CHARS_HOST: usize = 26; +const MAX_CHARS_SERVICE: usize = 13; + +/// Computes the body of the thumbnail view +pub fn thumbnail_page(sniffer: &Sniffer) -> Container { + let ConfigSettings { style, .. } = sniffer.configs.lock().unwrap().settings; + let font = style.get_extension().font; + + let filtered = sniffer.runtime_data.tot_out_packets + sniffer.runtime_data.tot_in_packets; + + if filtered == 0 { + return Container::new( + Column::new() + .push(vertical_space()) + .push(Text::new(&sniffer.waiting).font(font).size(50)) + .push(Space::with_height(Length::FillPortion(2))), + ) + .width(Length::Fill) + .align_x(Horizontal::Center); + } + + let info_traffic = sniffer.info_traffic.clone(); + let chart_type = sniffer.traffic_chart.chart_type; + + let lazy_report = lazy(filtered, move |_| { + Row::new() + .padding([5, 0]) + .height(Length::Fill) + .align_items(Alignment::Start) + .push(host_col(&info_traffic, chart_type, font)) + .push(Rule::vertical(10)) + .push(service_col(&info_traffic, chart_type, font)) + }); + + let content = Column::new() + .push(Container::new(sniffer.traffic_chart.view()).height(Length::Fill)) + .push(lazy_report); + + Container::new(content) +} + +fn host_col( + info_traffic: &Arc>, + chart_type: ChartType, + font: Font, +) -> Column<'static, Message, StyleType> { + let mut host_col = Column::new() + .padding([0, 5]) + .spacing(3) + .width(Length::FillPortion(2)); + let hosts = get_host_entries(info_traffic, chart_type, SortType::Neutral); + let n_entry = min(hosts.len(), MAX_ENTRIES); + for (host, data_info_host) in hosts.get(..n_entry).unwrap_or_default() { + let flag = get_flag_tooltip( + host.country, + data_info_host, + Language::default(), + font, + true, + ); + let host_row = Row::new() + .align_items(Alignment::Center) + .spacing(5) + .push(flag) + .push(Text::new(host_text(host)).font(font).size(FONT_SIZE_FOOTER)); + host_col = host_col.push(host_row); + } + host_col +} + +fn service_col( + info_traffic: &Arc>, + chart_type: ChartType, + font: Font, +) -> Column<'static, Message, StyleType> { + let mut service_col = Column::new().padding([0, 5]).spacing(3).width(Length::Fill); + let services = get_service_entries(info_traffic, chart_type, SortType::Neutral); + let n_entry = min(services.len(), MAX_ENTRIES); + for (service, _) in services.get(..n_entry).unwrap_or_default() { + service_col = service_col.push( + Text::new(clip_text(&service.to_string(), MAX_CHARS_SERVICE)) + .font(font) + .size(FONT_SIZE_FOOTER), + ); + } + service_col +} + +fn host_text(host: &Host) -> String { + let domain = &host.domain; + let asn = &host.asn.name; + + let text = if asn.is_empty() || (!domain.trim().is_empty() && domain.parse::().is_err()) + { + domain + } else { + asn + }; + + clip_text(text, MAX_CHARS_HOST) +} + +fn clip_text(text: &str, max_chars: usize) -> String { + let text = text.trim(); + let chars = text.chars().collect::>(); + let tot_len = chars.len(); + let slice_len = min(max_chars, tot_len); + + let suspensions = if tot_len > max_chars { "…" } else { "" }; + let slice = if tot_len > max_chars { + &chars[..slice_len - 2] + } else { + &chars[..slice_len] + } + .iter() + .collect::(); + + [slice.trim(), suspensions].concat() +} + +#[cfg(test)] +mod tests { + use crate::gui::pages::thumbnail_page::{ + clip_text, host_text, MAX_CHARS_HOST, MAX_CHARS_SERVICE, + }; + use crate::networking::types::asn::Asn; + use crate::networking::types::host::Host; + + fn host_for_tests(domain: &str, asn: &str) -> Host { + Host { + domain: domain.to_string(), + asn: Asn { + name: asn.to_string(), + number: 512, + }, + country: Default::default(), + } + } + + #[test] + fn test_clip_text() { + assert_eq!( + clip_text("iphone-di-doofenshmirtz.local", MAX_CHARS_HOST), + "iphone-di-doofenshmirtz.…" + ); + assert_eq!(clip_text("github.com", MAX_CHARS_HOST), "github.com"); + + assert_eq!(clip_text("https6789012", MAX_CHARS_SERVICE), "https6789012"); + assert_eq!( + clip_text("https67890123", MAX_CHARS_SERVICE), + "https67890123" + ); + assert_eq!( + clip_text("https678901234", MAX_CHARS_SERVICE), + "https678901…" + ); + assert_eq!( + clip_text("https6789012345", MAX_CHARS_SERVICE), + "https678901…" + ); + + assert_eq!( + clip_text("protocol with space", MAX_CHARS_SERVICE), + "protocol wi…" + ); + assert_eq!( + clip_text("protocol90 23456", MAX_CHARS_SERVICE), + "protocol90…" + ); + + assert_eq!( + clip_text(" \n\t sniffnet.net ", MAX_CHARS_HOST), + "sniffnet.net" + ); + assert_eq!( + clip_text(" protocol90 23456 \n ", MAX_CHARS_SERVICE), + "protocol90…" + ); + assert_eq!( + clip_text(" protocol90 23456 ", MAX_CHARS_HOST), + "protocol90 23456" + ); + } + + #[test] + fn test_host_text() { + let host = host_for_tests("iphone-di-doofenshmirtz.local", "AS1234"); + assert_eq!(host_text(&host), "iphone-di-doofenshmirtz.…"); + + let host = host_for_tests("", ""); + assert_eq!(host_text(&host), ""); + + let host = host_for_tests("192.168.1.113", "AS1234"); + assert_eq!(host_text(&host), "AS1234"); + + let host = host_for_tests("192.168.1.113", ""); + assert_eq!(host_text(&host), "192.168.1.113"); + + let host = host_for_tests("", "FASTLY"); + assert_eq!(host_text(&host), "FASTLY"); + + let host = host_for_tests("::", "GOOGLE"); + assert_eq!(host_text(&host), "GOOGLE"); + + let host = host_for_tests("::f", "AKAMAI-TECHNOLOGIES-INCORPORATED"); + assert_eq!(host_text(&host), "AKAMAI-TECHNOLOGIES-INCO…"); + + let host = host_for_tests("::g", "GOOGLE"); + assert_eq!(host_text(&host), "::g"); + + let host = host_for_tests(" ", "GOOGLE"); + assert_eq!(host_text(&host), "GOOGLE"); + } +} diff --git a/src/gui/styles/button.rs b/src/gui/styles/button.rs index c2bd5e0ad..2bdc5ea2e 100644 --- a/src/gui/styles/button.rs +++ b/src/gui/styles/button.rs @@ -28,6 +28,7 @@ pub enum ButtonType { Gradient(GradientType), SortArrows, SortArrowActive, + Thumbnail, } impl button::StyleSheet for StyleType { @@ -47,6 +48,7 @@ impl button::StyleSheet for StyleType { ..ext.buttons_color }), ButtonType::Neutral + | ButtonType::Thumbnail | ButtonType::NotStarred | ButtonType::SortArrows | ButtonType::SortArrowActive => Background::Color(Color::TRANSPARENT), @@ -76,7 +78,8 @@ impl button::StyleSheet for StyleType { | ButtonType::SortArrowActive | ButtonType::Starred | ButtonType::NotStarred - | ButtonType::Neutral => 0.0, + | ButtonType::Neutral + | ButtonType::Thumbnail => 0.0, ButtonType::BorderedRound => BORDER_WIDTH * 2.0, _ => BORDER_WIDTH, }, @@ -101,6 +104,7 @@ impl button::StyleSheet for StyleType { }, ButtonType::SortArrowActive => colors.secondary, ButtonType::Gradient(_) => colors.text_headers, + ButtonType::Thumbnail => mix_colors(colors.text_headers, colors.secondary), _ => colors.text_body, }, shadow: match style { @@ -126,9 +130,10 @@ impl button::StyleSheet for StyleType { _ => Vector::new(0.0, 2.0), }, shadow: match style { - ButtonType::Neutral | ButtonType::SortArrows | ButtonType::SortArrowActive => { - Shadow::default() - } + ButtonType::Neutral + | ButtonType::SortArrows + | ButtonType::SortArrowActive + | ButtonType::Thumbnail => Shadow::default(), _ => Shadow { color: Color::BLACK, offset: match style { @@ -143,7 +148,7 @@ impl button::StyleSheet for StyleType { }, background: Some(match style { ButtonType::Starred => Background::Color(colors.starred), - ButtonType::SortArrows | ButtonType::SortArrowActive => { + ButtonType::SortArrows | ButtonType::SortArrowActive | ButtonType::Thumbnail => { Background::Color(Color::TRANSPARENT) } ButtonType::Neutral => Background::Color(Color { @@ -174,6 +179,7 @@ impl button::StyleSheet for StyleType { | ButtonType::SortArrows | ButtonType::SortArrowActive | ButtonType::TabInactive + | ButtonType::Thumbnail | ButtonType::BorderedRound => 0.0, _ => BORDER_WIDTH, }, @@ -189,7 +195,7 @@ impl button::StyleSheet for StyleType { }, text_color: match style { ButtonType::Starred => Color::BLACK, - ButtonType::Gradient(_) => colors.text_headers, + ButtonType::Gradient(_) | ButtonType::Thumbnail => colors.text_headers, ButtonType::SortArrowActive | ButtonType::SortArrows => colors.secondary, _ => colors.text_body, }, diff --git a/src/gui/styles/container.rs b/src/gui/styles/container.rs index b8816974b..2a8dceec3 100644 --- a/src/gui/styles/container.rs +++ b/src/gui/styles/container.rs @@ -17,10 +17,10 @@ pub enum ContainerType { Tooltip, Badge, Palette, - Neutral, Gradient(GradientType), Modal, Highlighted, + HighlightedOnHeader, } impl iced::widget::container::StyleSheet for StyleType { @@ -43,9 +43,6 @@ impl iced::widget::container::StyleSheet for StyleType { a: ext.alpha_round_containers, ..ext.buttons_color }), - ContainerType::Neutral | ContainerType::Palette => { - Background::Color(Color::TRANSPARENT) - } ContainerType::Badge => Background::Color(Color { a: ext.alpha_chart_badge, ..colors.secondary @@ -53,8 +50,12 @@ impl iced::widget::container::StyleSheet for StyleType { ContainerType::Gradient(gradient_type) => Background::Gradient( get_gradient_headers(&colors, *gradient_type, ext.is_nightly), ), - ContainerType::Modal => Background::Color(colors.primary), - ContainerType::Standard => Background::Color(Color::TRANSPARENT), + ContainerType::Modal | ContainerType::HighlightedOnHeader => { + Background::Color(colors.primary) + } + ContainerType::Standard | ContainerType::Palette => { + Background::Color(Color::TRANSPARENT) + } }), border: Border { radius: match style { @@ -63,14 +64,16 @@ impl iced::widget::container::StyleSheet for StyleType { [0.0, 0.0, BORDER_ROUNDED_RADIUS, BORDER_ROUNDED_RADIUS].into() } ContainerType::Tooltip => 7.0.into(), - ContainerType::Badge | ContainerType::Highlighted => 100.0.into(), + ContainerType::Badge + | ContainerType::Highlighted + | ContainerType::HighlightedOnHeader => 100.0.into(), _ => 0.0.into(), }, width: match style { ContainerType::Standard | ContainerType::Modal - | ContainerType::Neutral | ContainerType::Gradient(_) + | ContainerType::HighlightedOnHeader | ContainerType::Highlighted => 0.0, ContainerType::Tooltip => BORDER_WIDTH / 2.0, ContainerType::BorderedRound => BORDER_WIDTH * 2.0, diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs index 547c81ff0..a94b4f991 100644 --- a/src/gui/types/message.rs +++ b/src/gui/types/message.rs @@ -111,4 +111,8 @@ pub enum Message { OutputPcapDir(String), /// The output PCAP file name has been updated OutputPcapFile(String), + /// Toggle thumbnail mode + ToggleThumbnail(bool), + /// Drag the window + Drag, } diff --git a/src/gui/types/sniffer.rs b/src/gui/types/sniffer.rs index 70495f99b..da14511bb 100644 --- a/src/gui/types/sniffer.rs +++ b/src/gui/types/sniffer.rs @@ -6,12 +6,13 @@ use std::path::PathBuf; use std::sync::{Arc, Mutex}; use std::thread; -use iced::window::Id; +use iced::window::{Id, Level}; use iced::{window, Command}; use pcap::Device; use rfd::FileHandle; use crate::chart::manage_chart_data::update_charts_data; +use crate::configs::types::config_window::{ConfigWindow, Scale, ToPoint, ToSize}; use crate::gui::components::types::my_modal::MyModal; use crate::gui::pages::types::running_page::RunningPage; use crate::gui::pages::types::settings_page::SettingsPage; @@ -95,6 +96,8 @@ pub struct Sniffer { pub timing_events: TimingEvents, /// Information about PCAP file export pub export_pcap: ExportPcap, + /// Whether thumbnail mode is currently active + pub thumbnail: bool, } impl Sniffer { @@ -135,6 +138,7 @@ impl Sniffer { asn_mmdb_reader: Arc::new(MmdbReader::from(&mmdb_asn, ASN_MMDB)), timing_events: TimingEvents::default(), export_pcap: ExportPcap::default(), + thumbnail: false, } } @@ -277,14 +281,21 @@ impl Sniffer { self.configs.lock().unwrap().settings.scale_factor = multiplier; } Message::WindowMoved(x, y) => { - self.configs.lock().unwrap().window.position = (x, y); + let scale_factor = self.configs.lock().unwrap().settings.scale_factor; + let scaled = (x, y).scale(scale_factor); + if self.thumbnail { + self.configs.lock().unwrap().window.thumbnail_position = scaled; + } else { + self.configs.lock().unwrap().window.position = scaled; + } } - #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] Message::WindowResized(width, height) => { - let scale_factor = self.configs.lock().unwrap().settings.scale_factor; - let scaled_width = (f64::from(width) * scale_factor) as u32; - let scaled_height = (f64::from(height) * scale_factor) as u32; - self.configs.lock().unwrap().window.size = (scaled_width, scaled_height); + if !self.thumbnail { + let scale_factor = self.configs.lock().unwrap().settings.scale_factor; + self.configs.lock().unwrap().window.size = (width, height).scale(scale_factor); + } else if !self.timing_events.was_just_thumbnail_enter() { + return self.update(Message::ToggleThumbnail(true)); + } } Message::CustomCountryDb(db) => { self.configs.lock().unwrap().settings.mmdb_country = db.clone(); @@ -294,9 +305,6 @@ impl Sniffer { self.configs.lock().unwrap().settings.mmdb_asn = db.clone(); self.asn_mmdb_reader = Arc::new(MmdbReader::from(&db, ASN_MMDB)); } - // Message::CustomReport(path) => { - // self.settings.output_path = path; - // } Message::CloseRequested => { self.configs.lock().unwrap().clone().store(); return window::close(Id::MAIN); @@ -330,6 +338,41 @@ impl Sniffer { Message::OutputPcapFile(name) => { self.export_pcap.set_file_name(name); } + Message::ToggleThumbnail(triggered_by_resize) => { + self.thumbnail = !self.thumbnail; + self.traffic_chart.thumbnail = self.thumbnail; + + return if self.thumbnail { + let scale_factor = self.configs.lock().unwrap().settings.scale_factor; + let size = ConfigWindow::thumbnail_size(scale_factor).to_size(); + let position = self.configs.lock().unwrap().window.thumbnail_position; + self.timing_events.thumbnail_enter_now(); + Command::batch([ + window::resize(Id::MAIN, size), + window::toggle_decorations(Id::MAIN), + window::move_to(Id::MAIN, position.to_point()), + window::change_level(Id::MAIN, Level::AlwaysOnTop), + ]) + } else { + if self.running_page.eq(&RunningPage::Notifications) { + self.unread_notifications = 0; + } + let mut commands = vec![ + window::toggle_decorations(Id::MAIN), + window::change_level(Id::MAIN, Level::Normal), + ]; + if !triggered_by_resize { + let size = self.configs.lock().unwrap().window.size.to_size(); + let position = self.configs.lock().unwrap().window.position.to_point(); + commands.push(window::resize(Id::MAIN, size)); + commands.push(window::move_to(Id::MAIN, position)); + } + Command::batch(commands) + }; + } + Message::Drag => { + return window::drag(Id::MAIN); + } Message::TickInit => {} } Command::none() @@ -356,7 +399,7 @@ impl Sniffer { ); self.info_traffic.lock().unwrap().favorites_last_interval = HashSet::new(); self.runtime_data.tot_emitted_notifications += emitted_notifications; - if self.running_page.ne(&RunningPage::Notifications) { + if self.thumbnail || self.running_page.ne(&RunningPage::Notifications) { self.unread_notifications += emitted_notifications; } update_charts_data(&mut self.runtime_data, &mut self.traffic_chart); @@ -1692,12 +1735,15 @@ mod tests { ConfigWindow { position: (0, 0), size: (1190, 670), + thumbnail_position: (0, 0), } ); // change window properties by sending messages sniffer.update(Message::WindowMoved(-10, 555)); sniffer.update(Message::WindowResized(1000, 999)); + sniffer.thumbnail = true; + sniffer.update(Message::WindowMoved(40, 40)); // quit the app by sending a CloseRequested message sniffer.update(Message::CloseRequested); @@ -1716,7 +1762,125 @@ mod tests { ConfigWindow { position: (-10, 555), size: (1000, 999), + thumbnail_position: (40, 40), } ); } + + #[test] + #[parallel] // needed to not collide with other tests generating configs files + fn test_window_resized() { + let mut sniffer = new_sniffer(); + assert!(!sniffer.thumbnail); + let factor = sniffer.configs.lock().unwrap().settings.scale_factor; + assert_eq!(factor, 1.0); + assert_eq!(sniffer.configs.lock().unwrap().window.size, (1190, 670)); + assert_eq!(ConfigWindow::thumbnail_size(factor), (360, 222)); + + sniffer.update(Message::WindowResized(850, 600)); + assert_eq!(sniffer.configs.lock().unwrap().window.size, (850, 600)); + + sniffer.update(Message::ChangeScaleFactor(1.5)); + let factor = sniffer.configs.lock().unwrap().settings.scale_factor; + assert_eq!(factor, 1.5); + assert_eq!(ConfigWindow::thumbnail_size(factor), (540, 333)); + sniffer.update(Message::WindowResized(1000, 800)); + assert_eq!(sniffer.configs.lock().unwrap().window.size, (1500, 1200)); + + sniffer.update(Message::ChangeScaleFactor(0.5)); + let factor = sniffer.configs.lock().unwrap().settings.scale_factor; + assert_eq!(factor, 0.5); + assert_eq!(ConfigWindow::thumbnail_size(factor), (180, 111)); + sniffer.update(Message::WindowResized(1000, 800)); + assert_eq!(sniffer.configs.lock().unwrap().window.size, (500, 400)); + } + + #[test] + #[parallel] // needed to not collide with other tests generating configs files + fn test_window_moved() { + let mut sniffer = new_sniffer(); + assert!(!sniffer.thumbnail); + assert_eq!(sniffer.configs.lock().unwrap().settings.scale_factor, 1.0); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (0, 0)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (0, 0) + ); + + sniffer.update(Message::WindowMoved(850, 600)); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (850, 600)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (0, 0) + ); + sniffer.thumbnail = true; + sniffer.update(Message::WindowMoved(400, 1000)); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (850, 600)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (400, 1000) + ); + + sniffer.update(Message::ChangeScaleFactor(1.5)); + assert_eq!(sniffer.configs.lock().unwrap().settings.scale_factor, 1.5); + sniffer.update(Message::WindowMoved(20, 40)); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (850, 600)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (30, 60) + ); + sniffer.thumbnail = false; + sniffer.update(Message::WindowMoved(-400, 1000)); + assert_eq!( + sniffer.configs.lock().unwrap().window.position, + (-600, 1500) + ); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (30, 60) + ); + + sniffer.update(Message::ChangeScaleFactor(0.5)); + assert_eq!(sniffer.configs.lock().unwrap().settings.scale_factor, 0.5); + sniffer.update(Message::WindowMoved(500, -100)); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (250, -50)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (30, 60) + ); + sniffer.thumbnail = true; + sniffer.update(Message::WindowMoved(-2, -250)); + assert_eq!(sniffer.configs.lock().unwrap().window.position, (250, -50)); + assert_eq!( + sniffer.configs.lock().unwrap().window.thumbnail_position, + (-1, -125) + ); + } + + #[test] + #[parallel] // needed to not collide with other tests generating configs files + fn test_toggle_thumbnail() { + let mut sniffer = new_sniffer(); + assert!(!sniffer.thumbnail); + assert!(!sniffer.traffic_chart.thumbnail); + + sniffer.update(Message::ToggleThumbnail(false)); + assert!(sniffer.thumbnail); + assert!(sniffer.traffic_chart.thumbnail); + + sniffer.unread_notifications = 8; + sniffer.update(Message::ToggleThumbnail(false)); + assert!(!sniffer.thumbnail); + assert!(!sniffer.traffic_chart.thumbnail); + assert_eq!(sniffer.unread_notifications, 8); + + sniffer.update(Message::ChangeRunningPage(RunningPage::Notifications)); + assert_eq!(sniffer.unread_notifications, 0); + + sniffer.update(Message::ToggleThumbnail(false)); + sniffer.unread_notifications = 8; + assert_eq!(sniffer.unread_notifications, 8); + sniffer.update(Message::ToggleThumbnail(false)); + assert_eq!(sniffer.unread_notifications, 0); + } } diff --git a/src/gui/types/timing_events.rs b/src/gui/types/timing_events.rs index 5c83d2309..74320c995 100644 --- a/src/gui/types/timing_events.rs +++ b/src/gui/types/timing_events.rs @@ -1,15 +1,18 @@ use std::time::Duration; pub struct TimingEvents { - /// Timestamp of last window focus - pub focus: std::time::Instant, - /// Timestamp of the last press on Copy IP button, with the related IP address - pub copy_ip: (std::time::Instant, String), + /// Instant of the last window focus + pub(crate) focus: std::time::Instant, + /// Instant of the last press on Copy IP button, with the related IP address + copy_ip: (std::time::Instant, String), + /// Instant of the last thumbnail mode enter + thumbnail_enter: std::time::Instant, } impl TimingEvents { const TIMEOUT_FOCUS: u64 = 200; const TIMEOUT_COPY_IP: u64 = 1500; + const TIMEOUT_THUMBNAIL_ENTER: u64 = 1000; pub fn focus_now(&mut self) { self.focus = std::time::Instant::now(); @@ -27,6 +30,15 @@ impl TimingEvents { self.copy_ip.0.elapsed() < Duration::from_millis(TimingEvents::TIMEOUT_COPY_IP) && self.copy_ip.1.eq(ip) } + + pub fn thumbnail_enter_now(&mut self) { + self.thumbnail_enter = std::time::Instant::now(); + } + + pub fn was_just_thumbnail_enter(&self) -> bool { + self.thumbnail_enter.elapsed() + < Duration::from_millis(TimingEvents::TIMEOUT_THUMBNAIL_ENTER) + } } impl Default for TimingEvents { @@ -34,6 +46,7 @@ impl Default for TimingEvents { Self { focus: std::time::Instant::now(), copy_ip: (std::time::Instant::now(), String::new()), + thumbnail_enter: std::time::Instant::now(), } } } diff --git a/src/main.rs b/src/main.rs index 58b3d0563..eb8d5720f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -87,7 +87,7 @@ pub fn main() -> iced::Result { print_cli_welcome_message(); - let ConfigWindow { size, position } = configs1.lock().unwrap().window; + let ConfigWindow { size, position, .. } = configs1.lock().unwrap().window; Sniffer::run(Settings { // id needed for Linux Wayland; should match StartupWMClass in .desktop file; see issue #292 @@ -95,7 +95,7 @@ pub fn main() -> iced::Result { window: window::Settings { size: size.to_size(), // start size position: position.to_position(), - min_size: Some((800, 500).to_size()), // min size allowed + min_size: Some(ConfigWindow::MIN_SIZE.to_size()), // min size allowed max_size: None, visible: true, resizable: true, diff --git a/src/translations/translations_3.rs b/src/translations/translations_3.rs index b09cb596f..2d63b0c5c 100644 --- a/src/translations/translations_3.rs +++ b/src/translations/translations_3.rs @@ -185,3 +185,11 @@ pub fn file_name_translation(language: Language) -> &'static str { _ => "File name", } } + +pub fn thumbnail_mode_translation(language: Language) -> &'static str { + match language { + Language::EN => "Thumbnail mode", + Language::IT => "Modalità miniatura", + _ => "Thumbnail mode", + } +} diff --git a/src/utils/types/icon.rs b/src/utils/types/icon.rs index 62606a1de..da6fe1fa5 100644 --- a/src/utils/types/icon.rs +++ b/src/utils/types/icon.rs @@ -39,6 +39,8 @@ pub enum Icon { SortDescending, SortNeutral, Star, + ThumbnailOpen, + ThumbnailClose, Warning, Waves, } @@ -83,6 +85,8 @@ impl Icon { Icon::SortDescending => 'l', Icon::SortNeutral => 'n', Icon::OpenLink => 'o', + Icon::ThumbnailOpen => 's', + Icon::ThumbnailClose => 'r', } }