From 471fea6183ccdf8a79c64a8f8ff41244c780b59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Fri, 20 Mar 2026 00:31:22 +0100 Subject: [PATCH] feat: add blog info section in Settings with site metadata Display cached Ghost site metadata (logo/icon, title, description, URL, version, locale) in a card above the Current Account section. Add "Open Ghost Admin" button that launches the blog's admin panel in browser. Show version warning banner if Ghost version is older than v5. --- .../microblog/ui/settings/SettingsScreen.kt | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) diff --git a/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt index 28e7248..526cb4a 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/settings/SettingsScreen.kt @@ -1,24 +1,37 @@ package com.swoosh.microblog.ui.settings +import android.content.Intent +import android.net.Uri import androidx.compose.animation.* import androidx.compose.animation.core.* +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.BrightnessAuto import androidx.compose.material.icons.filled.DarkMode import androidx.compose.material.icons.filled.LightMode +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.* import androidx.compose.runtime.* 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.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import com.swoosh.microblog.data.AccountManager +import com.swoosh.microblog.data.SiteMetadataCache import com.swoosh.microblog.data.api.ApiClient +import com.swoosh.microblog.data.model.GhostAccount import com.swoosh.microblog.ui.animation.SwooshMotion import com.swoosh.microblog.ui.components.ConfirmationDialog import com.swoosh.microblog.ui.feed.AccountAvatar @@ -35,6 +48,8 @@ fun SettingsScreen( val context = LocalContext.current val accountManager = remember { AccountManager(context) } val activeAccount = remember { accountManager.getActiveAccount() } + val siteMetadataCache = remember { SiteMetadataCache(context) } + val siteData = remember { activeAccount?.let { siteMetadataCache.get(it.id) } } val currentThemeMode = themeViewModel?.themeMode?.collectAsStateWithLifecycle() @@ -75,6 +90,162 @@ fun SettingsScreen( Spacer(modifier = Modifier.height(24.dp)) } + // --- Blog Info section --- + if (siteData != null) { + Text("Blog", style = MaterialTheme.typography.titleMedium) + Spacer(modifier = Modifier.height(12.dp)) + + Card(modifier = Modifier.fillMaxWidth()) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Site logo/icon or fallback initial + val siteImageUrl = siteData.logo ?: siteData.icon + if (siteImageUrl != null) { + AsyncImage( + model = siteImageUrl, + contentDescription = "Site icon", + modifier = Modifier + .size(48.dp) + .clip(CircleShape), + contentScale = ContentScale.Crop + ) + } else { + val initial = siteData.title?.firstOrNull()?.uppercase() ?: "?" + val color = activeAccount?.let { + Color(GhostAccount.colorForIndex(it.colorIndex)) + } ?: MaterialTheme.colorScheme.primary + Box( + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + .background(color), + contentAlignment = Alignment.Center + ) { + Text( + text = initial, + color = Color.White, + style = MaterialTheme.typography.titleMedium + ) + } + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = siteData.title ?: "Ghost Blog", + style = MaterialTheme.typography.titleMedium + ) + if (!siteData.description.isNullOrBlank()) { + Text( + text = siteData.description!!, + style = MaterialTheme.typography.bodySmall, + fontStyle = FontStyle.Italic, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // URL + val siteUrl = siteData.url + if (!siteUrl.isNullOrBlank()) { + Text( + text = siteUrl + .removePrefix("https://") + .removePrefix("http://") + .removeSuffix("/"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Version + locale + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + if (siteData.version != null) { + Text( + text = "Ghost ${siteData.version}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (siteData.locale != null) { + Text( + text = "Locale: ${siteData.locale}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + + // Version warning + val majorVersion = siteData.version?.split(".")?.firstOrNull()?.toIntOrNull() + if (majorVersion != null && majorVersion < 5) { + Spacer(modifier = Modifier.height(8.dp)) + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.errorContainer + ) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Ghost ${siteData.version} is older than v5. Some features may not work.", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + // Open Ghost Admin button + val ghostAdminUrl = remember(activeAccount) { + activeAccount?.blogUrl?.trimEnd('/') + "/ghost/" + } + OutlinedButton( + onClick = { + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(ghostAdminUrl)) + context.startActivity(intent) + }, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Open Ghost Admin") + } + + Spacer(modifier = Modifier.height(24.dp)) + HorizontalDivider() + Spacer(modifier = Modifier.height(24.dp)) + } + // --- Current Account section --- Text("Current Account", style = MaterialTheme.typography.titleMedium) Spacer(modifier = Modifier.height(12.dp))