Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
db3f8c736e
commit
5bc6e5f855
14 files changed
+279
No files matched your search
@@ -54,10 +54,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
val themePreference by AuthManager.themePreferenceFlow.collectAsState()
|
val themePreference by AuthManager.themePreferenceFlow.collectAsState()
|
||||||
val useDynamicColors by AuthManager.useDynamicColorsFlow.collectAsState()
|
val useDynamicColors by AuthManager.useDynamicColorsFlow.collectAsState()
|
||||||
val themePreset by AuthManager.themePresetFlow.collectAsState()
|
val themePreset by AuthManager.themePresetFlow.collectAsState()
|
||||||
|
val chatFontScale by AuthManager.chatFontScaleFlow.collectAsState()
|
||||||
HermesControlTheme(
|
HermesControlTheme(
|
||||||
themePreference = themePreference,
|
themePreference = themePreference,
|
||||||
useDynamicColors = useDynamicColors,
|
useDynamicColors = useDynamicColors,
|
||||||
themePreset = themePreset,
|
themePreset = themePreset,
|
||||||
|
chatFontScale = chatFontScale,
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ data class ServerStoreState(
|
|||||||
val wsAuthParam: String = "token",
|
val wsAuthParam: String = "token",
|
||||||
val typingEffectEnabled: Boolean = true,
|
val typingEffectEnabled: Boolean = true,
|
||||||
val typingEffectDelayMs: Int = 30,
|
val typingEffectDelayMs: Int = 30,
|
||||||
|
val chatFontScale: Float = 1.0f,
|
||||||
// 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",
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ object AuthManager {
|
|||||||
private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT)
|
private val _themePresetFlow = MutableStateFlow<ThemePreset>(ThemePreset.DEFAULT)
|
||||||
val themePresetFlow: StateFlow<ThemePreset> = _themePresetFlow.asStateFlow()
|
val themePresetFlow: StateFlow<ThemePreset> = _themePresetFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val _chatFontScaleFlow = MutableStateFlow<Float>(1.0f)
|
||||||
|
val chatFontScaleFlow: StateFlow<Float> = _chatFontScaleFlow.asStateFlow()
|
||||||
|
|
||||||
private val _tokenFlow = MutableStateFlow<String?>(null)
|
private val _tokenFlow = MutableStateFlow<String?>(null)
|
||||||
val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow()
|
val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow()
|
||||||
|
|
||||||
@@ -180,6 +183,7 @@ object AuthManager {
|
|||||||
_themePreferenceFlow.value = state.themePreference
|
_themePreferenceFlow.value = state.themePreference
|
||||||
_useDynamicColorsFlow.value = state.useDynamicColors
|
_useDynamicColorsFlow.value = state.useDynamicColors
|
||||||
_themePresetFlow.value = state.themePreset
|
_themePresetFlow.value = state.themePreset
|
||||||
|
_chatFontScaleFlow.value = state.chatFontScale
|
||||||
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
|
// B7 (Jul 08 2026, kanban t_470): keep cookie scope aligned with active profile.
|
||||||
appScope?.launch { syncCookieStoreForProfile(state.selectedProfileId) }
|
appScope?.launch { syncCookieStoreForProfile(state.selectedProfileId) }
|
||||||
}
|
}
|
||||||
@@ -640,6 +644,15 @@ object AuthManager {
|
|||||||
serverStore.update { it.copy(typingEffectDelayMs = delayMs) }
|
serverStore.update { it.copy(typingEffectDelayMs = delayMs) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Chat Font Scale (issue #1004) ───────────────────────────────────
|
||||||
|
|
||||||
|
fun getChatFontScale(): Float = serverStore.getLatestState().chatFontScale
|
||||||
|
|
||||||
|
fun setChatFontScale(scale: Float) {
|
||||||
|
serverStore.update { it.copy(chatFontScale = scale) }
|
||||||
|
_chatFontScaleFlow.value = scale
|
||||||
|
}
|
||||||
|
|
||||||
// ── In-app update check (issue #867) ─────────────────────────────────
|
// ── In-app update check (issue #867) ─────────────────────────────────
|
||||||
|
|
||||||
/** App version the silent update check last completed for (null = never). */
|
/** App version the silent update check last completed for (null = never). */
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ enum class ThemePreset { DEFAULT, MONOCHROME, GRUVBOX, CATPPUCCIN, AMOLED, NORD
|
|||||||
|
|
||||||
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
val LocalThemePreference = compositionLocalOf { ThemePreference.SYSTEM }
|
||||||
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
val LocalThemePreset = compositionLocalOf { ThemePreset.DEFAULT }
|
||||||
|
val LocalChatFontScale = compositionLocalOf { 1.0f }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The 6 preset themes — one file each, all built from the same
|
* The 6 preset themes — one file each, all built from the same
|
||||||
@@ -75,6 +76,7 @@ fun HermesControlTheme(
|
|||||||
themePreference: ThemePreference = LocalThemePreference.current,
|
themePreference: ThemePreference = LocalThemePreference.current,
|
||||||
useDynamicColors: Boolean = true,
|
useDynamicColors: Boolean = true,
|
||||||
themePreset: ThemePreset = ThemePreset.DEFAULT,
|
themePreset: ThemePreset = ThemePreset.DEFAULT,
|
||||||
|
chatFontScale: Float = LocalChatFontScale.current,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val darkTheme =
|
val darkTheme =
|
||||||
@@ -103,6 +105,7 @@ fun HermesControlTheme(
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalThemePreference provides themePreference,
|
LocalThemePreference provides themePreference,
|
||||||
LocalThemePreset provides themePreset,
|
LocalThemePreset provides themePreset,
|
||||||
|
LocalChatFontScale provides chatFontScale,
|
||||||
LocalHermesStatusColors provides statusColors,
|
LocalHermesStatusColors provides statusColors,
|
||||||
LocalSpacing provides SpacingDefaults,
|
LocalSpacing provides SpacingDefaults,
|
||||||
LocalMotion provides MotionDefaults,
|
LocalMotion provides MotionDefaults,
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -43,14 +44,17 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.testTag
|
import androidx.compose.ui.platform.testTag
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.LocalChatFontScale
|
||||||
import com.m57.hermescontrol.ui.chat.MarkdownText
|
import com.m57.hermescontrol.ui.chat.MarkdownText
|
||||||
import com.m57.hermescontrol.ui.common.BotAvatar
|
import com.m57.hermescontrol.ui.common.BotAvatar
|
||||||
import com.m57.hermescontrol.ui.common.EmptyState
|
import com.m57.hermescontrol.ui.common.EmptyState
|
||||||
@@ -141,6 +145,17 @@ fun GroupChatScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
val currentDensity = LocalDensity.current
|
||||||
|
val chatFontScale = LocalChatFontScale.current
|
||||||
|
val chatDensity =
|
||||||
|
remember(currentDensity, chatFontScale) {
|
||||||
|
Density(
|
||||||
|
density = currentDensity.density,
|
||||||
|
fontScale = currentDensity.fontScale * chatFontScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalDensity provides chatDensity) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier =
|
modifier =
|
||||||
@@ -179,6 +194,7 @@ fun GroupChatScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── COMPOSER (uses TextFieldValue + embedded Send button matching ChatComposer) ──
|
// ── COMPOSER (uses TextFieldValue + embedded Send button matching ChatComposer) ──
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
|
import com.m57.hermescontrol.theme.LocalChatFontScale
|
||||||
import com.m57.hermescontrol.ui.chat.ChatBubble
|
import com.m57.hermescontrol.ui.chat.ChatBubble
|
||||||
import com.m57.hermescontrol.ui.chat.ChatMessage
|
import com.m57.hermescontrol.ui.chat.ChatMessage
|
||||||
import com.m57.hermescontrol.ui.chat.ChatSearchState
|
import com.m57.hermescontrol.ui.chat.ChatSearchState
|
||||||
@@ -116,6 +120,17 @@ fun FullBleedChatList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val currentDensity = LocalDensity.current
|
||||||
|
val chatFontScale = LocalChatFontScale.current
|
||||||
|
val chatDensity =
|
||||||
|
remember(currentDensity, chatFontScale) {
|
||||||
|
Density(
|
||||||
|
density = currentDensity.density,
|
||||||
|
fontScale = currentDensity.fontScale * chatFontScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalDensity provides chatDensity) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
@@ -293,6 +308,7 @@ fun FullBleedChatList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -191,6 +191,8 @@ internal fun SettingsChatPage(
|
|||||||
onTypingEffectEnabledChange = viewModel::onTypingEffectEnabledChange,
|
onTypingEffectEnabledChange = viewModel::onTypingEffectEnabledChange,
|
||||||
typingEffectDelayMs = state.typingEffectDelayMs,
|
typingEffectDelayMs = state.typingEffectDelayMs,
|
||||||
onTypingEffectDelayMsChange = viewModel::onTypingEffectDelayMsChange,
|
onTypingEffectDelayMsChange = viewModel::onTypingEffectDelayMsChange,
|
||||||
|
chatFontScale = state.chatFontScale,
|
||||||
|
onChatFontScaleChange = viewModel::onChatFontScaleChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ data class SettingsUiState(
|
|||||||
val isSaved: Boolean = false,
|
val isSaved: Boolean = false,
|
||||||
val typingEffectEnabled: Boolean = false,
|
val typingEffectEnabled: Boolean = false,
|
||||||
val typingEffectDelayMs: Int = 30,
|
val typingEffectDelayMs: Int = 30,
|
||||||
|
val chatFontScale: Float = 1.0f,
|
||||||
val profiles: List<ConnectionProfile> = emptyList(),
|
val profiles: List<ConnectionProfile> = emptyList(),
|
||||||
val selectedProfileId: String? = null,
|
val selectedProfileId: String? = null,
|
||||||
val renameProfileName: String = "",
|
val renameProfileName: String = "",
|
||||||
@@ -76,6 +77,7 @@ class SettingsViewModel(
|
|||||||
val themePreset = AuthManager.getThemePreset()
|
val themePreset = AuthManager.getThemePreset()
|
||||||
val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
|
val typingEffectEnabled = AuthManager.isTypingEffectEnabled()
|
||||||
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
|
val typingEffectDelayMs = AuthManager.getTypingEffectDelayMs()
|
||||||
|
val chatFontScale = AuthManager.getChatFontScale()
|
||||||
val profiles = AuthManager.getConnectionProfiles()
|
val profiles = AuthManager.getConnectionProfiles()
|
||||||
val appLanguage = AuthManager.getAppLanguage()
|
val appLanguage = AuthManager.getAppLanguage()
|
||||||
val renameProfileName =
|
val renameProfileName =
|
||||||
@@ -95,6 +97,7 @@ class SettingsViewModel(
|
|||||||
themePreset = themePreset,
|
themePreset = themePreset,
|
||||||
typingEffectEnabled = typingEffectEnabled,
|
typingEffectEnabled = typingEffectEnabled,
|
||||||
typingEffectDelayMs = typingEffectDelayMs,
|
typingEffectDelayMs = typingEffectDelayMs,
|
||||||
|
chatFontScale = chatFontScale,
|
||||||
profiles = profiles,
|
profiles = profiles,
|
||||||
selectedProfileId = selectedId,
|
selectedProfileId = selectedId,
|
||||||
renameProfileName = renameProfileName,
|
renameProfileName = renameProfileName,
|
||||||
@@ -310,6 +313,11 @@ class SettingsViewModel(
|
|||||||
AuthManager.setTypingEffectDelayMs(delayMs)
|
AuthManager.setTypingEffectDelayMs(delayMs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onChatFontScaleChange(scale: Float) {
|
||||||
|
_uiState.update { it.copy(chatFontScale = scale, isSaved = false) }
|
||||||
|
AuthManager.setChatFontScale(scale)
|
||||||
|
}
|
||||||
|
|
||||||
/** Clear all auth credentials — logs out and returns to landing screen. */
|
/** Clear all auth credentials — logs out and returns to landing screen. */
|
||||||
fun logout() {
|
fun logout() {
|
||||||
AuthManager.setToken(null)
|
AuthManager.setToken(null)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -18,22 +19,29 @@ import androidx.compose.material3.SegmentedButton
|
|||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||||
import androidx.compose.material3.Slider
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.SwitchDefaults
|
import androidx.compose.material3.SwitchDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.m57.hermescontrol.R
|
import com.m57.hermescontrol.R
|
||||||
import com.m57.hermescontrol.theme.ThemePreference
|
import com.m57.hermescontrol.theme.ThemePreference
|
||||||
import com.m57.hermescontrol.theme.ThemePreset
|
import com.m57.hermescontrol.theme.ThemePreset
|
||||||
import com.m57.hermescontrol.ui.settings.SectionCard
|
import com.m57.hermescontrol.ui.settings.SectionCard
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun AppearanceSection(
|
internal fun AppearanceSection(
|
||||||
@@ -194,10 +202,172 @@ internal fun ChatSection(
|
|||||||
onTypingEffectEnabledChange: (Boolean) -> Unit,
|
onTypingEffectEnabledChange: (Boolean) -> Unit,
|
||||||
typingEffectDelayMs: Int,
|
typingEffectDelayMs: Int,
|
||||||
onTypingEffectDelayMsChange: (Int) -> Unit,
|
onTypingEffectDelayMsChange: (Int) -> Unit,
|
||||||
|
chatFontScale: Float = 1.0f,
|
||||||
|
onChatFontScaleChange: (Float) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val fontScaleOptions = listOf(0.85f, 1.0f, 1.15f, 1.30f, 1.50f)
|
||||||
|
val currentIndex =
|
||||||
|
fontScaleOptions.indexOfFirst { abs(it - chatFontScale) < 0.05f }
|
||||||
|
.takeIf { it >= 0 } ?: 1
|
||||||
|
|
||||||
SectionCard {
|
SectionCard {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_item_chat_font_size),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_desc_chat_font_size),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val scaleLabel =
|
||||||
|
when (currentIndex) {
|
||||||
|
0 -> stringResource(R.string.settings_font_scale_small)
|
||||||
|
1 -> stringResource(R.string.settings_font_scale_default)
|
||||||
|
2 -> stringResource(R.string.settings_font_scale_large)
|
||||||
|
3 -> stringResource(R.string.settings_font_scale_xlarge)
|
||||||
|
else -> stringResource(R.string.settings_font_scale_huge)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = scaleLabel,
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
Slider(
|
||||||
|
value = currentIndex.toFloat(),
|
||||||
|
onValueChange = { onChatFontScaleChange(fontScaleOptions[it.roundToInt()]) },
|
||||||
|
valueRange = 0f..(fontScaleOptions.size - 1).toFloat(),
|
||||||
|
steps = fontScaleOptions.size - 2,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "0.85×",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "1.0×",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "1.5×",
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelSmall.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_chat_preview_title),
|
||||||
|
style =
|
||||||
|
MaterialTheme.typography.labelMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
val currentDensity = LocalDensity.current
|
||||||
|
val previewDensity =
|
||||||
|
remember(currentDensity, chatFontScale) {
|
||||||
|
Density(
|
||||||
|
density = currentDensity.density,
|
||||||
|
fontScale = currentDensity.fontScale * chatFontScale,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalDensity provides previewDensity) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
// User bubble preview
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape =
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 14.dp,
|
||||||
|
topEnd = 14.dp,
|
||||||
|
bottomStart = 14.dp,
|
||||||
|
bottomEnd = 4.dp,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_chat_preview_user),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assistant bubble preview
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.Start,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape =
|
||||||
|
RoundedCornerShape(
|
||||||
|
topStart = 14.dp,
|
||||||
|
topEnd = 14.dp,
|
||||||
|
bottomStart = 4.dp,
|
||||||
|
bottomEnd = 14.dp,
|
||||||
|
),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.settings_chat_preview_agent),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(20.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
|||||||
@@ -187,6 +187,17 @@
|
|||||||
<string name="settings_item_typing_delay">지연: %1$d ms</string>
|
<string name="settings_item_typing_delay">지연: %1$d ms</string>
|
||||||
<string name="settings_delay_10ms">10 ms</string>
|
<string name="settings_delay_10ms">10 ms</string>
|
||||||
<string name="settings_delay_100ms">100 ms</string>
|
<string name="settings_delay_100ms">100 ms</string>
|
||||||
|
<string name="settings_item_chat_font_size">채팅 글꼴 크기</string>
|
||||||
|
<string name="settings_desc_chat_font_size">채팅 메시지의 텍스트 크기를 조절합니다</string>
|
||||||
|
<string name="settings_font_scale_value">글꼴 크기: %1$s</string>
|
||||||
|
<string name="settings_font_scale_small">작게 (0.85×)</string>
|
||||||
|
<string name="settings_font_scale_default">기본 (1.0×)</string>
|
||||||
|
<string name="settings_font_scale_large">크게 (1.15×)</string>
|
||||||
|
<string name="settings_font_scale_xlarge">아주 크게 (1.3×)</string>
|
||||||
|
<string name="settings_font_scale_huge">최대 (1.5×)</string>
|
||||||
|
<string name="settings_chat_preview_title">미리보기</string>
|
||||||
|
<string name="settings_chat_preview_user">채팅 텍스트 크기를 조절할 수 있나요?</string>
|
||||||
|
<string name="settings_chat_preview_agent">완료되었습니다! 여기에서 글꼴 크기를 조정할 수 있습니다.</string>
|
||||||
<string name="settings_action_test_connection">연결 테스트</string>
|
<string name="settings_action_test_connection">연결 테스트</string>
|
||||||
<string name="settings_sec_about">정보</string>
|
<string name="settings_sec_about">정보</string>
|
||||||
<string name="settings_about_app_name">헤르메스 모바일</string>
|
<string name="settings_about_app_name">헤르메스 모바일</string>
|
||||||
|
|||||||
@@ -184,6 +184,17 @@
|
|||||||
<string name="settings_item_typing_delay">延迟:%1$d 毫秒</string>
|
<string name="settings_item_typing_delay">延迟:%1$d 毫秒</string>
|
||||||
<string name="settings_delay_10ms">10 毫秒</string>
|
<string name="settings_delay_10ms">10 毫秒</string>
|
||||||
<string name="settings_delay_100ms">100 毫秒</string>
|
<string name="settings_delay_100ms">100 毫秒</string>
|
||||||
|
<string name="settings_item_chat_font_size">聊天字体大小</string>
|
||||||
|
<string name="settings_desc_chat_font_size">缩放聊天消息文字大小</string>
|
||||||
|
<string name="settings_font_scale_value">字体大小:%1$s</string>
|
||||||
|
<string name="settings_font_scale_small">小 (0.85×)</string>
|
||||||
|
<string name="settings_font_scale_default">默认 (1.0×)</string>
|
||||||
|
<string name="settings_font_scale_large">大 (1.15×)</string>
|
||||||
|
<string name="settings_font_scale_xlarge">特大 (1.3×)</string>
|
||||||
|
<string name="settings_font_scale_huge">极大 (1.5×)</string>
|
||||||
|
<string name="settings_chat_preview_title">预览</string>
|
||||||
|
<string name="settings_chat_preview_user">可以调整聊天字体大小吗?</string>
|
||||||
|
<string name="settings_chat_preview_agent">已搞定!你可以在这里自定义字体缩放比例。</string>
|
||||||
<string name="settings_action_test_connection">测试连接</string>
|
<string name="settings_action_test_connection">测试连接</string>
|
||||||
<string name="settings_sec_about">关于</string>
|
<string name="settings_sec_about">关于</string>
|
||||||
<string name="settings_sec_language">语言</string>
|
<string name="settings_sec_language">语言</string>
|
||||||
|
|||||||
@@ -207,6 +207,17 @@
|
|||||||
<string name="settings_item_typing_delay">Delay: %1$d ms</string>
|
<string name="settings_item_typing_delay">Delay: %1$d ms</string>
|
||||||
<string name="settings_delay_10ms">10 ms</string>
|
<string name="settings_delay_10ms">10 ms</string>
|
||||||
<string name="settings_delay_100ms">100 ms</string>
|
<string name="settings_delay_100ms">100 ms</string>
|
||||||
|
<string name="settings_item_chat_font_size">Chat Font Size</string>
|
||||||
|
<string name="settings_desc_chat_font_size">Scale text size in chat messages</string>
|
||||||
|
<string name="settings_font_scale_value">Font Size: %1$s</string>
|
||||||
|
<string name="settings_font_scale_small">Small (0.85×)</string>
|
||||||
|
<string name="settings_font_scale_default">Default (1.0×)</string>
|
||||||
|
<string name="settings_font_scale_large">Large (1.15×)</string>
|
||||||
|
<string name="settings_font_scale_xlarge">Extra Large (1.3×)</string>
|
||||||
|
<string name="settings_font_scale_huge">Huge (1.5×)</string>
|
||||||
|
<string name="settings_chat_preview_title">Preview</string>
|
||||||
|
<string name="settings_chat_preview_user">Can you adjust the chat text size?</string>
|
||||||
|
<string name="settings_chat_preview_agent">Done! You can customize the font scale right here.</string>
|
||||||
<string name="settings_action_test_connection">Test Connection</string>
|
<string name="settings_action_test_connection">Test Connection</string>
|
||||||
<string name="settings_sec_about">About</string>
|
<string name="settings_sec_about">About</string>
|
||||||
<string name="settings_sec_language">Language</string>
|
<string name="settings_sec_language">Language</string>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ class ServerStoreTest {
|
|||||||
assertEquals(9119, state.port)
|
assertEquals(9119, state.port)
|
||||||
assertTrue(state.autoReconnect)
|
assertTrue(state.autoReconnect)
|
||||||
assertEquals("token", state.wsAuthParam)
|
assertEquals("token", state.wsAuthParam)
|
||||||
|
assertEquals(1.0f, state.chatFontScale)
|
||||||
assertTrue(state.connectionProfiles.isEmpty())
|
assertTrue(state.connectionProfiles.isEmpty())
|
||||||
assertNull(state.selectedProfileId)
|
assertNull(state.selectedProfileId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,7 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
|
every { AuthManager.getThemePreset() } returns ThemePreset.DEFAULT
|
||||||
every { AuthManager.isTypingEffectEnabled() } returns true
|
every { AuthManager.isTypingEffectEnabled() } returns true
|
||||||
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
every { AuthManager.getTypingEffectDelayMs() } returns 30
|
||||||
|
every { AuthManager.getChatFontScale() } returns 1.0f
|
||||||
every { AuthManager.getConnectionProfiles() } returns emptyList()
|
every { AuthManager.getConnectionProfiles() } returns emptyList()
|
||||||
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
|
every { AuthManager.getSelectedProfileId() } answers { storedSelectedProfileId }
|
||||||
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
|
every { AuthManager.baseUrl() } returns "http://127.0.0.1:9119/"
|
||||||
@@ -79,6 +80,7 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.setThemePreset(any()) } returns Unit
|
every { AuthManager.setThemePreset(any()) } returns Unit
|
||||||
every { AuthManager.setTypingEffectEnabled(any()) } returns Unit
|
every { AuthManager.setTypingEffectEnabled(any()) } returns Unit
|
||||||
every { AuthManager.setTypingEffectDelayMs(any()) } returns Unit
|
every { AuthManager.setTypingEffectDelayMs(any()) } returns Unit
|
||||||
|
every { AuthManager.setChatFontScale(any()) } returns Unit
|
||||||
every { AuthManager.setSelectedProfileId(any()) } answers {
|
every { AuthManager.setSelectedProfileId(any()) } answers {
|
||||||
storedSelectedProfileId = firstArg()
|
storedSelectedProfileId = firstArg()
|
||||||
}
|
}
|
||||||
@@ -130,6 +132,7 @@ class SettingsViewModelTest {
|
|||||||
every { AuthManager.getThemePreset() } returns ThemePreset.CATPPUCCIN
|
every { AuthManager.getThemePreset() } returns ThemePreset.CATPPUCCIN
|
||||||
every { AuthManager.isTypingEffectEnabled() } returns false
|
every { AuthManager.isTypingEffectEnabled() } returns false
|
||||||
every { AuthManager.getTypingEffectDelayMs() } returns 15
|
every { AuthManager.getTypingEffectDelayMs() } returns 15
|
||||||
|
every { AuthManager.getChatFontScale() } returns 1.30f
|
||||||
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
||||||
every { AuthManager.getAppLanguage() } returns "fr"
|
every { AuthManager.getAppLanguage() } returns "fr"
|
||||||
|
|
||||||
@@ -146,12 +149,23 @@ class SettingsViewModelTest {
|
|||||||
assertEquals(ThemePreset.CATPPUCCIN, state.themePreset)
|
assertEquals(ThemePreset.CATPPUCCIN, state.themePreset)
|
||||||
assertEquals(false, state.typingEffectEnabled)
|
assertEquals(false, state.typingEffectEnabled)
|
||||||
assertEquals(15, state.typingEffectDelayMs)
|
assertEquals(15, state.typingEffectDelayMs)
|
||||||
|
assertEquals(1.30f, state.chatFontScale)
|
||||||
assertEquals(testProfiles, state.profiles)
|
assertEquals(testProfiles, state.profiles)
|
||||||
assertEquals("prof-2", state.selectedProfileId)
|
assertEquals("prof-2", state.selectedProfileId)
|
||||||
assertEquals("Home", state.renameProfileName)
|
assertEquals("Home", state.renameProfileName)
|
||||||
assertEquals("fr", state.appLanguage)
|
assertEquals("fr", state.appLanguage)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testOnChatFontScaleChange_updatesStateAndAuthManager() {
|
||||||
|
val viewModel = createViewModel()
|
||||||
|
|
||||||
|
viewModel.onChatFontScaleChange(1.15f)
|
||||||
|
|
||||||
|
assertEquals(1.15f, viewModel.uiState.value.chatFontScale)
|
||||||
|
verify { AuthManager.setChatFontScale(1.15f) }
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun testLoadSettings_noSelectedProfile_renameEmpty() {
|
fun testLoadSettings_noSelectedProfile_renameEmpty() {
|
||||||
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
every { AuthManager.getConnectionProfiles() } returns testProfiles
|
||||||
|
|||||||
Reference in new issue
Block a user