diff --git a/Cargo.toml b/Cargo.toml index 9975bb0..75d1bc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2024" [dependencies] -blocking = "1.6.2" +blocking = "1.6" iced = { version = "0.14", features = ["smol", "image"] } iced_moving_picture = "0" rust-i18n = "3" diff --git a/src/locales/en.json b/src/locales/en.json index c744897..1c7ca9a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -17,5 +17,9 @@ "network.ok": "Network connection active!", "network.no_connection": "Network inactive.", "network.check_error": "Error while cheking network connection.", - "network.button.check": "Check Network" + "network.button.check": "Check Network", + "timezone.select_region": "Select region:", + "timezone.select_timezone": "Select time zone:", + "timezone.init_error": "Erro getting time zones data from system." + } \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a347d6f..f2bd347 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,7 +20,6 @@ stages loading, switching between stages, and stuff. */ - use iced::widget; use std::process::ExitCode; @@ -42,6 +41,7 @@ enum Views { Welcome(welcome::WelcomeStage), License(license::LicenseStage), Network(stages::network::NetworkStage), + TimeZone(stages::timezone::TimeZoneStage), } enum Message { @@ -49,6 +49,7 @@ enum Message { Welcome(welcome::Message), License(license::Message), Network(stages::network::Message), + TimeZone(stages::timezone::Message), } struct KiraState { @@ -105,6 +106,7 @@ fn view(k_state: &KiraState) -> Element<'_, Message> { Views::Welcome(wellcome_stage) => wellcome_stage.view().map(Message::Welcome), Views::License(license_stage) => license_stage.view().map(Message::License), Views::Network(network_stage) => network_stage.view().map(Message::Network), + Views::TimeZone(timezone_stage) => timezone_stage.view().map(Message::TimeZone), } } @@ -158,7 +160,9 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { network::UpdateResult::StageAction(action) => match action { StageAction::Next(network_res) => { k_state.config.config_trail.push(network_res); - iced::exit() + k_state.current_view = + Views::TimeZone(stages::timezone::TimeZoneStage::new()); + Task::none() } StageAction::Back => { k_state.current_view = Views::License(license::LicenseStage {}); @@ -171,6 +175,22 @@ fn update(k_state: &mut KiraState, message: Message) -> Task { Task::none() } } + Message::TimeZone(timezone_message) => { + if let Views::TimeZone(timezone_view) = &mut k_state.current_view { + let action = timezone_view.update(timezone_message); + match action { + StageAction::Next(tz_res) => { + k_state.config.config_trail.push(tz_res); + iced::exit() + } + StageAction::Abort(_) => iced::exit(), + StageAction::Back => iced::exit(), + StageAction::None => Task::none(), + } + } else { + Task::none() + } + } } } @@ -187,7 +207,7 @@ pub fn main() -> ExitCode { let iced_result = iced::application(KiraState::boot, update, view) .window(iced::window::Settings { icon: Some( - iced::window::icon::from_file_data(include_bytes!("icon.png"), None) + iced::window::icon::from_file_data(include_bytes!("media/icon.png"), None) .expect("icon should be a valid PNG"), ), ..Default::default() diff --git a/src/icon.png b/src/media/icon.png similarity index 100% rename from src/icon.png rename to src/media/icon.png diff --git a/src/stages/welcome/media/logo.png b/src/media/logo.png similarity index 100% rename from src/stages/welcome/media/logo.png rename to src/media/logo.png diff --git a/src/spiner.apng b/src/media/spiner.apng similarity index 100% rename from src/spiner.apng rename to src/media/spiner.apng diff --git a/src/stages/mod.rs b/src/stages/mod.rs index 4c45e5e..56dcaf8 100644 --- a/src/stages/mod.rs +++ b/src/stages/mod.rs @@ -17,4 +17,5 @@ pub mod welcome; pub mod license; -pub mod network; \ No newline at end of file +pub mod network; +pub mod timezone; \ No newline at end of file diff --git a/src/stages/network/mod.rs b/src/stages/network/mod.rs index cfba353..4d8ce12 100644 --- a/src/stages/network/mod.rs +++ b/src/stages/network/mod.rs @@ -111,7 +111,7 @@ impl NetworkStage { } let spinner_frames = - apng::Frames::from_bytes(include_bytes!("spiner.apng").to_vec()).unwrap(); + apng::Frames::from_bytes(crate::theme::get_spiner_bytes()).unwrap(); Self { internet_active: false, diff --git a/src/stages/network/spiner.apng b/src/stages/network/spiner.apng deleted file mode 100644 index d028973..0000000 Binary files a/src/stages/network/spiner.apng and /dev/null differ diff --git a/src/stages/timezone/mod.rs b/src/stages/timezone/mod.rs new file mode 100644 index 0000000..58a1810 --- /dev/null +++ b/src/stages/timezone/mod.rs @@ -0,0 +1,240 @@ +// +// Copyright (C) <2026> + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +/* + This is Welcome stage, used to greet used and allow user to choose program language +*/ + +use crate::stage::{ConfigValue, StageAction, StageResult}; +use iced::{Alignment, widget}; +use rust_i18n::t; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TimeZoneData { + region: String, + zone: String, +} + +impl TimeZoneData { + fn to_string(&self) -> String { + format!("{}/{}", self.region, self.zone) + } + fn from_str(s: &str) -> Option { + let idx = s.find('/')?; + let (region, zone) = s.split_at(idx); + Some(Self { + region: region.into(), + zone: zone.into(), + }) + } +} + +impl std::fmt::Display for TimeZoneData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}/{}", self.region, self.zone) + } +} + +#[derive(Debug, Clone)] +pub struct TimeZoneStage { + time_zone: Option, + time_zone_region: Option, + time_zones: Result>, String>, + regions: Vec, + zones: Vec, +} + +#[derive(Debug, Clone)] +pub enum Message { + SelectRegion(String), + SelectZone(TimeZoneData), + Next, + Back, +} + +/// Getting list of awailable timezones from system. +fn get_timezones_blocking() -> Result, String> { + use std::process::Command; + //timedatectl list-timezones --no-pager + match Command::new("timedatectl") + .arg("list-timezones") + .arg("--no-pager") + .output() + { + Ok(cmd_output) => { + if cmd_output.status.success() { + //Etc/UTC + match String::from_utf8(cmd_output.stdout) { + Ok(str_tz_list) => Ok(str_tz_list + .split("\n") + .filter(|sb| sb.contains('/')) + .filter_map(|str_t_zone| TimeZoneData::from_str(str_t_zone.trim())) + .collect::>()), + Err(ex) => Err(format!("Exception while converting to UTF8 {}", ex)), + } + } else { + Err(format!( + "Error getting timezones list: {}", + String::from_utf8(cmd_output.stderr).unwrap_or("UNKNOWN".into()) + )) + } + } + Err(ex) => Err(format!("Exception while trying to list time zones {}", ex)), + } +} + +impl TimeZoneStage { + pub fn new() -> Self { + match get_timezones_blocking() { + Ok(tzs) => { + let mut time_zones: HashMap> = HashMap::new(); + for tz in &tzs { + if let Some(z) = time_zones.get_mut(tz.region.as_str()) { + z.push(tz.clone()); + } else { + time_zones.insert(tz.region.clone(), vec![tz.clone()]); + } + } + //let time_zones: HashMap = HashMap::from_iter(tzs.iter().map(|tz| (tz.region.clone(), tz.clone()))); + Self { + regions: time_zones.keys().cloned().collect(), + zones: tzs.clone(), + time_zone: None, + time_zone_region: None, + time_zones: Ok(time_zones), + } + } + + Err(ex) => { + println!( + "Exception while trying to get time zones list from system {}", + ex + ); + Self { + time_zone: None, + time_zone_region: None, + time_zones: Err(ex), + regions: Vec::new(), + zones: Vec::new(), + } + } + } + } + + fn gen_result(&self) -> StageResult { + if let Some(tz) = &self.time_zone { + StageResult { + name: "time_zone".to_string(), + config: Some(HashMap::from([ + ("name".to_string(), ConfigValue::String(tz.to_string())), + ("region".to_string(), ConfigValue::String(tz.region.clone())), + ("zone".to_string(), ConfigValue::String(tz.zone.clone())), + ])), + resuts: None, + error: None, + } + } else { + StageResult { + name: "time_zone".to_string(), + config: None, + resuts: None, + error: None, + } + } + } + + pub fn update(&mut self, message: Message) -> StageAction { + match message { + Message::SelectRegion(region) => { + self.time_zone_region = Some(region); + StageAction::None + } + Message::SelectZone(zone) => { + self.time_zone = Some(zone); + StageAction::None + } + Message::Next => StageAction::Next(self.gen_result()), + Message::Back => StageAction::Back, + } + } + + pub fn view(&self) -> iced::Element<'_, Message> { + let (next_button, main_content) = match &self.time_zones { + Ok(time_zones) => ( + widget::button(widget::text(t!("button.next"))).on_press(Message::Next), + widget::container( + widget::column![ + widget::text(t!("timezone.select_timezone")), + widget::row![ + widget::text(t!("timezone.select_region")), + widget::pick_list( + self.regions.clone(), + self.time_zone_region.clone(), + Message::SelectRegion, + ) + .placeholder("Select Region") + ], + widget::row![ + widget::text(t!("timezone.select_timezone")), + widget::pick_list( + time_zones + .get( + self.time_zone_region + .clone() + .unwrap_or("Etc".into()) + .as_str() + ) + .unwrap() + .clone(), + self.time_zone.clone(), + Message::SelectZone, + ) + .placeholder("Select Timezone") + ] + ] + .padding(10) + .spacing(10) + .align_x(Alignment::Center), + ), + ), + Err(_) => ( + widget::button(widget::text(t!("button.next"))), + widget::container(widget::text(t!("timezone.init_error"))), + ), + }; + let back_button = widget::button(widget::text(t!("button.back"))).on_press(Message::Back); + + widget::column![ + main_content + .height(iced::Length::Fill) + .width(iced::Length::Fill) + .align_x(Alignment::Center) + .align_y(Alignment::Center), + widget::row![ + back_button, + widget::space::horizontal(), // Pushes the right button to the far right + next_button + ] + .width(iced::Length::Fill) + .align_y(Alignment::End) + .padding(10), + ] + .align_x(Alignment::Center) + .spacing(10) + .into() + } +} diff --git a/src/stages/welcome/mod.rs b/src/stages/welcome/mod.rs index f657b3b..c02625e 100644 --- a/src/stages/welcome/mod.rs +++ b/src/stages/welcome/mod.rs @@ -20,7 +20,7 @@ */ -use crate::stage::{ConfigValue, StageAction, StageResult}; +use crate::{stage::{ConfigValue, StageAction, StageResult}, theme}; use iced::{Alignment, widget}; use rust_i18n::t; use std::collections::HashMap; @@ -127,7 +127,7 @@ impl WelcomeStage { // Embed the image bytes into the executable let welcom_logo_handle = - widget::image::Handle::from_bytes(include_bytes!("media/logo.png").to_vec()); + widget::image::Handle::from_bytes(theme::get_logo_bytes()); widget::column![ widget::container( diff --git a/src/theme.rs b/src/theme.rs index 64a92e9..728160d 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -50,3 +50,11 @@ pub fn text_with_border(_theme: &Theme) -> container::Style { ..container::Style::default() } } + +pub fn get_spiner_bytes() -> Vec{ + return include_bytes!("media/spiner.apng").to_vec(); +} + +pub fn get_logo_bytes() -> Vec{ + return include_bytes!("media/logo.png").to_vec(); +}