From a14d3830c3285dba479603528f5155c59969ee0a Mon Sep 17 00:00:00 2001 From: Vadim Khitrin Date: Sat, 5 Jul 2025 21:05:37 +0300 Subject: [PATCH 1/3] feat: WIP Add Animation --- Cargo.lock | 14 ++++ Cargo.toml | 1 + src/app.rs | 84 +++++++++++++-------- src/app/actions.rs | 11 ++- src/rec_icon.rs | 129 +++++++++++++++++++++++++++++++++ src/style/animation/mod.rs | 2 + src/style/animation/refresh.rs | 128 ++++++++++++++++++++++++++++++++ src/style/mod.rs | 1 + 8 files changed, 336 insertions(+), 34 deletions(-) create mode 100644 src/rec_icon.rs create mode 100644 src/style/animation/mod.rs create mode 100644 src/style/animation/refresh.rs diff --git a/Cargo.lock b/Cargo.lock index 6bb1662..2ddfe30 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,6 +1170,16 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "cosmic-time" +version = "0.4.0" +source = "git+https://github.com/D-Brox/cosmic-time.git#bbb9fb829b25e0bc2bbb382c25543646e82cda62" +dependencies = [ + "float-cmp", + "libcosmic", + "once_cell", +] + [[package]] name = "cosmicding" version = "2025.5.1" @@ -1177,6 +1187,7 @@ dependencies = [ "anyhow", "chrono", "constcat", + "cosmic-time", "directories", "env_logger 0.11.8", "futures", @@ -1818,6 +1829,9 @@ name = "float-cmp" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] [[package]] name = "float_next_after" diff --git a/Cargo.toml b/Cargo.toml index 412477c..76bb0fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = "1.0.89" chrono = "0.4.38" constcat = "0.5.1" +cosmic-time = { git = "https://github.com/D-Brox/cosmic-time.git", version = "0.4.0", features = ["once_cell"] } directories = "5.0.1" env_logger = "0.11.5" futures = "0.3.31" diff --git a/src/app.rs b/src/app.rs index ee67a3f..3d9785e 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,13 +1,3 @@ -use crate::db::{self}; -use crate::fl; -use crate::http::{self}; -use crate::models::account::{Account, LinkdingAccountApiResponse}; -use crate::models::bookmarks::{ - Bookmark, BookmarkCheckDetailsResponse, BookmarkRemoveResponse, DetailedResponse, -}; -use crate::models::db_cursor::{AccountsPaginationCursor, BookmarksPaginationCursor, Pagination}; -use crate::pages::accounts::{add_account, edit_account, PageAccountsView}; -use crate::pages::bookmarks::{edit_bookmark, new_bookmark, view_notes, PageBookmarksView}; use crate::{ app::{ actions::ApplicationAction, @@ -17,29 +7,51 @@ use crate::{ menu as app_menu, nav::AppNavPage, }, - models::favicon_cache::Favicon, -}; -use cosmic::cosmic_config::{self, Update}; -use cosmic::cosmic_theme::{self, ThemeMode}; -use cosmic::iced::{ - event, - futures::executor::block_on, - keyboard::{Event as KeyEvent, Modifiers}, - Event, Length, Subscription, + db::{self}, + fl, + http::{self}, + models::{ + account::{Account, LinkdingAccountApiResponse}, + bookmarks::{ + Bookmark, BookmarkCheckDetailsResponse, BookmarkRemoveResponse, DetailedResponse, + }, + db_cursor::{AccountsPaginationCursor, BookmarksPaginationCursor, Pagination}, + favicon_cache::Favicon, + }, + pages::{ + accounts::{add_account, edit_account, PageAccountsView}, + bookmarks::{edit_bookmark, new_bookmark, view_notes, PageBookmarksView}, + }, + style::animation::refresh, }; -use cosmic::iced_core::image::Bytes; -use cosmic::iced_widget::tooltip; -use cosmic::widget::menu::key_bind::KeyBind; -use cosmic::widget::{self, about::About, icon, nav_bar}; use cosmic::{ app::{context_drawer, Core, Task}, - widget::menu::Action, + cosmic_config::{self, Update}, + cosmic_theme::{self, ThemeMode}, + iced::{ + event, + futures::executor::block_on, + keyboard::{Event as KeyEvent, Modifiers}, + Event, Length, Subscription, + }, + iced_core::image::Bytes, + iced_widget::tooltip, + widget::{ + self, + about::About, + icon, + menu::{key_bind::KeyBind, Action}, + nav_bar, Id, + }, + Application, ApplicationExt, Element, }; -use cosmic::{Application, ApplicationExt, Element}; +use cosmic_time::{anim, chain, once_cell::sync::Lazy, Timeline}; use key_bind::key_binds; -use std::any::TypeId; -use std::collections::{HashMap, VecDeque}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::{ + any::TypeId, + collections::{HashMap, VecDeque}, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; pub mod actions; pub mod config; @@ -56,6 +68,8 @@ pub const APPID: &str = constcat::concat!(QUALIFIER, ".", ORG, ".", APP); const REPOSITORY: &str = "https://github.com/vkhitrin/cosmicding"; +pub static REFRESH_ICON: Lazy = Lazy::new(refresh::Id::unique); + pub struct Flags { pub config_handler: Option, pub config: CosmicConfig, @@ -84,6 +98,7 @@ pub struct Cosmicding { pub config: CosmicConfig, pub state: ApplicationState, search_id: widget::Id, + timeline: Timeline, toasts: widget::toaster::Toasts, } @@ -113,6 +128,7 @@ impl Application for Cosmicding { } fn init(core: Core, flags: Self::Flags) -> (Self, Task) { + let timeline = Timeline::new(); let db_pool = Some(block_on(async { db::SqliteDatabase::create().await.unwrap() })); @@ -181,6 +197,7 @@ impl Application for Cosmicding { placeholder_selected_account_index: 0, state: ApplicationState::NoEnabledAccounts, search_id: widget::Id::unique(), + timeline, toasts: widget::toaster::Toasts::new(ApplicationAction::CloseToast), }; @@ -360,6 +377,9 @@ impl Application for Cosmicding { struct ThemeSubscription; let subscriptions = vec![ + self.timeline + .as_subscription() + .map(|(_id, instant)| ApplicationAction::Tick(instant)), event::listen_with(|event, status, _| match event { Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status { event::Status::Ignored => Some(ApplicationAction::Key(modifiers, key)), @@ -386,7 +406,6 @@ impl Application for Cosmicding { ApplicationAction::SystemThemeModeChange }), ]; - Subscription::batch(subscriptions) } @@ -453,7 +472,10 @@ impl Application for Cosmicding { let account_page_entity = &self.nav.entity_at(0); self.nav.activate(account_page_entity.unwrap()); } - + ApplicationAction::Tick(now) => { + println!("WTF"); + self.timeline.now(now); + } ApplicationAction::ToggleContextPage(context_page) => { if self.context_page == context_page { self.core.window.show_context = !self.core.window.show_context; @@ -461,7 +483,6 @@ impl Application for Cosmicding { self.context_page = context_page; self.core.window.show_context = true; } - //self.set_context_title(context_page.title()); } ApplicationAction::ContextClose => self.core.window.show_context = false, ApplicationAction::AccountsView(message) => { @@ -747,6 +768,7 @@ impl Application for Cosmicding { } } ApplicationAction::StartRefreshBookmarksForAccount(account) => { + self.timeline.set_chain(chain![REFRESH_ICON]).start(); if let ApplicationState::Refreshing = self.state { } else if account.enabled { self.state = ApplicationState::Refreshing; diff --git a/src/app/actions.rs b/src/app/actions.rs index 23a0d26..b753048 100644 --- a/src/app/actions.rs +++ b/src/app/actions.rs @@ -1,5 +1,7 @@ -use crate::models::account::{Account, LinkdingAccountApiResponse}; -use crate::models::bookmarks::{Bookmark, BookmarkRemoveResponse, DetailedResponse}; +use crate::models::{ + account::{Account, LinkdingAccountApiResponse}, + bookmarks::{Bookmark, BookmarkRemoveResponse, DetailedResponse}, +}; use crate::{ app::{ config::{AppTheme, CosmicConfig, SortOption}, @@ -8,12 +10,14 @@ use crate::{ }, models::bookmarks::BookmarkCheckDetailsResponse, }; -use cosmic::widget::{self}; use cosmic::{ iced::keyboard::{Key, Modifiers}, iced_core::image::Bytes, + widget::{self}, }; +use std::time::Instant; + #[derive(Debug, Clone)] pub enum ApplicationAction { AccountsView(AccountsAction), @@ -81,6 +85,7 @@ pub enum ApplicationAction { StartRemoveBookmark(i64, Bookmark), StartupCompleted, SystemThemeModeChange, + Tick(Instant), ToggleContextPage(ContextPage), UpdateConfig(CosmicConfig), ViewBookmarkNotes(Bookmark), diff --git a/src/rec_icon.rs b/src/rec_icon.rs new file mode 100644 index 0000000..90fde0d --- /dev/null +++ b/src/rec_icon.rs @@ -0,0 +1,129 @@ +use std::rc::Rc; +use std::time::Duration; + +use cosmic::cosmic_theme::palette::Mix; +use cosmic::iced_widget::svg::Style as SvgStyle; +use cosmic::theme::{Svg, Theme}; +use cosmic::widget::Icon; +use cosmic::widget::Id as CosmicId; +use cosmic::widget::{icon, icon::Handle}; +use cosmic_time::once_cell::sync::Lazy; +use cosmic_time::timeline::{self, Interped}; +use cosmic_time::*; +use cosmic_time::{timeline::Frame, Timeline}; + +pub static REC_ICON_HANDLE: Lazy = + Lazy::new(|| icon::from_name("media-record-symbolic").into()); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(CosmicId); + +impl Id { + pub fn new(id: impl Into>) -> Self { + Self(CosmicId::new(id)) + } + + pub fn unique() -> Self { + Self(CosmicId::unique()) + } + + pub fn into_chain(self) -> Chain { + Chain::new(self) + } + + pub fn as_widget(self, timeline: &Timeline, size: u16) -> Icon { + RecIcon::as_widget(self, timeline, size) + } +} + +impl From for CosmicId { + fn from(value: Id) -> Self { + value.0 + } +} + +#[derive(Debug)] +pub struct Chain { + id: Id, + links: Vec, +} + +impl Chain { + pub fn new(id: Id) -> Self { + Chain { + id, + links: vec![ + RecIcon::new(Duration::ZERO).alpha(0.0), + RecIcon::new(Duration::from_millis(1000)).alpha(1.0), + RecIcon::new(Duration::from_millis(250)).alpha(1.0), + RecIcon::new(Duration::from_millis(1000)).alpha(0.0), + RecIcon::new(Duration::from_millis(250)).alpha(0.0), + ], + } + } +} + +impl From for timeline::Chain { + fn from(chain: Chain) -> Self { + timeline::Chain::new( + chain.id.0, + Repeat::Forever, + chain + .links + .into_iter() + .map(std::convert::Into::into) + .collect::>(), + ) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RecIcon { + at: MovementType, + ease: Ease, + alpha: f32, +} + +impl RecIcon { + pub fn new(at: impl Into) -> RecIcon { + let at = at.into(); + RecIcon { + at, + ease: Quadratic::InOut.into(), + alpha: 1.0, + } + } + + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } + + pub fn as_widget(id: Id, timeline: &Timeline, size: u16) -> Icon { + let value = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { + value + } else { + 1.0 + }; + icon(REC_ICON_HANDLE.clone()) + .class(Svg::Custom(Rc::new(move |theme: &Theme| { + let cosmic = theme.cosmic(); + SvgStyle { + color: Some( + cosmic + .background + .base + .mix(cosmic.destructive_text_color(), value) + .into(), + ), + } + }))) + .size(size) + } +} + +impl From for Vec> { + fn from(icon: RecIcon) -> Vec> { + vec![Some(Frame::lazy(icon.at, icon.alpha, icon.ease))] + } +} diff --git a/src/style/animation/mod.rs b/src/style/animation/mod.rs new file mode 100644 index 0000000..5d684c6 --- /dev/null +++ b/src/style/animation/mod.rs @@ -0,0 +1,2 @@ + +pub mod refresh; diff --git a/src/style/animation/refresh.rs b/src/style/animation/refresh.rs new file mode 100644 index 0000000..e3b4e15 --- /dev/null +++ b/src/style/animation/refresh.rs @@ -0,0 +1,128 @@ +use std::{rc::Rc, time::Duration}; + +use cosmic::cosmic_theme::palette::Mix; +use cosmic::iced_widget::svg::Style as SvgStyle; +use cosmic::theme::{Svg, Theme}; +use cosmic::widget::{icon, icon::Handle}; +use cosmic::widget::{Icon, Id as CosmicId}; +use cosmic_time::{ + once_cell::sync::Lazy, + timeline::{self, Frame, Interped}, + Timeline, *, +}; + +static REC_ICON_HANDLE: Lazy = + Lazy::new(|| icon::from_name("media-record-symbolic").into()); + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(CosmicId); + +impl Id { + pub fn new(id: impl Into>) -> Self { + Self(CosmicId::new(id)) + } + + pub fn unique() -> Self { + Self(CosmicId::unique()) + } + + pub fn into_chain(self) -> Chain { + Chain::new(self) + } + + pub fn as_widget(self, timeline: &Timeline, size: u16) -> Icon { + RecIcon::as_widget(self, timeline, size) + } +} + +impl From for CosmicId { + fn from(value: Id) -> Self { + value.0 + } +} + +#[derive(Debug)] +pub struct Chain { + id: Id, + links: Vec, +} + +impl Chain { + pub fn new(id: Id) -> Self { + Chain { + id, + links: vec![ + RecIcon::new(Duration::ZERO).alpha(0.0), + RecIcon::new(Duration::from_millis(1000)).alpha(1.0), + RecIcon::new(Duration::from_millis(250)).alpha(1.0), + RecIcon::new(Duration::from_millis(1000)).alpha(0.0), + RecIcon::new(Duration::from_millis(250)).alpha(0.0), + ], + } + } +} + +impl From for timeline::Chain { + fn from(chain: Chain) -> Self { + timeline::Chain::new( + chain.id.0, + Repeat::Forever, + chain + .links + .into_iter() + .map(std::convert::Into::into) + .collect::>(), + ) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RecIcon { + at: MovementType, + ease: Ease, + alpha: f32, +} + +impl RecIcon { + pub fn new(at: impl Into) -> RecIcon { + let at = at.into(); + RecIcon { + at, + ease: Quadratic::InOut.into(), + alpha: 1.0, + } + } + + pub fn alpha(mut self, alpha: f32) -> Self { + self.alpha = alpha; + self + } + + pub fn as_widget(id: Id, timeline: &Timeline, size: u16) -> Icon { + let value = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { + value + } else { + 1.0 + }; + icon(REC_ICON_HANDLE.clone()) + .class(Svg::Custom(Rc::new(move |theme: &Theme| { + let cosmic = theme.cosmic(); + SvgStyle { + color: Some( + cosmic + .background + .base + .mix(cosmic.destructive_text_color(), value) + .into(), + ), + } + }))) + .size(size) + } +} + +impl From for Vec> { + fn from(icon: RecIcon) -> Vec> { + vec![Some(Frame::lazy(icon.at, icon.alpha, icon.ease))] + } +} diff --git a/src/style/mod.rs b/src/style/mod.rs index 2de1eb4..856d4c0 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -1,2 +1,3 @@ pub mod button; pub mod text_editor; +pub mod animation; From 07c16ea258ece56939c7f44be5346947a1a89425 Mon Sep 17 00:00:00 2001 From: Vadim Khitrin Date: Sat, 5 Jul 2025 21:05:37 +0300 Subject: [PATCH 2/3] feat: WIP Add Animation --- Cargo.lock | 114 ++++++++++++++--------------- Cargo.toml | 2 +- README.md | 1 + i18n/en/cosmicding.ftl | 1 + i18n/sv/cosmicding.ftl | 1 + justfile | 4 + src/app.rs | 32 ++++++-- src/app/nav.rs | 4 +- src/models/mod.rs | 1 + src/models/sync_status.rs | 11 +++ src/pages/accounts.rs | 63 ++++++++++++++-- src/pages/bookmarks.rs | 80 ++++++++++++++++---- src/rec_icon.rs | 129 --------------------------------- src/style/animation/refresh.rs | 87 ++++++++++------------ 14 files changed, 265 insertions(+), 265 deletions(-) create mode 100644 src/models/sync_status.rs delete mode 100644 src/rec_icon.rs diff --git a/Cargo.lock b/Cargo.lock index 2ddfe30..e3a33fe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -317,7 +317,7 @@ dependencies = [ "wayland-backend", "wayland-client", "wayland-protocols", - "zbus 5.7.1", + "zbus 5.8.0", ] [[package]] @@ -344,9 +344,9 @@ dependencies = [ [[package]] name = "async-channel" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16c74e56284d2188cabb6ad99603d1ace887a5d7e7b695d01b728155ed9ed427" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" dependencies = [ "concurrent-queue", "event-listener-strategy", @@ -701,9 +701,9 @@ dependencies = [ [[package]] name = "blocking" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" dependencies = [ "async-channel", "async-task", @@ -790,9 +790,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.28" +version = "1.2.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad45f4f74e4e20eaa392913b7b33a7091c87e59628f4dd27888205ad888843c" +checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" dependencies = [ "jobserver", "libc", @@ -1054,7 +1054,7 @@ dependencies = [ [[package]] name = "cosmic-config" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "atomicwrites", "cosmic-config-derive", @@ -1070,13 +1070,13 @@ dependencies = [ "tokio", "tracing", "xdg", - "zbus 5.7.1", + "zbus 5.8.0", ] [[package]] name = "cosmic-config-derive" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "quote", "syn 2.0.104", @@ -1128,13 +1128,13 @@ name = "cosmic-settings-daemon" version = "0.1.0" source = "git+https://github.com/pop-os/dbus-settings-bindings#3b86984332be2c930a3536ab714b843c851fa8ca" dependencies = [ - "zbus 5.7.1", + "zbus 5.8.0", ] [[package]] name = "cosmic-text" version = "0.14.2" -source = "git+https://github.com/pop-os/cosmic-text.git#d15011fba547ae6bd2cbc7591991ff74424c6db7" +source = "git+https://github.com/pop-os/cosmic-text.git#85b07b4c8c6b825aca984648fed7ee49b6591d97" dependencies = [ "bitflags 2.9.1", "fontdb 0.23.0", @@ -1156,7 +1156,7 @@ dependencies = [ [[package]] name = "cosmic-theme" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "almost", "cosmic-config", @@ -1173,7 +1173,7 @@ dependencies = [ [[package]] name = "cosmic-time" version = "0.4.0" -source = "git+https://github.com/D-Brox/cosmic-time.git#bbb9fb829b25e0bc2bbb382c25543646e82cda62" +source = "git+https://github.com/pop-os/cosmic-time.git#bbb9fb829b25e0bc2bbb382c25543646e82cda62" dependencies = [ "float-cmp", "libcosmic", @@ -1389,9 +1389,9 @@ dependencies = [ [[package]] name = "derive_setters" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c848e86c87e5cc305313041c5677d4d95d60baa71cf95e5f6ea2554bb629ff" +checksum = "ae5c625eda104c228c06ecaf988d1c60e542176bd7a490e60eeda3493244c0c9" dependencies = [ "darling", "proc-macro2", @@ -2573,9 +2573,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" +checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" dependencies = [ "base64", "bytes", @@ -2599,9 +2599,9 @@ dependencies = [ [[package]] name = "i18n-config" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e88074831c0be5b89181b05e6748c4915f77769ecc9a4c372f88b169a8509c9" +checksum = "3e06b90c8a0d252e203c94344b21e35a30f3a3a85dc7db5af8f8df9f3e0c63ef" dependencies = [ "basic-toml", "log", @@ -2691,7 +2691,7 @@ dependencies = [ [[package]] name = "iced" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "dnd", "iced_accessibility", @@ -2709,7 +2709,7 @@ dependencies = [ [[package]] name = "iced_accessibility" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "accesskit", "accesskit_winit", @@ -2718,7 +2718,7 @@ dependencies = [ [[package]] name = "iced_core" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "bitflags 2.9.1", "bytes", @@ -2743,7 +2743,7 @@ dependencies = [ [[package]] name = "iced_futures" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "futures", "iced_core", @@ -2769,7 +2769,7 @@ dependencies = [ [[package]] name = "iced_graphics" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "bitflags 2.9.1", "bytemuck", @@ -2791,7 +2791,7 @@ dependencies = [ [[package]] name = "iced_renderer" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "iced_graphics", "iced_tiny_skia", @@ -2803,7 +2803,7 @@ dependencies = [ [[package]] name = "iced_runtime" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "bytes", "cosmic-client-toolkit", @@ -2819,7 +2819,7 @@ dependencies = [ [[package]] name = "iced_tiny_skia" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "bytemuck", "cosmic-text", @@ -2835,7 +2835,7 @@ dependencies = [ [[package]] name = "iced_wgpu" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "as-raw-xcb-connection", "bitflags 2.9.1", @@ -2866,7 +2866,7 @@ dependencies = [ [[package]] name = "iced_widget" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -2886,7 +2886,7 @@ dependencies = [ [[package]] name = "iced_winit" version = "0.14.0-dev" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "cosmic-client-toolkit", "dnd", @@ -3398,7 +3398,7 @@ checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libcosmic" version = "0.1.0" -source = "git+https://github.com/pop-os/libcosmic#aaa4b83577a70c15af8b91d1fb161e2a2931596b" +source = "git+https://github.com/pop-os/libcosmic#50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed" dependencies = [ "apply", "ashpd", @@ -3443,7 +3443,7 @@ dependencies = [ "tracing", "unicode-segmentation", "url", - "zbus 5.7.1", + "zbus 5.8.0", ] [[package]] @@ -5069,9 +5069,9 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.50" +version = "0.8.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" +checksum = "a457e416a0f90d246a4c3288bd7a25b2304ca727f253f95be383dd17af56be8f" dependencies = [ "bytemuck", ] @@ -5223,9 +5223,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" dependencies = [ "once_cell", "rustls-pki-types", @@ -5245,9 +5245,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.3" +version = "0.103.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" dependencies = [ "ring", "rustls-pki-types", @@ -5315,9 +5315,9 @@ dependencies = [ [[package]] name = "schemars" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1375ba8ef45a6f15d83fa8748f1079428295d403d6ea991d09ab100155fbc06d" +checksum = "82d20c4491bc164fa2f6c5d44565947a52ad80b9505d8e36f8d54c27c739fcd0" dependencies = [ "dyn-clone", "ref-cast", @@ -5456,7 +5456,7 @@ dependencies = [ "indexmap 1.9.3", "indexmap 2.10.0", "schemars 0.9.0", - "schemars 1.0.3", + "schemars 1.0.4", "serde", "serde_derive", "serde_json", @@ -7691,9 +7691,9 @@ dependencies = [ [[package]] name = "xml-rs" -version = "0.8.26" +version = "0.8.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a62ce76d9b56901b19a74f19431b0d8b3bc7ca4ad685a746dfd78ca8f4fc6bda" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" [[package]] name = "xmlwriter" @@ -7775,9 +7775,9 @@ dependencies = [ [[package]] name = "zbus" -version = "5.7.1" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3a7c7cee313d044fca3f48fa782cb750c79e4ca76ba7bc7718cd4024cdf6f68" +checksum = "597f45e98bc7e6f0988276012797855613cd8269e23b5be62cc4e5d28b7e515d" dependencies = [ "async-broadcast 0.7.2", "async-executor", @@ -7802,9 +7802,9 @@ dependencies = [ "uds_windows", "windows-sys 0.59.0", "winnow 0.7.11", - "zbus_macros 5.7.1", + "zbus_macros 5.8.0", "zbus_names 4.2.0", - "zvariant 5.5.3", + "zvariant 5.6.0", ] [[package]] @@ -7823,16 +7823,16 @@ dependencies = [ [[package]] name = "zbus_macros" -version = "5.7.1" +version = "5.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a17e7e5eec1550f747e71a058df81a9a83813ba0f6a95f39c4e218bdc7ba366a" +checksum = "e5c8e4e14dcdd9d97a98b189cd1220f30e8394ad271e8c987da84f73693862c2" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", "quote", "syn 2.0.104", "zbus_names 4.2.0", - "zvariant 5.5.3", + "zvariant 5.6.0", "zvariant_utils 3.2.0", ] @@ -7856,7 +7856,7 @@ dependencies = [ "serde", "static_assertions", "winnow 0.7.11", - "zvariant 5.5.3", + "zvariant 5.6.0", ] [[package]] @@ -7976,16 +7976,16 @@ dependencies = [ [[package]] name = "zvariant" -version = "5.5.3" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d30786f75e393ee63a21de4f9074d4c038d52c5b1bb4471f955db249f9dffb1" +checksum = "d91b3680bb339216abd84714172b5138a4edac677e641ef17e1d8cb1b3ca6e6f" dependencies = [ "endi", "enumflags2", "serde", "url", "winnow 0.7.11", - "zvariant_derive 5.5.3", + "zvariant_derive 5.6.0", "zvariant_utils 3.2.0", ] @@ -8004,9 +8004,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "5.5.3" +version = "5.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75fda702cd42d735ccd48117b1630432219c0e9616bf6cb0f8350844ee4d9580" +checksum = "3a8c68501be459a8dbfffbe5d792acdd23b4959940fc87785fb013b32edbc208" dependencies = [ "proc-macro-crate 3.3.0", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 76bb0fa..6b8f7e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" anyhow = "1.0.89" chrono = "0.4.38" constcat = "0.5.1" -cosmic-time = { git = "https://github.com/D-Brox/cosmic-time.git", version = "0.4.0", features = ["once_cell"] } +cosmic-time = { git = "https://github.com/pop-os/cosmic-time.git", version = "0.4.0", features = ["once_cell"] } directories = "5.0.1" env_logger = "0.11.5" futures = "0.3.31" diff --git a/README.md b/README.md index 779766a..2c0fe32 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Dependencies (Linux): Dependencies (macOS): +- `brew` - `cargo` - `just` - `libxkbcommon` diff --git a/i18n/en/cosmicding.ftl b/i18n/en/cosmicding.ftl index 0dc0798..2e22701 100644 --- a/i18n/en/cosmicding.ftl +++ b/i18n/en/cosmicding.ftl @@ -36,6 +36,7 @@ enabled-public-sharing = Public bookmarks sharing enabled enabled-sharing = Bookmarks sharing enabled failed = failed failed-refreshing-accounts = Failed refreshing some accounts ({$accounts}) +failed-refreshing-all-accounts = Failed refreshing all accounts failed-refreshing-bookmarks-for-account = Failed refreshing account {$account} failed-to-add-account = Failed to add account {$acc}: {$err} failed-to-edit-account = Failed to edit account {$acc}: {$err} diff --git a/i18n/sv/cosmicding.ftl b/i18n/sv/cosmicding.ftl index 34a50ed..06bbd0a 100644 --- a/i18n/sv/cosmicding.ftl +++ b/i18n/sv/cosmicding.ftl @@ -36,6 +36,7 @@ enabled-public-sharing = Delning av offentliga bokmärken har aktiverats enabled-sharing = Bokmärkesdelning har aktiverats failed = misslyckades failed-refreshing-accounts = Misslyckades att uppdatera vissa konton ({$accounts}) +failed-refreshing-all-accounts = Misslyckades med att uppdatera alla konton failed-refreshing-bookmarks-for-account = Misslyckades att uppdatera konto {$account} failed-to-add-account = Misslyckades med att lägga till konto {$acc}: {$err} failed-to-edit-account = Misslyckades med att redigera konto {$acc}: {$err} diff --git a/justfile b/justfile index 537eaaa..c067ffe 100644 --- a/justfile +++ b/justfile @@ -48,6 +48,10 @@ build-release *args: if [ "$(uname)" = "Linux" ]; then just build-release-linux elif [ "$(uname)" = "Darwin" ]; then + export HOMEBREW_PREFIX="$(brew --prefix)" + export PKG_CONFIG_PATH="${HOMEBREW_PREFIX}/lib/pkgconfig" + export LIBRARY_PATH="${HOMEBREW_PREFIX}/lib" + export C_INCLUDE_PATH="${HOMEBREW_PREFIX}/include" if [ "$(uname -m)" = "arm64" ]; then just build-release-macos-aarch64 elif [ "$(uname -m)" = "x86_64" ]; then diff --git a/src/app.rs b/src/app.rs index 3d9785e..47e0780 100644 --- a/src/app.rs +++ b/src/app.rs @@ -17,6 +17,7 @@ use crate::{ }, db_cursor::{AccountsPaginationCursor, BookmarksPaginationCursor, Pagination}, favicon_cache::Favicon, + sync_status::SyncStatus, }, pages::{ accounts::{add_account, edit_account, PageAccountsView}, @@ -41,11 +42,11 @@ use cosmic::{ about::About, icon, menu::{key_bind::KeyBind, Action}, - nav_bar, Id, + nav_bar, }, Application, ApplicationExt, Element, }; -use cosmic_time::{anim, chain, once_cell::sync::Lazy, Timeline}; +use cosmic_time::{chain, once_cell::sync::Lazy, Timeline}; use key_bind::key_binds; use std::{ any::TypeId, @@ -99,6 +100,7 @@ pub struct Cosmicding { pub state: ApplicationState, search_id: widget::Id, timeline: Timeline, + sync_status: SyncStatus, toasts: widget::toaster::Toasts, } @@ -198,6 +200,7 @@ impl Application for Cosmicding { state: ApplicationState::NoEnabledAccounts, search_id: widget::Id::unique(), timeline, + sync_status: SyncStatus::default(), toasts: widget::toaster::Toasts::new(ApplicationAction::CloseToast), }; @@ -217,6 +220,7 @@ impl Application for Cosmicding { tokio::runtime::Runtime::new().unwrap().block_on(async { app.bookmarks_cursor.refresh_count().await; }); + app.timeline.set_chain(chain![REFRESH_ICON]).start(); (app, Task::batch(commands)) } @@ -377,9 +381,6 @@ impl Application for Cosmicding { struct ThemeSubscription; let subscriptions = vec![ - self.timeline - .as_subscription() - .map(|(_id, instant)| ApplicationAction::Tick(instant)), event::listen_with(|event, status, _| match event { Event::Keyboard(KeyEvent::KeyPressed { key, modifiers, .. }) => match status { event::Status::Ignored => Some(ApplicationAction::Key(modifiers, key)), @@ -405,6 +406,11 @@ impl Application for Cosmicding { } ApplicationAction::SystemThemeModeChange }), + // NOTE: (vkhitrin) native implementation is too resource heavy. + // self.timeline + // .as_subscription() + // .map(|(_id, instant)| ApplicationAction::Tick(instant)), + cosmic::iced::time::every(Duration::from_millis(250)).map(ApplicationAction::Tick), ]; Subscription::batch(subscriptions) } @@ -473,7 +479,6 @@ impl Application for Cosmicding { self.nav.activate(account_page_entity.unwrap()); } ApplicationAction::Tick(now) => { - println!("WTF"); self.timeline.now(now); } ApplicationAction::ToggleContextPage(context_page) => { @@ -731,7 +736,7 @@ impl Application for Cosmicding { ApplicationAction::DoneRefreshBookmarksForAllAccounts(remote_responses) => { let mut failed_accounts: Vec = Vec::new(); if let Some(ref mut database) = &mut self.bookmarks_cursor.database { - for response in remote_responses { + for response in remote_responses.iter().cloned() { if !response.successful { failed_accounts.push(response.account.display_name.clone()); } @@ -750,12 +755,23 @@ impl Application for Cosmicding { commands.push(self.update(ApplicationAction::LoadBookmarks)); self.state = ApplicationState::Ready; if failed_accounts.is_empty() { + self.sync_status = SyncStatus::Successful; commands.push( self.toasts .push(widget::toaster::Toast::new(fl!("refreshed-bookmarks"))) .map(cosmic::Action::App), ); + } else if remote_responses.len() == failed_accounts.len() { + self.sync_status = SyncStatus::Failed; + commands.push( + self.toasts + .push(widget::toaster::Toast::new(fl!( + "failed-refreshing-all-accounts" + ))) + .map(cosmic::Action::App), + ); } else { + self.sync_status = SyncStatus::Warning; commands.push( self.toasts .push(widget::toaster::Toast::new(fl!( @@ -768,7 +784,6 @@ impl Application for Cosmicding { } } ApplicationAction::StartRefreshBookmarksForAccount(account) => { - self.timeline.set_chain(chain![REFRESH_ICON]).start(); if let ApplicationState::Refreshing = self.state { } else if account.enabled { self.state = ApplicationState::Refreshing; @@ -866,6 +881,7 @@ impl Application for Cosmicding { } } self.state = ApplicationState::Loading; + self.sync_status = SyncStatus::InProgress; } } ApplicationAction::AddBookmarkForm => { diff --git a/src/app/nav.rs b/src/app/nav.rs index b632736..25f3cfa 100644 --- a/src/app/nav.rs +++ b/src/app/nav.rs @@ -36,14 +36,16 @@ impl AppNavPage { match self { AppNavPage::AccountsView => app .accounts_view - .view(app.state, &app.accounts_cursor) + .view(app.state, app.sync_status, &app.accounts_cursor, &app.timeline) .map(ApplicationAction::AccountsView), AppNavPage::BookmarksView => app .bookmarks_view .view( app.state, + app.sync_status, &app.bookmarks_cursor, app.accounts_cursor.total_entries == 0, + &app.timeline, ) .map(ApplicationAction::BookmarksView), } diff --git a/src/models/mod.rs b/src/models/mod.rs index cdc84cc..9e1321c 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -2,3 +2,4 @@ pub mod account; pub mod bookmarks; pub mod db_cursor; pub mod favicon_cache; +pub mod sync_status; diff --git a/src/models/sync_status.rs b/src/models/sync_status.rs new file mode 100644 index 0000000..d0b1e38 --- /dev/null +++ b/src/models/sync_status.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, Default)] +pub enum SyncStatus { + #[default] + None, + InProgress, + Successful, + Warning, + Failed, +} diff --git a/src/pages/accounts.rs b/src/pages/accounts.rs index ae45318..18601d4 100644 --- a/src/pages/accounts.rs +++ b/src/pages/accounts.rs @@ -1,22 +1,23 @@ use crate::app::{ actions::{AccountsAction, ApplicationAction}, - ApplicationState, + ApplicationState, REFRESH_ICON, }; use crate::fl; -use crate::models::account::Account; -use crate::models::db_cursor::AccountsPaginationCursor; -use crate::style::button::ButtonStyle; +use crate::{ + models::{account::Account, db_cursor::AccountsPaginationCursor, sync_status::SyncStatus}, + style::button::ButtonStyle, +}; use chrono::{DateTime, Local}; -use cosmic::iced::Length; -use cosmic::iced_widget::tooltip; use cosmic::{ app::Task, cosmic_theme, - iced::Alignment, + iced::{Alignment, Length}, + iced_widget::tooltip, style, theme, widget::{self}, Apply, Element, }; +use cosmic_time::{anim, Timeline}; #[derive(Debug, Clone, Default)] pub struct PageAccountsView { @@ -29,7 +30,9 @@ impl PageAccountsView { pub fn view( &self, app_state: ApplicationState, + sync_status: SyncStatus, accounts_cursor: &AccountsPaginationCursor, + refresh_animation: &Timeline, ) -> Element<'_, AccountsAction> { let spacing = theme::active().cosmic().spacing; if self.accounts.is_empty() { @@ -251,6 +254,51 @@ impl PageAccountsView { .apply(widget::container) .into(), ])); + let animation_widget = match app_state { + ApplicationState::Refreshing => anim![REFRESH_ICON, &refresh_animation, 16], + _ => match sync_status { + SyncStatus::InProgress => anim![REFRESH_ICON, &refresh_animation, 16], + SyncStatus::Warning => cosmic::widget::icon( + widget::icon::from_name("dialog-warning-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.warning.base.into()), + } + }, + ))) + .size(16), + SyncStatus::Failed => widget::icon::from_name("dialog-error-symbolic") + .size(16) + .into(), + SyncStatus::Successful => cosmic::widget::icon( + widget::icon::from_name("checkbox-checked-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.success.base.into()), + } + }, + ))) + .size(16), + SyncStatus::None => cosmic::widget::icon( + widget::icon::from_name("media-record-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.background.base.into()), + } + }, + ))) + .size(16), + }, + }; widget::container( widget::column::with_children(vec![widget::row::with_capacity(2) @@ -266,6 +314,7 @@ impl PageAccountsView { spacing.space_xxs, spacing.space_none, ]) + .push(animation_widget) .push(widget::horizontal_space()) .push( widget::button::standard(fl!("add-account")) diff --git a/src/pages/bookmarks.rs b/src/pages/bookmarks.rs index 77d779a..d77ffbe 100644 --- a/src/pages/bookmarks.rs +++ b/src/pages/bookmarks.rs @@ -1,23 +1,28 @@ -use crate::app::{ - actions::{ApplicationAction, BookmarksAction}, - ApplicationState, +use core::sync; + +use crate::{ + app::{ + actions::{ApplicationAction, BookmarksAction}, + ApplicationState, REFRESH_ICON, + }, + fl, + models::{ + account::Account, bookmarks::Bookmark, db_cursor::BookmarksPaginationCursor, + sync_status::SyncStatus, + }, + style::{button::ButtonStyle, text_editor::text_editor_class}, }; -use crate::fl; -use crate::models::account::Account; -use crate::models::bookmarks::Bookmark; -use crate::models::db_cursor::BookmarksPaginationCursor; -use crate::style::{button::ButtonStyle, text_editor::text_editor_class}; use chrono::{DateTime, Local}; -use cosmic::iced::Length; -use cosmic::iced_core::text; use cosmic::{ app::Task, - iced::Alignment, - style, + cosmic_theme, + iced::{Alignment, Length}, + iced_core::text, + style, theme, widget::{self}, Apply, Element, }; -use cosmic::{cosmic_theme, theme}; +use cosmic_time::{anim, Timeline}; #[derive(Debug, Default, Clone)] pub struct PageBookmarksView { @@ -32,8 +37,10 @@ impl PageBookmarksView { pub fn view( &self, app_state: ApplicationState, + sync_status: SyncStatus, bookmarks_cursor: &BookmarksPaginationCursor, no_accounts: bool, + refresh_animation: &Timeline, ) -> Element<'_, BookmarksAction> { let spacing = theme::active().cosmic().spacing; if no_accounts { @@ -274,6 +281,52 @@ impl PageBookmarksView { }; let mut new_bookmark_button = widget::button::standard(fl!("add-bookmark")); + let animation_widget = match app_state { + ApplicationState::Refreshing => anim![REFRESH_ICON, &refresh_animation, 16], + _ => match sync_status { + SyncStatus::InProgress => anim![REFRESH_ICON, &refresh_animation, 16], + SyncStatus::Warning => cosmic::widget::icon( + widget::icon::from_name("dialog-warning-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.warning.base.into()), + } + }, + ))) + .size(16), + SyncStatus::Failed => widget::icon::from_name("dialog-error-symbolic") + .size(16) + .into(), + SyncStatus::Successful => cosmic::widget::icon( + widget::icon::from_name("checkbox-checked-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.success.base.into()), + } + }, + ))) + .size(16), + SyncStatus::None => cosmic::widget::icon( + widget::icon::from_name("media-record-symbolic").handle(), + ) + .class(cosmic::theme::Svg::Custom(std::rc::Rc::new( + move |theme: &cosmic::theme::Theme| { + let cosmic = theme.cosmic(); + cosmic::iced_widget::svg::Style { + color: Some(cosmic.background.base.into()), + } + }, + ))) + .size(16), + }, + }; + let mut search_input_widget = widget::text_input::search_input(fl!("search"), self.query_placeholder.clone()) .id(self.search_id.clone().unwrap()); @@ -348,6 +401,7 @@ impl PageBookmarksView { spacing.space_xxs, spacing.space_none, ]) + .push(animation_widget) .push(search_input_widget) .push(refresh_button) .push(new_bookmark_button) diff --git a/src/rec_icon.rs b/src/rec_icon.rs deleted file mode 100644 index 90fde0d..0000000 --- a/src/rec_icon.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::rc::Rc; -use std::time::Duration; - -use cosmic::cosmic_theme::palette::Mix; -use cosmic::iced_widget::svg::Style as SvgStyle; -use cosmic::theme::{Svg, Theme}; -use cosmic::widget::Icon; -use cosmic::widget::Id as CosmicId; -use cosmic::widget::{icon, icon::Handle}; -use cosmic_time::once_cell::sync::Lazy; -use cosmic_time::timeline::{self, Interped}; -use cosmic_time::*; -use cosmic_time::{timeline::Frame, Timeline}; - -pub static REC_ICON_HANDLE: Lazy = - Lazy::new(|| icon::from_name("media-record-symbolic").into()); - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub struct Id(CosmicId); - -impl Id { - pub fn new(id: impl Into>) -> Self { - Self(CosmicId::new(id)) - } - - pub fn unique() -> Self { - Self(CosmicId::unique()) - } - - pub fn into_chain(self) -> Chain { - Chain::new(self) - } - - pub fn as_widget(self, timeline: &Timeline, size: u16) -> Icon { - RecIcon::as_widget(self, timeline, size) - } -} - -impl From for CosmicId { - fn from(value: Id) -> Self { - value.0 - } -} - -#[derive(Debug)] -pub struct Chain { - id: Id, - links: Vec, -} - -impl Chain { - pub fn new(id: Id) -> Self { - Chain { - id, - links: vec![ - RecIcon::new(Duration::ZERO).alpha(0.0), - RecIcon::new(Duration::from_millis(1000)).alpha(1.0), - RecIcon::new(Duration::from_millis(250)).alpha(1.0), - RecIcon::new(Duration::from_millis(1000)).alpha(0.0), - RecIcon::new(Duration::from_millis(250)).alpha(0.0), - ], - } - } -} - -impl From for timeline::Chain { - fn from(chain: Chain) -> Self { - timeline::Chain::new( - chain.id.0, - Repeat::Forever, - chain - .links - .into_iter() - .map(std::convert::Into::into) - .collect::>(), - ) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct RecIcon { - at: MovementType, - ease: Ease, - alpha: f32, -} - -impl RecIcon { - pub fn new(at: impl Into) -> RecIcon { - let at = at.into(); - RecIcon { - at, - ease: Quadratic::InOut.into(), - alpha: 1.0, - } - } - - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = alpha; - self - } - - pub fn as_widget(id: Id, timeline: &Timeline, size: u16) -> Icon { - let value = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { - value - } else { - 1.0 - }; - icon(REC_ICON_HANDLE.clone()) - .class(Svg::Custom(Rc::new(move |theme: &Theme| { - let cosmic = theme.cosmic(); - SvgStyle { - color: Some( - cosmic - .background - .base - .mix(cosmic.destructive_text_color(), value) - .into(), - ), - } - }))) - .size(size) - } -} - -impl From for Vec> { - fn from(icon: RecIcon) -> Vec> { - vec![Some(Frame::lazy(icon.at, icon.alpha, icon.ease))] - } -} diff --git a/src/style/animation/refresh.rs b/src/style/animation/refresh.rs index e3b4e15..0c8f7ca 100644 --- a/src/style/animation/refresh.rs +++ b/src/style/animation/refresh.rs @@ -1,18 +1,19 @@ -use std::{rc::Rc, time::Duration}; +use std::rc::Rc; +use std::time::Duration; -use cosmic::cosmic_theme::palette::Mix; +use cosmic::iced::{ContentFit, Rotation}; use cosmic::iced_widget::svg::Style as SvgStyle; use cosmic::theme::{Svg, Theme}; +use cosmic::widget::Icon; +use cosmic::widget::Id as CosmicId; use cosmic::widget::{icon, icon::Handle}; -use cosmic::widget::{Icon, Id as CosmicId}; -use cosmic_time::{ - once_cell::sync::Lazy, - timeline::{self, Frame, Interped}, - Timeline, *, -}; +use cosmic_time::once_cell::sync::Lazy; +use cosmic_time::timeline::{self, Interped}; +use cosmic_time::*; +use cosmic_time::{timeline::Frame, Timeline}; -static REC_ICON_HANDLE: Lazy = - Lazy::new(|| icon::from_name("media-record-symbolic").into()); +static REFRESH_ICON_HANDLE: Lazy = + Lazy::new(|| icon::from_name("view-refresh-symbolic").into()); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Id(CosmicId); @@ -31,7 +32,7 @@ impl Id { } pub fn as_widget(self, timeline: &Timeline, size: u16) -> Icon { - RecIcon::as_widget(self, timeline, size) + RefreshIcon::as_widget(self, timeline, size) } } @@ -44,21 +45,20 @@ impl From for CosmicId { #[derive(Debug)] pub struct Chain { id: Id, - links: Vec, + links: Vec, } impl Chain { pub fn new(id: Id) -> Self { - Chain { - id, - links: vec![ - RecIcon::new(Duration::ZERO).alpha(0.0), - RecIcon::new(Duration::from_millis(1000)).alpha(1.0), - RecIcon::new(Duration::from_millis(250)).alpha(1.0), - RecIcon::new(Duration::from_millis(1000)).alpha(0.0), - RecIcon::new(Duration::from_millis(250)).alpha(0.0), - ], - } + let links = (1..=72) // doubled the steps from 36 to 72 for smoother animation + .map(|i| { + let angle = i as f32 * 5.0; // reduced step size from 10 to 5 degrees + RefreshIcon::new(Duration::from_millis(50)) // shorter duration per frame + .angle(angle) + }) + .collect(); + + Chain { id, links } } } @@ -77,52 +77,41 @@ impl From for timeline::Chain { } #[derive(Debug, Clone, Copy)] -pub struct RecIcon { +pub struct RefreshIcon { at: MovementType, ease: Ease, - alpha: f32, + angle: f32, } -impl RecIcon { - pub fn new(at: impl Into) -> RecIcon { +impl RefreshIcon { + pub fn new(at: impl Into) -> RefreshIcon { let at = at.into(); - RecIcon { + RefreshIcon { at, - ease: Quadratic::InOut.into(), - alpha: 1.0, + ease: Cubic::InOut.into(), + angle: 0.0, } } - pub fn alpha(mut self, alpha: f32) -> Self { - self.alpha = alpha; + pub fn angle(mut self, angle: f32) -> Self { + self.angle = angle; self } pub fn as_widget(id: Id, timeline: &Timeline, size: u16) -> Icon { - let value = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { + let angle = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { value } else { - 1.0 + 0.0 }; - icon(REC_ICON_HANDLE.clone()) - .class(Svg::Custom(Rc::new(move |theme: &Theme| { - let cosmic = theme.cosmic(); - SvgStyle { - color: Some( - cosmic - .background - .base - .mix(cosmic.destructive_text_color(), value) - .into(), - ), - } - }))) + icon(REFRESH_ICON_HANDLE.clone()) + .rotation(Rotation::Floating((angle.to_radians()).into())) .size(size) } } -impl From for Vec> { - fn from(icon: RecIcon) -> Vec> { - vec![Some(Frame::lazy(icon.at, icon.alpha, icon.ease))] +impl From for Vec> { + fn from(icon: RefreshIcon) -> Vec> { + vec![Some(Frame::lazy(icon.at, icon.angle, icon.ease))] } } From 4d9fd65cfcc0058c2a75a95c2d456a24c6e89c5e Mon Sep 17 00:00:00 2001 From: Vadim Khitrin Date: Sat, 5 Jul 2025 21:05:37 +0300 Subject: [PATCH 3/3] feat: Add Sync Status Indicator Adding an icon which shows the sync status. Resolves #41. --- Cargo.lock | 16 ++++++++-------- src/app.rs | 5 +++-- src/app/dialog.rs | 3 +-- src/app/key_bind.rs | 7 ++++--- src/app/menu.rs | 8 +++++--- src/app/nav.rs | 7 ++++++- src/core/settings.rs | 2 +- src/db/mod.rs | 13 +++++-------- src/http/mod.rs | 23 ++++++++++++++--------- src/models/db_cursor.rs | 3 +-- src/pages/bookmarks.rs | 2 -- src/style/animation/mod.rs | 1 - src/style/animation/refresh.rs | 16 ++++++---------- src/style/mod.rs | 2 +- 14 files changed, 55 insertions(+), 53 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3a33fe..c0355bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1112,7 +1112,7 @@ dependencies = [ [[package]] name = "cosmic-settings-config" version = "0.1.0" -source = "git+https://github.com/pop-os/cosmic-settings-daemon#54b4418e1e7757d965166ae9dc00c522aebf4451" +source = "git+https://github.com/pop-os/cosmic-settings-daemon#a36683fd5b7dea4011da278c91d0645117dc39f8" dependencies = [ "cosmic-config", "ron", @@ -6325,7 +6325,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", "toml_datetime", - "winnow 0.7.11", + "winnow 0.7.12", ] [[package]] @@ -7580,9 +7580,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74c7b26e3480b707944fc872477815d29a8e429d2f93a1ce000f5fa84a15cbcd" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" dependencies = [ "memchr", ] @@ -7801,7 +7801,7 @@ dependencies = [ "tracing", "uds_windows", "windows-sys 0.59.0", - "winnow 0.7.11", + "winnow 0.7.12", "zbus_macros 5.8.0", "zbus_names 4.2.0", "zvariant 5.6.0", @@ -7855,7 +7855,7 @@ checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" dependencies = [ "serde", "static_assertions", - "winnow 0.7.11", + "winnow 0.7.12", "zvariant 5.6.0", ] @@ -7984,7 +7984,7 @@ dependencies = [ "enumflags2", "serde", "url", - "winnow 0.7.11", + "winnow 0.7.12", "zvariant_derive 5.6.0", "zvariant_utils 3.2.0", ] @@ -8037,5 +8037,5 @@ dependencies = [ "serde", "static_assertions", "syn 2.0.104", - "winnow 0.7.11", + "winnow 0.7.12", ] diff --git a/src/app.rs b/src/app.rs index 47e0780..6f988df 100644 --- a/src/app.rs +++ b/src/app.rs @@ -46,7 +46,7 @@ use cosmic::{ }, Application, ApplicationExt, Element, }; -use cosmic_time::{chain, once_cell::sync::Lazy, Timeline}; +use cosmic_time::{chain, Timeline}; use key_bind::key_binds; use std::{ any::TypeId, @@ -69,7 +69,8 @@ pub const APPID: &str = constcat::concat!(QUALIFIER, ".", ORG, ".", APP); const REPOSITORY: &str = "https://github.com/vkhitrin/cosmicding"; -pub static REFRESH_ICON: Lazy = Lazy::new(refresh::Id::unique); +pub static REFRESH_ICON: std::sync::LazyLock = + std::sync::LazyLock::new(refresh::Id::unique); pub struct Flags { pub config_handler: Option, diff --git a/src/app/dialog.rs b/src/app/dialog.rs index 8985872..a56bbd5 100644 --- a/src/app/dialog.rs +++ b/src/app/dialog.rs @@ -1,5 +1,4 @@ -use crate::models::account::Account; -use crate::models::bookmarks::Bookmark; +use crate::models::{account::Account, bookmarks::Bookmark}; #[derive(Clone, Debug, Eq, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum DialogPage { diff --git a/src/app/key_bind.rs b/src/app/key_bind.rs index 10474f1..5df8f1f 100644 --- a/src/app/key_bind.rs +++ b/src/app/key_bind.rs @@ -1,8 +1,9 @@ use std::collections::HashMap; -use cosmic::iced::keyboard::Key; -use cosmic::widget::menu::key_bind::KeyBind; -use cosmic::widget::menu::key_bind::Modifier; +use cosmic::{ + iced::keyboard::Key, + widget::menu::key_bind::{KeyBind, Modifier}, +}; use crate::app::menu::MenuAction; diff --git a/src/app/menu.rs b/src/app/menu.rs index 0a86e8e..ba5abe1 100644 --- a/src/app/menu.rs +++ b/src/app/menu.rs @@ -1,10 +1,12 @@ use std::collections::HashMap; use crate::app::config::SortOption; -use cosmic::widget::RcElementWrapper; use cosmic::{ - widget::menu::{ - self, action::MenuAction as _MenuAction, key_bind::KeyBind, Item, ItemHeight, ItemWidth, + widget::{ + menu::{ + self, action::MenuAction as _MenuAction, key_bind::KeyBind, Item, ItemHeight, ItemWidth, + }, + RcElementWrapper, }, Element, }; diff --git a/src/app/nav.rs b/src/app/nav.rs index 25f3cfa..b207039 100644 --- a/src/app/nav.rs +++ b/src/app/nav.rs @@ -36,7 +36,12 @@ impl AppNavPage { match self { AppNavPage::AccountsView => app .accounts_view - .view(app.state, app.sync_status, &app.accounts_cursor, &app.timeline) + .view( + app.state, + app.sync_status, + &app.accounts_cursor, + &app.timeline, + ) .map(ApplicationAction::AccountsView), AppNavPage::BookmarksView => app .bookmarks_view diff --git a/src/core/settings.rs b/src/core/settings.rs index 2b26a8c..24e842b 100644 --- a/src/core/settings.rs +++ b/src/core/settings.rs @@ -6,7 +6,7 @@ pub fn settings() -> cosmic::app::Settings { .min_width(360.0) .min_height(180.0), ) - // NOTE: An example of client decorations enabled. + // NOTE: (vkhitrin) An example of client decorations enabled. // Useless as long as we render the native // libcosmic menu // #[cfg(target_os = "macos")] diff --git a/src/db/mod.rs b/src/db/mod.rs index c8f337c..529f521 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,15 +1,12 @@ -use crate::app::config::SortOption; -use crate::models::favicon_cache::Favicon; +use crate::{ + app::{config::SortOption, APP, APPID, ORG, QUALIFIER}, + models::{account::Account, bookmarks::Bookmark, favicon_cache::Favicon}, +}; use anyhow::{anyhow, Result}; use std::path::Path; -use sqlx::sqlite::Sqlite; -use sqlx::{migrate::MigrateDatabase, prelude::*, SqlitePool}; - -use crate::app::{APP, APPID, ORG, QUALIFIER}; -use crate::models::account::Account; -use crate::models::bookmarks::Bookmark; +use sqlx::{migrate::MigrateDatabase, prelude::*, sqlite::Sqlite, SqlitePool}; const DB_PATH: &str = constcat::concat!(APPID, "-db", ".sqlite"); diff --git a/src/http/mod.rs b/src/http/mod.rs index d1fac8a..0f2ccb3 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -1,10 +1,14 @@ -use crate::fl; -use crate::models::account::{Account, LinkdingAccountApiResponse}; -use crate::models::bookmarks::{ - Bookmark, BookmarkCheckDetailsResponse, BookmarkRemoveResponse, DetailedResponse, - LinkdingBookmarksApiCheckResponse, LinkdingBookmarksApiResponse, +use crate::{ + fl, + models::{ + account::{Account, LinkdingAccountApiResponse}, + bookmarks::{ + Bookmark, BookmarkCheckDetailsResponse, BookmarkRemoveResponse, DetailedResponse, + LinkdingBookmarksApiCheckResponse, LinkdingBookmarksApiResponse, + }, + }, + utils::json::parse_serde_json_value_to_raw_string, }; -use crate::utils::json::parse_serde_json_value_to_raw_string; use anyhow::Result; use chrono::{DateTime, Utc}; use cosmic::iced_core::image::Bytes; @@ -13,9 +17,10 @@ use reqwest::{ ClientBuilder, StatusCode, }; use serde_json::Value; -use std::fmt::Write; -use std::time::Duration; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + fmt::Write, + time::{Duration, SystemTime, UNIX_EPOCH}, +}; use urlencoding::encode; pub async fn fetch_bookmarks_from_all_accounts(accounts: Vec) -> Vec { diff --git a/src/models/db_cursor.rs b/src/models/db_cursor.rs index 6de921a..d909cf6 100644 --- a/src/models/db_cursor.rs +++ b/src/models/db_cursor.rs @@ -1,7 +1,6 @@ use crate::app::config::SortOption; use crate::db::SqliteDatabase; -use crate::models::account::Account; -use crate::models::bookmarks::Bookmark; +use crate::models::{account::Account, bookmarks::Bookmark}; pub trait Pagination { async fn refresh_count(&mut self); diff --git a/src/pages/bookmarks.rs b/src/pages/bookmarks.rs index d77ffbe..75e2522 100644 --- a/src/pages/bookmarks.rs +++ b/src/pages/bookmarks.rs @@ -1,5 +1,3 @@ -use core::sync; - use crate::{ app::{ actions::{ApplicationAction, BookmarksAction}, diff --git a/src/style/animation/mod.rs b/src/style/animation/mod.rs index 5d684c6..1d2e0f5 100644 --- a/src/style/animation/mod.rs +++ b/src/style/animation/mod.rs @@ -1,2 +1 @@ - pub mod refresh; diff --git a/src/style/animation/refresh.rs b/src/style/animation/refresh.rs index 0c8f7ca..e1cf62a 100644 --- a/src/style/animation/refresh.rs +++ b/src/style/animation/refresh.rs @@ -1,19 +1,15 @@ -use std::rc::Rc; use std::time::Duration; -use cosmic::iced::{ContentFit, Rotation}; -use cosmic::iced_widget::svg::Style as SvgStyle; -use cosmic::theme::{Svg, Theme}; +use cosmic::iced::Rotation; use cosmic::widget::Icon; use cosmic::widget::Id as CosmicId; use cosmic::widget::{icon, icon::Handle}; -use cosmic_time::once_cell::sync::Lazy; use cosmic_time::timeline::{self, Interped}; -use cosmic_time::*; use cosmic_time::{timeline::Frame, Timeline}; +use cosmic_time::{Cubic, Ease, MovementType, Repeat}; -static REFRESH_ICON_HANDLE: Lazy = - Lazy::new(|| icon::from_name("view-refresh-symbolic").into()); +static REFRESH_ICON_HANDLE: std::sync::LazyLock = + std::sync::LazyLock::new(|| icon::from_name("view-refresh-symbolic").into()); #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Id(CosmicId); @@ -32,7 +28,7 @@ impl Id { } pub fn as_widget(self, timeline: &Timeline, size: u16) -> Icon { - RefreshIcon::as_widget(self, timeline, size) + RefreshIcon::as_widget(&self, timeline, size) } } @@ -98,7 +94,7 @@ impl RefreshIcon { self } - pub fn as_widget(id: Id, timeline: &Timeline, size: u16) -> Icon { + pub fn as_widget(id: &Id, timeline: &Timeline, size: u16) -> Icon { let angle = if let Some(Interped { value, .. }) = timeline.get(&id.0, 0) { value } else { diff --git a/src/style/mod.rs b/src/style/mod.rs index 856d4c0..eb114a1 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -1,3 +1,3 @@ +pub mod animation; pub mod button; pub mod text_editor; -pub mod animation;