feat: sidebar redesign — glasier look, functional search, generate title, duplicate
Design: lower tintAlpha (0.80/0.86) for more blob bleed-through, accent-tinted
gradient borders + level1 shadow on active items, specular borders on search bar
and user card, accent gradient avatar, separator line in context menu.
Features: live search filter, "Titel generieren" (Sparkles, manual POST with
{ manual: true }), "Duplizieren" (copies all messages with new IDs + parentId
chain), titleGenerated field on ConversationSummary/Entity (Room v4).
This commit is contained in:
parent
2f9ada646c
commit
558b1be675
8 changed files with 175 additions and 38 deletions
|
|
@ -681,11 +681,9 @@ fun ChatScreen(
|
|||
}
|
||||
is SidebarAction.ToggleLock -> {
|
||||
if (action.locked) {
|
||||
// Locking — no auth needed
|
||||
KaizenApi.patchConversation(cfg.baseUrl, cfg.token, action.id, mapOf("locked" to true))
|
||||
refreshConversations()
|
||||
} else {
|
||||
// Unlocking — require biometric or password
|
||||
val summary = conversations.find { it.id == action.id }
|
||||
if (summary != null) {
|
||||
requestBiometricOrPassword(summary, forToggle = true) {
|
||||
|
|
@ -697,6 +695,40 @@ fun ChatScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
is SidebarAction.GenerateTitle -> {
|
||||
val title = KaizenApi.generateTitle(cfg.baseUrl, cfg.token, action.id, manual = true)
|
||||
if (title != null) {
|
||||
chat.conversationRepo.updateTitle(action.id, title)
|
||||
refreshConversations()
|
||||
}
|
||||
}
|
||||
is SidebarAction.Duplicate -> {
|
||||
val srcMessages = KaizenApi.fetchMessages(cfg.baseUrl, cfg.token, action.id)
|
||||
if (srcMessages.isNotEmpty()) {
|
||||
val srcTitle = conversations.find { it.id == action.id }?.title ?: ""
|
||||
val newId = KaizenApi.createConversation(cfg.baseUrl, cfg.token, "$srcTitle (2)")
|
||||
if (newId != null) {
|
||||
val saveMessages = srcMessages.mapIndexed { i, m ->
|
||||
dev.kaizen.app.net.SaveMessage(
|
||||
id = java.util.UUID.randomUUID().toString(),
|
||||
parentId = if (i == 0) null else null,
|
||||
role = m.role,
|
||||
content = m.content,
|
||||
model = m.model,
|
||||
attachments = m.attachments.takeIf { it.isNotEmpty() },
|
||||
reasoning = m.reasoning,
|
||||
sources = m.sources,
|
||||
)
|
||||
}
|
||||
// Build parentId chain
|
||||
val chained = saveMessages.mapIndexed { i, m ->
|
||||
m.copy(parentId = if (i == 0) null else saveMessages[i - 1].id)
|
||||
}
|
||||
KaizenApi.saveMessages(cfg.baseUrl, cfg.token, newId, chained)
|
||||
refreshConversations()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
|
|
@ -48,8 +49,9 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import dev.kaizen.app.net.ConversationSummary
|
||||
import dev.kaizen.app.ui.effect.GlassSurface
|
||||
import dev.kaizen.app.ui.effect.GlassTiers
|
||||
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||
import dev.kaizen.app.ui.effect.kaizenShadow
|
||||
import dev.kaizen.app.ui.effect.innerTopHighlight
|
||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||
import dev.kaizen.app.R
|
||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||
|
|
@ -88,6 +90,8 @@ sealed interface SidebarAction {
|
|||
data class Delete(val id: String) : SidebarAction
|
||||
data class TogglePin(val id: String, val pinned: Boolean) : SidebarAction
|
||||
data class ToggleLock(val id: String, val locked: Boolean) : SidebarAction
|
||||
data class GenerateTitle(val id: String) : SidebarAction
|
||||
data class Duplicate(val id: String) : SidebarAction
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
|
@ -105,10 +109,12 @@ fun KaizenSidebar(
|
|||
) {
|
||||
val cs = MaterialTheme.colorScheme
|
||||
val isDark = isSystemInDarkTheme()
|
||||
val accent = LocalKaizenAccent.current
|
||||
|
||||
var deleteTarget by remember { mutableStateOf<ConversationSummary?>(null) }
|
||||
var renameTarget by remember { mutableStateOf<ConversationSummary?>(null) }
|
||||
var renameText by remember { mutableStateOf("") }
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
GlassSurface(
|
||||
modifier = modifier
|
||||
|
|
@ -116,7 +122,7 @@ fun KaizenSidebar(
|
|||
.width(300.dp)
|
||||
.padding(start = 12.dp, top = 12.dp, end = 4.dp, bottom = 12.dp),
|
||||
shape = KaizenShapes.lg,
|
||||
tintAlpha = if (isDark) 0.88f else 0.92f,
|
||||
tintAlpha = if (isDark) 0.80f else 0.86f,
|
||||
shadowStack = KaizenShadows.level3,
|
||||
cornerRadius = 24.dp,
|
||||
) {
|
||||
|
|
@ -126,7 +132,7 @@ fun KaizenSidebar(
|
|||
.statusBarsPadding()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
// Header
|
||||
// ── Header ──
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
|
|
@ -152,25 +158,70 @@ fun KaizenSidebar(
|
|||
|
||||
Spacer(Modifier.height(14.dp))
|
||||
|
||||
// Search
|
||||
// ── Search (functional) ──
|
||||
val searchBorder = remember(isDark) {
|
||||
if (isDark) Brush.verticalGradient(
|
||||
listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.03f))
|
||||
) else Brush.verticalGradient(
|
||||
listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(KaizenShapes.pill)
|
||||
.background(if (isDark) Color(0x0DFFFFFF) else Color(0x08000000))
|
||||
.border(0.8.dp, searchBorder, KaizenShapes.pill)
|
||||
.background(if (isDark) Color(0x12FFFFFF) else Color(0x08000000))
|
||||
.padding(horizontal = 14.dp, vertical = 9.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.45f), modifier = Modifier.size(16.dp))
|
||||
Icon(KaizenIcons.Search, null, tint = cs.onSurfaceVariant.copy(alpha = 0.40f), modifier = Modifier.size(15.dp))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(stringResource(R.string.chat_search), color = cs.onSurfaceVariant.copy(alpha = 0.45f), fontSize = 13.sp)
|
||||
BasicTextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
singleLine = true,
|
||||
textStyle = TextStyle(
|
||||
color = cs.onBackground,
|
||||
fontSize = 13.sp,
|
||||
),
|
||||
cursorBrush = SolidColor(cs.onSurfaceVariant),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (searchQuery.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.chat_search),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.40f),
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (searchQuery.isNotEmpty()) {
|
||||
Icon(
|
||||
KaizenIcons.X,
|
||||
null,
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.40f),
|
||||
modifier = Modifier
|
||||
.size(14.dp)
|
||||
.clickable { searchQuery = "" },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(Modifier.height(18.dp))
|
||||
|
||||
// Conversation list
|
||||
// ── Conversation list ──
|
||||
val filteredConversations = remember(conversations, searchQuery) {
|
||||
if (searchQuery.isBlank()) conversations
|
||||
else conversations.filter { it.title.contains(searchQuery, ignoreCase = true) }
|
||||
}
|
||||
|
||||
Box(Modifier.weight(1f).fillMaxWidth()) {
|
||||
if (conversations.isEmpty()) {
|
||||
if (filteredConversations.isEmpty()) {
|
||||
Text(
|
||||
stringResource(R.string.chat_no_conversations),
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.4f),
|
||||
|
|
@ -184,7 +235,7 @@ fun KaizenSidebar(
|
|||
earlier = stringResource(R.string.sidebar_earlier),
|
||||
pinned = stringResource(R.string.sidebar_pinned),
|
||||
)
|
||||
val grouped = remember(conversations, labels) { groupConversations(conversations, labels) }
|
||||
val grouped = remember(filteredConversations, labels) { groupConversations(filteredConversations, labels) }
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
|
|
@ -193,9 +244,10 @@ fun KaizenSidebar(
|
|||
item(key = "header_$dateGroup") {
|
||||
Text(
|
||||
dateGroup,
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.4f),
|
||||
fontSize = 11.sp,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
fontSize = 10.sp,
|
||||
fontWeight = FontWeight.Bold,
|
||||
letterSpacing = 1.sp,
|
||||
modifier = Modifier.padding(start = 8.dp, top = 14.dp, bottom = 6.dp)
|
||||
)
|
||||
}
|
||||
|
|
@ -211,6 +263,8 @@ fun KaizenSidebar(
|
|||
onDelete = { deleteTarget = item },
|
||||
onTogglePin = { onAction(SidebarAction.TogglePin(item.id, !item.pinned)) },
|
||||
onToggleLock = { onAction(SidebarAction.ToggleLock(item.id, !item.locked)) },
|
||||
onGenerateTitle = { onAction(SidebarAction.GenerateTitle(item.id)) },
|
||||
onDuplicate = { onAction(SidebarAction.Duplicate(item.id)) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -220,12 +274,22 @@ fun KaizenSidebar(
|
|||
|
||||
Spacer(Modifier.height(12.dp))
|
||||
|
||||
// User card — compact footer with settings + logout
|
||||
// ── User card ──
|
||||
val cardBorder = remember(isDark) {
|
||||
if (isDark) Brush.verticalGradient(
|
||||
listOf(Color.White.copy(alpha = 0.12f), Color.White.copy(alpha = 0.03f))
|
||||
) else Brush.verticalGradient(
|
||||
listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))
|
||||
)
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(KaizenShapes.md)
|
||||
.background(if (isDark) Color(0x0DFFFFFF) else Color(0x06000000))
|
||||
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 12.dp)
|
||||
.background(if (isDark) Color(0x14FFFFFF) else Color(0x0A000000))
|
||||
.border(0.8.dp, cardBorder, KaizenShapes.md)
|
||||
.innerTopHighlight()
|
||||
.clickable { onOpenSettings() }
|
||||
.padding(horizontal = 12.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
|
@ -234,7 +298,11 @@ fun KaizenSidebar(
|
|||
modifier = Modifier
|
||||
.size(30.dp)
|
||||
.clip(KaizenShapes.circle)
|
||||
.background(if (isDark) Color(0xFF1E293B) else Color(0xFF0F172A)),
|
||||
.background(
|
||||
Brush.verticalGradient(
|
||||
listOf(accent.primary.copy(alpha = 0.8f), accent.primary.copy(alpha = 0.6f))
|
||||
)
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
|
|
@ -253,8 +321,8 @@ fun KaizenSidebar(
|
|||
Icon(
|
||||
KaizenIcons.Settings,
|
||||
stringResource(R.string.sidebar_settings),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.45f),
|
||||
modifier = Modifier.size(17.dp),
|
||||
)
|
||||
Spacer(Modifier.width(6.dp))
|
||||
Box(
|
||||
|
|
@ -267,8 +335,8 @@ fun KaizenSidebar(
|
|||
Icon(
|
||||
KaizenIcons.LogOut,
|
||||
stringResource(R.string.sidebar_logout),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.5f),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.45f),
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -356,6 +424,8 @@ private fun HistoryItemRow(
|
|||
onDelete: () -> Unit,
|
||||
onTogglePin: () -> Unit,
|
||||
onToggleLock: () -> Unit,
|
||||
onGenerateTitle: () -> Unit,
|
||||
onDuplicate: () -> Unit,
|
||||
) {
|
||||
val cs = MaterialTheme.colorScheme
|
||||
val isDark = isSystemInDarkTheme()
|
||||
|
|
@ -363,17 +433,31 @@ private fun HistoryItemRow(
|
|||
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val itemBg = when {
|
||||
selected -> accent.primary.copy(alpha = if (isDark) 0.14f else 0.10f)
|
||||
else -> Color.Transparent
|
||||
val itemBorder = if (selected) {
|
||||
remember(isDark, accent) {
|
||||
if (isDark) Brush.verticalGradient(
|
||||
listOf(accent.primary.copy(alpha = 0.25f), accent.primary.copy(alpha = 0.06f))
|
||||
) else Brush.verticalGradient(
|
||||
listOf(accent.primary.copy(alpha = 0.20f), accent.primary.copy(alpha = 0.05f))
|
||||
)
|
||||
}
|
||||
} else null
|
||||
|
||||
Box {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(KaizenShapes.sm)
|
||||
.background(itemBg)
|
||||
.then(
|
||||
if (selected) {
|
||||
Modifier
|
||||
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 10.dp)
|
||||
.background(accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f))
|
||||
.border(0.8.dp, itemBorder!!, KaizenShapes.sm)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.clickable { onClick() }
|
||||
.padding(start = 10.dp, end = 4.dp, top = 8.dp, bottom = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
|
|
@ -384,8 +468,8 @@ private fun HistoryItemRow(
|
|||
}
|
||||
Text(
|
||||
text = if (item.locked) stringResource(R.string.chat_locked) else item.title,
|
||||
color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.45f) else cs.onBackground.copy(alpha = 0.85f),
|
||||
fontSize = 13.5.sp,
|
||||
color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.45f) else cs.onBackground.copy(alpha = if (selected) 0.95f else 0.80f),
|
||||
fontSize = 13.sp,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal,
|
||||
maxLines = 1,
|
||||
|
|
@ -406,8 +490,8 @@ private fun HistoryItemRow(
|
|||
Icon(
|
||||
KaizenIcons.MoreVertical,
|
||||
stringResource(R.string.sidebar_more),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.35f),
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = cs.onSurfaceVariant.copy(alpha = 0.30f),
|
||||
modifier = Modifier.size(15.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -416,18 +500,30 @@ private fun HistoryItemRow(
|
|||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
|
||||
shadowElevation = 12.dp,
|
||||
shadowElevation = 16.dp,
|
||||
shape = KaizenShapes.md,
|
||||
) {
|
||||
if (!item.locked) {
|
||||
MenuRow(KaizenIcons.Pencil, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() }
|
||||
}
|
||||
if (!item.titleGenerated) {
|
||||
MenuRow(KaizenIcons.Sparkles, stringResource(R.string.sidebar_generate_title), cs.onBackground) { showMenu = false; onGenerateTitle() }
|
||||
}
|
||||
MenuRow(KaizenIcons.Pin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() }
|
||||
MenuRow(KaizenIcons.Copy, stringResource(R.string.sidebar_duplicate), cs.onBackground) { showMenu = false; onDuplicate() }
|
||||
MenuRow(
|
||||
if (item.locked) KaizenIcons.LockOpen else KaizenIcons.Lock,
|
||||
stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock),
|
||||
cs.onBackground,
|
||||
) { showMenu = false; onToggleLock() }
|
||||
// Separator
|
||||
Box(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = 4.dp)
|
||||
.height(0.5.dp)
|
||||
.background(if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f))
|
||||
)
|
||||
MenuRow(KaizenIcons.Trash, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() }
|
||||
}
|
||||
}
|
||||
|
|
@ -439,11 +535,11 @@ private fun MenuRow(icon: ImageVector, label: String, tint: Color, onClick: () -
|
|||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onClick)
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 11.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(icon, null, tint = tint.copy(alpha = 0.7f), modifier = Modifier.size(18.dp))
|
||||
Icon(icon, null, tint = tint.copy(alpha = 0.6f), modifier = Modifier.size(16.dp))
|
||||
Spacer(Modifier.width(12.dp))
|
||||
Text(label, color = tint, fontSize = 14.sp, fontWeight = FontWeight.Medium)
|
||||
Text(label, color = tint.copy(alpha = 0.9f), fontSize = 13.5.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ data class ConversationEntity(
|
|||
val pinned: Boolean,
|
||||
val updatedAt: String?,
|
||||
val cachedAt: Long,
|
||||
val titleGenerated: Boolean = false,
|
||||
)
|
||||
|
||||
@Entity(
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import androidx.room.TypeConverters
|
|||
|
||||
@Database(
|
||||
entities = [ConversationEntity::class, MessageEntity::class],
|
||||
version = 3,
|
||||
version = 4,
|
||||
exportSchema = false,
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ fun ConversationSummary.toEntity() = ConversationEntity(
|
|||
pinned = pinned,
|
||||
updatedAt = updatedAt,
|
||||
cachedAt = System.currentTimeMillis(),
|
||||
titleGenerated = titleGenerated,
|
||||
)
|
||||
|
||||
fun ConversationEntity.toSummary() = ConversationSummary(
|
||||
|
|
@ -18,6 +19,7 @@ fun ConversationEntity.toSummary() = ConversationSummary(
|
|||
locked = locked,
|
||||
pinned = pinned,
|
||||
updatedAt = updatedAt,
|
||||
titleGenerated = titleGenerated,
|
||||
)
|
||||
|
||||
fun StoredMessage.toEntity(conversationId: String, sortOrder: Int) = MessageEntity(
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ data class ConversationSummary(
|
|||
val locked: Boolean = false,
|
||||
val pinned: Boolean = false,
|
||||
val updatedAt: String? = null,
|
||||
val titleGenerated: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable private data class ConversationsResponse(val conversations: List<ConversationSummary> = emptyList())
|
||||
|
|
@ -446,11 +447,12 @@ object KaizenApi {
|
|||
}
|
||||
|
||||
/** POST /api/v1/conversations/[id]/title — returns the generated title or null. */
|
||||
suspend fun generateTitle(baseUrl: String, token: String, id: String): String? =
|
||||
suspend fun generateTitle(baseUrl: String, token: String, id: String, manual: Boolean = false): String? =
|
||||
withContext(Dispatchers.IO) {
|
||||
val body = if (manual) """{"manual":true}""" else "{}"
|
||||
val req = Request.Builder()
|
||||
.url("$baseUrl/api/v1/conversations/$id/title")
|
||||
.post("{}".toRequestBody(jsonMedia))
|
||||
.post(body.toRequestBody(jsonMedia))
|
||||
.header("Authorization", "Bearer $token")
|
||||
.build()
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@
|
|||
<string name="sidebar_lock">Lock</string>
|
||||
<string name="sidebar_unlock">Unlock</string>
|
||||
<string name="sidebar_more">More</string>
|
||||
<string name="sidebar_generate_title">Generate Title</string>
|
||||
<string name="sidebar_duplicate">Duplicate</string>
|
||||
<string name="sidebar_rename_title">Rename Chat</string>
|
||||
<string name="sidebar_delete_confirm">Permanently delete this chat?</string>
|
||||
<string name="sidebar_cancel">Cancel</string>
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@
|
|||
<string name="sidebar_lock">Sperren</string>
|
||||
<string name="sidebar_unlock">Entsperren</string>
|
||||
<string name="sidebar_more">Mehr</string>
|
||||
<string name="sidebar_generate_title">Titel generieren</string>
|
||||
<string name="sidebar_duplicate">Duplizieren</string>
|
||||
<string name="sidebar_rename_title">Chat umbenennen</string>
|
||||
<string name="sidebar_delete_confirm">Chat endgültig löschen?</string>
|
||||
<string name="sidebar_cancel">Abbrechen</string>
|
||||
|
|
|
|||
Loading…
Reference in a new issue