diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0a57c94 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +on: + merge_group: + pull_request: + push: + branches: + - master + +env: + CARGO_INCREMENTAL: false + CARGO_TERM_COLOR: always + RUST_BACKTRACE: full + RUSTDOCFLAGS: -Dwarnings + +name: CI +jobs: + lint: + name: rust code lint + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v3 + - uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2023-10-15 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - name: format style check + run: cargo fmt --all -- --check + - name: cargo clippy check + run: cargo clippy --all-targets --all-features -- -D warnings + - name: cargo check + run: cargo check diff --git a/cli/src/handler.rs b/cli/src/handler.rs index 59cf67b..5c4533a 100644 --- a/cli/src/handler.rs +++ b/cli/src/handler.rs @@ -94,10 +94,10 @@ pub fn msg_handler(args: ArgMatches, app_data: &mut AppData) -> ReplResult ReplResult<()> { .about("Send message")]), msg_handler, ); + repl.run() } diff --git a/config/bot.json b/config/bot.json index 7777354..7e0e0c7 100644 --- a/config/bot.json +++ b/config/bot.json @@ -1,12 +1,6 @@ { "vars": {}, - "providers": [ - { - "name": "OpenAI", - "base_url": "https://api.ribir.org/stream/open_ai", - "token": "v1.eyJ1c2VyX2lkIjoxMDAxMTgsImV4cCI6MTcwNTQ2NDI4MSwidmVyIjoidjEifQ.-64yece5gSrdUwjY7sTnwoQ858kX-YyzRM7EuKCTI10" - } - ], + "providers": [], "bots": [ { "id": "PoleStar Assistant", diff --git a/core/src/model/app_data.rs b/core/src/model/app_data.rs index 6d53d7b..dda5645 100644 --- a/core/src/model/app_data.rs +++ b/core/src/model/app_data.rs @@ -311,7 +311,7 @@ impl AppData { pub fn new_channel(&mut self, name: String, desc: Option, cfg: ChannelCfg) -> Uuid { let channel_id = Uuid::new_v4(); let db = self.db.as_mut().map(|db| NonNull::from(&**db)); - let info = &mut self.info; + let info = &self.info; let app_info = Some(NonNull::from(&**info)); let channel = Channel::new(channel_id, name, desc, cfg, app_info, db); @@ -373,7 +373,6 @@ impl AppData { self.db = db; let cur_channel_id = local_state.cur_channel_id(); - let cur_channel = channels .iter() .find(|channel| Some(*channel.id()) == cur_channel_id.map(|id| id)); @@ -386,7 +385,6 @@ impl AppData { }; self.info.as_mut().set_cur_channel_id(cur_channel_id); - let ptr = NonNull::from(&*self.info); channels.iter_mut().for_each(|channel| { channel.set_app_info(ptr); diff --git a/core/src/model/bot.rs b/core/src/model/bot.rs index 55434d0..e56bf2c 100644 --- a/core/src/model/bot.rs +++ b/core/src/model/bot.rs @@ -50,7 +50,7 @@ pub struct PartialBot { impl PartialBot { pub fn id(&self) -> &BotId { &self.id } - + pub fn to_bot(self) -> Option { self.is_complete().then(|| Bot { id: self.id, diff --git a/core/src/model/channel.rs b/core/src/model/channel.rs index 55171b7..72646e3 100644 --- a/core/src/model/channel.rs +++ b/core/src/model/channel.rs @@ -5,7 +5,7 @@ use uuid::Uuid; use crate::db::{executor::ActionPersist, pool::PersistenceDB}; -use super::{msg::Msg, AppInfo, Bot, MsgAction, BotId}; +use super::{msg::Msg, AppInfo, Bot, BotId, MsgAction}; pub type ChannelId = Uuid; diff --git a/core/src/model/msg.rs b/core/src/model/msg.rs index 37a0169..9c020b6 100644 --- a/core/src/model/msg.rs +++ b/core/src/model/msg.rs @@ -94,13 +94,10 @@ impl Msg { cur_idx: 0, cont_list, meta, - created_at: created_at.map_or_else( - || Utc::now(), - |t| { - let dt = NaiveDateTime::from_timestamp_millis(t).unwrap(); - DateTime::from_naive_utc_and_offset(dt, Utc) - }, - ), + created_at: created_at.map_or_else(Utc::now, |t| { + let dt = NaiveDateTime::from_timestamp_millis(t).unwrap(); + DateTime::from_naive_utc_and_offset(dt, Utc) + }), } } @@ -156,10 +153,9 @@ impl Msg { } #[inline] - pub fn add_cont(&mut self, cont: MsgCont) { - let last_idx = self.cont_list.len(); + pub fn add_cont(&mut self, cont: MsgCont) -> usize { self.cont_list.push(cont); - self.cur_idx = last_idx as _; + self.cont_list.len() - 1 } #[inline] diff --git a/core/src/model/user.rs b/core/src/model/user.rs index d03273d..f6c881b 100644 --- a/core/src/model/user.rs +++ b/core/src/model/user.rs @@ -1,8 +1,6 @@ use derive_builder::Builder; use serde::Deserialize; -use crate::service::req::request_quota; - use super::GLOBAL_VARS; #[derive(Builder, Debug, Default, Clone)] @@ -46,11 +44,6 @@ impl User { #[inline] pub fn uid(&self) -> u64 { self.uid } - - pub fn set_quota(&mut self, quota: Option) { self.quota = quota; } - - #[inline] - pub fn quota(&self) -> Option<&Quota> { self.quota.as_ref() } } #[derive(Deserialize, Debug, Clone)] @@ -85,8 +78,8 @@ impl Quota { #[derive(Deserialize, Debug)] pub struct QuotaInfo { - user_id: u64, - limits: f32, - used: f32, + // user_id: u64, + // limits: f32, + // used: f32, pub statistics: serde_json::Value, } diff --git a/core/src/service/req.rs b/core/src/service/req.rs index b272fde..4b5dfe2 100644 --- a/core/src/service/req.rs +++ b/core/src/service/req.rs @@ -13,8 +13,8 @@ use serde_json_path::JsonPath; use crate::{ error::{PolestarError, PolestarResult}, model::{ - AppInfo, Bot, FeedbackMessageListForServer, FeedbackTimestamp, GlbVar, Quota, ServerProvider, - UserFeedbackMessageForServer, GLOBAL_VARS, + AppInfo, Bot, BotId, FeedbackMessageListForServer, FeedbackTimestamp, GlbVar, Quota, + ServerProvider, UserFeedbackMessageForServer, GLOBAL_VARS, }, }; @@ -52,11 +52,12 @@ async fn req_stream( Ok(stream) } -pub fn create_text_request<'a>(bot: &'a Bot, info: &'a AppInfo) -> TextStreamReq { +pub fn create_text_request(info: &AppInfo, bot_id: BotId) -> TextStreamReq { + let bot = info.bot(&bot_id).unwrap(); let sp_name = bot.sp(); let sp = info.providers().get(sp_name); if let Some(sp) = sp { - create_req_from_bot(bot, Some(&sp)) + create_req_from_bot(bot, Some(sp)) } else { create_req_from_bot(bot, default_polestar_provider(sp_name, info).as_ref()) } @@ -187,7 +188,7 @@ fn create_req_from_bot(bot: &Bot, sp: Option<&ServerProvider>) -> TextStreamReq if let Some(base_url) = JsonPath::parse("$.sp.base_url") .ok() .and_then(|path| path.query(&env).exactly_one().ok()) - .map(|val| to_value_str(val)) + .map(to_value_str) { url = format!("{}{}", base_url, url); } @@ -206,7 +207,7 @@ fn replace_val(src: &str, path_rex: &Regex, env: &JsonValue) -> String { val.push_str(&src[pos..rg.start]); pos = rg.end; - if let Some(replaced) = JsonPath::parse(&cap[1].to_string()) + if let Some(replaced) = JsonPath::parse(&cap[1]) .ok() .and_then(|path| path.query(env).exactly_one().ok()) { diff --git a/core/src/utils/config.rs b/core/src/utils/config.rs index 24a8927..6b7377e 100644 --- a/core/src/utils/config.rs +++ b/core/src/utils/config.rs @@ -134,19 +134,15 @@ fn parse_user_bot_cfgs(user_data_path: PathBuf, file: &str) -> PolestarResult, + info: impl StateReader, bot_id: BotId, content: String, delta_op: impl FnMut(String), ) -> Result { - let req = { - let channel = channel.read(); - let bot = channel - .bots() - .and_then(|bots| bots.iter().find(|bot| bot.id() == &bot_id)) - .unwrap(); - let info = channel.app_info().unwrap(); - create_text_request(bot, info) - }; + let req = { create_text_request(&info.read(), bot_id) }; let mut stream = req .request(content) diff --git a/gui/src/style/color.rs b/gui/src/style/color.rs index 6ca75f6..ed6b733 100644 --- a/gui/src/style/color.rs +++ b/gui/src/style/color.rs @@ -6,10 +6,10 @@ pub static LIGHT_SILVER_15: u32 = 0xD9D9D915; pub static LIGHT_SILVER_FF: u32 = 0xD9D9D9FF; pub static WHITE: u32 = 0xFFFFFFFF; pub static BRIGHT_GRAY_E9EEF7_FF: u32 = 0xE9EEF7FF; -pub static BRIGHT_GRAY_EEEDED_FF: u32 = 0xEEEEEDFF; +// pub static BRIGHT_GRAY_EEEDED_FF: u32 = 0xEEEEEDFF; pub static ALICE_BLUE: u32 = 0xEDF7FBFF; pub static GAINSBORO: u32 = 0xDCDBDAFF; -pub static GAINSBORO_DFDFDF_FF: u32 = 0xDFDFDFFF; +// pub static GAINSBORO_DFDFDF_FF: u32 = 0xDFDFDFFF; pub static DARK_CHARCOAL: u32 = 0x333333FF; pub static CHINESE_WHITE: u32 = 0xE0E0DEFF; pub static ISABELLINE: u32 = 0xF1F1EFFF; diff --git a/gui/src/widgets.rs b/gui/src/widgets.rs index 436db15..352846a 100644 --- a/gui/src/widgets.rs +++ b/gui/src/widgets.rs @@ -5,7 +5,3 @@ mod home; mod login; mod modify_channel; mod permission; -mod quick_launcher; - -pub use quick_launcher::launcher::*; -pub use quick_launcher::*; diff --git a/gui/src/widgets/app.rs b/gui/src/widgets/app.rs index 6aac7e0..c45e3ba 100644 --- a/gui/src/widgets/app.rs +++ b/gui/src/widgets/app.rs @@ -1,7 +1,10 @@ -use polestar_core::model::{init_app_data, AppData, Channel, ChannelId}; +use polestar_core::model::{ + init_app_data, AppData, AppInfo, Bot, BotId, Channel, ChannelCfg, ChannelId, Msg, MsgAction, + MsgCont, MsgId, User, +}; use ribir::prelude::*; use ribir_algo::Sc; -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc}; use url::Url; use uuid::Uuid; @@ -10,14 +13,61 @@ use super::{ home::w_home, login::w_login, permission::w_permission, - quick_launcher::QuickLauncher, }; use crate::{theme::polestar_theme, widgets::modify_channel::w_modify_channel_modal, WINDOW_MGR}; +pub trait Chat: 'static { + fn add_msg(&mut self, channel_id: &ChannelId, msg: Msg); + fn add_msg_cont( + &mut self, + channel_id: &ChannelId, + msg_id: &MsgId, + cont: MsgCont, + ) -> Option; + + fn switch_cont(&mut self, channel_id: &ChannelId, msg_id: &MsgId, idx: usize); + + fn update_msg_cont(&mut self, channel_id: &ChannelId, msg_id: &MsgId, idx: usize, act: MsgAction); + + fn channel(&self, channel_id: &ChannelId) -> Option<&Channel>; + + fn msg(&self, channel_id: &ChannelId, msg_id: &MsgId) -> Option<&Msg>; + + fn info(&self) -> &AppInfo; +} + +pub trait ChannelMgr: 'static { + fn cur_channel_id(&self) -> Option<&ChannelId>; + fn channel_ids(&self) -> Vec; + fn channel(&self, channel_id: &ChannelId) -> Option<&Channel>; + fn switch_channel(&mut self, channel_id: &ChannelId); + fn new_channel(&mut self, name: String, desc: Option, cfg: ChannelCfg) -> Uuid; + fn update_channel_desc(&mut self, channel_id: &ChannelId, desc: Option); + fn update_channel_name(&mut self, channel_id: &ChannelId, name: String); + fn update_channel_cfg(&mut self, channel_id: &ChannelId, cfg: ChannelCfg); + fn remove_channel(&mut self, channel_id: &ChannelId); +} + +pub trait UIState: 'static { + fn cur_path(&self) -> String; + fn navigate_to(&mut self, path: &str); + fn modify_channel_id(&self) -> Option<&Uuid>; + fn set_modify_channel_id(&mut self, modify_channel_id: Option); + fn tooltip(&self) -> Option; + fn set_tooltip(&mut self, tooltip: Option<&str>); +} + +pub trait UserConfig: 'static { + fn login(&mut self, user: User) -> ChannelId; + fn user(&self) -> Option<&User>; + fn logout(&mut self); + fn need_login(&self) -> bool; + fn bots(&self) -> Rc>; + fn default_bot_id(&self) -> BotId; +} + pub struct AppGUI { - pub data: AppData, - // XXX: here `QuickLauncher` in `AppData`'s local_state field, here is repeat. - pub quick_launcher: Option, + data: AppData, cur_router_path: String, modify_channel_id: Option, tooltip: Option, @@ -25,11 +75,6 @@ pub struct AppGUI { impl AppGUI { fn new(data: AppData) -> Self { - let quick_launcher = data - .info() - .quick_launcher_id() - .and_then(|id| data.get_channel(id).map(|_| QuickLauncher::new(*id))); - let cur_router_path = if data.info().need_login() { "/login".to_owned() } else { @@ -38,7 +83,6 @@ impl AppGUI { Self { data, - quick_launcher, // It can get by open app url, like this: PoleStarChat://ribir.org/home/chat cur_router_path, modify_channel_id: None, @@ -46,49 +90,163 @@ impl AppGUI { } } - pub fn cur_router_path(&self) -> &str { &self.cur_router_path } + fn chat(&self) -> &dyn Chat { self } + fn chat_mut(&mut self) -> &mut dyn Chat { self } + + fn channel_mgr(&self) -> &dyn ChannelMgr { self } + fn channel_mgr_mut(&mut self) -> &mut dyn ChannelMgr { self } + + fn ui_state(&self) -> &dyn UIState { self } + fn ui_state_mut(&mut self) -> &mut dyn UIState { self } + + fn user_config(&self) -> &dyn UserConfig { self } + fn user_config_mut(&mut self) -> &mut dyn UserConfig { self } +} + +impl ChannelMgr for AppGUI { + fn channel_ids(&self) -> Vec { + self + .data + .channels() + .iter() + .map(|channel| channel.id()) + .cloned() + .collect() + } + + fn cur_channel_id(&self) -> Option<&ChannelId> { self.data.info().cur_channel_id() } + + fn channel(&self, channel_id: &ChannelId) -> Option<&Channel> { + self.data.get_channel(channel_id) + } - pub fn navigate_to(&mut self, path: &str) { self.cur_router_path = path.to_owned(); } + fn new_channel(&mut self, name: String, desc: Option, cfg: ChannelCfg) -> Uuid { + self.data.new_channel(name, desc, cfg) + } - pub fn tooltip(&self) -> Option { self.tooltip.clone() } + fn remove_channel(&mut self, channel_id: &ChannelId) { self.data.remove_channel(channel_id); } - pub fn set_tooltip(&mut self, tooltip: Option<&str>) { - self.tooltip = tooltip.map(|s| s.to_owned()); + fn switch_channel(&mut self, channel_id: &ChannelId) { self.data.switch_channel(channel_id); } + + fn update_channel_desc(&mut self, channel_id: &ChannelId, desc: Option) { + if let Some(channel) = self.data.get_channel_mut(channel_id) { + channel.set_desc(desc); + } } - pub fn modify_channel_id(&self) -> Option<&Uuid> { self.modify_channel_id.as_ref() } + fn update_channel_name(&mut self, channel_id: &ChannelId, name: String) { + if let Some(channel) = self.data.get_channel_mut(channel_id) { + channel.set_name(name); + } + } - pub fn set_modify_channel_id(&mut self, modify_channel_id: Option) { - self.modify_channel_id = modify_channel_id; + fn update_channel_cfg(&mut self, channel_id: &ChannelId, cfg: ChannelCfg) { + if let Some(channel) = self.data.get_channel_mut(channel_id) { + channel.set_cfg(cfg); + } } +} - pub fn has_quick_launcher_msg(&self) -> bool { - self - .quick_launcher - .as_ref() - .map(|quick_launcher| quick_launcher.msg.is_some()) - .unwrap_or_default() +impl Chat for AppGUI { + fn add_msg(&mut self, channel_id: &ChannelId, msg: Msg) { + if let Some(ch) = self.data.get_channel_mut(channel_id) { + ch.add_msg(msg); + } } - pub fn quick_launcher_id(&self) -> Option { + fn add_msg_cont( + &mut self, + channel_id: &ChannelId, + msg_id: &MsgId, + cont: MsgCont, + ) -> Option { self - .quick_launcher - .as_ref() - .map(|quick_launcher| quick_launcher.channel_id) + .data + .get_channel_mut(channel_id) + .and_then(|ch| ch.msg_mut(msg_id)) + .map(|msg| msg.add_cont(cont)) + } + + fn switch_cont(&mut self, channel_id: &ChannelId, msg_id: &MsgId, idx: usize) { + if let Some(msg) = self + .data + .get_channel_mut(channel_id) + .and_then(|ch| ch.msg_mut(msg_id)) + { + msg.switch_cont(idx); + } + } + + fn update_msg_cont( + &mut self, + channel_id: &ChannelId, + msg_id: &MsgId, + idx: usize, + act: MsgAction, + ) { + if let Some(ch) = self.data.get_channel_mut(channel_id) { + ch.update_msg(msg_id, idx, act); + } + } + + fn channel(&self, channel_id: &ChannelId) -> Option<&Channel> { + self.data.get_channel(channel_id) } - pub fn quick_launcher_channel(&self) -> Option<&Channel> { + fn info(&self) -> &AppInfo { self.data.info() } + + fn msg(&self, channel_id: &ChannelId, msg_id: &MsgId) -> Option<&Msg> { self - .quick_launcher - .as_ref() - .and_then(|quick_launcher| self.data.get_channel(&quick_launcher.channel_id)) + .data + .get_channel(channel_id) + .and_then(|ch| ch.msg(msg_id)) + } +} + +impl UIState for AppGUI { + fn cur_path(&self) -> String { self.cur_router_path.clone() } + + fn navigate_to(&mut self, path: &str) { self.cur_router_path = path.to_owned(); } + + fn modify_channel_id(&self) -> Option<&Uuid> { self.modify_channel_id.as_ref() } + + fn set_modify_channel_id(&mut self, modify_channel_id: Option) { + self.modify_channel_id = modify_channel_id; } + + fn set_tooltip(&mut self, tooltip: Option<&str>) { self.tooltip = tooltip.map(|s| s.to_owned()); } + + fn tooltip(&self) -> Option { self.tooltip.clone() } +} + +impl UserConfig for AppGUI { + fn login(&mut self, user: User) -> ChannelId { + self.data.login(user); + self.data.info().cur_channel_id().cloned().unwrap() + } + + fn logout(&mut self) { self.data.logout(); } + + fn need_login(&self) -> bool { self.data.info().need_login() } + + fn user(&self) -> Option<&User> { self.data.info().user() } + + fn bots(&self) -> Rc> { self.data.info().bots_rc() } + + fn default_bot_id(&self) -> BotId { self.data.info().cfg().def_bot_id().clone() } } impl Compose for AppGUI { fn compose(this: impl StateWriter) -> impl WidgetBuilder { fn_widget! { - App::events_stream().subscribe(gen_handler(this.clone_writer())); + let chat = this.split_writer(|app| app.chat(), |app| app.chat_mut()); + let channel_mgr = this.split_writer(|app| app.channel_mgr(), |app| app.channel_mgr_mut()); + let ui_state = this.split_writer(|app| app.ui_state(), |app| app.ui_state_mut()); + let config = this.split_writer(|app| app.user_config(), |app| app.user_config_mut()); + App::events_stream() + .subscribe( + gen_handler(config.clone_writer(), channel_mgr.clone_writer(), ui_state.clone_writer()) + ); @ThemeWidget { // Polestar custom theme. @@ -97,18 +255,25 @@ impl Compose for AppGUI { Box::new(fn_widget! { @Stack { @Router { - cur_path: pipe!($this.cur_router_path().to_owned()), + cur_path: pipe!($ui_state.cur_path().to_owned()), @Route { path: PartialPath::new("/login", 0), - @ { w_login(this.clone_writer()) } + @ { w_login(config.clone_writer()) } } @Route { path: PartialPath::new("/permission", 0), - @ { w_permission(this.clone_writer()) } + @ { w_permission() } } @Route { path: PartialPath::new("/home", 0), - @ { w_home(this.clone_writer()) } + @ { + w_home( + chat.clone_writer(), + channel_mgr.clone_writer(), + config.clone_writer(), + ui_state.clone_writer() + ) + } } } @ { @@ -117,12 +282,23 @@ impl Compose for AppGUI { } } @ { - let this2 = this.clone_writer(); - pipe! { - ($this.modify_channel_id()).map(|modify_channel_id| { - w_modify_channel_modal(this2.clone_writer(), modify_channel_id) + pipe!($ui_state;) + .map(move |_| { + let _ = || { + $channel_mgr.write(); + $ui_state.write(); + $config.write(); + }; + let modify_channel_id = $ui_state.modify_channel_id().cloned(); + modify_channel_id.map(|modify_channel_id| { + w_modify_channel_modal( + channel_mgr.clone_writer(), + ui_state.clone_writer(), + config.clone_writer(), + &modify_channel_id + ) + }) }) - } } } }) @@ -175,9 +351,13 @@ fn handle_open_url(url: &str) -> Option { None } -fn gen_handler(app: impl StateWriter) -> impl for<'a> FnMut(&'a mut AppEvent) { - move |event: &mut AppEvent| match event { - AppEvent::OpenUrl(url) => { +fn gen_handler( + config: impl StateWriter, + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl for<'a> FnMut(&'a mut AppEvent) { + move |event: &mut AppEvent| { + if let AppEvent::OpenUrl(url) = event { // TODO: user module need login let route = handle_open_url(url); if let Some(AppRoute::Login { token, uid }) = route { @@ -195,11 +375,10 @@ fn gen_handler(app: impl StateWriter) -> impl for<'a> FnMut(&'a .build() .expect("Failed to build user"); user.set_token(Some(token)); - app.write().data.info_mut().set_user(Some(user.clone())); - - app.write().data.login(user); - app.write().navigate_to("/home/chat"); + let channel_id = config.write().login(user); + channel_mgr.write().switch_channel(&channel_id); + ui_state.write().navigate_to("/home/chat"); // active main window if let Some(wnd_info) = WINDOW_MGR.lock().unwrap().main.as_ref() { @@ -207,15 +386,6 @@ fn gen_handler(app: impl StateWriter) -> impl for<'a> FnMut(&'a } } } - AppEvent::Hotkey(hotkey_event) => { - use crate::hotkey::handler::hotkey_handler; - hotkey_handler(hotkey_event, app.clone_writer()); - } - AppEvent::WndFocusChanged(wnd_id, is_focus) => { - use crate::hotkey::handler::focus_handler; - focus_handler(app.clone_writer(), wnd_id, is_focus); - } - _ => {} } } @@ -227,10 +397,6 @@ fn w_tooltip(content: Option) -> Option { }) } -impl> AppExtraWidgets for T {} - -pub trait AppExtraWidgets: StateWriter + Sized {} - // launch App need to do some init work. // 1. [x] load bot config file. // 2. [ ] if has user module, need check user login status. diff --git a/gui/src/widgets/common/carousel.rs b/gui/src/widgets/common/carousel.rs index 19fe532..4bb72a5 100644 --- a/gui/src/widgets/common/carousel.rs +++ b/gui/src/widgets/common/carousel.rs @@ -8,20 +8,6 @@ pub struct GraphicIntro { desc: Option>, } -impl GraphicIntro { - pub fn new( - image: ShareResource, - title: Option>>, - desc: Option>>, - ) -> Self { - Self { - image, - title: title.map(Into::into), - desc: desc.map(Into::into), - } - } -} - #[derive(Declare)] pub struct Carousel { contents: Vec, diff --git a/gui/src/widgets/common/double_column.rs b/gui/src/widgets/common/double_column.rs index d3e70c3..db1368e 100644 --- a/gui/src/widgets/common/double_column.rs +++ b/gui/src/widgets/common/double_column.rs @@ -5,7 +5,7 @@ use crate::style::CULTURED_F7F7F5_FF; #[derive(PartialEq, Eq)] pub enum FixedSide { Left, - Right, + // Right, } #[derive(Declare)] diff --git a/gui/src/widgets/common/interactive_lists.rs b/gui/src/widgets/common/interactive_lists.rs index f826b75..4f33005 100644 --- a/gui/src/widgets/common/interactive_lists.rs +++ b/gui/src/widgets/common/interactive_lists.rs @@ -14,10 +14,6 @@ pub struct InteractiveList { highlight_rect_list: Vec, } -impl InteractiveList { - fn derain(&mut self, count: usize) { self.highlight_rect_list.drain(count..); } -} - impl ComposeChild for InteractiveList { type Child = BoxPipe>; fn compose_child(this: impl StateWriter, child: Self::Child) -> impl WidgetBuilder { diff --git a/gui/src/widgets/common/router.rs b/gui/src/widgets/common/router.rs index 36348c7..e198562 100644 --- a/gui/src/widgets/common/router.rs +++ b/gui/src/widgets/common/router.rs @@ -26,6 +26,7 @@ impl PartialPath { false } + #[allow(dead_code)] pub fn get_param(&self, path: &str) -> Option { if path.chars().nth(0) == Some('/') && self.partial.chars().nth(1) == Some(':') { return path[1..].split('/').nth(self.level).map(|s| s.to_owned()); @@ -57,7 +58,10 @@ impl ComposeChild for Router { let (route, child) = p.unzip(); let path = $route.path.clone(); @Visibility { - visible: pipe!(path.is_match($this.cur_path.as_ref())), + visible: pipe! { + let this = $this; + path.is_match(this.cur_path.as_ref()) + }, @ { child } } }) diff --git a/gui/src/widgets/helper.rs b/gui/src/widgets/helper.rs index 944953c..79352bc 100644 --- a/gui/src/widgets/helper.rs +++ b/gui/src/widgets/helper.rs @@ -1,26 +1,34 @@ use std::{cell::RefCell, rc::Rc}; use crate::req::query_open_ai; -use polestar_core::model::{BotId, Channel, MsgAction, MsgBody}; +use polestar_core::model::{BotId, ChannelId, MsgAction, MsgBody}; use ribir::prelude::*; use uuid::Uuid; +use super::app::Chat; + pub fn send_msg( - channel: impl StateWriter, - content: String, - idx: usize, + chat: impl StateWriter, + channel_id: ChannelId, msg_id: Uuid, + idx: usize, bot_id: BotId, + content: String, ) { let _ = AppCtx::spawn_local(async move { let update_msg = |act| { - let mut channel = channel.write(); - channel.update_msg(&msg_id, idx, act); + let mut chat = chat.write(); + chat.update_msg_cont(&channel_id, &msg_id, idx, act); }; - let _ = query_open_ai(channel.clone_reader(), bot_id, content, |delta| { - update_msg(MsgAction::Receiving(MsgBody::Text(Some(delta)))); - }) + let _ = query_open_ai( + chat.map_reader(|chat| chat.info()), + bot_id, + content, + |delta| { + update_msg(MsgAction::Receiving(MsgBody::Text(Some(delta)))); + }, + ) .await; update_msg(MsgAction::Fulfilled); diff --git a/gui/src/widgets/home.rs b/gui/src/widgets/home.rs index ed91937..2a64714 100644 --- a/gui/src/widgets/home.rs +++ b/gui/src/widgets/home.rs @@ -3,7 +3,7 @@ use ribir::prelude::*; use crate::style::{APP_SIDEBAR_WIDTH, CULTURED_F4F4F4_FF, WHITE}; use crate::widgets::home::bot_store::w_bot_store; -use super::app::AppGUI; +use super::app::{ChannelMgr, Chat, UIState, UserConfig}; use super::common::{PartialPath, Route, Router}; mod bot_store; @@ -15,64 +15,83 @@ use chat::w_chat; use settings::w_settings; use sidebar::w_sidebar; -pub fn w_home(app: impl StateWriter) -> impl WidgetBuilder { +pub fn w_home( + chat: impl StateWriter, + channel_mgr: impl StateWriter, + config: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @ { - pipe!(!$app.data.channels().is_empty()).map(move |v| if v { - @Row { - @ConstrainedBox { - clamp: BoxClamp::EXPAND_Y.with_fixed_width(APP_SIDEBAR_WIDTH), - @ { w_sidebar(app.clone_writer()) } - } - @Expanded { - flex: 1., - margin: EdgeInsets::new(10., 10., 10., 2.), - background: Color::from_u32(WHITE), - border_radius: Radius::all(8.), - border: Border::all(BorderSide { - color: Color::from_u32(CULTURED_F4F4F4_FF).into(), - width: 1., - }), - @Router { - cur_path: pipe!($app.cur_router_path().to_owned()), - @Route { - path: PartialPath::new("/chat", 1), - @ { - pipe! { - let _ = || $app.write(); - let def_bot_id = ($app.data.info().def_bot().id()).clone(); - let channel_writer = app.split_writer( - |app| { app.data.cur_channel().expect("current channel must be existed") }, - |app| { app.data.cur_channel_mut().expect("current channel must be existed") }, - ); - w_chat(channel_writer.clone_writer(), $app.data.info().bots_rc(), def_bot_id) + pipe!(!$channel_mgr.channel_ids().is_empty()) + .value_chain(|s| s.distinct_until_changed().box_it()) + .map(move |v| if v { + let _ = || $channel_mgr.write(); + let _ = || $ui_state.write(); + @Row { + @ConstrainedBox { + clamp: BoxClamp::EXPAND_Y.with_fixed_width(APP_SIDEBAR_WIDTH), + @ { w_sidebar(channel_mgr.clone_writer(), ui_state.clone_writer()) } + } + @Expanded { + flex: 1., + margin: EdgeInsets::new(10., 10., 10., 2.), + background: Color::from_u32(WHITE), + border_radius: Radius::all(8.), + border: Border::all(BorderSide { + color: Color::from_u32(CULTURED_F4F4F4_FF).into(), + width: 1., + }), + @Router { + cur_path: pipe!($ui_state.cur_path()), + @Route { + path: PartialPath::new("/chat", 1), + @ { + pipe!($channel_mgr.cur_channel_id().cloned()) + .value_chain(|s| s.distinct_until_changed().box_it()) + .map(move |_| { + let channel_id = $channel_mgr.cur_channel_id().cloned().unwrap(); + let _ = || $chat.write(); + let def_bot_id = ($chat.info().def_bot().id()).clone(); + w_chat(chat.clone_writer(), channel_id, $chat.info().bots_rc(), def_bot_id) + }) } } - } - @Route { - path: PartialPath::new("/settings", 1), - @ { - pipe! { - let _ = || $app.write(); - w_settings(app.clone_writer()) + @Route { + path: PartialPath::new("/settings", 1), + @ { + pipe! { + let _ = || { + $config.write(); + $ui_state.write(); + }; + w_settings(config.clone_writer(), ui_state.clone_writer()) + } } } - } - @Route { - path: PartialPath::new("/bot_store", 1), - @ { - pipe! { - let _ = || $app.write(); - w_bot_store(app.clone_writer()) + @Route { + path: PartialPath::new("/bot_store", 1), + @ { + pipe! { + let _ = || { + $chat.write(); + $channel_mgr.write(); + $ui_state.write(); + }; + w_bot_store( + chat.clone_writer(), + channel_mgr.clone_writer(), + ui_state.clone_writer() + ) + } } } } } - } - }.widget_build(ctx!()) - } else { - @ { Void }.widget_build(ctx!()) - }) + }.widget_build(ctx!()) + } else { + @ { Void }.widget_build(ctx!()) + }) } } } diff --git a/gui/src/widgets/home/bot_store.rs b/gui/src/widgets/home/bot_store.rs index e8dffc6..0ad7754 100644 --- a/gui/src/widgets/home/bot_store.rs +++ b/gui/src/widgets/home/bot_store.rs @@ -1,12 +1,19 @@ -use polestar_core::model::{ChannelCfg, MsgMeta, Msg}; +use polestar_core::model::{ChannelCfg, Msg, MsgMeta}; use ribir::prelude::*; use crate::{ style::{ANTI_FLASH_WHITE, COMMON_RADIUS, LIGHT_SILVER_15, SPANISH_GRAY, WHITE}, - widgets::{app::AppGUI, helper::send_msg}, + widgets::{ + app::{ChannelMgr, Chat, UIState}, + helper::send_msg, + }, }; -pub fn w_bot_store(app: impl StateWriter) -> impl WidgetBuilder { +pub fn w_bot_store( + chat: impl StateWriter, + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @ConstrainedBox { clamp: BoxClamp::EXPAND_BOTH, @@ -16,7 +23,7 @@ pub fn w_bot_store(app: impl StateWriter) -> impl WidgetBuilder align_items: Align::Stretch, h_align: HAlign::Center, @VScrollBar { - @ { w_bot_list(app.clone_writer()) } + @ { w_bot_list(chat, channel_mgr, ui_state) } } } } @@ -36,12 +43,16 @@ const CATEGORY_LIST: [&str; 10] = [ "Interviewer", ]; -fn w_bot_list(app: impl StateWriter) -> impl WidgetBuilder { +fn w_bot_list( + chat: impl StateWriter, + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @Column { margin: EdgeInsets::all(14.), @ { - CATEGORY_LIST.iter().map(|cat| { + CATEGORY_LIST.iter().map(move |cat| { @Column { @Text { margin: EdgeInsets::new(24., 0., 14., 0.), @@ -53,7 +64,7 @@ fn w_bot_list(app: impl StateWriter) -> impl WidgetBuilder { item_gap: 8., line_gap: 8., @ { - $app.data.info().bots().iter().filter(|bot| bot.cat() == Some(cat)).map(move |bot| { + $chat.info().bots().iter().filter(|bot| bot.cat() == Some(cat)).map(move |bot| { let bot_name = bot.name().to_owned(); let bot_id = bot.id().clone(); let bot_id_2 = bot_id.clone(); @@ -62,27 +73,36 @@ fn w_bot_list(app: impl StateWriter) -> impl WidgetBuilder { }); @Clip { on_tap: move |_| { + let _ = || $ui_state.write(); let (channel_id, bot_msg_id) = { - let mut app_write = $app.write(); - let channel_id = app_write.data.new_channel(bot_name.clone(), None, ChannelCfg::def_bot_id_cfg(bot_id_2.clone())); - let channel = app_write.data.get_channel_mut(&channel_id).unwrap(); + let channel_id = $channel_mgr + .write() + .new_channel( + bot_name.clone(), + None, + ChannelCfg::def_bot_id_cfg(bot_id_2.clone()) + ); let msg = Msg::new_user_text(&bot_onboarding, MsgMeta::default()); let user_msg_id = *msg.id(); - channel.add_msg(msg); - let bot_msg = Msg::new_bot_text(bot_id_2.clone(), MsgMeta::reply(user_msg_id)); + $chat.write().add_msg(&channel_id, msg); + let bot_msg = Msg::new_bot_text( + bot_id_2.clone(), + MsgMeta::reply(user_msg_id) + ); let bot_msg_id = *bot_msg.id(); - channel.add_msg(bot_msg); - app_write.data.switch_channel(&channel_id); - app_write.navigate_to("/home/chat"); + $chat.write().add_msg(&channel_id, bot_msg); (channel_id, bot_msg_id) }; - - let channel_writer = app.map_writer( - move |app| { app.data.get_channel(&channel_id).unwrap() }, - move |app| { app.data.get_channel_mut(&channel_id).unwrap() }, + send_msg( + chat.clone_writer(), + channel_id, + bot_msg_id, + 0, + bot_id_2.clone(), + bot_onboarding.clone() ); - - send_msg(channel_writer, bot_onboarding.clone(), 0, bot_msg_id, bot_id_2.clone()); + channel_mgr.write().switch_channel(&channel_id); + ui_state.write().navigate_to("/home/chat"); }, @SizedBox { size: Size::new(200., 110.), diff --git a/gui/src/widgets/home/chat.rs b/gui/src/widgets/home/chat.rs index 2c3d83e..50488dc 100644 --- a/gui/src/widgets/home/chat.rs +++ b/gui/src/widgets/home/chat.rs @@ -1,7 +1,7 @@ use std::rc::Rc; use polestar_core::model::{ - Bot, BotId, Channel, FeedbackUserIdForServer, Msg, MsgCont, MsgMeta, MsgRole, + Bot, BotId, ChannelId, FeedbackUserIdForServer, Msg, MsgCont, MsgMeta, MsgRole, }; use ribir::prelude::*; @@ -15,27 +15,39 @@ use uuid::Uuid; use crate::{ req::query_fetch_feedback, - widgets::helper::{new_channel, StateSink, StateSource}, + widgets::{ + app::Chat, + helper::{new_channel, StateSink, StateSource}, + }, }; pub fn w_chat( - channel: impl StateWriter, + chat: impl StateWriter, + channel_id: ChannelId, bots: Rc>, def_bot_id: BotId, ) -> impl WidgetBuilder { fn_widget! { let quote_id: State> = State::value(None); let mut guard = None; - if $channel.is_feedback() { + let is_feedback = + $chat.channel(&channel_id).map(|channel| channel.is_feedback()).unwrap_or_default(); + if is_feedback { let (sx, rx) = new_channel(); - let mut time_stamp = $channel.msgs().last().map(|msg| msg.create_at().timestamp_millis()); + let mut time_stamp = + $chat + .channel(&channel_id) + .unwrap() + .msgs() + .last() + .map(|msg| msg.create_at().timestamp_millis()); fetch_feedbacks(sx, time_stamp); guard = Some(watch!($rx.take_all()).subscribe(move |feedbacks|{ feedbacks.into_iter().for_each(|msg| { let t = msg.create_at().timestamp_millis(); if time_stamp.is_none() || t > time_stamp.unwrap() { time_stamp = Some(t); - $channel.write().add_msg(msg); + $chat.write().add_msg(&channel_id, msg); } }); }).unsubscribe_when_dropped()); @@ -45,9 +57,9 @@ pub fn w_chat( @Column { @Expanded { flex: 1., - @ { w_msg_list(channel.clone_writer(), quote_id.clone_writer()) } + @ { w_msg_list(chat.clone_writer(), channel_id, quote_id.clone_writer()) } } - @ { w_editor(channel, bots, def_bot_id, quote_id) } + @ { w_editor(chat.clone_writer(), channel_id, bots, def_bot_id, quote_id) } } } } diff --git a/gui/src/widgets/home/chat/editor.rs b/gui/src/widgets/home/chat/editor.rs index 15c66ef..e8cc02c 100644 --- a/gui/src/widgets/home/chat/editor.rs +++ b/gui/src/widgets/home/chat/editor.rs @@ -1,5 +1,9 @@ -use crate::{widgets::{helper::send_msg, common::BotList}, style::WHITE, req::query_feedback}; -use polestar_core::model::{Bot, Channel, Msg, MsgMeta, BotId}; +use crate::{ + req::query_feedback, + style::WHITE, + widgets::{app::Chat, common::BotList, helper::send_msg}, +}; +use polestar_core::model::{Bot, BotId, ChannelId, Msg, MsgMeta}; use ribir::{core::ticker::FrameMsg, prelude::*}; use std::ops::Range; use std::rc::Rc; @@ -8,7 +12,8 @@ use uuid::Uuid; use crate::{style::CULTURED_F4F4F4_FF, theme::polestar_svg, widgets::common::IconButton}; pub fn w_editor( - channel: impl StateWriter, + chat: impl StateWriter, + channel_id: ChannelId, bots: Rc>, def_bot_id: BotId, quote_id: impl StateWriter>, @@ -17,27 +22,31 @@ pub fn w_editor( let mut bots = @BotList { bots, visible: false }; let ignore_pointer = @IgnorePointer { ignore: false }; let mut text_area = @MessageEditor {}; - let send_msg_by_char_channel = channel.clone_writer(); - let send_msg_by_icon_channel = channel.clone_writer(); - let quote_id_channel = channel.clone_writer(); let send_msg_by_char_quote_id = quote_id.clone_writer(); let send_msg_by_icon_quote_id = quote_id.clone_writer(); - + let is_feedback = $chat.channel(&channel_id).unwrap().is_feedback(); let def_bot_id_2 = def_bot_id.clone(); let send_icon = @IconButton { on_tap: move |_| { - if $channel.is_feedback() { - send_feedback(&mut $text_area.write(), send_msg_by_icon_channel.clone_writer()); + let _hint = || $chat.write(); + if is_feedback { + send_feedback(&mut $text_area.write(), chat.clone_writer(), channel_id); } else { - send_question(&mut $text_area.write(), send_msg_by_icon_channel.clone_writer(), def_bot_id.clone(), send_msg_by_icon_quote_id.clone_writer()); + send_question( + &mut $text_area.write(), + chat.clone_writer(), + channel_id, + def_bot_id.clone(), + send_msg_by_icon_quote_id.clone_writer() + ); } $text_area.write().reset(); }, @ { polestar_svg::SEND } }; - if !$channel.is_feedback() { + if !is_feedback { watch!($text_area.bot_hint()) .distinct_until_changed() .subscribe(move |hint| { @@ -71,10 +80,17 @@ pub fn w_editor( select_bot(&mut $text_area.write(), &$bots); e.stop_propagation(); } else if !e.with_shift_key() { - if $channel.is_feedback() { - send_feedback(&mut $text_area.write(), send_msg_by_char_channel.clone_writer()); + let _hint = || $chat.write(); + if is_feedback { + send_feedback(&mut $text_area.write(), chat.clone_writer(), channel_id); } else { - send_question(&mut $text_area.write(), send_msg_by_char_channel.clone_writer(), def_bot_id_2.clone(), send_msg_by_char_quote_id.clone_writer()); + send_question( + &mut $text_area.write(), + chat.clone_writer(), + channel_id, + def_bot_id_2.clone(), + send_msg_by_char_quote_id.clone_writer() + ); } $text_area.write().reset(); e.stop_propagation(); @@ -96,12 +112,17 @@ pub fn w_editor( padding: EdgeInsets::new(0., 11., 11., 11.), @Column { @ { - let quote_id_channel = quote_id_channel.clone_reader(); pipe! { - let _ = || $quote_id.write(); + let _ = || { + $quote_id.write(); + $chat.write(); + }; let non_quote_id = quote_id.clone_writer(); (*$quote_id).map(move |id| { - let text = $quote_id_channel.msg(&id).and_then(|msg| msg.cur_cont_ref().text().map(str::to_string)).unwrap_or_default(); + let text = $chat + .msg(&channel_id, &id) + .and_then(|msg| msg.cur_cont_ref().text().map(str::to_string)) + .unwrap_or_default(); @Row { background: Color::from_u32(WHITE), @Icon { @@ -136,7 +157,8 @@ pub fn w_editor( fn send_feedback( text_area: &mut MessageEditor, - channel: impl StateWriter, + chat: impl StateWriter, + channel_id: ChannelId, ) { let text = text_area.display_text(); @@ -145,7 +167,7 @@ fn send_feedback( } let user_msg = Msg::new_user_text(&text, MsgMeta::default()); - channel.write().add_msg(user_msg); + chat.write().add_msg(&channel_id, user_msg); submit_feedback(text); } @@ -158,7 +180,8 @@ fn submit_feedback(content: String) { fn send_question( text_area: &mut MessageEditor, - channel: impl StateWriter, + chat: impl StateWriter, + channel_id: ChannelId, def_bot_id: BotId, quote_id: impl StateWriter>, ) { @@ -170,21 +193,31 @@ fn send_question( let user_msg = Msg::new_user_text(&text, MsgMeta::default()); let user_msg_id = *user_msg.id(); - channel.write().add_msg(user_msg); - + chat.write().add_msg(&channel_id, user_msg); + let bots = text_area.edit_message.related_bot(); let bot_id = bots.last().map_or(def_bot_id, |id| id.clone()); - + let msg_quote_id = *quote_id.read(); - let bot_msg = Msg::new_bot_text(bot_id.clone(), MsgMeta::new(msg_quote_id, Some(user_msg_id))); + let bot_msg = Msg::new_bot_text( + bot_id.clone(), + MsgMeta::new(msg_quote_id, Some(user_msg_id)), + ); let id = *bot_msg.id(); let idx = bot_msg.cur_idx(); - channel.write().add_msg(bot_msg); + chat.write().add_msg(&channel_id, bot_msg); *quote_id.write() = None; - send_msg(channel.clone_writer(), text_area.edit_message.message_content(), idx, id, bot_id); + send_msg( + chat, + channel_id, + id, + idx, + bot_id, + text_area.edit_message.message_content(), + ); } fn select_bot(text_area: &mut MessageEditor, bots: &BotList) { diff --git a/gui/src/widgets/home/chat/msg_list.rs b/gui/src/widgets/home/chat/msg_list.rs index 09b4a0a..26fbbee 100644 --- a/gui/src/widgets/home/chat/msg_list.rs +++ b/gui/src/widgets/home/chat/msg_list.rs @@ -1,23 +1,23 @@ -use polestar_core::model::{Channel, Msg, MsgCont, MsgRole}; +use polestar_core::model::{ChannelId, MsgCont, MsgId, MsgRole}; use ribir::prelude::*; use uuid::Uuid; use crate::style::decorator::channel::message_style; use crate::style::{GAINSBORO, WHITE}; use crate::theme::polestar_svg; +use crate::widgets::app::Chat; use crate::widgets::common::IconButton; use crate::widgets::helper::send_msg; use super::onboarding::w_msg_onboarding; -pub fn w_msg_list( - channel: S, +pub fn w_msg_list( + chat: impl StateWriter, + channel_id: ChannelId, quote_id: impl StateWriter>, -) -> impl WidgetBuilder -where - S: StateWriter, -{ +) -> impl WidgetBuilder { fn_widget! { + let channel = chat.map_reader(move |chat| chat.channel(&channel_id).unwrap()); let scrollable_container = @VScrollBar {}; let mut content_constrained_box = @ConstrainedBox { @@ -60,21 +60,13 @@ where @ { w_msg_onboarding() } @ { pipe! { - let _ = || $channel.write(); - let channel_cloned = channel.clone_writer(); + let _ = || $chat.write(); let quote_id = quote_id.clone_writer(); - + let channel_id = *$channel.id(); + let chat = chat.clone_writer(); $channel.msgs().iter().map(move |m| { let id = *m.id(); - let msg = channel_cloned.split_writer( - move |channel| { - channel.msg(&id).expect("msg must be existed") - }, - move |channel| { - channel.msg_mut(&id).expect("msg must be existed") - }, - ); - @ { w_msg(msg, quote_id.clone_writer()) } + @ { w_msg(chat.clone_writer(), channel_id, id, quote_id.clone_writer()) } }).collect::>() } } @@ -138,25 +130,18 @@ impl ComposeChild for MsgOps { } } -fn w_msg(msg: S, quote_id: impl StateWriter>) -> impl WidgetBuilder -where - S: StateWriter, - S::Writer: StateWriter, - S::OriginWriter: StateWriter, - S::OriginReader: StateReader, - R: StateReader, - W: StateWriter, - ::OriginWriter: StateWriter, - <::Writer as StateWriter>::OriginWriter: StateWriter, - <<::Writer as StateWriter>::Writer as StateWriter>::OriginWriter: - StateWriter, -{ +fn w_msg( + chat: impl StateWriter, + channel_id: ChannelId, + msg_id: MsgId, + quote_id: impl StateWriter>, +) -> impl WidgetBuilder { fn_widget! { @ { - pipe!($msg;).map(move |_| { let mut stack = @Stack {}; - - let role = $msg.role().clone(); + let chat_ref = $chat; + let msg = chat_ref.msg(&channel_id, &msg_id).unwrap(); + let role = msg.role().clone(); let mut row = @Row { item_gap: 8., reverse: matches!(role, MsgRole::User) @@ -173,8 +158,6 @@ where } }; - let retry_msg = msg.clone_writer(); - let role_2 = role.clone(); let role_3 = role.clone(); let msg_ops = @$msg_ops_anchor { @@ -184,15 +167,18 @@ where @ { match role_3.clone() { MsgRole::User | MsgRole::Bot(_) => { - let channel = msg.origin_writer(); @MsgOps { @ { let quote_id = quote_id.clone_writer(); - (!$channel.is_feedback()).then(move || { + let quote_fn = Box::new(move || { + *quote_id.write() = Some(msg_id) + }) as Box; + let is_feedback = $chat + .channel(&channel_id) + .map_or(false, |ch| ch.is_feedback()); + (!is_feedback).then(move || { @MsgOp { - cb: Box::new(move || { - *quote_id.write() = Some(*$msg.id()) - }) as Box, + cb: quote_fn, @IconButton { padding: EdgeInsets::all(4.), size: IconSize::of(ctx!()).tiny, @@ -203,7 +189,9 @@ where } @MsgOp { cb: Box::new(move || { - if let Some(text) = $msg.cur_cont_ref().text() { + let chat = $chat; + let msg = chat.msg(&channel_id, &msg_id).unwrap(); + if let Some(text) = msg.cur_cont_ref().text() { let clipboard = AppCtx::clipboard(); let _ = clipboard.borrow_mut().clear(); let _ = clipboard.borrow_mut().write_text(text); @@ -216,32 +204,38 @@ where } } @ { - let retry_msg = retry_msg.clone_writer(); - let channel = msg.origin_writer().clone_writer(); - ($msg.role().is_bot()).then(move || { + let source_id = msg.meta().source_id().cloned(); + let bot_id = msg.role().bot().cloned(); + let msg_id = *msg.id(); + let chat = chat.clone_writer(); + let role = msg.role().clone(); + (role.is_bot()).then(move || { + let _hint = || $chat.write(); @MsgOp { // TODO: cb code is messy, need to refactor. cb: Box::new(move || { - let _ = || $retry_msg.write(); - let _ = || $channel.write(); - let source_id = { - let retry_msg_write = $retry_msg.write(); - *retry_msg_write.meta().source_id().unwrap() - }; - let msg_id = *$retry_msg.id(); - let source_msg_text = $channel - .msg(&source_id) - .and_then(|msg| msg.cur_cont_ref().text().map(|text| text.to_owned())) - .unwrap_or_default().clone(); - let (cur_idx, bot_id) = { - let mut retry_msg_write = $retry_msg.write(); - let cont = MsgCont::init_text(); - retry_msg_write.add_cont(cont); - let cur_idx = retry_msg_write.cur_idx(); - let bot_id = retry_msg_write.role().bot().unwrap().clone(); - (cur_idx, bot_id) + let _hint = || $chat.write(); + let (source_msg, idx) = { + let mut chat = $chat.write(); + ( + chat + .msg(&channel_id, &source_id.unwrap()) + .and_then( + |msg| msg.cur_cont_ref().text().map(|text| text.to_owned()) + ) + .unwrap_or_default().clone(), + chat.add_msg_cont(&channel_id, &msg_id, MsgCont::init_text()).unwrap() + ) }; - send_msg(channel.clone_writer(), source_msg_text, cur_idx, msg_id, bot_id); + $chat.write().switch_cont(&channel_id, &msg_id, idx); + send_msg( + chat.clone_writer(), + channel_id, + msg_id, + idx, + bot_id.clone().unwrap(), + source_msg + ); }) as Box, @IconButton { padding: EdgeInsets::all(4.), @@ -261,7 +255,7 @@ where }; @$stack { @Row { - h_align: match $msg.role() { + h_align: match msg.role() { MsgRole::User => HAlign::Right, _ => HAlign::Left, }, @@ -271,12 +265,10 @@ where } @Column { @ { - let channel = msg.origin_writer().clone_writer(); - $msg.role().bot().and_then(move |bot_id| { - $channel.app_info().map(|info| { - let bot = info.get_bot_or_default(Some(bot_id)); - @Text { text: bot.name().to_owned() } - }) + msg.role().bot().map(move |bot_id| { + let chat = $chat; + let bot = chat.info().get_bot_or_default(Some(bot_id)); + @Text { text: bot.name().to_owned() } }) } @ConstrainedBox { @@ -285,25 +277,21 @@ where max: Size::new(560., f32::INFINITY), }, @ { - pipe!($msg.cur_idx()).map(move |_| { - let _msg_capture = || $msg.write(); let default_txt = String::new(); // TODO: support Image Type. - let text = $msg + let text = msg .cur_cont_ref() .text() .unwrap_or_else(|| &default_txt).to_owned(); - let msg2 = msg.clone_writer(); + let quote_id = msg.meta().quote_id().cloned(); message_style( @Column { - @ { w_msg_quote(msg2) } + @ { w_msg_quote(&*$chat, &channel_id, quote_id) } @ { - let msg2 = msg.clone_writer(); - pipe! { - ($msg.cont_list().len() > 1).then(|| { - w_msg_multi_rst(msg2.clone_writer()) - }) - } + let chat = chat.clone_writer(); + (msg.cont_list().len() > 1).then(move || { + w_msg_multi_rst(chat, channel_id, msg_id) + }) } @TextSelectable { @Text { @@ -313,9 +301,8 @@ where } } }.widget_build(ctx!()), - $msg.role().clone() + msg.role().clone() ) - }) } } } @@ -323,24 +310,19 @@ where } @ { msg_ops } } - }) } } } -fn w_msg_quote(msg: S) -> Option -where - S: StateWriter, - S::OriginWriter: StateWriter, -{ - let channel = msg.origin_writer().clone_writer(); - let msg_state = msg.read(); - let quote_id = msg_state.meta().quote_id(); - +fn w_msg_quote( + chat: &dyn Chat, + channel_id: &ChannelId, + quote_id: Option, +) -> Option { let quote_text = quote_id.and_then(move |id| { - let channel_state = channel.read(); - let msg = channel_state.msgs().iter().find(|msg| msg.id() == id); - msg.and_then(|msg| msg.cur_cont_ref().text().map(|s| s.to_owned())) + chat + .msg(channel_id, &id) + .and_then(|msg| msg.cur_cont_ref().text().map(|s| s.to_owned())) }); quote_text.map(|text| { @@ -352,12 +334,15 @@ where }) } -fn w_msg_multi_rst(msg: impl StateWriter) -> impl WidgetBuilder { +fn w_msg_multi_rst( + chat: impl StateWriter, + channel_id: ChannelId, + msg_id: MsgId, +) -> impl WidgetBuilder { fn_widget! { let scrollable_widget = @ScrollableWidget { scrollable: Scrollable::X, }; - let thumbnail_msg = msg.clone_writer(); @Row { @Visibility { @Void {} @@ -368,16 +353,21 @@ fn w_msg_multi_rst(msg: impl StateWriter) -> impl WidgetBuilder { @Row { item_gap: 8., @ { - $msg.cont_list().iter().enumerate().map(|(idx, cont)| { - let text = cont.text().map(|s| s.to_owned()).unwrap_or_default(); - let w_thumbnail = w_msg_thumbnail(text); - @$w_thumbnail { - on_tap: move |_| { - let mut msg_write = $thumbnail_msg.write(); - msg_write.switch_cont(idx); + $chat + .msg(&channel_id, &msg_id) + .unwrap() + .cont_list() + .iter() + .enumerate() + .map(|(idx, cont)| { + let text = cont.text().map(|s| s.to_owned()).unwrap_or_default(); + let w_thumbnail = w_msg_thumbnail(text); + @$w_thumbnail { + on_tap: move |_| { + $chat.write().switch_cont(&channel_id, &msg_id, idx); + } } - } - }).collect::>() + }).collect::>() } } } diff --git a/gui/src/widgets/home/chat/onboarding.rs b/gui/src/widgets/home/chat/onboarding.rs index 8c9df1d..178968e 100644 --- a/gui/src/widgets/home/chat/onboarding.rs +++ b/gui/src/widgets/home/chat/onboarding.rs @@ -130,9 +130,3 @@ pub fn w_msg_onboarding() -> impl WidgetBuilder { } } } - -pub fn w_feedback_onboarding() -> impl WidgetBuilder { - fn_widget! { - @Void {} - } -} diff --git a/gui/src/widgets/home/settings.rs b/gui/src/widgets/home/settings.rs index 030d966..644601a 100644 --- a/gui/src/widgets/home/settings.rs +++ b/gui/src/widgets/home/settings.rs @@ -3,7 +3,7 @@ use ribir::prelude::*; use crate::{ platform, style::{COMMON_RADIUS, WHITE}, - widgets::app::AppGUI, + widgets::app::{UIState, UserConfig}, }; mod account; @@ -13,7 +13,10 @@ use account::{w_email, w_subscription, AccountItem}; use general::w_general_settings; use network::w_network_settings; -pub fn w_settings(app: impl StateWriter) -> impl WidgetBuilder { +pub fn w_settings( + config: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @ConstrainedBox { clamp: BoxClamp::EXPAND_BOTH, @@ -27,11 +30,11 @@ pub fn w_settings(app: impl StateWriter) -> impl WidgetBuilder { name: "Account", @AccountItem { name: "Email", - @ { w_email(&app) } + @ { w_email(config.clone_writer(), ui_state) } } @AccountItem { name: "Subscription", - @ { w_subscription(app.clone_writer()) } + @ { w_subscription(config) } } } @ { diff --git a/gui/src/widgets/home/settings/account.rs b/gui/src/widgets/home/settings/account.rs index 3eeb499..3b9c277 100644 --- a/gui/src/widgets/home/settings/account.rs +++ b/gui/src/widgets/home/settings/account.rs @@ -1,9 +1,9 @@ - +use polestar_core::model::Quota; use ribir::prelude::*; use crate::req::query_quota; use crate::style::{BLACK, CHINESE_WHITE, COMMON_RADIUS, ISABELLINE, WHITE}; -use crate::widgets::app::AppGUI; +use crate::widgets::app::{UIState, UserConfig}; use crate::widgets::common::ProgressBar; #[derive(Declare)] @@ -27,8 +27,10 @@ impl ComposeChild for AccountItem { } } -pub(super) fn w_email(app: &impl StateWriter) -> impl WidgetBuilder { - let app = app.clone_writer(); +pub(super) fn w_email( + config: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @Row { justify_content: JustifyContent::SpaceBetween, @@ -37,11 +39,11 @@ pub(super) fn w_email(app: &impl StateWriter) -> impl WidgetBuil @Row { item_gap: 10., @Text { - text: $app.data.info().user().and_then(|u| u.email()).cloned().unwrap_or("Anonymous".to_string()), + text: $config.user().and_then(|u| u.email()).cloned().unwrap_or("Anonymous".to_string()), } @TextSelectable { @Text { - text: $app.data.info().user().map(|user| format!("ID: {}", user.uid())).unwrap_or_default(), + text: $config.user().map(|user| format!("ID: {}", user.uid())).unwrap_or_default(), foreground: Palette::of(ctx!()).outline(), } } @@ -50,8 +52,8 @@ pub(super) fn w_email(app: &impl StateWriter) -> impl WidgetBuil cursor: CursorIcon::Pointer, color: Color::RED, on_tap: move |_| { - $app.write().data.logout(); - $app.write().navigate_to("/login"); + $config.write().logout(); + $ui_state.write().navigate_to("/login"); }, @ { Label::new("Logout") } } @@ -59,15 +61,16 @@ pub(super) fn w_email(app: &impl StateWriter) -> impl WidgetBuil } } -pub(super) fn w_subscription(app: impl StateWriter) -> impl WidgetBuilder { +pub(super) fn w_subscription( + config: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { - println!("subscription"); - let _ = || $app.write(); - let spawn_app = app.clone_writer(); - let token = $spawn_app.data.info().user().and_then(|user| user.token().map(|s| s.to_owned())); + let token = $config.user().and_then(|user| user.token().map(|s| s.to_owned())); + let quota = State::value(None); + let quota_writer = quota.clone_writer(); let _ = AppCtx::spawn_local(async move { let quota = query_quota(token).await.ok(); - spawn_app.silent().data.info_mut().user_mut().unwrap().set_quota(quota); + *quota_writer.write() = quota; }); @ConstrainedBox { clamp: BoxClamp { @@ -84,7 +87,7 @@ pub(super) fn w_subscription(app: impl StateWriter) -> impl Widg // TODO: check here why need `Stack`? it's only one child. @Stack { @Row { - @ { w_free_plan(app.clone_reader()) } + @ { w_free_plan(quota.clone_reader()) } @Divider { direction: Direction::Vertical, } @@ -95,7 +98,7 @@ pub(super) fn w_subscription(app: impl StateWriter) -> impl Widg } } -fn w_free_plan(app: impl StateReader) -> impl WidgetBuilder { +fn w_free_plan(config: impl StateReader>) -> impl WidgetBuilder { fn_widget! { @Row { item_gap: 16., @@ -114,7 +117,7 @@ fn w_free_plan(app: impl StateReader) -> impl WidgetBuilder { ) } @ { - w_quota_usage_amount(app.clone_reader()) + w_quota_usage_amount(config.clone_reader()) } } } @@ -143,26 +146,28 @@ fn w_quota_usage_progress_bar(total: f32, completed: f32, tip: String) -> impl W } } -fn w_quota_usage_amount(app: impl StateReader) -> impl WidgetBuilder { +fn w_quota_usage_amount(quota: impl StateReader>) -> impl WidgetBuilder { fn_widget! { @ { - if $app.data.info().user().map(|user| user.quota().map(|quota| quota.is_over()).unwrap_or_default()).unwrap_or_default() { + if $quota.as_ref().map(|quota| quota.is_over()).unwrap_or_default() { w_quota_over().widget_build(ctx!()) } else { - w_quota_usage(app.clone_reader()).widget_build(ctx!()) + w_quota_usage(quota.clone_reader()).widget_build(ctx!()) } } } } -fn w_quota_usage(app: impl StateReader) -> impl WidgetBuilder { +fn w_quota_usage(quota: impl StateReader>) -> impl WidgetBuilder { fn_widget! { @Column { // text message usage @ { - let text_total = $app.data.info().user().map(|user| user.quota().map(|quota| quota.max_texts()).unwrap_or_default()).unwrap_or_default(); - let text_used = $app.data.info().user().map(|user| user.quota().map(|quota| quota.text_used()).unwrap_or_default()).unwrap_or_default(); - w_quota_usage_progress_bar(text_total, text_used, "messages".to_owned()) + pipe!($quota;).map(move |_| { + let text_total = $quota.as_ref().map(|quota| quota.max_texts()).unwrap_or_default(); + let text_used = $quota.as_ref().map(|quota| quota.text_used()).unwrap_or_default(); + w_quota_usage_progress_bar(text_total, text_used, "messages".to_owned()) + }) } // image message usage // @ { w_quota_usage_progress_bar(100., 10., "image".to_owned()) } diff --git a/gui/src/widgets/home/sidebar.rs b/gui/src/widgets/home/sidebar.rs index aa87d49..74d51da 100644 --- a/gui/src/widgets/home/sidebar.rs +++ b/gui/src/widgets/home/sidebar.rs @@ -4,7 +4,7 @@ use ribir::prelude::*; use crate::{ style::APP_SIDEBAR_HEADER_HEIGHT, widgets::{ - app::AppGUI, + app::{ChannelMgr, UIState}, common::{IconButton, InteractiveList}, }, G_APP_NAME, @@ -13,20 +13,23 @@ use crate::{ mod channel_thumbnail_list; use channel_thumbnail_list::w_channel_thumbnail_list; -pub fn w_sidebar(app: impl StateWriter) -> impl WidgetBuilder { +pub fn w_sidebar( + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @Column { - @ { w_sidebar_header(app.clone_writer()) } + @ { w_sidebar_header(channel_mgr.clone_writer()) } @Expanded { flex: 1., - @ { w_channel_thumbnail_list(app.clone_writer()) } + @ { w_channel_thumbnail_list(channel_mgr.clone_writer(), ui_state.clone_writer()) } } - @ { w_sidebar_others(app.clone_writer()) } + @ { w_sidebar_others(channel_mgr.clone_writer(), ui_state) } } } } -fn w_sidebar_header(app: impl StateWriter) -> impl WidgetBuilder { +fn w_sidebar_header(chat_mgr: impl StateWriter) -> impl WidgetBuilder { fn_widget! { @ConstrainedBox { clamp: BoxClamp::fixed_height(APP_SIDEBAR_HEADER_HEIGHT), @@ -44,7 +47,11 @@ fn w_sidebar_header(app: impl StateWriter) -> impl WidgetBuilder @IconButton { size: IconSize::of(ctx!()).medium, on_tap: move |_| { - $app.write().data.new_channel("Untitled".to_owned(), None, ChannelCfg::default()); + let channel_id = $chat_mgr.write().new_channel("Untitled".to_owned(), None, ChannelCfg::default()); + let chat_mgr = chat_mgr.clone_writer(); + let _ = AppCtx::spawn_local(async move { + $chat_mgr.write().switch_channel(&channel_id); + }); }, @ { svgs::ADD } } @@ -53,38 +60,42 @@ fn w_sidebar_header(app: impl StateWriter) -> impl WidgetBuilder } } -fn w_sidebar_others(app: impl StateWriter) -> impl WidgetBuilder { +fn w_sidebar_others( + chat_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @InteractiveList { highlight_visible: pipe! { - let app = $app; - matches!(app.cur_router_path(), "/home/bot_store" | "/home/settings") + let path = $ui_state.cur_path(); + matches!(path.as_str(), "/home/bot_store" | "/home/settings") }, @ListItem { on_tap: move |_| { - $app.write().navigate_to("/home/bot_store"); + $ui_state.write().navigate_to("/home/bot_store"); }, @HeadlineText(Label::new("BotStore")) } @ListItem { on_tap: move |_| { - $app.write().navigate_to("/home/settings"); + $ui_state.write().navigate_to("/home/settings"); }, @HeadlineText(Label::new("Setting")) } @ListItem { on_tap: move |_| { - let feedback_id = $app.data.channels().iter().find(|channel| { - channel.is_feedback() - }).map(|channel| *channel.id()); + let ids = $chat_mgr.channel_ids(); + let feedback_id = ids.iter().find(|channel_id| { + $chat_mgr.channel(channel_id).unwrap().is_feedback() + }); if let Some(feedback_id) = feedback_id { - $app.write().data.switch_channel(&feedback_id); + $chat_mgr.write().switch_channel(feedback_id); } else { - let id = $app.write().data.new_channel("Feedback".to_owned(), None, ChannelCfg::feedback_cfg()); - $app.write().data.switch_channel(&id); + let id = $chat_mgr.write().new_channel("Feedback".to_owned(), None, ChannelCfg::feedback_cfg()); + $chat_mgr.write().switch_channel(&id); } - $app.write().navigate_to("/home/chat"); + $ui_state.write().navigate_to("/home/chat"); }, @HeadlineText(Label::new("Feedback")) } diff --git a/gui/src/widgets/home/sidebar/channel_thumbnail_list.rs b/gui/src/widgets/home/sidebar/channel_thumbnail_list.rs index 425864b..5ece3a3 100644 --- a/gui/src/widgets/home/sidebar/channel_thumbnail_list.rs +++ b/gui/src/widgets/home/sidebar/channel_thumbnail_list.rs @@ -1,53 +1,55 @@ use crate::theme::polestar_svg; -use crate::widgets::{ - app::AppGUI, - common::{w_avatar, IconButton, InteractiveList}, -}; +use crate::widgets::app::{ChannelMgr, UIState}; +use crate::widgets::common::{w_avatar, IconButton, InteractiveList}; use polestar_core::model::Channel; use ribir::prelude::*; -pub fn w_channel_thumbnail_list(app: impl StateWriter) -> impl WidgetBuilder { +pub fn w_channel_thumbnail_list( + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { @InteractiveList { active: pipe! { - let last_idx = if !$app.data.channels().is_empty() { - $app.data.channels().len() - 1 + let channels = $channel_mgr.channel_ids(); + let last_idx = if !channels.is_empty() { + channels.len() - 1 } else { 0 }; - $app.data.cur_channel().map(|channel| *channel.id()).and_then(|id| { - $app.data.channels().iter().position(|channel| *channel.id() == id).map(|idx| last_idx - idx) + $channel_mgr.cur_channel_id().and_then(|id| { + channels.iter().position(|ch| ch == id).map(|idx| last_idx - idx) }).unwrap_or(last_idx) }, on_tap: move |_| { - if $app.cur_router_path() != "/home/chat" { - $app.write().navigate_to("/home/chat"); + if $ui_state.cur_path() != "/home/chat" { + $ui_state.write().navigate_to("/home/chat"); } }, highlight_visible: pipe! { - let app = $app; - matches!(app.cur_router_path(), "/home/chat") + let path = $ui_state.cur_path(); + matches!(path.as_str(), "/home/chat") }, @ { pipe! { - let app2 = app.clone_writer(); let mut rst = vec![]; - for idx in (0..$app.data.channels().len()).rev() { - let channel = app2.split_writer( - move |app| { app.data.channels().iter().nth(idx).expect("channel must be existed") }, - move |app| { app.data.channels_mut().iter_mut().nth(idx).expect("channel must be existed") }, + for id in $channel_mgr.channel_ids().into_iter().rev() { + let channel = channel_mgr.map_reader( + move |channels| { channels.channel(&id).expect("channel must be existed") }, ); - let channel2 = channel.clone_writer(); - - let w_channel_thumbnail = w_channel_thumbnail(channel); - let w_channel_thumbnail = @ $w_channel_thumbnail { + let channel_thumbnail = + w_channel_thumbnail(channel, channel_mgr.clone_writer(),ui_state.clone_writer()); + let channel_thumbnail = @ $channel_thumbnail { on_tap: move |_| { - let id = *$channel2.id(); - $app.write().data.switch_channel(&id); + let _ = || $channel_mgr.write(); + let channel_mgr = channel_mgr.clone_writer(); + let _ = AppCtx::spawn_local(async move { + channel_mgr.write().switch_channel(&id); + }); }, }; - rst.push(w_channel_thumbnail); + rst.push(channel_thumbnail); } rst } @@ -56,12 +58,11 @@ pub fn w_channel_thumbnail_list(app: impl StateWriter) -> impl W } } -fn w_channel_thumbnail(channel: S) -> impl WidgetBuilder -where - S: StateWriter + 'static, - S::OriginWriter: StateWriter, -{ - let app = channel.origin_writer().clone_writer(); +fn w_channel_thumbnail( + channel: impl StateReader, + channel_mgr: impl StateWriter, + gui_helper: impl StateWriter, +) -> impl WidgetBuilder { fn_widget! { let mut item = @ListItem {}; // let support_text = $channel.desc().map(|desc| { @@ -72,12 +73,14 @@ where @Leading { @ { let channel_state = $channel; - let app_state = $app; - let channel_def_bot_id = channel_state.cfg().def_bot_id(); - let bot_id = (channel_def_bot_id.unwrap_or_else(|| app_state.data.info().cfg().def_bot_id())).clone(); - let avatar = app_state - .data - .info() + let channel_def_bot_id = channel_state.cfg().def_bot_id().cloned(); + let bot_id = channel_def_bot_id + .unwrap_or_else( + || $channel.app_info().unwrap().cfg().def_bot_id().clone() + ); + let avatar = channel_state + .app_info() + .unwrap() .bots() .iter() .find(|bot| *bot.id() == bot_id) @@ -99,7 +102,7 @@ where @IconButton { on_tap: move |e| { let id = *$channel.id(); - $app.write().set_modify_channel_id(Some(id)); + $gui_helper.write().set_modify_channel_id(Some(id)); e.stop_propagation(); }, @ { polestar_svg::EDIT } @@ -107,7 +110,7 @@ where @IconButton { on_tap: move |e| { let id = *$channel.id(); - $app.write().data.remove_channel(&id); + $channel_mgr.write().remove_channel(&id); e.stop_propagation(); }, @ { polestar_svg::TRASH } diff --git a/gui/src/widgets/login.rs b/gui/src/widgets/login.rs index f7be34f..57ffdd3 100644 --- a/gui/src/widgets/login.rs +++ b/gui/src/widgets/login.rs @@ -1,6 +1,6 @@ use ribir::prelude::*; -use super::app::AppGUI; +use super::app::UserConfig; use crate::{ oauth::{apple_login_uri, google_login_uri, microsoft_login_uri}, style::WHITE, @@ -8,7 +8,7 @@ use crate::{ widgets::common::{Carousel, DoubleColumn, LeftColumn, RightColumn}, }; -pub(super) fn w_login(app: impl StateWriter) -> impl WidgetBuilder { +pub(super) fn w_login(account: impl StateWriter) -> impl WidgetBuilder { let logo_png_data = include_bytes!(concat!( env!("CARGO_MANIFEST_DIR"), "/..", @@ -46,37 +46,33 @@ pub(super) fn w_login(app: impl StateWriter) -> impl WidgetBuild item_gap: 8., @LoginBtn { url: pipe! { - - $app - .data - .info() - .need_login() - .then(|| microsoft_login_uri()) - .unwrap_or_default() + if $account.need_login() { + microsoft_login_uri() + } else { + String::new() + } }, label: "Log in with Microsoft", svg: polestar_svg::MICROSOFT_LOGIN, } @LoginBtn { url: pipe! { - $app - .data - .info() - .need_login() - .then(|| google_login_uri()) - .unwrap_or_default() + if $account.need_login() { + google_login_uri() + } else { + String::new() + } }, label: "Log in with Google", svg: polestar_svg::GOOGLE_LOGIN, } @LoginBtn { url: pipe! { - $app - .data - .info() - .need_login() - .then(|| apple_login_uri()) - .unwrap_or_default() + if $account.need_login() { + apple_login_uri() + } else { + String::new() + } }, label: "Log in with Apple", svg: polestar_svg::APPLE_LOGIN, diff --git a/gui/src/widgets/modify_channel.rs b/gui/src/widgets/modify_channel.rs index ea85adc..75dc5bf 100644 --- a/gui/src/widgets/modify_channel.rs +++ b/gui/src/widgets/modify_channel.rs @@ -1,11 +1,11 @@ -use polestar_core::model::{ChannelMode, BotId}; +use polestar_core::model::{BotId, ChannelMode}; use ribir::prelude::*; use uuid::Uuid; use crate::style::{CHINESE_WHITE, COMMON_RADIUS, CULTURED_F7F7F5_FF, WHITE}; use crate::widgets::common::{w_avatar, BotList, Modal}; -use super::app::AppGUI; +use super::app::{ChannelMgr, UIState, UserConfig}; fn w_mode_options( channel: impl StateReader, @@ -58,48 +58,29 @@ struct ChannelState { } pub fn w_modify_channel_modal( - app: impl StateWriter, + channel_mgr: impl StateWriter, + ui_state: impl StateWriter, + config: impl StateWriter, channel_id: &Uuid, ) -> impl WidgetBuilder { let channel_id = *channel_id; - let channel = app.split_writer( - move |app| { - app - .data - .get_channel(&channel_id) - .expect("channel not found") - }, - move |app| { - app - .data - .get_channel_mut(&channel_id) - .expect("channel not found") - }, - ); - fn_widget! { - let channel_rename = @Input {}; - $channel_rename.write().set_text($channel.name()); - - watch!($channel.name().to_owned()) - .distinct_until_changed() - .subscribe(move |name| { - $channel_rename.write().set_text(&name); - }); + let mgr_ref = $channel_mgr; + let channel_ref = mgr_ref.channel(&channel_id).unwrap(); - let app_writer_cancel_ref = app.clone_writer(); - let app_writer_confirm_ref = app.clone_writer(); + let channel_rename = @Input {}; + $channel_rename.write().set_text(channel_ref.name()); let bot_list = @BotList { - bots: $app.data.info().bots_rc(), - selected_id: $channel.cfg().def_bot_id().cloned(), + bots: $config.bots(), + selected_id: channel_ref.cfg().def_bot_id().cloned(), }; let mut selected_bot_box = @LayoutBox {}; let channel_state = State::value(ChannelState { - channel_mode: $channel.cfg().mode(), - selected_bot: $channel.cfg().def_bot_id().cloned(), + channel_mode: channel_ref.cfg().mode(), + selected_bot: channel_ref.cfg().def_bot_id().cloned(), ..Default::default() }); @@ -120,25 +101,27 @@ pub fn w_modify_channel_modal( title: "Channel Settings", size: Size::new(480., 530.), confirm_cb: Box::new(move || { + let _ = || $ui_state.write(); let rename = $channel_rename; - $channel.write().set_name(rename.text().to_string()); + { + let mut mgr = $channel_mgr.write(); + let mut cfg = mgr.channel(&channel_id).unwrap().cfg().clone(); - if let Some(bot_id) = $channel_state.selected_bot.clone() { - let mut cfg = $channel.cfg().clone(); - cfg.set_def_bot_id(Some(bot_id)); - $channel.write().set_cfg(cfg); - } + mgr.update_channel_name(&channel_id, rename.text().to_string()); + if let Some(bot_id) = $channel_state.selected_bot.clone() { + cfg.set_def_bot_id(Some(bot_id)); + } - if $channel_state.channel_mode != $channel.cfg().mode() { - let mut cfg = $channel.cfg().clone(); - cfg.set_mode($channel_state.channel_mode); - $channel.write().set_cfg(cfg); + if $channel_state.channel_mode != cfg.mode() { + cfg.set_mode($channel_state.channel_mode); + } + mgr.update_channel_cfg(&channel_id, cfg); } - - app_writer_confirm_ref.write().set_modify_channel_id(None); + ui_state.write().set_modify_channel_id(None); }) as Box, cancel_cb: Box::new(move || { - app_writer_cancel_ref.write().set_modify_channel_id(None); + let _ = || $ui_state.write(); + ui_state.write().set_modify_channel_id(None); }) as Box, @Stack { @Column { @@ -166,8 +149,11 @@ pub fn w_modify_channel_modal( @$selected_bot_box { @ { pipe! { - let app_ref = $app; - let bot = app_ref.data.info().get_bot_or_default($channel_state.selected_bot.as_ref()); + let bot_id = $channel_state + .selected_bot.as_ref() + .map_or_else(|| $config.default_bot_id(), |id| id.clone()); + let bots = $config.bots(); + let bot = bots.iter().find(|b| b.id() == &bot_id).unwrap(); @ListItem { on_tap: move |e| { if !$channel_state.bot_list_visible { diff --git a/gui/src/widgets/permission.rs b/gui/src/widgets/permission.rs index 5263380..3a83e9c 100644 --- a/gui/src/widgets/permission.rs +++ b/gui/src/widgets/permission.rs @@ -1,13 +1,11 @@ use ribir::prelude::*; -use super::app::AppGUI; - use crate::{ style::BLACK, widgets::common::{Carousel, DoubleColumn, LeftColumn, RightColumn}, }; -pub(super) fn w_permission(app: impl StateWriter) -> impl WidgetBuilder { +pub(super) fn w_permission() -> impl WidgetBuilder { fn_widget! { @DoubleColumn { fixed_width: 390., diff --git a/gui/src/widgets/quick_launcher.rs b/gui/src/widgets/quick_launcher.rs deleted file mode 100644 index f8add4b..0000000 --- a/gui/src/widgets/quick_launcher.rs +++ /dev/null @@ -1,409 +0,0 @@ -use polestar_core::model::ChannelCfg; -use ribir::prelude::*; -use uuid::Uuid; - -use super::app::AppGUI; - -#[derive(Default, Clone)] -pub struct QuickLauncher { - pub channel_id: Uuid, - pub msg: Option, - pub selected_bot_id: Option, -} - -impl QuickLauncher { - pub fn new(channel_id: Uuid) -> Self { Self { channel_id, ..<_>::default() } } -} - -pub fn create_quick_launcher(app: impl StateWriter) { - if app.read().quick_launcher.is_none() { - let mut app = app.write(); - let channel_id = app - .data - .new_channel("Quick Launcher".to_owned(), None, ChannelCfg::default()); - app.data.info_mut().set_quick_launcher_id(Some(channel_id)); - let quick_launcher = QuickLauncher::new(channel_id); - app.quick_launcher = Some(quick_launcher); - } - - launcher::create_wnd(app.clone_writer()); -} - -#[cfg(target_os = "macos")] -pub mod launcher { - use std::time::Duration; - - use crate::{ - style::{COMMON_RADIUS, GAINSBORO_DFDFDF_FF, WHITE}, - theme::polestar_svg, - widgets::common::{w_avatar, BotList, IconButton}, - window::WindowInfo, - WINDOW_MGR, - }; - use icrate::AppKit::{ - NSApplication, NSWindowStyleMaskFullSizeContentView, NSWindowStyleMaskTitled, - }; - use polestar_core::{ - model::{Msg, MsgAction, MsgBody, MsgCont, MsgMeta, MsgRole}, - service::open_ai::mock_stream_string, - }; - use ribir::prelude::*; - use uuid::Uuid; - - use crate::widgets::app::AppGUI; - - static QUICK_LAUNCHER_WND_INIT_SIZE: Size = Size::new(780., 400.); - static QUICK_LAUNCHER_WND_SELECTED_BOT_SIZE: Size = Size::new(780., 60.); - static QUICK_LAUNCHER_CONTENT_SIZE: Size = Size::new(780., 380.); - static ICON_SIZE: Size = Size::new(20., 20.); - - fn set_quick_launcher_size(size: Size) { - let wnd_manager = WINDOW_MGR.lock().unwrap(); - let wnd = wnd_manager.quick_launcher.as_ref().unwrap(); - if let Some(wnd) = AppCtx::get_window(wnd.id) { - wnd.shell_wnd().borrow_mut().request_resize(size); - } - } - - pub fn create_wnd(app: impl StateWriter) { - let wnd = App::new_window( - w_quick_launcher(app.clone_writer()), - Some(QUICK_LAUNCHER_WND_INIT_SIZE), - ); - - wnd.shell_wnd().borrow_mut().set_decorations(false); - - { - let mut window_mgr = WINDOW_MGR.lock().unwrap(); - window_mgr.quick_launcher = Some(WindowInfo { id: wnd.id(), focused: None }); - } - - unsafe { - let ns_app = NSApplication::sharedApplication(); - // get the quick launcher window, it's index is 1. - let ns_wnd = ns_app.windows().objectAtIndex(1); - ns_wnd.setTitlebarAppearsTransparent(true); - ns_wnd.setTitleVisibility(1); - if let Some(close_btn) = ns_wnd.standardWindowButton(0) { - close_btn.setHidden(true); - } - if let Some(minimize_btn) = ns_wnd.standardWindowButton(1) { - minimize_btn.setHidden(true); - } - ns_wnd.setOpaque(false); - let style_mask = NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView; - ns_wnd.setStyleMask(style_mask); - } - - App::set_active_window(wnd.id()); - hide_main_window(); - } - - fn w_quick_launcher(app: impl StateWriter) -> impl WidgetBuilder { - fn_widget! { - @ConstrainedBox { - clamp: BoxClamp::EXPAND_BOTH, - border_radius: Radius::all(10.), - background: Color::from_u32(WHITE), - @ { w_quick_launcher_editor(app.clone_writer()) } - } - } - } - - fn w_quick_launcher_editor(app: impl StateWriter) -> impl WidgetBuilder { - fn_widget! { - let bot_list = @BotList { - bots: $app.data.info().bots_rc(), - visible: pipe! { - $app.quick_launcher.as_ref().map(|quick_launcher| { - quick_launcher.selected_bot_id.is_none() && quick_launcher.msg.is_none() - }).unwrap_or_default() - } - }; - - let input = @Input { - auto_focus: true, - }; - - let send_icon = @IconButton { - on_tap: move |_| { - - }, - @ { polestar_svg::SEND } - }; - - let msg_visibility = @Visibility { - visible: pipe!($app.has_quick_launcher_msg()) - }; - - let quick_launcher_app = app.clone_writer(); - - @FocusNode { - auto_focus: true, - @Column { - on_key_down_capture: move |e| { - match e.key_code() { - PhysicalKey::Code(KeyCode::Enter | KeyCode::NumpadEnter) => { - if $input.text().is_empty() { - if let Some((bot_id, _)) = $bot_list.selected_bot() { - if let Some(quick_launcher) = $app.write().quick_launcher.as_mut() { - quick_launcher.selected_bot_id = Some(bot_id); - } - e.stop_propagation(); - } - } else { - let input = $input; - let text = input.text(); - // create msg to channel. - let msg = Msg::new_user_text(text, MsgMeta::default()); - let id = *msg.id(); - - let mut app_write_ref = $app.write(); - let quick_launcher_id = app_write_ref.quick_launcher_id().expect("quick launcher id is none"); - let channel = app_write_ref.data.get_channel_mut(&quick_launcher_id).unwrap(); - - channel.add_msg(msg); - // receive msg from response. - - // TODO: get bot id - let bot_id = Uuid::nil(); - let mut msg = Msg::new_bot_text(bot_id, MsgMeta::reply(id)); - msg.cur_cont_mut().action(MsgAction::Receiving(MsgBody::Text(Some("Hi".to_owned())))); - let id = *msg.id(); - channel.add_msg(msg); - - if let Some(msg) = channel.msg_mut(&id) { - let cur_cont = msg.cur_cont_mut(); - mock_stream_string("", |delta| { - cur_cont.action(MsgAction::Receiving(MsgBody::Text(Some(delta.to_string())))); - }); - } - - if let Some(quick_launcher) = app_write_ref.quick_launcher.as_mut() { - quick_launcher.msg = Some(id); - } - - e.stop_propagation(); - } - } - PhysicalKey::Code(KeyCode::KeyJ) => { - if e.with_logo_key() { - let channel_id = $app.quick_launcher.as_ref().unwrap().channel_id; - $app.write().data.switch_channel(&channel_id); - hide_quick_launcher(app.clone_writer(), true); - } - } - PhysicalKey::Code(KeyCode::KeyR) => { - if e.with_logo_key() { - - } - } - PhysicalKey::Code(KeyCode::Backspace) => {} - PhysicalKey::Code(KeyCode::Escape) => { - hide_quick_launcher(app.clone_writer(), false); - e.stop_propagation(); - } - _ => {} - } - }, - on_key_down: move |e| { - match e.key_code() { - PhysicalKey::Code(KeyCode::ArrowUp) => { - $bot_list.write().move_up(); - } - PhysicalKey::Code(KeyCode::ArrowDown) => { - $bot_list.write().move_down(); - } - _ => {} - } - }, - @ConstrainedBox { - clamp: BoxClamp::fixed_height(60.), - padding: EdgeInsets::new(0., 10., 0., 8.), - @Row { - @ { - pipe!{ - $app.quick_launcher.as_ref().and_then(|quick_launcher| { - quick_launcher.selected_bot_id - }) - }.map(move |id| { - let app = app.clone_writer(); - id.and_then(move |id| { - set_quick_launcher_size(QUICK_LAUNCHER_WND_SELECTED_BOT_SIZE); - let app_ref = $app; - let bot = app_ref.data.info().bots().iter().find(|bot| bot.id() == &id); - bot.map(|bot| { - w_avatar(bot.avatar().clone()) - }) - }) - }) - } - @$input { @ { Placeholder::new("Type a message") } } - @ { send_icon } - } - } - @Divider {} - @Expanded { - flex: 1., - @Stack { - fit: StackFit::Expand, - @ { bot_list } - @$msg_visibility { - @ { - w_quick_launcher_msg(quick_launcher_app.clone_writer()) - } - } - } - } - } - } - } - } - - fn w_quick_launcher_msg(app: impl StateWriter) -> impl WidgetBuilder { - fn_widget! { - @Column { - padding: EdgeInsets::new(10., 8., 10., 8.), - @Container { - size: QUICK_LAUNCHER_CONTENT_SIZE, - background: Color::from_u32(GAINSBORO_DFDFDF_FF), - border_radius: COMMON_RADIUS, - @Stack { - @VScrollBar { - margin: EdgeInsets::new(12., 12., 40., 12.), - @Column { - @ { - pipe! { - $app.quick_launcher.as_ref().and_then(|quick_launcher| { - quick_launcher.msg.as_ref().and_then(|msg_id| { - set_quick_launcher_size(QUICK_LAUNCHER_CONTENT_SIZE); - let app_ref = $app; - let quick_launcher_id = app_ref.quick_launcher.as_ref().unwrap().channel_id; - let channel = app_ref.data.get_channel(&quick_launcher_id).unwrap(); - channel.msg(msg_id).and_then(|msg| { - msg.cur_cont_ref().text().map(|text| { - @TextSelectable { - cursor: CursorIcon::Text, - @Text { - text: text.to_owned(), - foreground: Palette::of(ctx!()).on_background(), - overflow: Overflow::AutoWrap, - text_style: TypographyTheme::of(ctx!()).body_medium.text.clone(), - } - } - }) - }) - }) - }) - } - } - } - } - @Row { - item_gap: 4., - align_items: Align::Center, - anchor: Anchor::right_bottom(15., 15.), - @Text { - text: "Copy to clipboard", - } - @ { w_label_button("⏎") } - @Text { text: " | " } - @Text { text: "Retry" } - @ { w_label_button("⌘") } - @ { w_label_button("R") } - } - } - } - @Row { - justify_content: JustifyContent::SpaceBetween, - margin: EdgeInsets::only_top(10.), - @Row { - align_items: Align::Center, - item_gap: 4., - @Text { - text: "Back", - } - @ { w_label_button("Esc") } - } - @Row { - align_items: Align::Center, - item_gap: 4., - @Text { - text: "Continue in chat", - } - @ { w_label_button("⌘") } - @ { w_label_button("J") } - } - } - - } - } - } - - fn w_label_button(str: &str) -> impl WidgetBuilder + '_ { - fn_widget! { - @SizedBox { - size: ICON_SIZE, - border_radius: Radius::all(4.), - background: Color::from_u32(GAINSBORO_DFDFDF_FF), - @Text { - h_align: HAlign::Center, - text: str.to_owned() - } - } - } - } - - pub fn hide_quick_launcher(app: impl StateWriter, is_need_active: bool) { - let is_hidden = hide_app(); - let app = app.clone_writer(); - - timer(is_hidden, Duration::from_millis(100), AppCtx::scheduler()).subscribe(move |is_hidden| { - app.write().quick_launcher = None; - let mut window_mgr = WINDOW_MGR.lock().unwrap(); - window_mgr.dispose_quick_launcher(); - - window_mgr.main.as_ref().map(|wnd_info| { - if let Some(wnd) = AppCtx::get_window(wnd_info.id) { - wnd.shell_wnd().borrow_mut().set_visible(true); - } - }); - - if !is_hidden { - unsafe { - let ns_app = NSApplication::sharedApplication(); - ns_app.activateIgnoringOtherApps(is_need_active); - } - } - }); - } - - fn hide_main_window() { - let mut window_mgr = WINDOW_MGR.lock().unwrap(); - if let Some(wnd_info) = window_mgr.main.as_ref() { - if let Some(wnd) = AppCtx::get_window(wnd_info.id) { - wnd.shell_wnd().borrow_mut().set_visible(false); - } - } - } - - fn hide_app() -> bool { - unsafe { - let ns_app = NSApplication::sharedApplication(); - let is_hidden = ns_app.isHidden(); - ns_app.hide(None); - is_hidden - } - } -} - -#[cfg(target_os = "windows")] -pub mod launcher { - use super::QuickLauncher; - use crate::widgets::app::AppGUI; - use ribir::prelude::*; - - pub fn create_wnd(app: impl StateWriter) {} - - pub fn hide_quick_launcher(app: impl StateWriter, is_need_active: bool) {} -}