From 0718a9e74415cc8e35df0f689b63df06fd1732a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Orzech?= Date: Fri, 20 Mar 2026 09:18:30 +0100 Subject: [PATCH] feat: add tags toggle in Settings, move newsletter to bottom tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- .../swoosh/microblog/data/TagsPreferences.kt | 32 ++ .../microblog/ui/composer/ComposerScreen.kt | 22 +- .../ui/composer/ComposerViewModel.kt | 8 +- .../swoosh/microblog/ui/feed/FeedScreen.kt | 7 +- .../swoosh/microblog/ui/feed/FeedViewModel.kt | 32 +- .../microblog/ui/navigation/NavGraph.kt | 16 +- .../ui/newsletter/NewsletterScreen.kt | 296 ++++++++++++++++++ .../microblog/ui/settings/SettingsScreen.kt | 115 ++----- 8 files changed, 416 insertions(+), 112 deletions(-) create mode 100644 app/src/main/java/com/swoosh/microblog/data/TagsPreferences.kt create mode 100644 app/src/main/java/com/swoosh/microblog/ui/newsletter/NewsletterScreen.kt diff --git a/app/src/main/java/com/swoosh/microblog/data/TagsPreferences.kt b/app/src/main/java/com/swoosh/microblog/data/TagsPreferences.kt new file mode 100644 index 0000000..999369d --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/data/TagsPreferences.kt @@ -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" + } +} diff --git a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt index 269c17b..90da9d6 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerScreen.kt @@ -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)) diff --git a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt index 110a7d1..b68a37f 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/composer/ComposerViewModel.kt @@ -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( diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt index 7d69cb7..df5651d 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedScreen.kt @@ -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 ) } diff --git a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt index 0fabab6..31f29ac 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/feed/FeedViewModel.kt @@ -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>(emptyList()) val popularTags: StateFlow> = _popularTags.asStateFlow() + private val _tagsEnabled = MutableStateFlow(true) + val tagsEnabled: StateFlow = _tagsEnabled.asStateFlow() + private val _accounts = MutableStateFlow>(emptyList()) val accounts: StateFlow> = _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( diff --git a/app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt b/app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt index 08ce345..030f5f3 100644 --- a/app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt +++ b/app/src/main/java/com/swoosh/microblog/ui/navigation/NavGraph.kt @@ -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)) }, diff --git a/app/src/main/java/com/swoosh/microblog/ui/newsletter/NewsletterScreen.kt b/app/src/main/java/com/swoosh/microblog/ui/newsletter/NewsletterScreen.kt new file mode 100644 index 0000000..d4fd730 --- /dev/null +++ b/app/src/main/java/com/swoosh/microblog/ui/newsletter/NewsletterScreen.kt @@ -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(null) } + var newsletters by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var subscriberCount by remember { mutableStateOf(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 + ) + } + } + } + } + } + } +} 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 c33bb40..51616af 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 @@ -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(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") + } } } }