mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 20:15:41 +00:00
feat: add tags toggle in Settings, move newsletter to bottom tab
- Add TagsPreferences with per-account toggle (enabled by default) - Tags toggle in Settings → Features section with "Manage Tags" button - When tags disabled: hide tag filter chips, tag section in Composer, tag click handlers become no-ops in Feed - New Newsletter bottom tab (Home, Newsletter, Stats, Settings) - NewsletterScreen shows enable toggle, subscriber count, newsletters list - Remove newsletter section from Settings (moved to dedicated tab)
This commit is contained in:
parent
da8a90470d
commit
0718a9e744
8 changed files with 416 additions and 112 deletions
|
|
@ -0,0 +1,32 @@
|
|||
package com.swoosh.microblog.data
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
|
||||
class TagsPreferences private constructor(
|
||||
private val prefs: SharedPreferences,
|
||||
private val accountIdProvider: () -> String
|
||||
) {
|
||||
|
||||
constructor(context: Context) : this(
|
||||
prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE),
|
||||
accountIdProvider = { AccountManager(context).getActiveAccount()?.id ?: "" }
|
||||
)
|
||||
|
||||
constructor(prefs: SharedPreferences, accountId: String) : this(
|
||||
prefs = prefs,
|
||||
accountIdProvider = { accountId }
|
||||
)
|
||||
|
||||
private fun activeAccountId(): String = accountIdProvider()
|
||||
|
||||
fun isTagsEnabled(): Boolean =
|
||||
prefs.getBoolean("tags_enabled_${activeAccountId()}", true)
|
||||
|
||||
fun setTagsEnabled(enabled: Boolean) =
|
||||
prefs.edit().putBoolean("tags_enabled_${activeAccountId()}", enabled).apply()
|
||||
|
||||
companion object {
|
||||
const val PREFS_NAME = "tags_prefs"
|
||||
}
|
||||
}
|
||||
|
|
@ -432,16 +432,18 @@ fun ComposerScreen(
|
|||
}
|
||||
)
|
||||
|
||||
// Tags section: input + suggestions + chips
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TagsSection(
|
||||
tagInput = state.tagInput,
|
||||
onTagInputChange = viewModel::updateTagInput,
|
||||
tagSuggestions = state.tagSuggestions,
|
||||
extractedTags = state.extractedTags,
|
||||
onAddTag = viewModel::addTag,
|
||||
onRemoveTag = viewModel::removeTag
|
||||
)
|
||||
// Tags section: input + suggestions + chips (only when tags enabled)
|
||||
if (viewModel.isTagsEnabled()) {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
TagsSection(
|
||||
tagInput = state.tagInput,
|
||||
onTagInputChange = viewModel::updateTagInput,
|
||||
tagSuggestions = state.tagSuggestions,
|
||||
extractedTags = state.extractedTags,
|
||||
onAddTag = viewModel::addTag,
|
||||
onRemoveTag = viewModel::removeTag
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.lifecycle.viewModelScope
|
|||
import com.swoosh.microblog.data.HashtagParser
|
||||
import com.swoosh.microblog.data.MobiledocBuilder
|
||||
import com.swoosh.microblog.data.NewsletterPreferences
|
||||
import com.swoosh.microblog.data.TagsPreferences
|
||||
import com.swoosh.microblog.data.PreviewHtmlBuilder
|
||||
import com.swoosh.microblog.data.db.Converters
|
||||
import com.swoosh.microblog.data.model.*
|
||||
|
|
@ -29,6 +30,7 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
|||
private val repository = PostRepository(application)
|
||||
private val tagRepository = TagRepository(application)
|
||||
private val newsletterPreferences = NewsletterPreferences(application)
|
||||
private val tagsPreferences = TagsPreferences(application)
|
||||
private val appContext = application
|
||||
|
||||
private val _uiState = MutableStateFlow(ComposerUiState())
|
||||
|
|
@ -41,10 +43,14 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
|||
private var previewDebounceJob: Job? = null
|
||||
|
||||
init {
|
||||
loadAvailableTags()
|
||||
if (tagsPreferences.isTagsEnabled()) {
|
||||
loadAvailableTags()
|
||||
}
|
||||
loadNewsletterData()
|
||||
}
|
||||
|
||||
fun isTagsEnabled(): Boolean = tagsPreferences.isTagsEnabled()
|
||||
|
||||
private fun loadAvailableTags() {
|
||||
viewModelScope.launch {
|
||||
tagRepository.fetchTags().fold(
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ fun FeedScreen(
|
|||
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
|
||||
val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle()
|
||||
val popularTags by viewModel.popularTags.collectAsStateWithLifecycle()
|
||||
val tagsEnabled by viewModel.tagsEnabled.collectAsStateWithLifecycle()
|
||||
val listState = rememberLazyListState()
|
||||
val context = LocalContext.current
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
|
|
@ -331,7 +332,7 @@ fun FeedScreen(
|
|||
|
||||
// Tag filter chips
|
||||
AnimatedVisibility(
|
||||
visible = !isSearchActive && popularTags.isNotEmpty(),
|
||||
visible = !isSearchActive && tagsEnabled && popularTags.isNotEmpty(),
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically()
|
||||
) {
|
||||
|
|
@ -588,7 +589,7 @@ fun FeedScreen(
|
|||
onEdit = { onEditPost(post) },
|
||||
onDelete = { postPendingDelete = post },
|
||||
onTogglePin = { viewModel.toggleFeatured(post) },
|
||||
onTagClick = { tag -> viewModel.filterByTag(tag) },
|
||||
onTagClick = { tag -> if (tagsEnabled) viewModel.filterByTag(tag) },
|
||||
snackbarHostState = snackbarHostState
|
||||
)
|
||||
}
|
||||
|
|
@ -614,7 +615,7 @@ fun FeedScreen(
|
|||
onEdit = { onEditPost(post) },
|
||||
onDelete = { postPendingDelete = post },
|
||||
onTogglePin = { viewModel.toggleFeatured(post) },
|
||||
onTagClick = { tag -> viewModel.filterByTag(tag) },
|
||||
onTagClick = { tag -> if (tagsEnabled) viewModel.filterByTag(tag) },
|
||||
snackbarHostState = snackbarHostState
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import com.swoosh.microblog.data.AccountManager
|
|||
import com.swoosh.microblog.data.CredentialsManager
|
||||
import com.swoosh.microblog.data.FeedPreferences
|
||||
import com.swoosh.microblog.data.HashtagParser
|
||||
import com.swoosh.microblog.data.TagsPreferences
|
||||
import com.swoosh.microblog.data.api.ApiClient
|
||||
import com.swoosh.microblog.data.db.Converters
|
||||
import com.swoosh.microblog.data.model.*
|
||||
|
|
@ -42,6 +43,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
private var repository = PostRepository(application)
|
||||
private var tagRepository = TagRepository(application)
|
||||
private val feedPreferences = FeedPreferences(application)
|
||||
private val tagsPreferences = TagsPreferences(application)
|
||||
private val searchHistoryManager = SearchHistoryManager(application)
|
||||
|
||||
private val _uiState = MutableStateFlow(FeedUiState())
|
||||
|
|
@ -77,6 +79,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
private val _popularTags = MutableStateFlow<List<GhostTagFull>>(emptyList())
|
||||
val popularTags: StateFlow<List<GhostTagFull>> = _popularTags.asStateFlow()
|
||||
|
||||
private val _tagsEnabled = MutableStateFlow(true)
|
||||
val tagsEnabled: StateFlow<Boolean> = _tagsEnabled.asStateFlow()
|
||||
|
||||
private val _accounts = MutableStateFlow<List<GhostAccount>>(emptyList())
|
||||
val accounts: StateFlow<List<GhostAccount>> = _accounts.asStateFlow()
|
||||
|
||||
|
|
@ -303,16 +308,23 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
|||
val sort = _sortOrder.value
|
||||
val tagFilter = _uiState.value.activeTagFilter
|
||||
|
||||
// Fetch popular tags in parallel
|
||||
launch {
|
||||
tagRepository.fetchTags().fold(
|
||||
onSuccess = { tags ->
|
||||
_popularTags.value = tags
|
||||
.sortedByDescending { it.count?.posts ?: 0 }
|
||||
.take(10)
|
||||
},
|
||||
onFailure = { /* silently ignore tag fetch failures */ }
|
||||
)
|
||||
// Fetch popular tags in parallel (only when tags feature is enabled)
|
||||
val tagsOn = tagsPreferences.isTagsEnabled()
|
||||
_tagsEnabled.value = tagsOn
|
||||
if (tagsOn) {
|
||||
launch {
|
||||
tagRepository.fetchTags().fold(
|
||||
onSuccess = { tags ->
|
||||
_popularTags.value = tags
|
||||
.sortedByDescending { it.count?.posts ?: 0 }
|
||||
.take(10)
|
||||
},
|
||||
onFailure = { /* silently ignore tag fetch failures */ }
|
||||
)
|
||||
}
|
||||
} else {
|
||||
_popularTags.value = emptyList()
|
||||
_uiState.update { it.copy(activeTagFilter = null) }
|
||||
}
|
||||
|
||||
repository.fetchPosts(page = 1, filter = filter, sortOrder = sort, tagFilter = tagFilter).fold(
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import androidx.compose.animation.core.tween
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.BarChart
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
|
|
@ -34,6 +35,7 @@ import com.swoosh.microblog.ui.pages.PagesScreen
|
|||
import com.swoosh.microblog.ui.feed.FeedViewModel
|
||||
import com.swoosh.microblog.ui.members.MemberDetailScreen
|
||||
import com.swoosh.microblog.ui.members.MembersScreen
|
||||
import com.swoosh.microblog.ui.newsletter.NewsletterScreen
|
||||
import com.swoosh.microblog.ui.preview.PreviewScreen
|
||||
import com.swoosh.microblog.ui.settings.SettingsScreen
|
||||
import com.swoosh.microblog.ui.setup.SetupScreen
|
||||
|
|
@ -55,6 +57,7 @@ object Routes {
|
|||
const val MEMBERS = "members"
|
||||
const val MEMBER_DETAIL = "member_detail"
|
||||
const val TAGS = "tags"
|
||||
const val NEWSLETTER = "newsletter"
|
||||
}
|
||||
|
||||
data class BottomNavItem(
|
||||
|
|
@ -65,12 +68,13 @@ data class BottomNavItem(
|
|||
|
||||
val bottomNavItems = listOf(
|
||||
BottomNavItem(Routes.FEED, "Home", Icons.Default.Home),
|
||||
BottomNavItem(Routes.NEWSLETTER, "Newsletter", Icons.Default.Email),
|
||||
BottomNavItem(Routes.STATS, "Stats", Icons.Default.BarChart),
|
||||
BottomNavItem(Routes.SETTINGS, "Settings", Icons.Default.Settings)
|
||||
)
|
||||
|
||||
/** Routes where the bottom navigation bar should be visible */
|
||||
private val bottomBarRoutes = setOf(Routes.FEED, Routes.STATS, Routes.SETTINGS)
|
||||
private val bottomBarRoutes = setOf(Routes.FEED, Routes.NEWSLETTER, Routes.STATS, Routes.SETTINGS)
|
||||
|
||||
@Composable
|
||||
fun SwooshNavGraph(
|
||||
|
|
@ -289,6 +293,16 @@ fun SwooshNavGraph(
|
|||
)
|
||||
}
|
||||
|
||||
composable(
|
||||
Routes.NEWSLETTER,
|
||||
enterTransition = { fadeIn(tween(200)) },
|
||||
exitTransition = { fadeOut(tween(150)) },
|
||||
popEnterTransition = { fadeIn(tween(200)) },
|
||||
popExitTransition = { fadeOut(tween(150)) }
|
||||
) {
|
||||
NewsletterScreen()
|
||||
}
|
||||
|
||||
composable(
|
||||
Routes.TAGS,
|
||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(250)) + fadeIn(tween(200)) },
|
||||
|
|
|
|||
|
|
@ -0,0 +1,296 @@
|
|||
package com.swoosh.microblog.ui.newsletter
|
||||
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.animation.core.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Email
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.swoosh.microblog.data.NewsletterPreferences
|
||||
import com.swoosh.microblog.data.repository.PostRepository
|
||||
import com.swoosh.microblog.data.model.GhostNewsletter
|
||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun NewsletterScreen() {
|
||||
val context = LocalContext.current
|
||||
val newsletterPreferences = remember { NewsletterPreferences(context) }
|
||||
var newsletterEnabled by remember { mutableStateOf(newsletterPreferences.isNewsletterEnabled()) }
|
||||
var validationStatus by remember { mutableStateOf<String?>(null) }
|
||||
var newsletters by remember { mutableStateOf<List<GhostNewsletter>>(emptyList()) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
var subscriberCount by remember { mutableStateOf<Int?>(null) }
|
||||
|
||||
// Load newsletters on launch if enabled
|
||||
LaunchedEffect(newsletterEnabled) {
|
||||
if (newsletterEnabled) {
|
||||
isLoading = true
|
||||
try {
|
||||
val repository = PostRepository(context)
|
||||
val result = repository.fetchNewsletters()
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
newsletters = it
|
||||
validationStatus = "${it.size} newsletter(s) found"
|
||||
},
|
||||
onFailure = {
|
||||
validationStatus = "Could not load newsletters"
|
||||
}
|
||||
)
|
||||
// Fetch subscriber count
|
||||
val membersResult = repository.fetchSubscriberCount()
|
||||
membersResult.fold(
|
||||
onSuccess = { subscriberCount = it },
|
||||
onFailure = { /* ignore */ }
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
validationStatus = "Could not load newsletters"
|
||||
}
|
||||
isLoading = false
|
||||
} else {
|
||||
newsletters = emptyList()
|
||||
validationStatus = null
|
||||
subscriberCount = null
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Newsletter") }
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
.padding(24.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Enable/Disable toggle
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Enable newsletter features",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = newsletterEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
newsletterEnabled = enabled
|
||||
newsletterPreferences.setNewsletterEnabled(enabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Show newsletter sending options when publishing posts",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Newsletter details (when enabled)
|
||||
AnimatedVisibility(
|
||||
visible = newsletterEnabled,
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
if (isLoading) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(32.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Subscriber count card
|
||||
if (subscriberCount != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.People,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Column {
|
||||
Text(
|
||||
text = "$subscriberCount",
|
||||
style = MaterialTheme.typography.headlineMedium,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = "Subscribers",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
}
|
||||
|
||||
// Newsletters list
|
||||
if (newsletters.isNotEmpty()) {
|
||||
Text(
|
||||
"Your Newsletters",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
newsletters.forEach { newsletter ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Email,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = newsletter.name,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
if (!newsletter.description.isNullOrBlank()) {
|
||||
Text(
|
||||
text = newsletter.description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "Status: ${newsletter.status}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
text = "Visibility: ${newsletter.visibility}",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (validationStatus != null) {
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
text = validationStatus!!,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
// Info card
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = "How it works",
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "When enabled, the publish dialog offers options to send posts as newsletters or email-only content to your subscribers.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Disabled state info
|
||||
AnimatedVisibility(
|
||||
visible = !newsletterEnabled,
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
||||
) {
|
||||
Column {
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Email,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Text(
|
||||
text = "Newsletter features are disabled",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Text(
|
||||
text = "Enable to send posts as newsletters to your subscribers",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,7 +13,6 @@ 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.ChevronRight
|
||||
import androidx.compose.material.icons.filled.DarkMode
|
||||
import androidx.compose.material.icons.filled.LightMode
|
||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||
|
|
@ -33,11 +32,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import coil.compose.AsyncImage
|
||||
import com.swoosh.microblog.data.AccountManager
|
||||
import com.swoosh.microblog.data.toDisplayUrl
|
||||
import com.swoosh.microblog.data.NewsletterPreferences
|
||||
import com.swoosh.microblog.data.TagsPreferences
|
||||
import com.swoosh.microblog.data.SiteMetadataCache
|
||||
import com.swoosh.microblog.data.api.ApiClient
|
||||
import com.swoosh.microblog.data.repository.PostRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import com.swoosh.microblog.data.model.GhostAccount
|
||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
||||
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||
|
|
@ -252,50 +249,11 @@ fun SettingsScreen(
|
|||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
// --- Content Management section ---
|
||||
Text("Content", style = MaterialTheme.typography.titleMedium)
|
||||
// --- Features section ---
|
||||
Text("Features", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
|
||||
OutlinedCard(
|
||||
onClick = onNavigateToTags,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Tag,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
Text(
|
||||
"Tags",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Icon(
|
||||
Icons.Default.ChevronRight,
|
||||
contentDescription = "Navigate to tags",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// --- Newsletter section ---
|
||||
NewsletterSettingsSection()
|
||||
TagsSettingsSection(onNavigateToTags = onNavigateToTags)
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
HorizontalDivider()
|
||||
|
|
@ -450,15 +408,10 @@ fun SettingsScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun NewsletterSettingsSection() {
|
||||
fun TagsSettingsSection(onNavigateToTags: () -> Unit = {}) {
|
||||
val context = LocalContext.current
|
||||
val newsletterPreferences = remember { NewsletterPreferences(context) }
|
||||
var newsletterEnabled by remember { mutableStateOf(newsletterPreferences.isNewsletterEnabled()) }
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
var validationStatus by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
Text("Newsletter", style = MaterialTheme.typography.titleMedium)
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
val tagsPreferences = remember { TagsPreferences(context) }
|
||||
var tagsEnabled by remember { mutableStateOf(tagsPreferences.isTagsEnabled()) }
|
||||
|
||||
Card(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
|
|
@ -473,57 +426,45 @@ fun NewsletterSettingsSection() {
|
|||
) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(
|
||||
text = "Enable newsletter features",
|
||||
text = "Enable tags",
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = newsletterEnabled,
|
||||
checked = tagsEnabled,
|
||||
onCheckedChange = { enabled ->
|
||||
newsletterEnabled = enabled
|
||||
newsletterPreferences.setNewsletterEnabled(enabled)
|
||||
if (enabled) {
|
||||
// Best effort: validate by fetching newsletters
|
||||
validationStatus = "Checking..."
|
||||
coroutineScope.launch {
|
||||
try {
|
||||
val repository = PostRepository(context)
|
||||
val result = repository.fetchNewsletters()
|
||||
validationStatus = if (result.isSuccess) {
|
||||
val count = result.getOrNull()?.size ?: 0
|
||||
"$count newsletter(s) found"
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
validationStatus = null
|
||||
}
|
||||
}
|
||||
} else {
|
||||
validationStatus = null
|
||||
}
|
||||
tagsEnabled = enabled
|
||||
tagsPreferences.setTagsEnabled(enabled)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = "Show newsletter sending options when publishing posts",
|
||||
text = "Show tag management and tag filters in feed",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
)
|
||||
|
||||
// Validation status
|
||||
// Manage tags navigation
|
||||
AnimatedVisibility(
|
||||
visible = validationStatus != null,
|
||||
visible = tagsEnabled,
|
||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
||||
) {
|
||||
Text(
|
||||
text = validationStatus ?: "",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
)
|
||||
OutlinedButton(
|
||||
onClick = onNavigateToTags,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Tag,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text("Manage Tags")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue