diff --git a/CHANGELOG.md b/CHANGELOG.md index 39f8cd92..04e16bcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ All Sniffnet releases with the relative changes are documented in this file. +## [UNRELEASED] +- Add animated welcome screen ([#1002](https://github.com/GyulyVGC/sniffnet/pull/1002)) + ## [1.4.2] - 2025-11-04 - Send remote notifications via webhook ([#991](https://github.com/GyulyVGC/sniffnet/pull/991) — fixes [#841](https://github.com/GyulyVGC/sniffnet/issues/841)) - Pause and resume packet captures ([#992](https://github.com/GyulyVGC/sniffnet/pull/992) — fixes [#551](https://github.com/GyulyVGC/sniffnet/issues/551)) diff --git a/resources/fonts/subset/icons.ttf b/resources/fonts/subset/icons.ttf index f577e3c1..c2fc694d 100644 Binary files a/resources/fonts/subset/icons.ttf and b/resources/fonts/subset/icons.ttf differ diff --git a/src/gui/pages/mod.rs b/src/gui/pages/mod.rs index befc6f6c..2d11c739 100644 --- a/src/gui/pages/mod.rs +++ b/src/gui/pages/mod.rs @@ -8,3 +8,4 @@ pub mod settings_notifications_page; pub mod settings_style_page; pub mod thumbnail_page; pub mod types; +pub mod welcome_page; diff --git a/src/gui/pages/welcome_page.rs b/src/gui/pages/welcome_page.rs new file mode 100644 index 00000000..3bfa8e71 --- /dev/null +++ b/src/gui/pages/welcome_page.rs @@ -0,0 +1,55 @@ +use crate::StyleType; +use crate::gui::styles::text::TextType; +use crate::gui::types::message::Message; +use crate::utils::types::icon::Icon; +use iced::alignment::{Horizontal, Vertical}; +use iced::widget::{Column, Container, Space, Text, vertical_space}; +use iced::{Alignment, Font, Length}; + +pub fn welcome_page<'a>(font: Font, x: u8) -> Container<'a, Message, StyleType> { + let icon = match x { + 0..=3 | 20.. => Text::new(""), + 4 => Icon::Sniffnet1.to_text(), + 5 => Icon::Sniffnet2.to_text(), + 6 => Icon::Sniffnet3.to_text(), + 7 => Icon::Sniffnet4.to_text(), + 8..=19 => Icon::Sniffnet.to_text(), + }; + + let text = Text::new(match x { + 0..=3 | 20.. => "", + 4 => "S", + 5 => "Sn", + 6 => "Sni", + 7 => "Snif", + 8 => "Sniff", + 9 => "Sniffn", + 10 => "Sniffne", + 11..=19 => "Sniffnet", + }); + + let text_type = match x { + 0..=3 | 20.. => TextType::Welcome(0.0), + 4 => TextType::Welcome(0.125), + 5 => TextType::Welcome(0.25), + 6 => TextType::Welcome(0.375), + 7 => TextType::Welcome(0.5), + 8 => TextType::Welcome(0.625), + 9 => TextType::Welcome(0.750), + 10 => TextType::Welcome(0.875), + 11..=19 => TextType::Welcome(1.0), + }; + + let body = Column::new() + .align_x(Alignment::Center) + .push(vertical_space()) + .push(icon.size(200).line_height(0.9).class(text_type)) + .push(text.font(font).size(75).class(text_type)) + .push(Space::with_height(Length::FillPortion(2))); + + Container::new(body) + .height(Length::Fill) + .width(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) +} diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs index 34e89b8c..44c5b5f4 100644 --- a/src/gui/sniffer.rs +++ b/src/gui/sniffer.rs @@ -33,6 +33,7 @@ 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::pages::welcome_page::welcome_page; use crate::gui::styles::types::custom_palette::{CustomPalette, ExtraStyles}; use crate::gui::styles::types::palette::Palette; use crate::gui::types::conf::Conf; @@ -77,6 +78,8 @@ pub const ICON_FONT_FAMILY_NAME: &str = "Icons for Sniffnet"; pub struct Sniffer { /// Parameters that are persistent across runs pub conf: Conf, + /// Are we during welcome / quit animation? None if not, true if welcome, false if quit + pub welcome: Option<(bool, u8)>, /// Capture receiver clone (to close the channel after every run), with the current capture id (to ignore pending messages from previous captures) pub current_capture_rx: (usize, Option>), /// Capture data @@ -140,6 +143,7 @@ impl Sniffer { let capture_source = CaptureSource::from_conf(&conf); Self { conf, + welcome: Some((true, 0)), current_capture_rx: (0, None), info_traffic: InfoTraffic::default(), addresses_resolved: HashMap::new(), @@ -173,6 +177,10 @@ impl Sniffer { fn keyboard_subscription(&self) -> Subscription { const NO_MODIFIER: Modifiers = Modifiers::empty(); + if self.welcome.is_some() { + return Subscription::none(); + } + if self.thumbnail { iced::event::listen_with(|event, _, _| match event { Keyboard(Event::KeyPressed { @@ -231,8 +239,17 @@ impl Sniffer { } } - fn time_subscription() -> Subscription { - iced::time::every(Duration::from_millis(1000)).map(|_| Message::Periodic) + fn time_subscription(&self) -> Subscription { + if let Some((w, _)) = self.welcome { + let sub = iced::time::every(Duration::from_millis(100)); + if w { + sub.map(|_| Message::Welcome) + } else { + sub.map(|_| Message::Quit) + } + } else { + iced::time::every(Duration::from_millis(1000)).map(|_| Message::Periodic) + } } fn window_subscription() -> Subscription { @@ -430,8 +447,24 @@ impl Sniffer { } Message::QuitWrapper => return self.quit_wrapper(), Message::Quit => { - let _ = self.conf.clone().store(); - return window::close(self.id.unwrap_or_else(Id::unique)); + if self.welcome.is_none() { + self.welcome = Some((false, 13)); + } else if let Some((false, x)) = self.welcome { + if x <= 2 { + let _ = self.conf.clone().store(); + return window::close(self.id.unwrap_or_else(Id::unique)); + } + self.welcome = Some((false, x.saturating_sub(1))); + } + } + Message::Welcome => { + if let Some((true, x)) = self.welcome { + if x >= 19 { + self.welcome = None; + } else { + self.welcome = Some((true, x + 1)); + } + } } Message::CopyIp(ip) => { self.timing_events.copy_ip_now(ip); @@ -595,6 +628,10 @@ impl Sniffer { let font = style.get_extension().font; let font_headers = style.get_extension().font_headers; + if let Some((_, x)) = self.welcome { + return welcome_page(font, x).into(); + } + let header = header(self); let body = if self.thumbnail { @@ -676,7 +713,7 @@ impl Sniffer { Subscription::batch([ self.keyboard_subscription(), self.mouse_subscription(), - Sniffer::time_subscription(), + self.time_subscription(), Sniffer::window_subscription(), ]) } @@ -1895,7 +1932,8 @@ mod tests { sniffer.update(Message::ChangeRunningPage(RunningPage::Notifications)); sniffer.update(Message::DataReprSelection(DataRepr::Bits)); - // quit the app by sending a CloseRequested message + // force saving configs by quitting the app + sniffer.welcome = Some((false, 0)); sniffer.update(Message::Quit); assert!(path.exists()); diff --git a/src/gui/styles/text.rs b/src/gui/styles/text.rs index 917b3949..ed8337f4 100644 --- a/src/gui/styles/text.rs +++ b/src/gui/styles/text.rs @@ -18,6 +18,7 @@ pub enum TextType { Subtitle, Danger, Sponsor, + Welcome(f32), } /// Returns a formatted caption followed by subtitle, new line, tab, and desc @@ -71,6 +72,12 @@ pub fn highlight(style: &StyleType, element: TextType) -> Color { a: 1.0, } } + TextType::Welcome(mut n) => { + if !(0.0..=1.0).contains(&n) { + n = 1.0; + } + Color { a: n, ..secondary } + } TextType::Incoming => colors.secondary, TextType::Outgoing => colors.outgoing, TextType::Danger | TextType::Sponsor => ext.red_alert_color, diff --git a/src/gui/types/message.rs b/src/gui/types/message.rs index bee2beab..d54b8a2e 100644 --- a/src/gui/types/message.rs +++ b/src/gui/types/message.rs @@ -21,6 +21,8 @@ use crate::{Language, StyleType}; pub enum Message { /// Run tasks to initialize the app StartApp(Option), + /// Animate welcome page + Welcome, /// Sent by the backend parsing packets; includes the capture id, new data, new hosts batched data, and whether an offline capture has finished TickRun(usize, InfoTraffic, Vec, bool), /// Capture source selected from the picklist diff --git a/src/utils/types/icon.rs b/src/utils/types/icon.rs index 7ff3065a..6fa845b6 100644 --- a/src/utils/types/icon.rs +++ b/src/utils/types/icon.rs @@ -42,6 +42,10 @@ pub enum Icon { // Rocket, Settings, Sniffnet, + Sniffnet1, + Sniffnet2, + Sniffnet3, + Sniffnet4, SortAscending, SortDescending, SortNeutral, @@ -90,6 +94,10 @@ impl Icon { // Icon::Rocket => 'S', Icon::Settings => 'a', Icon::Sniffnet => 'A', + Icon::Sniffnet1 => '!', + Icon::Sniffnet2 => '"', + Icon::Sniffnet3 => '#', + Icon::Sniffnet4 => '$', Icon::Star => 'g', Icon::Warning => 'T', Icon::Waves => 'y',