feat(profiles): add local on-device profile hiding and filtering
This commit is contained in:
1 parent
5c8ab7ed6c
commit
cdcecb9734
11 files changed
+240
-10
No files matched your search
@@ -25,6 +25,8 @@ data class ServerStoreState(
|
|||||||
// App display language. "system" = follow device locale; otherwise a BCP-47
|
// App display language. "system" = follow device locale; otherwise a BCP-47
|
||||||
// language code such as "en" or "ko". Applied via ContextWrapper in MainActivity.
|
// language code such as "en" or "ko". Applied via ContextWrapper in MainActivity.
|
||||||
val appLanguage: String = "system",
|
val appLanguage: String = "system",
|
||||||
|
// Local on-device hidden profile names (filtered out from list views unless revealed).
|
||||||
|
val hiddenProfiles: List<String> = emptyList(),
|
||||||
// App version the silent update check (issue #867) last completed for.
|
// App version the silent update check (issue #867) last completed for.
|
||||||
// Null / mismatched with BuildConfig.VERSION_NAME → the About tab runs
|
// Null / mismatched with BuildConfig.VERSION_NAME → the About tab runs
|
||||||
// its one-time check again (re-check on app version bump).
|
// its one-time check again (re-check on app version bump).
|
||||||
|
|||||||
@@ -357,6 +357,28 @@ object AuthManager {
|
|||||||
serverStore.update { it.copy(pinnedModels = pinned) }
|
serverStore.update { it.copy(pinnedModels = pinned) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Hidden Profiles (Local On-Device Filtering) ──────────────────────
|
||||||
|
|
||||||
|
fun getHiddenProfiles(): List<String> = serverStore.getLatestState().hiddenProfiles
|
||||||
|
|
||||||
|
fun isProfileHidden(name: String): Boolean = serverStore.getLatestState().hiddenProfiles.contains(name)
|
||||||
|
|
||||||
|
fun hideProfile(name: String) {
|
||||||
|
serverStore.update { s ->
|
||||||
|
s.copy(hiddenProfiles = (s.hiddenProfiles + name).distinct())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unhideProfile(name: String) {
|
||||||
|
serverStore.update { s ->
|
||||||
|
s.copy(hiddenProfiles = s.hiddenProfiles.filter { it != name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHiddenProfiles(hidden: List<String>) {
|
||||||
|
serverStore.update { it.copy(hiddenProfiles = hidden.distinct()) }
|
||||||
|
}
|
||||||
|
|
||||||
fun getProfileToken(profileId: String): String? = requirePrefs().getString("token_$profileId", null)
|
fun getProfileToken(profileId: String): String? = requirePrefs().getString("token_$profileId", null)
|
||||||
|
|
||||||
fun setProfileToken(
|
fun setProfileToken(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.bots
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||||
import com.m57.hermescontrol.data.model.BotRosterMeta
|
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||||
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
@@ -47,12 +48,13 @@ data class BotsUiState(
|
|||||||
val activeProfileName: String? = null,
|
val activeProfileName: String? = null,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
val showHidden: Boolean = false,
|
val showHidden: Boolean = false,
|
||||||
|
val hiddenProfiles: Set<String> = emptySet(),
|
||||||
val selectedTab: BotsTab = BotsTab.BOTS,
|
val selectedTab: BotsTab = BotsTab.BOTS,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
) {
|
) {
|
||||||
val hasHiddenBots: Boolean
|
val hasHiddenBots: Boolean
|
||||||
get() = profiles.any { it.isHidden }
|
get() = profiles.any { it.isHidden || it.name in hiddenProfiles }
|
||||||
|
|
||||||
val allGroups: List<GroupInfo>
|
val allGroups: List<GroupInfo>
|
||||||
get() {
|
get() {
|
||||||
@@ -185,7 +187,8 @@ data class BotsUiState(
|
|||||||
val query = searchQuery.trim().lowercase()
|
val query = searchQuery.trim().lowercase()
|
||||||
return profiles
|
return profiles
|
||||||
.filter { profile ->
|
.filter { profile ->
|
||||||
if (!showHidden && profile.isHidden) return@filter false
|
val isHidden = profile.isHidden || profile.name in hiddenProfiles
|
||||||
|
if (!showHidden && isHidden) return@filter false
|
||||||
if (query.isBlank()) return@filter true
|
if (query.isBlank()) return@filter true
|
||||||
profile.name.lowercase().contains(query) ||
|
profile.name.lowercase().contains(query) ||
|
||||||
profile.effectiveTitle.lowercase().contains(query) ||
|
profile.effectiveTitle.lowercase().contains(query) ||
|
||||||
@@ -212,6 +215,7 @@ class BotsViewModel(
|
|||||||
val uiState: StateFlow<BotsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<BotsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
_uiState.update { it.copy(hiddenProfiles = AuthManager.getHiddenProfiles().toSet()) }
|
||||||
if (autoLoad) {
|
if (autoLoad) {
|
||||||
loadBots()
|
loadBots()
|
||||||
}
|
}
|
||||||
@@ -262,6 +266,7 @@ class BotsViewModel(
|
|||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
profiles = profilesWithMeta,
|
profiles = profilesWithMeta,
|
||||||
activeProfileName = activeName ?: it.activeProfileName,
|
activeProfileName = activeName ?: it.activeProfileName,
|
||||||
|
hiddenProfiles = AuthManager.getHiddenProfiles().toSet(),
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -273,6 +278,7 @@ class BotsViewModel(
|
|||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
profiles = profilesResult.data.profiles.orEmpty(),
|
profiles = profilesResult.data.profiles.orEmpty(),
|
||||||
activeProfileName = activeName ?: it.activeProfileName,
|
activeProfileName = activeName ?: it.activeProfileName,
|
||||||
|
hiddenProfiles = AuthManager.getHiddenProfiles().toSet(),
|
||||||
errorMessage = null,
|
errorMessage = null,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1901,7 +1901,10 @@ class ChatViewModel(
|
|||||||
// Fetch available bot profiles for @ autocomplete
|
// Fetch available bot profiles for @ autocomplete
|
||||||
val profilesResult = safeApiCall { ApiClient.hermesApi.getProfiles() }
|
val profilesResult = safeApiCall { ApiClient.hermesApi.getProfiles() }
|
||||||
if (profilesResult is NetworkResult.Success) {
|
if (profilesResult is NetworkResult.Success) {
|
||||||
val profiles = profilesResult.data.profiles.orEmpty()
|
val profiles =
|
||||||
|
profilesResult.data.profiles.orEmpty().filter {
|
||||||
|
!AuthManager.isProfileHidden(it.name) && it.botMeta()?.hidden != true
|
||||||
|
}
|
||||||
_uiState.update { it.copy(availableBots = profiles) }
|
_uiState.update { it.copy(availableBots = profiles) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Visibility
|
||||||
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
@@ -49,6 +51,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.ClipEntry
|
import androidx.compose.ui.platform.ClipEntry
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -131,6 +134,29 @@ fun ProfilesScreen(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isBuildingProfile) {
|
if (!isBuildingProfile) {
|
||||||
|
if (state.hasHiddenProfiles) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.toggleShowHidden() },
|
||||||
|
modifier = Modifier.testTag("profiles_action_toggle_hidden"),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector =
|
||||||
|
if (state.showHidden) {
|
||||||
|
Icons.Default.VisibilityOff
|
||||||
|
} else {
|
||||||
|
Icons.Default.Visibility
|
||||||
|
},
|
||||||
|
contentDescription =
|
||||||
|
stringResource(
|
||||||
|
if (state.showHidden) {
|
||||||
|
R.string.profiles_hide_hidden
|
||||||
|
} else {
|
||||||
|
R.string.profiles_show_hidden
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
isBuildingProfile = true
|
isBuildingProfile = true
|
||||||
viewModel.loadBuilderData()
|
viewModel.loadBuilderData()
|
||||||
@@ -173,6 +199,16 @@ fun ProfilesScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state.displayProfiles.isEmpty() -> {
|
||||||
|
EmptyState(
|
||||||
|
title = stringResource(R.string.profiles_empty_title),
|
||||||
|
subtitle = stringResource(R.string.profiles_all_hidden_desc),
|
||||||
|
onAction = { viewModel.toggleShowHidden() },
|
||||||
|
actionLabel = stringResource(R.string.profiles_show_hidden),
|
||||||
|
modifier = Modifier.padding(paddingValues),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (state.isLoading && state.profiles.isEmpty()) {
|
if (state.isLoading && state.profiles.isEmpty()) {
|
||||||
@@ -194,7 +230,7 @@ fun ProfilesScreen(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
items(state.profiles, key = { it.name }) { profile ->
|
items(state.displayProfiles, key = { it.name }) { profile ->
|
||||||
val isActive = profile.name == state.activeProfileName
|
val isActive = profile.name == state.activeProfileName
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
@@ -225,11 +261,37 @@ fun ProfilesScreen(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
text = profile.name.replaceFirstChar { it.uppercase() },
|
Text(
|
||||||
style = MaterialTheme.typography.titleLarge,
|
text = profile.name.replaceFirstChar { it.uppercase() },
|
||||||
fontWeight = FontWeight.Bold,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
)
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
val isProfileHidden =
|
||||||
|
profile.name in state.hiddenProfiles ||
|
||||||
|
profile.botMeta()?.hidden == true
|
||||||
|
if (isProfileHidden) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(
|
||||||
|
MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
).padding(horizontal = 6.dp, vertical = 2.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text =
|
||||||
|
stringResource(
|
||||||
|
R.string.profiles_hidden_badge,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
val descriptionText =
|
val descriptionText =
|
||||||
if (!profile.description.isNullOrBlank()) {
|
if (!profile.description.isNullOrBlank()) {
|
||||||
profile.description
|
profile.description
|
||||||
@@ -376,6 +438,30 @@ fun ProfilesScreen(
|
|||||||
setupCmdProfileName = profile.name
|
setupCmdProfileName = profile.name
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
val isProfileHidden =
|
||||||
|
profile.name in state.hiddenProfiles ||
|
||||||
|
profile.botMeta()?.hidden == true
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (isProfileHidden) {
|
||||||
|
R.string.profiles_action_unhide
|
||||||
|
} else {
|
||||||
|
R.string.profiles_action_hide
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
|
showMenu = false
|
||||||
|
if (isProfileHidden) {
|
||||||
|
viewModel.unhideProfile(profile.name)
|
||||||
|
} else {
|
||||||
|
viewModel.hideProfile(profile.name)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
if (profile.is_default != true) {
|
if (profile.is_default != true) {
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ data class ProfilesUiState(
|
|||||||
val isLoadingSoul: Boolean = false,
|
val isLoadingSoul: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
|
val showHidden: Boolean = false,
|
||||||
|
val hiddenProfiles: Set<String> = emptySet(),
|
||||||
// Rename / delete / auto-describe / setup-command states
|
// Rename / delete / auto-describe / setup-command states
|
||||||
val isAutoDescribing: Boolean = false,
|
val isAutoDescribing: Boolean = false,
|
||||||
val setupCommand: String? = null,
|
val setupCommand: String? = null,
|
||||||
@@ -50,7 +52,20 @@ data class ProfilesUiState(
|
|||||||
val availableSkills: List<Skill> = emptyList(),
|
val availableSkills: List<Skill> = emptyList(),
|
||||||
val hubSearchResults: List<HubSkill> = emptyList(),
|
val hubSearchResults: List<HubSkill> = emptyList(),
|
||||||
val isSearchingHub: Boolean = false,
|
val isSearchingHub: Boolean = false,
|
||||||
)
|
) {
|
||||||
|
val hasHiddenProfiles: Boolean
|
||||||
|
get() = profiles.any { it.name in hiddenProfiles || it.botMeta()?.hidden == true }
|
||||||
|
|
||||||
|
val displayProfiles: List<ProfileInfo>
|
||||||
|
get() =
|
||||||
|
profiles.filter { profile ->
|
||||||
|
if (showHidden) {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
!(profile.name in hiddenProfiles || profile.botMeta()?.hidden == true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class ProfilesViewModel(
|
class ProfilesViewModel(
|
||||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
@@ -59,6 +74,34 @@ class ProfilesViewModel(
|
|||||||
private val _uiState = MutableStateFlow(ProfilesUiState())
|
private val _uiState = MutableStateFlow(ProfilesUiState())
|
||||||
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
_uiState.update { it.copy(hiddenProfiles = AuthManager.getHiddenProfiles().toSet()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleShowHidden() {
|
||||||
|
_uiState.update { it.copy(showHidden = !it.showHidden) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hideProfile(name: String) {
|
||||||
|
AuthManager.hideProfile(name)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
hiddenProfiles = AuthManager.getHiddenProfiles().toSet(),
|
||||||
|
toastMessage = "Profile $name hidden",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun unhideProfile(name: String) {
|
||||||
|
AuthManager.unhideProfile(name)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
hiddenProfiles = AuthManager.getHiddenProfiles().toSet(),
|
||||||
|
toastMessage = "Profile $name unhidden",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun loadProfiles() {
|
fun loadProfiles() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -77,6 +120,7 @@ class ProfilesViewModel(
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
profiles = profilesResult.data.profiles.orEmpty(),
|
profiles = profilesResult.data.profiles.orEmpty(),
|
||||||
activeProfileName = activeResult.data.active,
|
activeProfileName = activeResult.data.active,
|
||||||
|
hiddenProfiles = AuthManager.getHiddenProfiles().toSet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -453,6 +453,12 @@
|
|||||||
<string name="profiles_action_set_model">모델 설정</string>
|
<string name="profiles_action_set_model">모델 설정</string>
|
||||||
<string name="profiles_action_clone">복제</string>
|
<string name="profiles_action_clone">복제</string>
|
||||||
<string name="profiles_action_edit_description">설명 편집</string>
|
<string name="profiles_action_edit_description">설명 편집</string>
|
||||||
|
<string name="profiles_action_hide">프로필 숨기기</string>
|
||||||
|
<string name="profiles_action_unhide">프로필 숨김 해제</string>
|
||||||
|
<string name="profiles_show_hidden">숨겨진 프로필 표시</string>
|
||||||
|
<string name="profiles_hide_hidden">숨겨진 프로필 숨기기</string>
|
||||||
|
<string name="profiles_hidden_badge">숨김</string>
|
||||||
|
<string name="profiles_all_hidden_desc">모든 프로필이 현재 숨겨져 있습니다. 아래를 탭하여 숨겨진 프로필을 표시하세요.</string>
|
||||||
<string name="profiles_content_desc_active">활성 프로필</string>
|
<string name="profiles_content_desc_active">활성 프로필</string>
|
||||||
<string name="profiles_title_edit_soul">소울 프로필 편집: %1$s</string>
|
<string name="profiles_title_edit_soul">소울 프로필 편집: %1$s</string>
|
||||||
<string name="profiles_title_set_model">모델 설정: %1$s</string>
|
<string name="profiles_title_set_model">모델 설정: %1$s</string>
|
||||||
|
|||||||
@@ -596,6 +596,12 @@
|
|||||||
<string name="profiles_action_set_model">设置模型</string>
|
<string name="profiles_action_set_model">设置模型</string>
|
||||||
<string name="profiles_action_clone">克隆</string>
|
<string name="profiles_action_clone">克隆</string>
|
||||||
<string name="profiles_action_edit_description">编辑描述</string>
|
<string name="profiles_action_edit_description">编辑描述</string>
|
||||||
|
<string name="profiles_action_hide">隐藏配置文件</string>
|
||||||
|
<string name="profiles_action_unhide">取消隐藏配置文件</string>
|
||||||
|
<string name="profiles_show_hidden">显示隐藏的配置文件</string>
|
||||||
|
<string name="profiles_hide_hidden">隐藏已隐藏的配置文件</string>
|
||||||
|
<string name="profiles_hidden_badge">已隐藏</string>
|
||||||
|
<string name="profiles_all_hidden_desc">所有配置文件当前均已隐藏。点击下方以显示已隐藏的配置文件。</string>
|
||||||
<string name="profiles_content_desc_active">当前配置</string>
|
<string name="profiles_content_desc_active">当前配置</string>
|
||||||
<string name="profiles_title_edit_soul">编辑 Soul 配置:%1$s</string>
|
<string name="profiles_title_edit_soul">编辑 Soul 配置:%1$s</string>
|
||||||
<string name="profiles_title_set_model">设置模型:%1$s</string>
|
<string name="profiles_title_set_model">设置模型:%1$s</string>
|
||||||
|
|||||||
@@ -682,6 +682,12 @@
|
|||||||
<string name="profiles_action_set_model">Set Model</string>
|
<string name="profiles_action_set_model">Set Model</string>
|
||||||
<string name="profiles_action_clone">Clone</string>
|
<string name="profiles_action_clone">Clone</string>
|
||||||
<string name="profiles_action_edit_description">Edit Description</string>
|
<string name="profiles_action_edit_description">Edit Description</string>
|
||||||
|
<string name="profiles_action_hide">Hide Profile</string>
|
||||||
|
<string name="profiles_action_unhide">Unhide Profile</string>
|
||||||
|
<string name="profiles_show_hidden">Show hidden profiles</string>
|
||||||
|
<string name="profiles_hide_hidden">Hide hidden profiles</string>
|
||||||
|
<string name="profiles_hidden_badge">Hidden</string>
|
||||||
|
<string name="profiles_all_hidden_desc">All profiles are currently hidden. Tap below to show hidden profiles.</string>
|
||||||
<string name="profiles_content_desc_active">Active Profile</string>
|
<string name="profiles_content_desc_active">Active Profile</string>
|
||||||
<string name="profiles_title_edit_soul">Edit Soul Profile: %1$s</string>
|
<string name="profiles_title_edit_soul">Edit Soul Profile: %1$s</string>
|
||||||
<string name="profiles_title_set_model">Set Model: %1$s</string>
|
<string name="profiles_title_set_model">Set Model: %1$s</string>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.m57.hermescontrol.ui.bots
|
package com.m57.hermescontrol.ui.bots
|
||||||
|
|
||||||
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||||
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
import com.m57.hermescontrol.data.model.BotAvatarMeta
|
||||||
import com.m57.hermescontrol.data.model.BotRosterMeta
|
import com.m57.hermescontrol.data.model.BotRosterMeta
|
||||||
@@ -45,6 +46,8 @@ class BotsViewModelTest {
|
|||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
Dispatchers.setMain(testDispatcher)
|
Dispatchers.setMain(testDispatcher)
|
||||||
|
mockkObject(AuthManager)
|
||||||
|
every { AuthManager.getHiddenProfiles() } returns emptyList()
|
||||||
mockkObject(ApiClient)
|
mockkObject(ApiClient)
|
||||||
mockApi = mockk(relaxed = true)
|
mockApi = mockk(relaxed = true)
|
||||||
every { ApiClient.hermesApi } returns mockApi
|
every { ApiClient.hermesApi } returns mockApi
|
||||||
@@ -57,6 +60,7 @@ class BotsViewModelTest {
|
|||||||
|
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
|
unmockkObject(AuthManager)
|
||||||
unmockkObject(ApiClient)
|
unmockkObject(ApiClient)
|
||||||
unmockkObject(HermesWsClient)
|
unmockkObject(HermesWsClient)
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ class ProfilesViewModelTest {
|
|||||||
private var storedPinnedModels: MutableList<PinnedModel> = mutableListOf()
|
private var storedPinnedModels: MutableList<PinnedModel> = mutableListOf()
|
||||||
private var storedSelectedProfile: String? = null
|
private var storedSelectedProfile: String? = null
|
||||||
private var storedProfileToken: String? = null
|
private var storedProfileToken: String? = null
|
||||||
|
private var storedHiddenProfiles: MutableList<String> = mutableListOf()
|
||||||
|
|
||||||
private fun stubProfilesLoad() {
|
private fun stubProfilesLoad() {
|
||||||
coEvery { mockApi.getProfiles() } returns
|
coEvery { mockApi.getProfiles() } returns
|
||||||
@@ -88,6 +89,15 @@ class ProfilesViewModelTest {
|
|||||||
every { AuthManager.setProfileToken(any(), any()) } answers {
|
every { AuthManager.setProfileToken(any(), any()) } answers {
|
||||||
storedProfileToken = secondArg<String?>()
|
storedProfileToken = secondArg<String?>()
|
||||||
}
|
}
|
||||||
|
every { AuthManager.getHiddenProfiles() } answers { storedHiddenProfiles.toList() }
|
||||||
|
every { AuthManager.hideProfile(any()) } answers {
|
||||||
|
val name = firstArg<String>()
|
||||||
|
if (!storedHiddenProfiles.contains(name)) storedHiddenProfiles.add(name)
|
||||||
|
}
|
||||||
|
every { AuthManager.unhideProfile(any()) } answers {
|
||||||
|
val name = firstArg<String>()
|
||||||
|
storedHiddenProfiles.remove(name)
|
||||||
|
}
|
||||||
|
|
||||||
mockkObject(HermesWsClient)
|
mockkObject(HermesWsClient)
|
||||||
every { HermesWsClient.disconnect() } returns Unit
|
every { HermesWsClient.disconnect() } returns Unit
|
||||||
@@ -345,4 +355,39 @@ class ProfilesViewModelTest {
|
|||||||
assertTrue(vm.uiState.value.toastMessage!!.contains("Failed to clone profile"))
|
assertTrue(vm.uiState.value.toastMessage!!.contains("Failed to clone profile"))
|
||||||
assertFalse(vm.uiState.value.isLoading)
|
assertFalse(vm.uiState.value.isLoading)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `hideProfile and unhideProfile update local hidden state and display filtering`() {
|
||||||
|
coEvery { mockApi.getProfiles() } returns
|
||||||
|
Response.success(
|
||||||
|
ProfilesResponse(
|
||||||
|
listOf(
|
||||||
|
ProfileInfo(name = "default", is_default = true),
|
||||||
|
ProfileInfo(name = "secret-bot"),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.loadProfiles()
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertEquals(2, vm.uiState.value.displayProfiles.size)
|
||||||
|
assertFalse(vm.uiState.value.hasHiddenProfiles)
|
||||||
|
|
||||||
|
vm.hideProfile("secret-bot")
|
||||||
|
assertEquals(listOf("secret-bot"), storedHiddenProfiles)
|
||||||
|
assertTrue(vm.uiState.value.hasHiddenProfiles)
|
||||||
|
assertEquals(listOf("default"), vm.uiState.value.displayProfiles.map { it.name })
|
||||||
|
|
||||||
|
// Toggle show hidden
|
||||||
|
vm.toggleShowHidden()
|
||||||
|
assertTrue(vm.uiState.value.showHidden)
|
||||||
|
assertEquals(listOf("default", "secret-bot"), vm.uiState.value.displayProfiles.map { it.name })
|
||||||
|
|
||||||
|
// Unhide
|
||||||
|
vm.unhideProfile("secret-bot")
|
||||||
|
assertTrue(storedHiddenProfiles.isEmpty())
|
||||||
|
assertFalse(vm.uiState.value.hasHiddenProfiles)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user