feat: add support for multi-themes (#210)

* chore: update deps

* wip

* add themes

* add matrix theme

* add flexoki and spaceduck themes

* .

* simple theme change function

* .

* respect shadow and radius settings

* add rose pine themes

* toggle theme
This commit is contained in:
reya
2025-12-26 08:20:18 +07:00
committed by GitHub
parent 5b7780ec9b
commit 34e026751b
49 changed files with 4349 additions and 2743 deletions

View File

@@ -16,6 +16,7 @@ actions!(
DarkMode,
ViewProfile,
ViewRelays,
Themes,
Settings,
Logout,
Quit

View File

@@ -9,7 +9,7 @@ use encryption::Encryption;
use encryption_ui::EncryptionPanel;
use gpui::prelude::FluentBuilder;
use gpui::{
deferred, div, px, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
deferred, div, px, relative, rems, App, AppContext, Axis, ClipboardItem, Context, Entity,
InteractiveElement, IntoElement, ParentElement, Render, SharedString,
StatefulInteractiveElement, Styled, Subscription, Window,
};
@@ -20,7 +20,7 @@ use person::PersonRegistry;
use relay_auth::RelayAuth;
use settings::AppSettings;
use smallvec::{smallvec, SmallVec};
use theme::{ActiveTheme, Theme, ThemeMode};
use theme::{ActiveTheme, Theme, ThemeMode, ThemeRegistry};
use title_bar::TitleBar;
use ui::avatar::Avatar;
use ui::button::{Button, ButtonVariants};
@@ -32,7 +32,9 @@ use ui::popover::{Popover, PopoverContent};
use ui::popup_menu::PopupMenuExt;
use ui::{h_flex, v_flex, ContextModal, IconName, Root, Sizable, StyledExt};
use crate::actions::{reset, DarkMode, KeyringPopup, Logout, Settings, ViewProfile, ViewRelays};
use crate::actions::{
reset, DarkMode, KeyringPopup, Logout, Settings, Themes, ViewProfile, ViewRelays,
};
use crate::user::viewer;
use crate::views::compose::compose_button;
use crate::views::{onboarding, preferences, setup_relay, startup, welcome};
@@ -313,6 +315,58 @@ impl ChatSpace {
}
}
fn on_themes(&mut self, _ev: &Themes, window: &mut Window, cx: &mut Context<Self>) {
window.open_modal(cx, move |this, _window, cx| {
let registry = ThemeRegistry::global(cx);
let themes = registry.read(cx).themes();
this.title("Select theme")
.show_close(true)
.overlay_closable(true)
.child(v_flex().gap_2().pb_4().children({
let mut items = Vec::with_capacity(themes.len());
for (name, theme) in themes.iter() {
items.push(
h_flex()
.h_10()
.justify_between()
.child(
v_flex()
.child(
div()
.text_sm()
.text_color(cx.theme().text)
.line_height(relative(1.3))
.child(theme.name.clone()),
)
.child(
div()
.text_xs()
.text_color(cx.theme().text_muted)
.child(theme.author.clone()),
),
)
.child(
Button::new(format!("change-{name}"))
.label("Set")
.small()
.ghost()
.on_click({
let theme = theme.clone();
move |_ev, window, cx| {
Theme::apply_theme(theme.clone(), Some(window), cx);
}
}),
),
);
}
items
}))
})
}
fn on_sign_out(&mut self, _e: &Logout, _window: &mut Window, cx: &mut Context<Self>) {
reset(cx);
}
@@ -567,6 +621,7 @@ impl ChatSpace {
IconName::Sun,
Box::new(DarkMode),
)
.menu_with_icon("Themes", IconName::Moon, Box::new(Themes))
.menu_with_icon(
"Settings",
IconName::Settings,
@@ -646,6 +701,7 @@ impl Render for ChatSpace {
.on_action(cx.listener(Self::on_profile))
.on_action(cx.listener(Self::on_relays))
.on_action(cx.listener(Self::on_dark_mode))
.on_action(cx.listener(Self::on_themes))
.on_action(cx.listener(Self::on_sign_out))
.on_action(cx.listener(Self::on_open_pubkey))
.on_action(cx.listener(Self::on_copy_pubkey))

View File

@@ -83,6 +83,9 @@ fn main() {
// Initialize components
ui::init(cx);
// Initialize theme registry
theme::init(cx);
// Initialize backend for keys storage
key_store::init(cx);

View File

@@ -120,8 +120,8 @@ impl RenderOnce for RoomListItem {
.flex_1()
.flex()
.justify_between()
.child(Skeleton::new().w_32().h_2p5().rounded_sm())
.child(Skeleton::new().w_6().h_2p5().rounded_sm()),
.child(Skeleton::new().w_32().h_2p5().rounded(cx.theme().radius))
.child(Skeleton::new().w_6().h_2p5().rounded(cx.theme().radius)),
);
};

View File

@@ -151,18 +151,20 @@ impl Sidebar {
let mut results: Vec<Event> = Vec::with_capacity(FIND_LIMIT);
while let Some(event) = stream.next().await {
// Skip if author is match current user
if event.pubkey == public_key {
continue;
}
while let Some((_url, event)) = stream.next().await {
if let Ok(event) = event {
// Skip if author is match current user
if event.pubkey == public_key {
continue;
}
// Skip if the event has already been added
if results.iter().any(|this| this.pubkey == event.pubkey) {
continue;
}
// Skip if the event has already been added
if results.iter().any(|this| this.pubkey == event.pubkey) {
continue;
}
results.push(event);
results.push(event);
}
}
if results.is_empty() {

View File

@@ -191,7 +191,7 @@ impl Onboarding {
div()
.id(ix)
.flex_1()
.rounded_md()
.rounded(cx.theme().radius)
.py_0p5()
.px_2()
.bg(cx.theme().ghost_element_background_alt)
@@ -308,13 +308,13 @@ impl Render for Onboarding {
.p_2()
.flex_1()
.h_full()
.rounded_2xl()
.rounded(cx.theme().radius_lg)
.child(
v_flex()
.size_full()
.justify_center()
.bg(cx.theme().surface_background)
.rounded_2xl()
.rounded(cx.theme().radius_lg)
.child(
v_flex()
.gap_5()
@@ -324,8 +324,8 @@ impl Render for Onboarding {
this.child(
img(qr.clone())
.size(px(256.))
.rounded_xl()
.shadow_lg()
.rounded(cx.theme().radius_lg)
.when(cx.theme().shadow, |this| this.shadow_lg())
.border_1()
.border_color(cx.theme().element_active),
)

View File

@@ -77,8 +77,10 @@ impl Screening {
.stream_events_from(BOOTSTRAP_RELAYS, filter, Duration::from_secs(2))
.await
{
while let Some(event) = stream.next().await {
activity = Some(event.created_at);
while let Some((_url, event)) = stream.next().await {
if let Ok(event) = event {
activity = Some(event.created_at);
}
}
}

View File

@@ -249,7 +249,7 @@ impl Render for Startup {
.h_10()
.w_72()
.bg(cx.theme().elevated_surface_background)
.rounded_lg()
.rounded(cx.theme().radius_lg)
.text_sm()
.when(self.loading, |this| {
this.child(