feat: automatic dates; timezone setting support

This commit is contained in:
Michael Murphy 2024-08-02 17:59:09 +02:00 committed by GitHub
parent a9dda39526
commit 36542ea0b9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 593 additions and 147 deletions

View file

@ -20,7 +20,6 @@ cosmic-randr.workspace = true
cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon" }
cosmic-settings-page = { path = "../page" }
cosmic-settings-system = { path = "../pages/system" }
cosmic-settings-time = { path = "../pages/time" }
cosmic-settings-wallpaper = { path = "../pages/wallpapers" }
derivative = "2.2.0"
derive_setters = "0.1.6"
@ -31,6 +30,7 @@ futures = { package = "futures-lite", version = "2.3.0" }
generator = "=0.8.1"
hostname-validator = "1.1.1"
hostname1-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
timedate-zbus = { git = "https://github.com/pop-os/dbus-settings-bindings" }
i18n-embed-fl = "0.8.0"
image = "0.25"
itertools = "0.13.0"
@ -55,6 +55,10 @@ zbus = { version = "4.4.0", features = ["tokio"] }
tachyonix = "0.3.0"
slab = "0.4.9"
[dependencies.icu]
version = "1.4.0"
features = ["experimental", "compiled_data", "icu_datetime_experimental"]
[dependencies.i18n-embed]
version = "0.14.1"
features = ["fluent-system", "desktop-requester"]

View file

@ -1,7 +1,7 @@
use crate::app;
use cosmic::{
cosmic_config::{self, ConfigGet, ConfigSet},
iced,
Command,
};
use cosmic_comp_config::input::{
AccelConfig, AccelProfile, ClickMethod, InputConfig, ScrollConfig, ScrollMethod, TapButtonMap,
@ -93,7 +93,7 @@ impl Page {
}
#[allow(clippy::too_many_lines)]
pub fn update(&mut self, message: Message) -> iced::Command<app::Message> {
pub fn update(&mut self, message: Message) -> Command<app::Message> {
match message {
Message::SetAcceleration(value, touchpad) => {
let profile = if value {
@ -152,7 +152,7 @@ impl Page {
select_model.activate(entity);
let Some(left_entity) = select_model.entity_at(1) else {
return cosmic::Command::none();
return Command::none();
};
let left_handed = select_model.active() == left_entity;
@ -173,7 +173,7 @@ impl Page {
}
}
cosmic::Command::none()
Command::none()
}
}

View file

@ -1,53 +1,85 @@
// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only
use std::str::FromStr;
use chrono::{Datelike, Timelike};
use cosmic::{
cosmic_config::{self, ConfigGet, ConfigSet},
iced::{widget::horizontal_space, Length},
widget::{dropdown, settings},
Apply,
widget::{self, dropdown, settings},
Apply, Command,
};
use cosmic_settings_page::Section;
use cosmic_settings_page::{self as page, section};
use icu::{
calendar::{DateTime, Iso},
datetime::DateTimeFormatter,
locid::Locale,
};
use slab::Slab;
use slotmap::SlotMap;
pub use timedate_zbus::TimeDateProxy;
use tracing::error;
crate::cache_dynamic_lazy! {
static WEEKDAYS: [String; 4] = [fl!("time-format", "friday"), fl!("time-format", "saturday"), fl!("time-format", "sunday"), fl!("time-format", "monday")];
}
#[derive(Debug, Clone)]
pub struct Info {
pub ntp_enabled: bool,
pub timezone_id: Option<usize>,
pub timezone_list: Vec<String>,
}
pub struct Page {
config: cosmic_config::Config,
auto: bool,
auto_timezone: bool,
military_time: bool,
cosmic_applet_config: cosmic_config::Config,
first_day_of_week: usize,
military_time: bool,
ntp_enabled: bool,
show_date_in_top_panel: bool,
local_time: Option<DateTime<Iso>>,
timezone: Option<usize>,
timezone_list: Vec<String>,
formatted_date: String,
}
impl Default for Page {
fn default() -> Self {
let config = cosmic_config::Config::new("com.system76.CosmicAppletTime", 1).unwrap();
let military_time = config.get("military_time").unwrap_or_else(|err| {
error!(?err, "Failed to read config 'military_time'");
false
});
let first_day_of_week = config.get("first_day_of_week").unwrap_or_else(|err| {
error!(?err, "Failed to read config 'first_day_of_week'");
6
});
let show_date_in_top_panel = config.get("show_date_in_top_panel").unwrap_or_else(|err| {
error!(?err, "Failed to read config 'show_date_in_top_panel'");
true
});
let cosmic_applet_config =
cosmic_config::Config::new("com.system76.CosmicAppletTime", 1).unwrap();
let military_time = cosmic_applet_config
.get("military_time")
.unwrap_or_else(|err| {
error!(?err, "Failed to read config 'military_time'");
false
});
let first_day_of_week = cosmic_applet_config
.get("first_day_of_week")
.unwrap_or_else(|err| {
error!(?err, "Failed to read config 'first_day_of_week'");
6
});
let show_date_in_top_panel = cosmic_applet_config
.get("show_date_in_top_panel")
.unwrap_or_else(|err| {
error!(?err, "Failed to read config 'show_date_in_top_panel'");
true
});
Self {
config,
auto: false,
auto_timezone: false,
military_time,
cosmic_applet_config,
first_day_of_week,
formatted_date: String::new(),
local_time: None,
military_time,
ntp_enabled: false,
show_date_in_top_panel,
timezone: None,
timezone_list: Vec::new(),
}
}
}
@ -69,42 +101,168 @@ impl page::Page<crate::pages::Message> for Page {
.title(fl!("time-date"))
.description(fl!("time-date", "desc"))
}
fn on_enter(
&mut self,
_page: cosmic_settings_page::Entity,
_sender: tokio::sync::mpsc::Sender<crate::pages::Message>,
) -> Command<crate::pages::Message> {
cosmic::command::future(async move {
let client = match zbus::Connection::system().await {
Ok(client) => client,
Err(why) => {
return Message::Error(why.to_string());
}
};
let timedate_proxy = match TimeDateProxy::new(&client).await {
Ok(timedate_proxy) => timedate_proxy,
Err(why) => {
return Message::Error(why.to_string());
}
};
let can_ntp = timedate_proxy.can_ntp().await.unwrap_or_default();
let ntp_enabled = can_ntp && timedate_proxy.ntp().await.unwrap_or_default();
let timezone_list = timedate_proxy.list_timezones().await.unwrap_or_default();
let timezone = timedate_proxy.timezone().await.unwrap_or_default();
Message::Refresh(Info {
ntp_enabled,
timezone_id: timezone_list.iter().position(|tz| tz == &timezone),
timezone_list,
})
})
.map(crate::pages::Message::DateAndTime)
}
}
impl Page {
pub fn update(&mut self, message: Message) {
pub fn update(&mut self, message: Message) -> Command<crate::Message> {
match message {
Message::Automatic(enable) => self.auto = enable,
Message::AutomaticTimezone(enable) => self.auto_timezone = enable,
Message::Automatic(enable) => {
self.ntp_enabled = enable;
tokio::task::spawn(async move {
let client = match zbus::Connection::system().await {
Ok(client) => client,
Err(why) => {
tracing::error!(?why, "zbus client error");
return;
}
};
let timedate_proxy = match TimeDateProxy::new(&client).await {
Ok(timedate_proxy) => timedate_proxy,
Err(why) => {
tracing::error!(?why, "zbus client error");
return;
}
};
_ = timedate_proxy.set_ntp(enable, true).await;
});
}
Message::MilitaryTime(enable) => {
self.military_time = enable;
if let Err(err) = self.config.set("military_time", enable) {
self.update_local_time();
if let Err(err) = self.cosmic_applet_config.set("military_time", enable) {
error!(?err, "Failed to set config 'military_time'");
}
}
Message::FirstDayOfWeek(weekday) => {
self.first_day_of_week = weekday;
if let Err(err) = self.config.set("first_day_of_week", weekday) {
if let Err(err) = self.cosmic_applet_config.set("first_day_of_week", weekday) {
error!(?err, "Failed to set config 'first_day_of_week'");
}
}
Message::ShowDate(enable) => {
self.show_date_in_top_panel = enable;
if let Err(err) = self.config.set("show_date_in_top_panel", enable) {
if let Err(err) = self
.cosmic_applet_config
.set("show_date_in_top_panel", enable)
{
error!(?err, "Failed to set config 'show_date_in_top_panel'");
}
}
Message::Timezone(timezone_id) => {
self.timezone = Some(timezone_id);
if let Some(timezone) = self.timezone_list.get(timezone_id).cloned() {
return cosmic::command::future(async move {
let client = match zbus::Connection::system().await {
Ok(client) => client,
Err(why) => {
return Message::Error(why.to_string());
}
};
let timedate_proxy = match TimeDateProxy::new(&client).await {
Ok(timedate_proxy) => timedate_proxy,
Err(why) => {
return Message::Error(why.to_string());
}
};
match timedate_proxy.set_timezone(&timezone, true).await {
Ok(_) => Message::UpdateTime,
Err(why) => Message::Error(why.to_string()),
}
})
.map(crate::pages::Message::DateAndTime)
.map(crate::Message::PageMessage);
}
}
Message::Error(why) => {
tracing::error!(why, "failed to set timezone");
}
Message::UpdateTime => self.update_local_time(),
Message::Refresh(info) => {
self.ntp_enabled = info.ntp_enabled;
self.timezone_list = info.timezone_list;
self.timezone = info.timezone_id;
self.update_local_time();
}
Message::None => (),
}
Command::none()
}
pub fn update_local_time(&mut self) {
self.local_time = Some(update_local_time());
self.formatted_date = match self.local_time {
Some(ref time) => format_date(time, self.military_time),
None => fl!("unknown"),
}
}
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Debug)]
pub enum Message {
Automatic(bool),
AutomaticTimezone(bool),
Error(String),
MilitaryTime(bool),
None,
FirstDayOfWeek(usize),
Refresh(Info),
ShowDate(bool),
Timezone(usize),
UpdateTime,
}
impl page::AutoBind<crate::pages::Message> for Page {}
@ -122,11 +280,11 @@ fn date() -> Section<crate::pages::Message> {
settings::view_section(&section.title)
.add(
settings::item::builder(&*section.descriptions[auto])
.toggler(page.auto, Message::Automatic),
.toggler(page.ntp_enabled, Message::Automatic),
)
.add(settings::item(
&*section.descriptions[title],
horizontal_space(Length::Fill),
widget::text(&page.formatted_date),
))
.apply(cosmic::Element::from)
.map(crate::pages::Message::DateAndTime)
@ -183,8 +341,6 @@ fn format() -> Section<crate::pages::Message> {
fn timezone() -> Section<crate::pages::Message> {
let mut descriptions = Slab::new();
let auto = descriptions.insert(fl!("time-zone", "auto"));
let auto_info = descriptions.insert(fl!("time-zone", "auto-info"));
let time_zone = descriptions.insert(fl!("time-zone"));
Section::default()
@ -192,18 +348,66 @@ fn timezone() -> Section<crate::pages::Message> {
.descriptions(descriptions)
.view::<Page>(move |_binder, page, section| {
settings::view_section(&section.title)
// Automatic timezone toggle
.add(
settings::item::builder(&*section.descriptions[auto])
.description(&*section.descriptions[auto_info])
.toggler(page.auto_timezone, Message::AutomaticTimezone),
)
// Time zone select
.add(
settings::item::builder(&*section.descriptions[time_zone])
.control(horizontal_space(Length::Fill)),
settings::item::builder(&*section.descriptions[time_zone]).control(
widget::dropdown(&page.timezone_list, page.timezone, Message::Timezone),
),
)
.apply(cosmic::Element::from)
.map(crate::pages::Message::DateAndTime)
})
}
fn locale() -> Result<Locale, Box<dyn std::error::Error>> {
let locale = std::env::var("LANG")?;
let locale = locale
.split('.')
.next()
.ok_or(format!("Can't split the locale {locale}"))?;
let locale = Locale::from_str(locale).map_err(|e| format!("{e:?}"))?;
Ok(locale)
}
fn format_date(date: &DateTime<Iso>, military: bool) -> String {
let Ok(locale) = locale() else {
return String::new();
};
let mut bag = icu::datetime::options::components::Bag::empty();
bag.year = Some(icu::datetime::options::components::Year::Numeric);
bag.day = Some(icu::datetime::options::components::Day::NumericDayOfMonth);
bag.month = Some(icu::datetime::options::components::Month::Long);
bag.hour = Some(icu::datetime::options::components::Numeric::Numeric);
bag.minute = Some(icu::datetime::options::components::Numeric::Numeric);
bag.preferences = Some(icu::datetime::options::preferences::Bag::from_hour_cycle(
if military {
icu::datetime::options::preferences::HourCycle::H23
} else {
icu::datetime::options::preferences::HourCycle::H12
},
));
let dtf = DateTimeFormatter::try_new_experimental(&locale.into(), bag.into()).unwrap();
dtf.format(&date.to_any())
.expect("can't format value")
.to_string()
}
fn update_local_time() -> DateTime<Iso> {
let now = chrono::Local::now();
DateTime::try_new_gregorian_datetime(
now.year(),
now.month() as u8,
now.day() as u8,
now.hour() as u8,
now.minute() as u8,
now.second() as u8,
)
.unwrap()
.to_iso()
}