mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +00:00
feat: fetch site metadata on setup and show confirmation card
After successful connection test, fetch Ghost /site/ endpoint to get blog name, description, icon, and version. Show a confirmation card with site details before completing setup. Warn if Ghost version < 5. Cache site metadata per account via SiteMetadataCache. Falls back to existing behavior if site fetch fails.
This commit is contained in:
parent
492ee1ca11
commit
be37f6284f
2 changed files with 193 additions and 2 deletions
|
|
@ -9,24 +9,30 @@ import androidx.compose.animation.core.rememberInfiniteTransition
|
|||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Warning
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
|
|
@ -34,6 +40,7 @@ import androidx.compose.ui.text.withStyle
|
|||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
|
|
@ -102,6 +109,124 @@ fun SetupScreen(
|
|||
)
|
||||
|
||||
// Content layered on top
|
||||
if (state.showConfirmation) {
|
||||
// Confirmation card
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = fadeIn() + scaleIn(initialScale = 0.9f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(24.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
// Site icon
|
||||
if (state.siteIcon != null) {
|
||||
AsyncImage(
|
||||
model = state.siteIcon,
|
||||
contentDescription = "Site icon",
|
||||
modifier = Modifier
|
||||
.size(64.dp)
|
||||
.clip(CircleShape),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
// Site title
|
||||
Text(
|
||||
text = state.siteName ?: "Ghost Blog",
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
)
|
||||
|
||||
// Site description
|
||||
if (!state.siteDescription.isNullOrBlank()) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = state.siteDescription!!,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
|
||||
// Ghost version
|
||||
if (state.siteVersion != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "Ghost ${state.siteVersion}",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Version warning
|
||||
if (state.versionWarning) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = CardDefaults.elevatedCardColors(
|
||||
containerColor = MaterialTheme.colorScheme.errorContainer
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Warning,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.error,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = "Ghost version ${state.siteVersion} is older than v5. Some features may not work correctly.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onErrorContainer
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Buttons
|
||||
Button(
|
||||
onClick = viewModel::confirmConnection,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Tak, po\u0142\u0105cz")
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedButton(
|
||||
onClick = viewModel::cancelConfirmation,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("Wstecz")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
|
|
@ -252,6 +377,7 @@ fun SetupScreen(
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import android.app.Application
|
|||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.swoosh.microblog.data.AccountManager
|
||||
import com.swoosh.microblog.data.SiteMetadataCache
|
||||
import com.swoosh.microblog.data.api.ApiClient
|
||||
import com.swoosh.microblog.data.api.GhostJwtGenerator
|
||||
import com.swoosh.microblog.data.repository.PostRepository
|
||||
|
|
@ -16,6 +17,7 @@ import kotlinx.coroutines.launch
|
|||
class SetupViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
private val accountManager = AccountManager(application)
|
||||
private val siteMetadataCache = SiteMetadataCache(application)
|
||||
|
||||
private val _uiState = MutableStateFlow(SetupUiState(
|
||||
isAddingAccount = accountManager.hasAnyAccount
|
||||
|
|
@ -34,6 +36,30 @@ class SetupViewModel(application: Application) : AndroidViewModel(application) {
|
|||
_uiState.update { it.copy(accountName = name) }
|
||||
}
|
||||
|
||||
fun confirmConnection() {
|
||||
_uiState.update { it.copy(isSuccess = true) }
|
||||
}
|
||||
|
||||
fun cancelConfirmation() {
|
||||
// Remove the account that was added during save()
|
||||
val accountId = _uiState.value.pendingAccountId
|
||||
if (accountId != null) {
|
||||
accountManager.removeAccount(accountId)
|
||||
siteMetadataCache.remove(accountId)
|
||||
}
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
showConfirmation = false,
|
||||
siteName = null,
|
||||
siteDescription = null,
|
||||
siteIcon = null,
|
||||
siteVersion = null,
|
||||
versionWarning = false,
|
||||
pendingAccountId = null
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun save() {
|
||||
val state = _uiState.value
|
||||
if (state.url.isBlank() || state.apiKey.isBlank()) {
|
||||
|
|
@ -83,7 +109,39 @@ class SetupViewModel(application: Application) : AndroidViewModel(application) {
|
|||
accountManager.updateAccount(id = account.id, avatarUrl = avatarUrl)
|
||||
}
|
||||
} catch (_: Exception) { /* avatar is best-effort */ }
|
||||
_uiState.update { it.copy(isTesting = false, isSuccess = true) }
|
||||
|
||||
// Fetch site metadata
|
||||
var siteLoaded = false
|
||||
try {
|
||||
val service = ApiClient.getService(
|
||||
state.url,
|
||||
apiKeyProvider = { state.apiKey }
|
||||
)
|
||||
val siteResponse = service.getSite()
|
||||
val site = siteResponse.body()
|
||||
if (site != null) {
|
||||
siteMetadataCache.save(account.id, site)
|
||||
val majorVersion = site.version?.split(".")?.firstOrNull()?.toIntOrNull()
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isTesting = false,
|
||||
showConfirmation = true,
|
||||
siteName = site.title,
|
||||
siteDescription = site.description,
|
||||
siteIcon = site.icon ?: site.logo,
|
||||
siteVersion = site.version,
|
||||
versionWarning = majorVersion != null && majorVersion < 5,
|
||||
pendingAccountId = account.id
|
||||
)
|
||||
}
|
||||
siteLoaded = true
|
||||
}
|
||||
} catch (_: Exception) { /* site metadata is best-effort */ }
|
||||
|
||||
// Fallback: skip confirmation if site fetch failed
|
||||
if (!siteLoaded) {
|
||||
_uiState.update { it.copy(isTesting = false, isSuccess = true) }
|
||||
}
|
||||
},
|
||||
onFailure = { e ->
|
||||
// Remove the account since connection failed
|
||||
|
|
@ -110,5 +168,12 @@ data class SetupUiState(
|
|||
val isTesting: Boolean = false,
|
||||
val isSuccess: Boolean = false,
|
||||
val isAddingAccount: Boolean = false,
|
||||
val error: String? = null
|
||||
val error: String? = null,
|
||||
val siteName: String? = null,
|
||||
val siteDescription: String? = null,
|
||||
val siteIcon: String? = null,
|
||||
val siteVersion: String? = null,
|
||||
val showConfirmation: Boolean = false,
|
||||
val versionWarning: Boolean = false,
|
||||
val pendingAccountId: String? = null
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue