From 3355d1fe7cf10f924c83b0a6fc241293561a8966 Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Thu, 6 Nov 2025 21:15:26 +0100 Subject: [PATCH 1/5] add an animated welcome screen --- resources/fonts/subset/icons.ttf | Bin 20032 -> 21048 bytes src/gui/pages/mod.rs | 1 + src/gui/pages/welcome_page.rs | 55 +++++++++++++++++++++++++++++++ src/gui/sniffer.rs | 27 +++++++++++++-- src/gui/styles/text.rs | 14 ++++++++ src/gui/types/message.rs | 2 ++ src/utils/types/icon.rs | 8 +++++ 7 files changed, 104 insertions(+), 3 deletions(-) create mode 100644 src/gui/pages/welcome_page.rs diff --git a/resources/fonts/subset/icons.ttf b/resources/fonts/subset/icons.ttf index f577e3c153029418d7d8d56d8ea82da520572fa5..c2fc694d3ef72090f1865bd778175d6fd753ad73 100644 GIT binary patch delta 913 zcmY*YTWAz#6#o91*^9|$XLffs+uhmhb(%DZG3!>7G{&v(4u8UIUwg6b&}7U6;l^yZ}08$xm(QtW9>G>OBCk z&E$9Y4rKG^i>99e!bXaX@5~P64TG4_@qqI7o&EcElxfFT0TN+aXLEhouPZKB{zEyF zuP&GC8_C)$C5gZ)1@zoNVXvzo-X#Aj`RDovd$SXdF3kYqALLIAWcTLTF{y#_q=@Xw z4)g`Lk2`>NYDl<~9~>&!j@Z7W27;8IdN&*m*wi_g*d{m4qnJKL^4oW9qo#e56i)9s z$yTy9o?=^Bl%HX`ogKoQv0yClU)WD0-_tba4Fw_LTe2z?ikK`qYOoxQXhjc(Fidls z5=AuDdlvh)vK2JO6%%VtfIV@aaD^s z-_DEGzEGw(s*CP#_VJ5af|h11m_YGI6NqglRQV8^@bKq>cKJhap6}Os*x$S)SnZNb z3Q7K-Vlzff5`HakM0h*T)7lCCRO`InCp~YG+vwIA^(w&vD{P#8E5Ds0PWb5Qr^B$A Q4#N^;xYAxDCQtAA7e+nNO8@`> delta 701 zcmXX@OH30{6g_V`(6OD;X$#WI6xuS%|0Bw;LRCIf1VKg85J1iFZ3vY{P|=W7T@YfB zm{!pcO;B05B+-R28jUeViD9A0io}g3%F@IQ3+pX#U*6nz?m6$id+)qhWN#m^2p9kp zu0e;Bs@l=|cyr=9(56sS_BS-Dc#Son^#@_!iPI%zfk%Cl0P7?i=nHA#&beqez%@~9 zW=I;roknW82V zgOR~wn7kDdML0SYp&VWuLl4Gqm6}ah0ZLJc8ni&e#qC)9TYNSCHU1^OLVO~Vlo0Ni zfjkX~a@V1rd;=Pxk}P>!3dA?wSYA1HA{J_PWN z$?OJO=M?S|x2#iigSzLu!h8AoB+!eCpDk#6n)WBXEq&Pz`?Q01yv$gT6?s8kbBfM(=LKif`8iX~T5~PC zQ{86`?nRH_Y4&`~=ALy3znk=aDw%$cGve+KY6qYT-Y? CfREV# 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..96985f8e --- /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 | 26.. => Text::new(""), + 4 | 25 => Icon::Sniffnet1.to_text(), + 5 | 24 => Icon::Sniffnet2.to_text(), + 6 | 23 => Icon::Sniffnet3.to_text(), + 7 | 22 => Icon::Sniffnet4.to_text(), + 8..=21 => Icon::Sniffnet.to_text(), + }; + + let text = Text::new(match x { + 0..=3 | 26.. => "", + 4 | 25 => "S", + 5 | 24 => "Sn", + 6 | 23 => "Sni", + 7 | 22 => "Snif", + 8 | 21 => "Sniff", + 9 | 20 => "Sniffn", + 10 | 19 => "Sniffne", + 11..=18 => "Sniffnet", + }); + + let text_type = match x { + 0..=3 | 26.. => TextType::Welcome(0.0), + 4 | 25 => TextType::Welcome(0.125), + 5 | 24 => TextType::Welcome(0.25), + 6 | 23 => TextType::Welcome(0.375), + 7 | 22 => TextType::Welcome(0.5), + 8 | 21 => TextType::Welcome(0.625), + 9 | 20 => TextType::Welcome(0.750), + 10 | 19 => TextType::Welcome(0.875), + 11..=18 => 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..0b5be968 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 in welcome page + pub welcome: Option, /// 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(0), current_capture_rx: (0, None), info_traffic: InfoTraffic::default(), addresses_resolved: HashMap::new(), @@ -231,8 +235,12 @@ impl Sniffer { } } - fn time_subscription() -> Subscription { - iced::time::every(Duration::from_millis(1000)).map(|_| Message::Periodic) + fn time_subscription(&self) -> Subscription { + if self.welcome.is_some() { + iced::time::every(Duration::from_millis(100)).map(|_| Message::Welcome) + } else { + iced::time::every(Duration::from_millis(1000)).map(|_| Message::Periodic) + } } fn window_subscription() -> Subscription { @@ -257,6 +265,15 @@ impl Sniffer { Task::perform(set_newer_release_status(), Message::SetNewerReleaseStatus), ]); } + Message::Welcome => { + if let Some(x) = self.welcome { + if x >= 26 { + self.welcome = None; + } else { + self.welcome = Some(x + 1); + } + } + } Message::TickRun(cap_id, msg, host_msgs, no_more_packets) => { if cap_id == self.current_capture_rx.0 { for host_msg in host_msgs { @@ -595,6 +612,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 +697,7 @@ impl Sniffer { Subscription::batch([ self.keyboard_subscription(), self.mouse_subscription(), - Sniffer::time_subscription(), + self.time_subscription(), Sniffer::window_subscription(), ]) } diff --git a/src/gui/styles/text.rs b/src/gui/styles/text.rs index 917b3949..99a46eba 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,19 @@ pub fn highlight(style: &StyleType, element: TextType) -> Color { a: 1.0, } } + TextType::Welcome(mut n) => { + if n < 0.0 || n > 1.0 { + n = 1.0 + }; + let std = colors.text_body; + let end = secondary; + Color { + r: std.r * (1.0 - n) + end.r * n, + g: std.g * (1.0 - n) + end.g * n, + b: std.b * (1.0 - n) + end.b * n, + a: 1.0, + } + } 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', From 7f1c73a70c5f4cf1fe04d2f8c69efcbccc88ead9 Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Thu, 6 Nov 2025 23:22:01 +0100 Subject: [PATCH 2/5] ignore keyboard shortcuts during welcome screen --- src/gui/sniffer.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs index 0b5be968..fb6ba29b 100644 --- a/src/gui/sniffer.rs +++ b/src/gui/sniffer.rs @@ -175,6 +175,10 @@ impl Sniffer { } fn keyboard_subscription(&self) -> Subscription { + if self.welcome.is_some() { + return Subscription::none(); + } + const NO_MODIFIER: Modifiers = Modifiers::empty(); if self.thumbnail { From 2cbddaee369e8491041a65cb533070b2f0dadee9 Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Fri, 7 Nov 2025 11:51:26 +0100 Subject: [PATCH 3/5] use welcome screen reversed for quit animation --- src/gui/pages/welcome_page.rs | 48 +++++++++++++++++------------------ src/gui/sniffer.rs | 47 +++++++++++++++++++++------------- src/gui/styles/text.rs | 9 +------ 3 files changed, 55 insertions(+), 49 deletions(-) diff --git a/src/gui/pages/welcome_page.rs b/src/gui/pages/welcome_page.rs index 96985f8e..3bfa8e71 100644 --- a/src/gui/pages/welcome_page.rs +++ b/src/gui/pages/welcome_page.rs @@ -8,36 +8,36 @@ use iced::{Alignment, Font, Length}; pub fn welcome_page<'a>(font: Font, x: u8) -> Container<'a, Message, StyleType> { let icon = match x { - 0..=3 | 26.. => Text::new(""), - 4 | 25 => Icon::Sniffnet1.to_text(), - 5 | 24 => Icon::Sniffnet2.to_text(), - 6 | 23 => Icon::Sniffnet3.to_text(), - 7 | 22 => Icon::Sniffnet4.to_text(), - 8..=21 => Icon::Sniffnet.to_text(), + 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 | 26.. => "", - 4 | 25 => "S", - 5 | 24 => "Sn", - 6 | 23 => "Sni", - 7 | 22 => "Snif", - 8 | 21 => "Sniff", - 9 | 20 => "Sniffn", - 10 | 19 => "Sniffne", - 11..=18 => "Sniffnet", + 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 | 26.. => TextType::Welcome(0.0), - 4 | 25 => TextType::Welcome(0.125), - 5 | 24 => TextType::Welcome(0.25), - 6 | 23 => TextType::Welcome(0.375), - 7 | 22 => TextType::Welcome(0.5), - 8 | 21 => TextType::Welcome(0.625), - 9 | 20 => TextType::Welcome(0.750), - 10 | 19 => TextType::Welcome(0.875), - 11..=18 => TextType::Welcome(1.0), + 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() diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs index fb6ba29b..a6529c6e 100644 --- a/src/gui/sniffer.rs +++ b/src/gui/sniffer.rs @@ -78,8 +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 in welcome page - pub welcome: Option, + /// 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 @@ -143,7 +143,7 @@ impl Sniffer { let capture_source = CaptureSource::from_conf(&conf); Self { conf, - welcome: Some(0), + welcome: Some((true, 0)), current_capture_rx: (0, None), info_traffic: InfoTraffic::default(), addresses_resolved: HashMap::new(), @@ -240,8 +240,13 @@ impl Sniffer { } fn time_subscription(&self) -> Subscription { - if self.welcome.is_some() { - iced::time::every(Duration::from_millis(100)).map(|_| Message::Welcome) + 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) } @@ -269,15 +274,6 @@ impl Sniffer { Task::perform(set_newer_release_status(), Message::SetNewerReleaseStatus), ]); } - Message::Welcome => { - if let Some(x) = self.welcome { - if x >= 26 { - self.welcome = None; - } else { - self.welcome = Some(x + 1); - } - } - } Message::TickRun(cap_id, msg, host_msgs, no_more_packets) => { if cap_id == self.current_capture_rx.0 { for host_msg in host_msgs { @@ -451,8 +447,25 @@ 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)); + } else { + 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); @@ -616,7 +629,7 @@ impl Sniffer { let font = style.get_extension().font; let font_headers = style.get_extension().font_headers; - if let Some(x) = self.welcome { + if let Some((_, x)) = self.welcome { return welcome_page(font, x).into(); } diff --git a/src/gui/styles/text.rs b/src/gui/styles/text.rs index 99a46eba..1e32c901 100644 --- a/src/gui/styles/text.rs +++ b/src/gui/styles/text.rs @@ -76,14 +76,7 @@ pub fn highlight(style: &StyleType, element: TextType) -> Color { if n < 0.0 || n > 1.0 { n = 1.0 }; - let std = colors.text_body; - let end = secondary; - Color { - r: std.r * (1.0 - n) + end.r * n, - g: std.g * (1.0 - n) + end.g * n, - b: std.b * (1.0 - n) + end.b * n, - a: 1.0, - } + Color { a: n, ..secondary } } TextType::Incoming => colors.secondary, TextType::Outgoing => colors.outgoing, From cc09fe366a68d917c7e75efa1f1cad4c486037fb Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Fri, 7 Nov 2025 12:25:27 +0100 Subject: [PATCH 4/5] fix tests and clippy --- src/gui/sniffer.rs | 10 +++++----- src/gui/styles/text.rs | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/gui/sniffer.rs b/src/gui/sniffer.rs index a6529c6e..44c5b5f4 100644 --- a/src/gui/sniffer.rs +++ b/src/gui/sniffer.rs @@ -175,12 +175,12 @@ impl Sniffer { } fn keyboard_subscription(&self) -> Subscription { + const NO_MODIFIER: Modifiers = Modifiers::empty(); + if self.welcome.is_some() { return Subscription::none(); } - const NO_MODIFIER: Modifiers = Modifiers::empty(); - if self.thumbnail { iced::event::listen_with(|event, _, _| match event { Keyboard(Event::KeyPressed { @@ -453,9 +453,8 @@ impl Sniffer { if x <= 2 { let _ = self.conf.clone().store(); return window::close(self.id.unwrap_or_else(Id::unique)); - } else { - self.welcome = Some((false, x.saturating_sub(1))); } + self.welcome = Some((false, x.saturating_sub(1))); } } Message::Welcome => { @@ -1933,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 1e32c901..ed8337f4 100644 --- a/src/gui/styles/text.rs +++ b/src/gui/styles/text.rs @@ -73,9 +73,9 @@ pub fn highlight(style: &StyleType, element: TextType) -> Color { } } TextType::Welcome(mut n) => { - if n < 0.0 || n > 1.0 { - n = 1.0 - }; + if !(0.0..=1.0).contains(&n) { + n = 1.0; + } Color { a: n, ..secondary } } TextType::Incoming => colors.secondary, From 8152ed144aed237e9df334b7a155f21ccc7085a3 Mon Sep 17 00:00:00 2001 From: GyulyVGC Date: Sun, 9 Nov 2025 23:08:30 +0100 Subject: [PATCH 5/5] update CHANGELOG --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) 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))