diff --git a/Cargo.toml b/Cargo.toml index e68e242..3d83bd2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,8 @@ once_cell = "1.19.0" rust-embed = "8.3.0" log = "0.4" open = "5.0.2" +serde = { version = "1.0.202", features = ["serde_derive"] } +paste = "1.0" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic.git" diff --git a/i18n/en/cosmic_backups.ftl b/i18n/en/cosmic_backups.ftl index da6c2ec..e8c5430 100644 --- a/i18n/en/cosmic_backups.ftl +++ b/i18n/en/cosmic_backups.ftl @@ -5,8 +5,19 @@ welcome = Backup and backup often! ✨ # Context Pages ## About +about = About git-description = Git commit {$hash} on {$date} +## Settings +settings = Settings + +### Appearance +appearance = Appearance +theme = Theme +match-desktop = Match desktop +dark = Dark +light = Light + # Menu ## File @@ -20,4 +31,5 @@ cut = Cut ## View view = View +menu-settings = Settings... menu-about = About COSMIC Backups... diff --git a/src/app.rs b/src/app.rs index a84ff60..7d58749 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,26 +1,25 @@ // SPDX-License-Identifier: GPL-3.0-only -use std::{ - collections::{HashMap}, - env, - process, -}; +use crate::app::config::AppTheme; use crate::fl; use cosmic::app::{Command, Core}; -use cosmic::{ - cosmic_theme, ApplicationExt, - iced::{Alignment, Length}, -}; use cosmic::iced::alignment::{Horizontal, Vertical}; use cosmic::iced::window; use cosmic::iced_core::keyboard::Key; use cosmic::widget::menu::{ - action::{MenuAction}, + action::MenuAction, key_bind::{KeyBind, Modifier}, }; use cosmic::widget::segmented_button::Entity; +use cosmic::{ + cosmic_config, cosmic_theme, + iced::{Alignment, Length}, + ApplicationExt, +}; use cosmic::{widget, Application, Element}; +use std::{collections::HashMap, env, process}; +pub mod config; pub mod menu; /// This is the struct that represents your application. @@ -30,6 +29,9 @@ pub struct App { /// This is the core of your application, it is used to communicate with the Cosmic runtime. /// It is used to send messages to your application, and to access the resources of the Cosmic runtime. core: Core, + config_handler: Option, + config: config::CosmicBackupsConfig, + app_themes: Vec, context_page: ContextPage, key_binds: HashMap, } @@ -42,6 +44,8 @@ pub enum Message { // Cut(Option), ToggleContextPage(ContextPage), LaunchUrl(String), + AppTheme(usize), + SystemThemeModeChange(cosmic_theme::ThemeMode), WindowClose, WindowNew, } @@ -49,20 +53,29 @@ pub enum Message { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ContextPage { About, + Settings, } impl ContextPage { fn title(&self) -> String { match self { - Self::About => String::new(), + Self::About => fl!("about"), + Self::Settings => fl!("settings"), } } } +#[derive(Clone, Debug)] +pub struct Flags { + pub config_handler: Option, + pub config: config::CosmicBackupsConfig, +} + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { About, // Cut, + Settings, WindowClose, WindowNew, } @@ -73,6 +86,7 @@ impl MenuAction for Action { match self { Action::About => Message::ToggleContextPage(ContextPage::About), // Action::Cut => Message::Cut(entity_opt), + Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Action::WindowClose => Message::WindowClose, Action::WindowNew => Message::WindowNew, } @@ -80,9 +94,9 @@ impl MenuAction for Action { } impl App { -// fn update_config(&mut self) -> Command> { -// cosmic::app::command::set_theme(self.config.app_theme.theme()) -// } + fn update_config(&mut self) -> Command { + cosmic::app::command::set_theme(self.config.app_theme.theme()) + } fn about(&self) -> Element { let cosmic_theme::Spacing { space_xxs, .. } = cosmic::theme::active().cosmic().spacing; @@ -91,38 +105,57 @@ impl App { let short_hash: String = hash.chars().take(7).collect(); let date = env!("VERGEN_GIT_COMMIT_DATE"); widget::column::with_children(vec![ - widget::svg(widget::svg::Handle::from_memory( - &include_bytes!( - "../res/icons/hicolor/128x128/apps/com.example.CosmicAppTemplate.svg" - )[..], - )) + widget::svg(widget::svg::Handle::from_memory( + &include_bytes!( + "../res/icons/hicolor/128x128/apps/com.example.CosmicAppTemplate.svg" + )[..], + )) + .into(), + widget::text::title3(fl!("cosmic-backups")).into(), + widget::button::link(repository) + .on_press(Message::LaunchUrl(repository.to_string())) + .padding(0) .into(), - widget::text::title3(fl!("cosmic-backups")).into(), - widget::button::link(repository) - .on_press(Message::LaunchUrl(repository.to_string())) - .padding(0) - .into(), - widget::button::link(fl!( - "git-description", - hash = short_hash.as_str(), - date = date - )) - .on_press(Message::LaunchUrl(format!("{}/commits/{}", repository, hash))) - .padding(0) - .into(), - ]) + widget::button::link(fl!( + "git-description", + hash = short_hash.as_str(), + date = date + )) + .on_press(Message::LaunchUrl(format!( + "{}/commits/{}", + repository, hash + ))) + .padding(0) + .into(), + ]) .align_items(Alignment::Center) .spacing(space_xxs) .into() } - -} + fn settings(&self) -> Element { + let app_theme_selected = match self.config.app_theme { + AppTheme::Dark => 1, + AppTheme::Light => 2, + AppTheme::System => 0, + }; + widget::settings::view_column(vec![widget::settings::view_section(fl!("appearance")) + .add( + widget::settings::item::builder(fl!("theme")).control(widget::dropdown( + &self.app_themes, + Some(app_theme_selected), + Message::AppTheme, + )), + ) + .into()]) + .into() + } +} impl Application for App { type Executor = cosmic::executor::Default; - type Flags = (); + type Flags = Flags; type Message = Message; @@ -141,10 +174,13 @@ impl Application for App { vec![menu::menu_bar(&self.key_binds)] } - fn init(core: Core, _input: Self::Flags) -> (Self, Command) { + fn init(core: Core, flags: Self::Flags) -> (Self, Command) { let app = App { core, - context_page: ContextPage::About, + context_page: ContextPage::Settings, + config_handler: flags.config_handler, + config: flags.config, + app_themes: vec![fl!("match-desktop"), fl!("dark"), fl!("light")], key_binds: key_binds(), }; @@ -158,6 +194,7 @@ impl Application for App { Some(match self.context_page { ContextPage::About => self.about(), + ContextPage::Settings => self.settings(), }) } @@ -169,11 +206,37 @@ impl Application for App { .align_y(Vertical::Center) .into() } - + /// Handle application events here. fn update(&mut self, message: Self::Message) -> Command { - - match message { + // Helper for updating config values efficiently + macro_rules! config_set { + ($name: ident, $value: expr) => { + match &self.config_handler { + Some(config_handler) => { + match paste::paste! { self.config.[](config_handler, $value) } { + Ok(_) => {} + Err(err) => { + log::warn!( + "failed to save config {:?}: {}", + stringify!($name), + err + ); + } + } + } + None => { + self.config.$name = $value; + log::warn!( + "failed to save config {:?}: no config handler", + stringify!($name) + ); + } + } + }; + } + + match message { Message::ToggleContextPage(context_page) => { //TODO: ensure context menus are closed if self.context_page == context_page { @@ -204,11 +267,22 @@ impl Application for App { log::warn!("failed to open {:?}: {}", url, err); } }, + Message::AppTheme(index) => { + let app_theme = match index { + 1 => AppTheme::Dark, + 2 => AppTheme::Light, + _ => AppTheme::System, + }; + config_set!(app_theme, app_theme); + return self.update_config(); + } + Message::SystemThemeModeChange(_) => { + return self.update_config(); + } } - Command::none() - - } + Command::none() + } } pub fn key_binds() -> HashMap { diff --git a/src/app/config.rs b/src/app/config.rs new file mode 100644 index 0000000..7286a2f --- /dev/null +++ b/src/app/config.rs @@ -0,0 +1,52 @@ +use crate::app::App; +use cosmic::{ + cosmic_config::{self, cosmic_config_derive::CosmicConfigEntry, Config, CosmicConfigEntry}, + theme, Application, +}; +use serde::{Deserialize, Serialize}; + +pub const CONFIG_VERSION: u64 = 1; + +#[derive(Clone, Default, Debug, Eq, PartialEq, Deserialize, Serialize, CosmicConfigEntry)] +pub struct CosmicBackupsConfig { + pub app_theme: AppTheme, +} + +impl CosmicBackupsConfig { + pub fn config_handler() -> Option { + Config::new(App::APP_ID, CONFIG_VERSION).ok() + } + + pub fn config() -> CosmicBackupsConfig { + match Self::config_handler() { + Some(config_handler) => { + let config = CosmicBackupsConfig::get_entry(&config_handler).unwrap_or_else( + |(errs, config)| { + log::info!("errors loading config: {:?}", errs); + config + }, + ); + config + } + None => CosmicBackupsConfig::default(), + } + } +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub enum AppTheme { + Dark, + Light, + #[default] + System, +} + +impl AppTheme { + pub fn theme(&self) -> theme::Theme { + match self { + Self::Dark => theme::Theme::dark(), + Self::Light => theme::Theme::light(), + Self::System => theme::system_preference(), + } + } +} diff --git a/src/app/menu.rs b/src/app/menu.rs index 8d1532f..78b264b 100644 --- a/src/app/menu.rs +++ b/src/app/menu.rs @@ -38,6 +38,8 @@ pub fn menu_bar<'a>(key_binds: &HashMap) -> Element<'a, Message items( key_binds, vec![ + Item::Button(fl!("menu-settings"), Action::Settings), + Item::Divider, Item::Button(fl!("menu-about"), Action::About), ], ), diff --git a/src/main.rs b/src/main.rs index b0f01f5..5416903 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,9 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::app::App; +use app::{config::CosmicBackupsConfig, Flags}; +use cosmic::app::Settings; + /// The `app` module is used by convention to indicate the main component of our application. mod app; mod core; @@ -11,6 +14,15 @@ mod core; /// - `()` is the flags that your app needs to use before it starts. /// If your app does not need any flags, you can pass in `()`. fn main() -> cosmic::iced::Result { - let settings = cosmic::app::Settings::default(); - cosmic::app::run::(settings, ()) + let (settings, flags) = settings(); + cosmic::app::run::(settings, flags) +} + +fn settings() -> (Settings, Flags) { + let settings = Settings::default(); + let flags = Flags { + config_handler: CosmicBackupsConfig::config_handler(), + config: CosmicBackupsConfig::config(), + }; + (settings, flags) }