diff --git a/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppScaffold.kt b/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppScaffold.kt index 4a72f50..c51dc23 100644 --- a/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppScaffold.kt +++ b/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppScaffold.kt @@ -637,7 +637,12 @@ fun AppScaffold( onItemClick = { index -> val item = appMenuItemsList.getOrNull(index) ?: return@F7AppMenuSheet val external = item.externalUrl - if (!external.isNullOrBlank()) { + val isPersonalCabinet = item.label.trim().equals("Личный кабинет", ignoreCase = true) + if (isPersonalCabinet) { + // Профиль — нативная панель вместо веб-ЛК forbion + menuOpen = false + profileOpen = true + } else if (!external.isNullOrBlank()) { runCatching { context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(external))) } diff --git a/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppSheets.kt b/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppSheets.kt index 7787116..1559330 100644 --- a/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppSheets.kt +++ b/app/src/main/java/ru/forbion/f7cloud/mobile/ui/AppSheets.kt @@ -1,17 +1,38 @@ package ru.forbion.f7cloud.mobile.ui +import android.content.Intent +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.filled.Group +import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.QrCodeScanner +import androidx.compose.material.icons.filled.TaskAlt +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -21,22 +42,30 @@ import androidx.compose.runtime.rememberCoroutineScope 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.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.Credentials +import okhttp3.Request +import org.json.JSONObject import ru.forbion.f7cloud.core.auth.AuthSession import ru.forbion.f7cloud.core.designsystem.F7Colors import ru.forbion.f7cloud.core.designsystem.F7FloatingPanel import ru.forbion.f7cloud.core.designsystem.F7NotificationRow -import ru.forbion.f7cloud.core.designsystem.F7PrimaryButton -import ru.forbion.f7cloud.core.designsystem.F7TextButton import ru.forbion.f7cloud.core.designsystem.formatNotificationRelativeTime import ru.forbion.f7cloud.core.network.F7Notification +import ru.forbion.f7cloud.core.network.NetworkFactory import ru.forbion.f7cloud.core.network.NotificationsRepository import ru.forbion.f7cloud.core.network.UnauthorizedException +import ru.forbion.f7cloud.core.network.applyOcsJson private fun themeHeaderAsset(serverUrl: String, fileName: String): String = "${serverUrl.trimEnd('/')}/themes/forbion/images/header/$fileName" @@ -54,46 +83,178 @@ fun ProfileSheet( onLogout: () -> Unit, onScanBrowserQr: () -> Unit = {}, ) { + val context = LocalContext.current + val base = session.serverUrl.trimEnd('/') + val userId = session.davUserId ?: session.username + var displayName by remember(session.username) { mutableStateOf(userId) } + var aboutOpen by remember { mutableStateOf(false) } + + // Отображаемое имя тянем с сервера (OCS), пока — логин + LaunchedEffect(visible, session.username) { + if (visible) displayName = fetchDisplayName(session).ifBlank { userId } + } + + fun openWeb(path: String) { + runCatching { + context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse("$base$path"))) + } + onDismiss() + } + F7FloatingPanel( visible = visible, onDismiss = onDismiss, ) { - Text("Профиль", style = MaterialTheme.typography.titleMedium, color = F7Colors.TextPrimary) - Text( - text = session.username, - style = MaterialTheme.typography.bodyLarge, - color = F7Colors.TextPrimary, - ) - Text( - text = session.serverUrl, - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) - session.davUserId?.let { davId -> - Text( - text = "ID: $davId", - style = MaterialTheme.typography.bodySmall, - color = F7Colors.TextSecondary, - ) + // Шапка: аватар-инициал + имя/«Открыть профиль» + кнопка QR + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background(F7Colors.PrimaryLight), + contentAlignment = Alignment.Center, + ) { + Text( + displayName.firstOrNull()?.uppercaseChar()?.toString() ?: "?", + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = F7Colors.Primary, + ) + } + Column( + modifier = Modifier + .weight(1f) + .clip(RoundedCornerShape(8.dp)) + .clickable { openWeb("/u/$userId") } + .padding(vertical = 2.dp), + ) { + Text( + displayName, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = F7Colors.TextPrimary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Text( + "Открыть профиль", + style = MaterialTheme.typography.bodySmall, + color = F7Colors.Primary, + ) + } + Box( + modifier = Modifier + .size(40.dp) + .clip(RoundedCornerShape(10.dp)) + .background(F7Colors.Grey2) + .clickable { + onDismiss() + onScanBrowserQr() + }, + contentAlignment = Alignment.Center, + ) { + Icon( + Icons.Filled.QrCodeScanner, + contentDescription = "Сканировать QR браузера", + tint = F7Colors.TextSecondary, + modifier = Modifier.size(22.dp), + ) + } + } + Spacer(Modifier.height(10.dp)) + HorizontalDivider(color = F7Colors.Border.copy(alpha = 0.5f)) + Spacer(Modifier.height(4.dp)) + ProfileMenuRow(Icons.Filled.TaskAlt, "Установить статус") { openWeb("/settings/user") } + ProfileMenuRow(Icons.Filled.Person, "Личные настройки") { openWeb("/settings/user") } + ProfileMenuRow(Icons.Filled.Group, "Учётные записи") { openWeb("/settings/user/security") } + ProfileMenuRow(Icons.Outlined.Info, "О программе и что нового") { aboutOpen = true } + ProfileMenuRow( + icon = Icons.AutoMirrored.Filled.Logout, + label = "Выйти", + tint = F7Colors.Error, + ) { + onDismiss() + onLogout() } - F7PrimaryButton( - text = "Сканировать QR браузера", - onClick = { - onDismiss() - onScanBrowserQr() - }, - modifier = Modifier.fillMaxWidth(), - ) - F7PrimaryButton( - text = "Выйти", - onClick = { - onDismiss() - onLogout() - }, - modifier = Modifier.fillMaxWidth(), - ) - F7TextButton(text = "Закрыть", onClick = onDismiss, modifier = Modifier.fillMaxWidth()) } + + if (aboutOpen) { + F7AboutDialog(onDismiss = { aboutOpen = false }) + } +} + +@Composable +private fun ProfileMenuRow( + icon: ImageVector, + label: String, + tint: Color = F7Colors.TextPrimary, + onClick: () -> Unit, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(10.dp)) + .clickable(onClick = onClick) + .padding(vertical = 12.dp, horizontal = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + ) { + Icon(icon, contentDescription = null, tint = tint, modifier = Modifier.size(22.dp)) + Text(label, style = MaterialTheme.typography.bodyLarge, color = tint) + } +} + +@Composable +private fun F7AboutDialog(onDismiss: () -> Unit) { + val context = LocalContext.current + val version = remember { + runCatching { + context.packageManager.getPackageInfo(context.packageName, 0).versionName + }.getOrNull() ?: "—" + } + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onDismiss) { Text("Закрыть") } + }, + title = { Text("F7cloud Mobile") }, + text = { + Column { + Text("Версия $version", style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextPrimary) + Spacer(Modifier.height(8.dp)) + Text( + "Нативный клиент F7cloud: почта, файлы, календарь, контакты, задачи, конференции, поддержка.", + style = MaterialTheme.typography.bodySmall, + color = F7Colors.TextSecondary, + ) + } + }, + ) +} + +/** Отображаемое имя пользователя из OCS (fallback — пусто). */ +private suspend fun fetchDisplayName(session: AuthSession): String = withContext(Dispatchers.IO) { + runCatching { + val client = NetworkFactory.newAuthedClient( + session.username, + session.appPassword, + session.trustAllCerts, + ) + val url = "${session.serverUrl.trimEnd('/')}/ocs/v2.php/cloud/user?format=json" + val request = Request.Builder().url(url).applyOcsJson().get().build() + client.newCall(request).execute().use { response -> + val body = response.body?.string().orEmpty() + JSONObject(body) + .optJSONObject("ocs") + ?.optJSONObject("data") + ?.optString("displayname") + .orEmpty() + } + }.getOrDefault("") } @Composable