mirror of
https://github.com/pawelorzech/Swoosh.git
synced 2026-03-31 11:55:47 +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
|
// Tags section: input + suggestions + chips (only when tags enabled)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
if (viewModel.isTagsEnabled()) {
|
||||||
TagsSection(
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
tagInput = state.tagInput,
|
TagsSection(
|
||||||
onTagInputChange = viewModel::updateTagInput,
|
tagInput = state.tagInput,
|
||||||
tagSuggestions = state.tagSuggestions,
|
onTagInputChange = viewModel::updateTagInput,
|
||||||
extractedTags = state.extractedTags,
|
tagSuggestions = state.tagSuggestions,
|
||||||
onAddTag = viewModel::addTag,
|
extractedTags = state.extractedTags,
|
||||||
onRemoveTag = viewModel::removeTag
|
onAddTag = viewModel::addTag,
|
||||||
)
|
onRemoveTag = viewModel::removeTag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
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.HashtagParser
|
||||||
import com.swoosh.microblog.data.MobiledocBuilder
|
import com.swoosh.microblog.data.MobiledocBuilder
|
||||||
import com.swoosh.microblog.data.NewsletterPreferences
|
import com.swoosh.microblog.data.NewsletterPreferences
|
||||||
|
import com.swoosh.microblog.data.TagsPreferences
|
||||||
import com.swoosh.microblog.data.PreviewHtmlBuilder
|
import com.swoosh.microblog.data.PreviewHtmlBuilder
|
||||||
import com.swoosh.microblog.data.db.Converters
|
import com.swoosh.microblog.data.db.Converters
|
||||||
import com.swoosh.microblog.data.model.*
|
import com.swoosh.microblog.data.model.*
|
||||||
|
|
@ -29,6 +30,7 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
||||||
private val repository = PostRepository(application)
|
private val repository = PostRepository(application)
|
||||||
private val tagRepository = TagRepository(application)
|
private val tagRepository = TagRepository(application)
|
||||||
private val newsletterPreferences = NewsletterPreferences(application)
|
private val newsletterPreferences = NewsletterPreferences(application)
|
||||||
|
private val tagsPreferences = TagsPreferences(application)
|
||||||
private val appContext = application
|
private val appContext = application
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(ComposerUiState())
|
private val _uiState = MutableStateFlow(ComposerUiState())
|
||||||
|
|
@ -41,10 +43,14 @@ class ComposerViewModel(application: Application) : AndroidViewModel(application
|
||||||
private var previewDebounceJob: Job? = null
|
private var previewDebounceJob: Job? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadAvailableTags()
|
if (tagsPreferences.isTagsEnabled()) {
|
||||||
|
loadAvailableTags()
|
||||||
|
}
|
||||||
loadNewsletterData()
|
loadNewsletterData()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isTagsEnabled(): Boolean = tagsPreferences.isTagsEnabled()
|
||||||
|
|
||||||
private fun loadAvailableTags() {
|
private fun loadAvailableTags() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
tagRepository.fetchTags().fold(
|
tagRepository.fetchTags().fold(
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,7 @@ fun FeedScreen(
|
||||||
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
|
val accounts by viewModel.accounts.collectAsStateWithLifecycle()
|
||||||
val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle()
|
val activeAccount by viewModel.activeAccount.collectAsStateWithLifecycle()
|
||||||
val popularTags by viewModel.popularTags.collectAsStateWithLifecycle()
|
val popularTags by viewModel.popularTags.collectAsStateWithLifecycle()
|
||||||
|
val tagsEnabled by viewModel.tagsEnabled.collectAsStateWithLifecycle()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
@ -331,7 +332,7 @@ fun FeedScreen(
|
||||||
|
|
||||||
// Tag filter chips
|
// Tag filter chips
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = !isSearchActive && popularTags.isNotEmpty(),
|
visible = !isSearchActive && tagsEnabled && popularTags.isNotEmpty(),
|
||||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(),
|
enter = fadeIn(SwooshMotion.quick()) + expandVertically(),
|
||||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically()
|
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically()
|
||||||
) {
|
) {
|
||||||
|
|
@ -588,7 +589,7 @@ fun FeedScreen(
|
||||||
onEdit = { onEditPost(post) },
|
onEdit = { onEditPost(post) },
|
||||||
onDelete = { postPendingDelete = post },
|
onDelete = { postPendingDelete = post },
|
||||||
onTogglePin = { viewModel.toggleFeatured(post) },
|
onTogglePin = { viewModel.toggleFeatured(post) },
|
||||||
onTagClick = { tag -> viewModel.filterByTag(tag) },
|
onTagClick = { tag -> if (tagsEnabled) viewModel.filterByTag(tag) },
|
||||||
snackbarHostState = snackbarHostState
|
snackbarHostState = snackbarHostState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -614,7 +615,7 @@ fun FeedScreen(
|
||||||
onEdit = { onEditPost(post) },
|
onEdit = { onEditPost(post) },
|
||||||
onDelete = { postPendingDelete = post },
|
onDelete = { postPendingDelete = post },
|
||||||
onTogglePin = { viewModel.toggleFeatured(post) },
|
onTogglePin = { viewModel.toggleFeatured(post) },
|
||||||
onTagClick = { tag -> viewModel.filterByTag(tag) },
|
onTagClick = { tag -> if (tagsEnabled) viewModel.filterByTag(tag) },
|
||||||
snackbarHostState = snackbarHostState
|
snackbarHostState = snackbarHostState
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import com.swoosh.microblog.data.AccountManager
|
||||||
import com.swoosh.microblog.data.CredentialsManager
|
import com.swoosh.microblog.data.CredentialsManager
|
||||||
import com.swoosh.microblog.data.FeedPreferences
|
import com.swoosh.microblog.data.FeedPreferences
|
||||||
import com.swoosh.microblog.data.HashtagParser
|
import com.swoosh.microblog.data.HashtagParser
|
||||||
|
import com.swoosh.microblog.data.TagsPreferences
|
||||||
import com.swoosh.microblog.data.api.ApiClient
|
import com.swoosh.microblog.data.api.ApiClient
|
||||||
import com.swoosh.microblog.data.db.Converters
|
import com.swoosh.microblog.data.db.Converters
|
||||||
import com.swoosh.microblog.data.model.*
|
import com.swoosh.microblog.data.model.*
|
||||||
|
|
@ -42,6 +43,7 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private var repository = PostRepository(application)
|
private var repository = PostRepository(application)
|
||||||
private var tagRepository = TagRepository(application)
|
private var tagRepository = TagRepository(application)
|
||||||
private val feedPreferences = FeedPreferences(application)
|
private val feedPreferences = FeedPreferences(application)
|
||||||
|
private val tagsPreferences = TagsPreferences(application)
|
||||||
private val searchHistoryManager = SearchHistoryManager(application)
|
private val searchHistoryManager = SearchHistoryManager(application)
|
||||||
|
|
||||||
private val _uiState = MutableStateFlow(FeedUiState())
|
private val _uiState = MutableStateFlow(FeedUiState())
|
||||||
|
|
@ -77,6 +79,9 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
private val _popularTags = MutableStateFlow<List<GhostTagFull>>(emptyList())
|
private val _popularTags = MutableStateFlow<List<GhostTagFull>>(emptyList())
|
||||||
val popularTags: StateFlow<List<GhostTagFull>> = _popularTags.asStateFlow()
|
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())
|
private val _accounts = MutableStateFlow<List<GhostAccount>>(emptyList())
|
||||||
val accounts: StateFlow<List<GhostAccount>> = _accounts.asStateFlow()
|
val accounts: StateFlow<List<GhostAccount>> = _accounts.asStateFlow()
|
||||||
|
|
||||||
|
|
@ -303,16 +308,23 @@ class FeedViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
val sort = _sortOrder.value
|
val sort = _sortOrder.value
|
||||||
val tagFilter = _uiState.value.activeTagFilter
|
val tagFilter = _uiState.value.activeTagFilter
|
||||||
|
|
||||||
// Fetch popular tags in parallel
|
// Fetch popular tags in parallel (only when tags feature is enabled)
|
||||||
launch {
|
val tagsOn = tagsPreferences.isTagsEnabled()
|
||||||
tagRepository.fetchTags().fold(
|
_tagsEnabled.value = tagsOn
|
||||||
onSuccess = { tags ->
|
if (tagsOn) {
|
||||||
_popularTags.value = tags
|
launch {
|
||||||
.sortedByDescending { it.count?.posts ?: 0 }
|
tagRepository.fetchTags().fold(
|
||||||
.take(10)
|
onSuccess = { tags ->
|
||||||
},
|
_popularTags.value = tags
|
||||||
onFailure = { /* silently ignore tag fetch failures */ }
|
.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(
|
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.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.BarChart
|
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.Home
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.*
|
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.feed.FeedViewModel
|
||||||
import com.swoosh.microblog.ui.members.MemberDetailScreen
|
import com.swoosh.microblog.ui.members.MemberDetailScreen
|
||||||
import com.swoosh.microblog.ui.members.MembersScreen
|
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.preview.PreviewScreen
|
||||||
import com.swoosh.microblog.ui.settings.SettingsScreen
|
import com.swoosh.microblog.ui.settings.SettingsScreen
|
||||||
import com.swoosh.microblog.ui.setup.SetupScreen
|
import com.swoosh.microblog.ui.setup.SetupScreen
|
||||||
|
|
@ -55,6 +57,7 @@ object Routes {
|
||||||
const val MEMBERS = "members"
|
const val MEMBERS = "members"
|
||||||
const val MEMBER_DETAIL = "member_detail"
|
const val MEMBER_DETAIL = "member_detail"
|
||||||
const val TAGS = "tags"
|
const val TAGS = "tags"
|
||||||
|
const val NEWSLETTER = "newsletter"
|
||||||
}
|
}
|
||||||
|
|
||||||
data class BottomNavItem(
|
data class BottomNavItem(
|
||||||
|
|
@ -65,12 +68,13 @@ data class BottomNavItem(
|
||||||
|
|
||||||
val bottomNavItems = listOf(
|
val bottomNavItems = listOf(
|
||||||
BottomNavItem(Routes.FEED, "Home", Icons.Default.Home),
|
BottomNavItem(Routes.FEED, "Home", Icons.Default.Home),
|
||||||
|
BottomNavItem(Routes.NEWSLETTER, "Newsletter", Icons.Default.Email),
|
||||||
BottomNavItem(Routes.STATS, "Stats", Icons.Default.BarChart),
|
BottomNavItem(Routes.STATS, "Stats", Icons.Default.BarChart),
|
||||||
BottomNavItem(Routes.SETTINGS, "Settings", Icons.Default.Settings)
|
BottomNavItem(Routes.SETTINGS, "Settings", Icons.Default.Settings)
|
||||||
)
|
)
|
||||||
|
|
||||||
/** Routes where the bottom navigation bar should be visible */
|
/** 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
|
@Composable
|
||||||
fun SwooshNavGraph(
|
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(
|
composable(
|
||||||
Routes.TAGS,
|
Routes.TAGS,
|
||||||
enterTransition = { slideInHorizontally(initialOffsetX = { it }, animationSpec = tween(250)) + fadeIn(tween(200)) },
|
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.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.filled.BrightnessAuto
|
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.DarkMode
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
import androidx.compose.material.icons.automirrored.filled.OpenInNew
|
||||||
|
|
@ -33,11 +32,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import com.swoosh.microblog.data.AccountManager
|
import com.swoosh.microblog.data.AccountManager
|
||||||
import com.swoosh.microblog.data.toDisplayUrl
|
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.SiteMetadataCache
|
||||||
import com.swoosh.microblog.data.api.ApiClient
|
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.data.model.GhostAccount
|
||||||
import com.swoosh.microblog.ui.animation.SwooshMotion
|
import com.swoosh.microblog.ui.animation.SwooshMotion
|
||||||
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
import com.swoosh.microblog.ui.components.ConfirmationDialog
|
||||||
|
|
@ -252,50 +249,11 @@ fun SettingsScreen(
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Content Management section ---
|
// --- Features section ---
|
||||||
Text("Content", style = MaterialTheme.typography.titleMedium)
|
Text("Features", style = MaterialTheme.typography.titleMedium)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
OutlinedCard(
|
TagsSettingsSection(onNavigateToTags = onNavigateToTags)
|
||||||
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()
|
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(24.dp))
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
|
|
@ -450,15 +408,10 @@ fun SettingsScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NewsletterSettingsSection() {
|
fun TagsSettingsSection(onNavigateToTags: () -> Unit = {}) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val newsletterPreferences = remember { NewsletterPreferences(context) }
|
val tagsPreferences = remember { TagsPreferences(context) }
|
||||||
var newsletterEnabled by remember { mutableStateOf(newsletterPreferences.isNewsletterEnabled()) }
|
var tagsEnabled by remember { mutableStateOf(tagsPreferences.isTagsEnabled()) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
var validationStatus by remember { mutableStateOf<String?>(null) }
|
|
||||||
|
|
||||||
Text("Newsletter", style = MaterialTheme.typography.titleMedium)
|
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
|
||||||
|
|
||||||
Card(modifier = Modifier.fillMaxWidth()) {
|
Card(modifier = Modifier.fillMaxWidth()) {
|
||||||
Column(
|
Column(
|
||||||
|
|
@ -473,57 +426,45 @@ fun NewsletterSettingsSection() {
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
Text(
|
Text(
|
||||||
text = "Enable newsletter features",
|
text = "Enable tags",
|
||||||
style = MaterialTheme.typography.bodyLarge
|
style = MaterialTheme.typography.bodyLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Switch(
|
Switch(
|
||||||
checked = newsletterEnabled,
|
checked = tagsEnabled,
|
||||||
onCheckedChange = { enabled ->
|
onCheckedChange = { enabled ->
|
||||||
newsletterEnabled = enabled
|
tagsEnabled = enabled
|
||||||
newsletterPreferences.setNewsletterEnabled(enabled)
|
tagsPreferences.setTagsEnabled(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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "Show newsletter sending options when publishing posts",
|
text = "Show tag management and tag filters in feed",
|
||||||
style = MaterialTheme.typography.bodySmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
// Validation status
|
// Manage tags navigation
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = validationStatus != null,
|
visible = tagsEnabled,
|
||||||
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
enter = fadeIn(SwooshMotion.quick()) + expandVertically(animationSpec = SwooshMotion.snappy()),
|
||||||
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
exit = fadeOut(SwooshMotion.quick()) + shrinkVertically(animationSpec = SwooshMotion.snappy())
|
||||||
) {
|
) {
|
||||||
Text(
|
OutlinedButton(
|
||||||
text = validationStatus ?: "",
|
onClick = onNavigateToTags,
|
||||||
style = MaterialTheme.typography.labelSmall,
|
modifier = Modifier
|
||||||
color = MaterialTheme.colorScheme.primary,
|
.fillMaxWidth()
|
||||||
modifier = Modifier.padding(top = 4.dp)
|
.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