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.
This commit is contained in:
Paweł Orzech 2026-03-20 00:31:22 +01:00
parent be37f6284f
commit 471fea6183

View file

@ -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))