mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-31 11:01:36 +08:00
refactor(profiles): shared project entity with per-scope switching
Projects are now global shared entities; Claude and Codex groups switch independently via scoped current pointers and scoped payload slots. - Remove scope column from profiles; keep current_profile_id_<scope> - Use Option<Vec<String>> for mcp/skills to distinguish 'never captured' from 'captured empty', preventing cross-side accidental disable - Update/apply operations scoped to the active group via merge_scope_from - Tray menu nests same shared list under Claude Code/Codex groups - Add i18n for per-scope tooltips and 'not saved for this side' hint Refs: profile P1 shared-entity redesign
This commit is contained in:
+85
-21
@@ -338,34 +338,54 @@ fn sort_providers(
|
||||
|
||||
/// 处理项目 Profile 托盘事件,返回是否已处理
|
||||
///
|
||||
/// 事件 id 形如 `profile_<uuid>`;`profile_none` 表示"不使用项目"(只清标记,不动配置)。
|
||||
/// 事件 id 形如 `profile_<scope>_<uuid>`(同一项目在各分组子菜单里各有一项,
|
||||
/// 应用时只作用于该分组);`profile_none_<scope>` 表示某分组"不使用项目"
|
||||
/// (只清该分组标记,不动配置)。
|
||||
pub fn handle_profile_tray_event(app: &tauri::AppHandle, event_id: &str) -> bool {
|
||||
let Some(suffix) = event_id.strip_prefix("profile_") else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if suffix == "none" {
|
||||
if let Some(scope_str) = suffix.strip_prefix("none_") {
|
||||
let Ok(scope) = crate::services::profile::ProfileScope::parse(scope_str) else {
|
||||
log::error!("未知的项目分组托盘事件: {event_id}");
|
||||
return true;
|
||||
};
|
||||
if let Some(app_state) = app.try_state::<AppState>() {
|
||||
if let Err(e) = app_state.db.set_current_profile_id(None) {
|
||||
if let Err(e) = app_state.db.set_current_profile_id(scope.as_str(), None) {
|
||||
log::error!("清除当前项目失败: {e}");
|
||||
}
|
||||
}
|
||||
// 通知主窗口刷新(profileId=null 表示已清除当前项目)
|
||||
if let Err(e) = app.emit("profile-applied", serde_json::json!({ "profileId": null })) {
|
||||
// 通知主窗口刷新(profileId=null 表示该分组已清除当前项目)
|
||||
if let Err(e) = app.emit(
|
||||
"profile-applied",
|
||||
serde_json::json!({ "profileId": null, "scope": scope.as_str() }),
|
||||
) {
|
||||
log::error!("发射 profile-applied 事件失败: {e}");
|
||||
}
|
||||
refresh_tray_menu(app);
|
||||
return true;
|
||||
}
|
||||
|
||||
log::info!("应用项目: {suffix}");
|
||||
// scope 是固定枚举字符串(不含下划线),uuid 只含连字符,首个下划线即分界
|
||||
let Some((scope_str, profile_id)) = suffix.split_once('_') else {
|
||||
log::error!("无法解析项目托盘事件: {event_id}");
|
||||
return true;
|
||||
};
|
||||
let Ok(scope) = crate::services::profile::ProfileScope::parse(scope_str) else {
|
||||
log::error!("未知的项目分组托盘事件: {event_id}");
|
||||
return true;
|
||||
};
|
||||
|
||||
log::info!("应用项目: {profile_id}({scope_str} 组)");
|
||||
let app_handle = app.clone();
|
||||
let profile_id = suffix.to_string();
|
||||
let profile_id = profile_id.to_string();
|
||||
tauri::async_runtime::spawn_blocking(move || {
|
||||
let Some(app_state) = app_handle.try_state::<AppState>() else {
|
||||
return;
|
||||
};
|
||||
match crate::services::profile::ProfileService::apply(app_state.inner(), &profile_id) {
|
||||
match crate::services::profile::ProfileService::apply(app_state.inner(), &profile_id, scope)
|
||||
{
|
||||
Ok(warnings) => {
|
||||
for warning in &warnings {
|
||||
log::warn!("[Profile] 应用项目 {profile_id} 警告: {warning}");
|
||||
@@ -374,6 +394,7 @@ pub fn handle_profile_tray_event(app: &tauri::AppHandle, event_id: &str) -> bool
|
||||
&app_handle,
|
||||
app_state.inner(),
|
||||
&profile_id,
|
||||
scope,
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -664,40 +685,83 @@ pub fn create_tray_menu(
|
||||
menu_builder = menu_builder.separator();
|
||||
}
|
||||
|
||||
// 项目 Profile 子菜单(受支持应用至少一个可见且存在项目时才显示)
|
||||
if crate::services::profile::PROFILE_APPS
|
||||
.iter()
|
||||
.any(|app_type| visible_apps.is_visible(app_type))
|
||||
// 项目 Profile 子菜单:项目列表全应用共享,按分组嵌套子菜单各自勾选/应用
|
||||
// (组内应用可见且存在项目时才显示该组)
|
||||
{
|
||||
let profiles = app_state.db.get_all_profiles()?;
|
||||
if !profiles.is_empty() {
|
||||
let current_profile_id = app_state.db.get_current_profile_id()?.unwrap_or_default();
|
||||
let mut profiles_builder =
|
||||
SubmenuBuilder::with_id(app, "submenu_profiles", tray_texts.projects_label);
|
||||
use crate::services::profile::ProfileScope;
|
||||
|
||||
let any_scope_visible = ProfileScope::ALL.iter().any(|scope| {
|
||||
scope
|
||||
.apps()
|
||||
.iter()
|
||||
.any(|app_type| visible_apps.is_visible(app_type))
|
||||
});
|
||||
let profiles = if any_scope_visible {
|
||||
app_state.db.get_all_profiles()?
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
let mut scope_submenus = Vec::new();
|
||||
for scope in ProfileScope::ALL {
|
||||
if profiles.is_empty()
|
||||
|| !scope
|
||||
.apps()
|
||||
.iter()
|
||||
.any(|app_type| visible_apps.is_visible(app_type))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let current_profile_id = app_state
|
||||
.db
|
||||
.get_current_profile_id(scope.as_str())?
|
||||
.unwrap_or_default();
|
||||
// 分组标签用产品名,不进 i18n
|
||||
let scope_label = match scope {
|
||||
ProfileScope::Claude => "Claude Code",
|
||||
ProfileScope::Codex => "Codex",
|
||||
};
|
||||
let mut scope_builder = SubmenuBuilder::with_id(
|
||||
app,
|
||||
format!("submenu_profiles_{}", scope.as_str()),
|
||||
scope_label,
|
||||
);
|
||||
for profile in &profiles {
|
||||
let item = CheckMenuItem::with_id(
|
||||
app,
|
||||
format!("profile_{}", profile.id),
|
||||
format!("profile_{}_{}", scope.as_str(), profile.id),
|
||||
&profile.name,
|
||||
true,
|
||||
current_profile_id == profile.id,
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("创建项目菜单项失败: {e}")))?;
|
||||
profiles_builder = profiles_builder.item(&item);
|
||||
scope_builder = scope_builder.item(&item);
|
||||
}
|
||||
let none_item = CheckMenuItem::with_id(
|
||||
app,
|
||||
"profile_none",
|
||||
format!("profile_none_{}", scope.as_str()),
|
||||
tray_texts.no_project_label,
|
||||
true,
|
||||
current_profile_id.is_empty(),
|
||||
None::<&str>,
|
||||
)
|
||||
.map_err(|e| AppError::Message(format!("创建不使用项目菜单项失败: {e}")))?;
|
||||
let profiles_submenu = profiles_builder
|
||||
let scope_submenu = scope_builder
|
||||
.separator()
|
||||
.item(&none_item)
|
||||
.build()
|
||||
.map_err(|e| AppError::Message(format!("构建项目分组子菜单失败: {e}")))?;
|
||||
scope_submenus.push(scope_submenu);
|
||||
}
|
||||
|
||||
if !scope_submenus.is_empty() {
|
||||
let mut profiles_builder =
|
||||
SubmenuBuilder::with_id(app, "submenu_profiles", tray_texts.projects_label);
|
||||
for scope_submenu in &scope_submenus {
|
||||
profiles_builder = profiles_builder.item(scope_submenu);
|
||||
}
|
||||
let profiles_submenu = profiles_builder
|
||||
.build()
|
||||
.map_err(|e| AppError::Message(format!("构建项目子菜单失败: {e}")))?;
|
||||
menu_builder = menu_builder.item(&profiles_submenu).separator();
|
||||
|
||||
Reference in New Issue
Block a user