mirror of
https://github.com/LiveFastEatTrashRaccoon/RaccoonForLemmy.git
synced 2025-02-03 04:57:35 +01:00
feat: multi-communities (#36)
* chore: add persistence * chore: move serializable to core-utils * fix: leftover color in user detail * chore: new messages * feat: add multi-communities in subscription list * feat: add multi-community editor * feat: add multi-community detail
This commit is contained in:
parent
8414cf5019
commit
abc9b3632a
@ -0,0 +1,72 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.commonui.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
|
||||
@Composable
|
||||
fun MultiCommunityItem(
|
||||
community: MultiCommunityModel,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val title = community.name
|
||||
val communityIcon = community.icon.orEmpty()
|
||||
val iconSize = 30.dp
|
||||
Row(
|
||||
modifier = modifier.padding(
|
||||
vertical = Spacing.xs,
|
||||
horizontal = Spacing.s,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(Spacing.xs),
|
||||
) {
|
||||
if (communityIcon.isNotEmpty()) {
|
||||
CustomImage(
|
||||
modifier = Modifier.padding(Spacing.xxxs).size(iconSize)
|
||||
.clip(RoundedCornerShape(iconSize / 2)),
|
||||
url = communityIcon,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier = Modifier.padding(Spacing.xxxs).size(iconSize)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(iconSize / 2),
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = title.firstOrNull()?.toString().orEmpty().uppercase(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
Column {
|
||||
Text(
|
||||
modifier = Modifier.padding(vertical = Spacing.s),
|
||||
text = buildString {
|
||||
append(title)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -438,7 +438,7 @@ class UserDetailScreen(
|
||||
},
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.secondary
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
|
@ -15,4 +15,5 @@ object NotificationCenterContractKeys {
|
||||
const val PostUpdated = "postUpdated"
|
||||
const val PostDeleted = "postDeleted"
|
||||
const val ChangeColor = "changeColor"
|
||||
const val MultiCommunityCreated = "multiCommunityCreated"
|
||||
}
|
@ -48,6 +48,7 @@ kotlin {
|
||||
implementation(libs.koin.core)
|
||||
|
||||
implementation(projects.corePreferences)
|
||||
implementation(projects.coreUtils)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class AccountModel(
|
||||
val id: Long? = null,
|
||||
val username: String,
|
||||
@ -7,4 +9,4 @@ data class AccountModel(
|
||||
val instance: String,
|
||||
val jwt: String,
|
||||
val active: Boolean = false,
|
||||
)
|
||||
) : JavaSerializable
|
||||
|
@ -0,0 +1,10 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class MultiCommunityModel(
|
||||
val id: Long? = null,
|
||||
val name: String = "",
|
||||
val communityIds: List<Int> = emptyList(),
|
||||
val icon: String? = null,
|
||||
) : JavaSerializable
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class SettingsModel(
|
||||
val id: Long? = null,
|
||||
val theme: Int? = null,
|
||||
@ -16,4 +18,4 @@ data class SettingsModel(
|
||||
val enableSwipeActions: Boolean = true,
|
||||
val customSeedColor: Int? = null,
|
||||
val postLayout: Int = 0,
|
||||
)
|
||||
) : JavaSerializable
|
||||
|
@ -4,7 +4,9 @@ import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DatabaseProvide
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DefaultDatabaseProvider
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultAccountRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultMultiCommunityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.DefaultSettingsRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
|
||||
import org.koin.dsl.module
|
||||
|
||||
@ -26,4 +28,9 @@ val corePersistenceModule = module {
|
||||
keyStore = get(),
|
||||
)
|
||||
}
|
||||
single<MultiCommunityRepository> {
|
||||
DefaultMultiCommunityRepository(
|
||||
provider = get()
|
||||
)
|
||||
}
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.DatabaseProvider
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.MultiCommunityEntity
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
internal class DefaultMultiCommunityRepository(
|
||||
val provider: DatabaseProvider,
|
||||
) : MultiCommunityRepository {
|
||||
|
||||
private val db = provider.getDatabase()
|
||||
|
||||
override suspend fun getAll(accountId: Long?): List<MultiCommunityModel> =
|
||||
withContext(Dispatchers.IO) {
|
||||
db.multicommunitiesQueries.getAll(accountId)
|
||||
.executeAsList().map { it.toModel() }
|
||||
}
|
||||
|
||||
override suspend fun create(model: MultiCommunityModel, accountId: Long): Long =
|
||||
withContext(Dispatchers.IO) {
|
||||
db.multicommunitiesQueries.create(
|
||||
name = model.name,
|
||||
icon = model.icon,
|
||||
communityIds = model.communityIds.joinToString(","),
|
||||
account_id = accountId,
|
||||
)
|
||||
val id = db.multicommunitiesQueries.getBy(name = model.name, account_id = accountId)
|
||||
.executeAsOneOrNull()?.id
|
||||
id ?: 0L
|
||||
}
|
||||
|
||||
override suspend fun update(model: MultiCommunityModel) =
|
||||
withContext(Dispatchers.IO) {
|
||||
db.multicommunitiesQueries.update(
|
||||
name = model.name,
|
||||
icon = model.icon,
|
||||
communityIds = model.communityIds.joinToString(","),
|
||||
id = model.id ?: 0,
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun delete(model: MultiCommunityModel) =
|
||||
withContext(Dispatchers.IO) {
|
||||
db.multicommunitiesQueries.delete(model.id ?: 0)
|
||||
}
|
||||
}
|
||||
|
||||
private fun MultiCommunityEntity.toModel() = MultiCommunityModel(
|
||||
id = id,
|
||||
name = name,
|
||||
icon = icon,
|
||||
communityIds = communityIds
|
||||
.split(",")
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotEmpty() }
|
||||
.map { it.toInt() }
|
||||
)
|
@ -26,7 +26,7 @@ private object KeyStoreKeys {
|
||||
const val PostLayout = "postLayout"
|
||||
}
|
||||
|
||||
class DefaultSettingsRepository(
|
||||
internal class DefaultSettingsRepository(
|
||||
val provider: DatabaseProvider,
|
||||
private val keyStore: TemporaryKeyStore,
|
||||
) : SettingsRepository {
|
||||
@ -37,7 +37,7 @@ class DefaultSettingsRepository(
|
||||
|
||||
override suspend fun createSettings(settings: SettingsModel, accountId: Long) =
|
||||
withContext(Dispatchers.IO) {
|
||||
db.settingsQueries.createSettings(
|
||||
db.settingsQueries.create(
|
||||
theme = settings.theme?.toLong(),
|
||||
contentFontScale = settings.contentFontScale.toDouble(),
|
||||
locale = settings.locale,
|
||||
@ -77,7 +77,7 @@ class DefaultSettingsRepository(
|
||||
postLayout = keyStore[KeyStoreKeys.PostLayout, 0],
|
||||
)
|
||||
} else {
|
||||
db.settingsQueries.getSettings(accountId)
|
||||
db.settingsQueries.getBy(accountId)
|
||||
.executeAsOneOrNull()?.toModel() ?: SettingsModel()
|
||||
}
|
||||
}
|
||||
@ -110,7 +110,7 @@ class DefaultSettingsRepository(
|
||||
}
|
||||
keyStore.save(KeyStoreKeys.PostLayout, settings.postLayout)
|
||||
} else {
|
||||
db.settingsQueries.updateSettings(
|
||||
db.settingsQueries.update(
|
||||
theme = settings.theme?.toLong(),
|
||||
contentFontScale = settings.contentFontScale.toDouble(),
|
||||
locale = settings.locale,
|
||||
|
@ -0,0 +1,13 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
|
||||
interface MultiCommunityRepository {
|
||||
suspend fun getAll(accountId: Long?): List<MultiCommunityModel>
|
||||
|
||||
suspend fun create(model: MultiCommunityModel, accountId: Long): Long
|
||||
|
||||
suspend fun update(model: MultiCommunityModel)
|
||||
|
||||
suspend fun delete(model: MultiCommunityModel)
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
CREATE TABLE MultiCommunityEntity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT "",
|
||||
icon TEXT DEFAULT NULL,
|
||||
communityIds TEXT NOT NULL DEFAULT "",
|
||||
account_id INTEGER,
|
||||
FOREIGN KEY (account_id) REFERENCES AccountEntity(id) ON DELETE CASCADE
|
||||
);
|
@ -0,0 +1,44 @@
|
||||
CREATE TABLE MultiCommunityEntity (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL DEFAULT "",
|
||||
icon TEXT DEFAULT NULL,
|
||||
communityIds TEXT NOT NULL DEFAULT "",
|
||||
account_id INTEGER,
|
||||
FOREIGN KEY (account_id) REFERENCES AccountEntity(id) ON DELETE CASCADE,
|
||||
UNIQUE(name, account_id)
|
||||
);
|
||||
|
||||
getAll:
|
||||
SELECT *
|
||||
FROM MultiCommunityEntity
|
||||
WHERE account_id = ?;
|
||||
|
||||
getBy:
|
||||
SELECT *
|
||||
FROM MultiCommunityEntity
|
||||
WHERE name = ? AND account_id = ?;
|
||||
|
||||
create:
|
||||
INSERT OR IGNORE INTO MultiCommunityEntity (
|
||||
name,
|
||||
icon,
|
||||
communityIds,
|
||||
account_id
|
||||
) VALUES (
|
||||
?,
|
||||
?,
|
||||
?,
|
||||
?
|
||||
);
|
||||
|
||||
update:
|
||||
UPDATE OR IGNORE MultiCommunityEntity
|
||||
SET
|
||||
name = ?,
|
||||
icon = ?,
|
||||
communityIds = ?
|
||||
WHERE id = ?;
|
||||
|
||||
delete:
|
||||
DELETE FROM MultiCommunityEntity
|
||||
WHERE id = ?;
|
@ -19,7 +19,7 @@ CREATE TABLE SettingsEntity (
|
||||
UNIQUE(account_id)
|
||||
);
|
||||
|
||||
createSettings:
|
||||
create:
|
||||
INSERT OR IGNORE INTO SettingsEntity (
|
||||
theme,
|
||||
contentFontScale,
|
||||
@ -54,7 +54,7 @@ INSERT OR IGNORE INTO SettingsEntity (
|
||||
?
|
||||
);
|
||||
|
||||
updateSettings:
|
||||
update:
|
||||
UPDATE SettingsEntity
|
||||
SET theme = ?,
|
||||
contentFontScale = ?,
|
||||
@ -72,7 +72,7 @@ SET theme = ?,
|
||||
postLayout = ?
|
||||
WHERE account_id = ?;
|
||||
|
||||
getSettings:
|
||||
getBy:
|
||||
SELECT *
|
||||
FROM SettingsEntity
|
||||
WHERE account_id = ?;
|
@ -0,0 +1,3 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.utils
|
||||
|
||||
actual typealias JavaSerializable = java.io.Serializable
|
@ -0,0 +1,3 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.utils
|
||||
|
||||
expect interface JavaSerializable
|
@ -0,0 +1,3 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.core.utils
|
||||
|
||||
actual interface JavaSerializable
|
@ -31,7 +31,9 @@ kotlin {
|
||||
implementation(compose.runtime)
|
||||
implementation(compose.foundation)
|
||||
implementation(compose.materialIconsExtended)
|
||||
|
||||
implementation(projects.resources)
|
||||
implementation(projects.coreUtils)
|
||||
}
|
||||
}
|
||||
val commonTest by getting {
|
||||
|
@ -1,3 +0,0 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
actual typealias JavaSerializable = java.io.Serializable
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class CommentModel(
|
||||
val id: Int = 0,
|
||||
val postId: Int = 0,
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class CommunityModel(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
|
@ -1,6 +1,8 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class MetadataModel(
|
||||
val title: String = "",
|
||||
val description: String = "",
|
||||
): JavaSerializable
|
||||
) : JavaSerializable
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class PersonMentionModel(
|
||||
val id: Int = 0,
|
||||
val post: PostModel,
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class PostModel(
|
||||
val id: Int = 0,
|
||||
val title: String = "",
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class PrivateMessageModel(
|
||||
val id: Int = 0,
|
||||
val content: String? = null,
|
||||
@ -8,4 +10,4 @@ data class PrivateMessageModel(
|
||||
val publishDate: String? = null,
|
||||
val updateDate: String? = null,
|
||||
val read: Boolean = false,
|
||||
)
|
||||
) : JavaSerializable
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class UserModel(
|
||||
val id: Int = 0,
|
||||
val name: String = "",
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.JavaSerializable
|
||||
|
||||
data class UserScoreModel(
|
||||
val postScore: Int = 0,
|
||||
val commentScore: Int = 0,
|
||||
|
@ -1,3 +0,0 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
expect interface JavaSerializable
|
@ -1,3 +0,0 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
actual interface JavaSerializable
|
@ -1,3 +0,0 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data
|
||||
|
||||
actual interface JavaSerializable
|
@ -1,7 +1,11 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
|
||||
actual fun getExploreViewModel(): ExploreViewModel {
|
||||
@ -14,3 +18,18 @@ actual fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel {
|
||||
return res
|
||||
}
|
||||
|
||||
actual fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel {
|
||||
val res: MultiCommunityViewModel by inject(
|
||||
MultiCommunityViewModel::class.java,
|
||||
parameters = { parametersOf(community) }
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
||||
actual fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel {
|
||||
val res: MultiCommunityEditorViewModel by inject(
|
||||
MultiCommunityEditorViewModel::class.java,
|
||||
parameters = { parametersOf(editedCommunity) }
|
||||
)
|
||||
return res
|
||||
}
|
||||
|
@ -5,6 +5,12 @@ import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreMviMo
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.DefaultMultiCommunityPaginator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.MultiCommunityPaginator
|
||||
import org.koin.dsl.module
|
||||
|
||||
val searchTabModule = module {
|
||||
@ -27,7 +33,40 @@ val searchTabModule = module {
|
||||
mvi = DefaultMviModel(ManageSubscriptionsMviModel.UiState()),
|
||||
identityRepository = get(),
|
||||
communityRepository = get(),
|
||||
accountRepository = get(),
|
||||
multiCommunityRepository = get(),
|
||||
hapticFeedback = get(),
|
||||
notificationCenter = get(),
|
||||
)
|
||||
}
|
||||
factory { params ->
|
||||
MultiCommunityViewModel(
|
||||
mvi = DefaultMviModel(MultiCommunityMviModel.UiState()),
|
||||
community = params[0],
|
||||
postRepository = get(),
|
||||
identityRepository = get(),
|
||||
themeRepository = get(),
|
||||
shareHelper = get(),
|
||||
settingsRepository = get(),
|
||||
notificationCenter = get(),
|
||||
hapticFeedback = get(),
|
||||
paginator = get(),
|
||||
)
|
||||
}
|
||||
factory<MultiCommunityPaginator> {
|
||||
DefaultMultiCommunityPaginator(
|
||||
postRepository = get(),
|
||||
)
|
||||
}
|
||||
factory { params ->
|
||||
MultiCommunityEditorViewModel(
|
||||
mvi = DefaultMviModel(MultiCommunityEditorMviModel.UiState()),
|
||||
editedCommunity = params[0],
|
||||
identityRepository = get(),
|
||||
communityRepository = get(),
|
||||
accountRepository = get(),
|
||||
multiCommunityRepository = get(),
|
||||
notificationCenter = get(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,15 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
|
||||
|
||||
expect fun getExploreViewModel(): ExploreViewModel
|
||||
|
||||
expect fun getManageSubscriptionsViewModel(): ManageSubscriptionsViewModel
|
||||
|
||||
expect fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel
|
||||
|
||||
expect fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
|
||||
interface ManageSubscriptionsMviModel :
|
||||
@ -8,12 +9,13 @@ interface ManageSubscriptionsMviModel :
|
||||
sealed interface Intent {
|
||||
data object Refresh : Intent
|
||||
data object HapticIndication : Intent
|
||||
|
||||
data class Unsubscribe(val index: Int) : Intent
|
||||
data class DeleteMultiCommunity(val index: Int) : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val refreshing: Boolean = false,
|
||||
val multiCommunities: List<MultiCommunityModel> = emptyList(),
|
||||
val communities: List<CommunityModel> = emptyList(),
|
||||
)
|
||||
|
||||
|
@ -4,18 +4,22 @@ import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.DismissDirection
|
||||
import androidx.compose.material.DismissValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Unsubscribe
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
@ -41,10 +45,13 @@ import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommunityItem
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.MultiCommunityItem
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getManageSubscriptionsViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
@ -81,15 +88,11 @@ class ManageSubscriptionsScreen : Screen {
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
val pullRefreshState = rememberPullRefreshState(
|
||||
uiState.refreshing,
|
||||
{
|
||||
model.reduce(ManageSubscriptionsMviModel.Intent.Refresh)
|
||||
}
|
||||
)
|
||||
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
|
||||
model.reduce(ManageSubscriptionsMviModel.Intent.Refresh)
|
||||
})
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
modifier = Modifier.padding(paddingValues)
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||
.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
@ -97,6 +100,87 @@ class ManageSubscriptionsScreen : Screen {
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
|
||||
) {
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing.s),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.manage_subscriptions_header_multicommunities),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.push(MultiCommunityEditorScreen())
|
||||
},
|
||||
imageVector = Icons.Default.AddCircle,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(uiState.multiCommunities) { idx, community ->
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
else -> Color.Transparent
|
||||
}
|
||||
},
|
||||
onGestureBegin = {
|
||||
model.reduce(ManageSubscriptionsMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
navigator?.push(
|
||||
MultiCommunityEditorScreen(community),
|
||||
)
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(
|
||||
ManageSubscriptionsMviModel.Intent.DeleteMultiCommunity(idx),
|
||||
)
|
||||
},
|
||||
swipeContent = { direction ->
|
||||
val icon = when (direction) {
|
||||
DismissDirection.StartToEnd -> Icons.Default.Delete
|
||||
DismissDirection.EndToStart -> Icons.Default.Edit
|
||||
}
|
||||
Icon(
|
||||
modifier = Modifier.padding(Spacing.xs),
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
MultiCommunityItem(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background).onClick {
|
||||
navigator?.push(
|
||||
MultiCommunityScreen(community),
|
||||
)
|
||||
},
|
||||
community = community,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = Spacing.s),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.manage_subscriptions_header_subscriptions),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
itemsIndexed(uiState.communities) { idx, community ->
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -117,21 +201,16 @@ class ManageSubscriptionsScreen : Screen {
|
||||
},
|
||||
swipeContent = { _ ->
|
||||
Icon(
|
||||
modifier = Modifier.background(
|
||||
color = MaterialTheme.colorScheme.onSecondary,
|
||||
shape = CircleShape,
|
||||
).padding(Spacing.xs),
|
||||
modifier = Modifier.padding(Spacing.xs),
|
||||
imageVector = Icons.Default.Unsubscribe,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.secondary,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
CommunityItem(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.onClick {
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background).onClick {
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
|
@ -3,6 +3,11 @@ package com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscripti
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
@ -15,10 +20,29 @@ class ManageSubscriptionsViewModel(
|
||||
private val mvi: DefaultMviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect>,
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val communityRepository: CommunityRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val multiCommunityRepository: MultiCommunityRepository,
|
||||
private val hapticFeedback: HapticFeedback,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
) : ScreenModel,
|
||||
MviModel<ManageSubscriptionsMviModel.Intent, ManageSubscriptionsMviModel.UiState, ManageSubscriptionsMviModel.Effect> by mvi {
|
||||
|
||||
init {
|
||||
notificationCenter.addObserver(
|
||||
{ evt ->
|
||||
(evt as? MultiCommunityModel)?.also {
|
||||
handleMultiCommunityCreated(it)
|
||||
}
|
||||
},
|
||||
this::class.simpleName.orEmpty(),
|
||||
NotificationCenterContractKeys.MultiCommunityCreated
|
||||
)
|
||||
}
|
||||
|
||||
fun finalize() {
|
||||
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
|
||||
}
|
||||
|
||||
override fun onStarted() {
|
||||
mvi.onStarted()
|
||||
if (uiState.value.communities.isEmpty()) {
|
||||
@ -31,7 +55,11 @@ class ManageSubscriptionsViewModel(
|
||||
ManageSubscriptionsMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
|
||||
ManageSubscriptionsMviModel.Intent.Refresh -> refresh()
|
||||
is ManageSubscriptionsMviModel.Intent.Unsubscribe -> handleUnsubscription(
|
||||
community = uiState.value.communities[intent.index]
|
||||
community = uiState.value.communities[intent.index],
|
||||
)
|
||||
|
||||
is ManageSubscriptionsMviModel.Intent.DeleteMultiCommunity -> deleteMultiCommunity(
|
||||
community = uiState.value.multiCommunities[intent.index],
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -43,11 +71,15 @@ class ManageSubscriptionsViewModel(
|
||||
mvi.updateState { it.copy(refreshing = true) }
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
val auth = identityRepository.authToken.value
|
||||
val items = communityRepository.getSubscribed(auth).sortedBy { it.name }
|
||||
val communities = communityRepository.getSubscribed(auth).sortedBy { it.name }
|
||||
val accountId = accountRepository.getActive()?.id ?: 0L
|
||||
val multiCommunitites = multiCommunityRepository.getAll(accountId).sortedBy { it.name }
|
||||
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
refreshing = false,
|
||||
communities = items,
|
||||
communities = communities,
|
||||
multiCommunities = multiCommunitites,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -64,4 +96,30 @@ class ManageSubscriptionsViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteMultiCommunity(community: MultiCommunityModel) {
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
multiCommunityRepository.delete(community)
|
||||
mvi.updateState {
|
||||
val newCommunities = it.multiCommunities.filter { c -> c.id != community.id }
|
||||
it.copy(multiCommunities = newCommunities)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleMultiCommunityCreated(community: MultiCommunityModel) {
|
||||
val oldCommunities = uiState.value.multiCommunities
|
||||
val newCommunities = if (oldCommunities.any { it.id == community.id }) {
|
||||
oldCommunities.map {
|
||||
if (it.id == community.id) {
|
||||
community
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
} else {
|
||||
oldCommunities + community
|
||||
}.sortedBy { it.name }
|
||||
mvi.updateState { it.copy(multiCommunities = newCommunities) }
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
|
||||
interface MultiCommunityMviModel :
|
||||
MviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect> {
|
||||
sealed interface Intent {
|
||||
data object Refresh : Intent
|
||||
data object LoadNextPage : Intent
|
||||
data class ChangeSort(val value: SortType) : Intent
|
||||
data object HapticIndication : Intent
|
||||
data class UpVotePost(val index: Int, val feedback: Boolean = false) : Intent
|
||||
data class DownVotePost(val index: Int, val feedback: Boolean = false) : Intent
|
||||
data class SavePost(val index: Int, val feedback: Boolean = false) : Intent
|
||||
data class SharePost(val index: Int) : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val refreshing: Boolean = false,
|
||||
val loading: Boolean = false,
|
||||
val canFetchMore: Boolean = true,
|
||||
val instance: String = "",
|
||||
val isLogged: Boolean = false,
|
||||
val sortType: SortType? = null,
|
||||
val posts: List<PostModel> = emptyList(),
|
||||
val blurNsfw: Boolean = true,
|
||||
val swipeActionsEnabled: Boolean = true,
|
||||
val postLayout: PostLayout = PostLayout.Card,
|
||||
)
|
||||
|
||||
sealed interface Effect
|
||||
}
|
@ -0,0 +1,346 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.DismissDirection
|
||||
import androidx.compose.material.DismissValue
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.ArrowCircleDown
|
||||
import androidx.compose.material.icons.filled.ArrowCircleUp
|
||||
import androidx.compose.material.pullrefresh.PullRefreshIndicator
|
||||
import androidx.compose.material.pullrefresh.pullRefresh
|
||||
import androidx.compose.material.pullrefresh.rememberPullRefreshState
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import cafe.adriel.voyager.navigator.bottomSheet.LocalBottomSheetNavigator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.data.PostLayout
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.di.getThemeRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.communitydetail.CommunityDetailScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCard
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.PostCardPlaceholder
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.SwipeableCard
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.createcomment.CreateCommentScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.image.ZoomableImageScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.modals.SortBottomSheet
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.postdetail.PostDetailScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.userdetail.UserDetailScreen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.di.getNotificationCenter
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toIcon
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getMultiCommunityViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
|
||||
class MultiCommunityScreen(
|
||||
private val community: MultiCommunityModel,
|
||||
) : Screen {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val model = rememberScreenModel { getMultiCommunityViewModel(community) }
|
||||
model.bindToLifecycle(key)
|
||||
val uiState by model.uiState.collectAsState()
|
||||
val bottomSheetNavigator = LocalBottomSheetNavigator.current
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
val bottomNavCoordinator = remember { getNavigationCoordinator() }
|
||||
val navigator = remember { bottomNavCoordinator.getRootNavigator() }
|
||||
val notificationCenter = remember { getNotificationCenter() }
|
||||
DisposableEffect(key) {
|
||||
onDispose {
|
||||
notificationCenter.removeObserver(key)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val sortType = uiState.sortType
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = community.name,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
},
|
||||
scrollBehavior = scrollBehavior,
|
||||
navigationIcon = {
|
||||
Image(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.pop()
|
||||
},
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
Row {
|
||||
val additionalLabel = when (sortType) {
|
||||
SortType.Top.Day -> stringResource(MR.strings.home_sort_type_top_day_short)
|
||||
SortType.Top.Month -> stringResource(MR.strings.home_sort_type_top_month_short)
|
||||
SortType.Top.Past12Hours -> stringResource(MR.strings.home_sort_type_top_12_hours_short)
|
||||
SortType.Top.Past6Hours -> stringResource(MR.strings.home_sort_type_top_6_hours_short)
|
||||
SortType.Top.PastHour -> stringResource(MR.strings.home_sort_type_top_hour_short)
|
||||
SortType.Top.Week -> stringResource(MR.strings.home_sort_type_top_week_short)
|
||||
SortType.Top.Year -> stringResource(MR.strings.home_sort_type_top_year_short)
|
||||
else -> ""
|
||||
}
|
||||
if (additionalLabel.isNotEmpty()) {
|
||||
Text(
|
||||
text = buildString {
|
||||
append("(")
|
||||
append(additionalLabel)
|
||||
append(")")
|
||||
}
|
||||
)
|
||||
}
|
||||
if (sortType != null) {
|
||||
Image(
|
||||
modifier = Modifier.onClick {
|
||||
val sheet = SortBottomSheet(
|
||||
expandTop = true,
|
||||
)
|
||||
notificationCenter.addObserver({
|
||||
(it as? SortType)?.also { sortType ->
|
||||
model.reduce(
|
||||
MultiCommunityMviModel.Intent.ChangeSort(
|
||||
sortType
|
||||
)
|
||||
)
|
||||
}
|
||||
}, key, NotificationCenterContractKeys.ChangeSortType)
|
||||
bottomSheetNavigator.show(sheet)
|
||||
},
|
||||
imageVector = sortType.toIcon(),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
) { padding ->
|
||||
val pullRefreshState = rememberPullRefreshState(uiState.refreshing, {
|
||||
model.reduce(MultiCommunityMviModel.Intent.Refresh)
|
||||
})
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.fillMaxWidth()
|
||||
.nestedScroll(scrollBehavior.nestedScrollConnection).let {
|
||||
val connection = bottomNavCoordinator.getBottomBarScrollConnection()
|
||||
if (connection != null) {
|
||||
it.nestedScroll(connection)
|
||||
} else it
|
||||
}
|
||||
.pullRefresh(pullRefreshState),
|
||||
) {
|
||||
LazyColumn {
|
||||
if (uiState.posts.isEmpty() && uiState.loading) {
|
||||
items(5) {
|
||||
PostCardPlaceholder(
|
||||
postLayout = uiState.postLayout,
|
||||
)
|
||||
if (uiState.postLayout != PostLayout.Card) {
|
||||
Divider(modifier = Modifier.padding(vertical = Spacing.s))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(Spacing.s))
|
||||
}
|
||||
}
|
||||
}
|
||||
itemsIndexed(uiState.posts) { idx, post ->
|
||||
val themeRepository = remember { getThemeRepository() }
|
||||
val fontScale by themeRepository.contentFontScale.collectAsState()
|
||||
CompositionLocalProvider(
|
||||
LocalDensity provides Density(
|
||||
density = LocalDensity.current.density,
|
||||
fontScale = fontScale,
|
||||
),
|
||||
) {
|
||||
SwipeableCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = uiState.swipeActionsEnabled,
|
||||
backgroundColor = {
|
||||
when (it) {
|
||||
DismissValue.DismissedToStart -> MaterialTheme.colorScheme.surfaceTint
|
||||
DismissValue.DismissedToEnd -> MaterialTheme.colorScheme.tertiary
|
||||
DismissValue.Default -> Color.Transparent
|
||||
}
|
||||
},
|
||||
onGestureBegin = {
|
||||
model.reduce(MultiCommunityMviModel.Intent.HapticIndication)
|
||||
},
|
||||
onDismissToStart = {
|
||||
model.reduce(MultiCommunityMviModel.Intent.UpVotePost(idx))
|
||||
},
|
||||
onDismissToEnd = {
|
||||
model.reduce(MultiCommunityMviModel.Intent.DownVotePost(idx))
|
||||
},
|
||||
swipeContent = { direction ->
|
||||
val icon = when (direction) {
|
||||
DismissDirection.StartToEnd -> Icons.Default.ArrowCircleDown
|
||||
DismissDirection.EndToStart -> Icons.Default.ArrowCircleUp
|
||||
}
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = Color.White,
|
||||
)
|
||||
},
|
||||
content = {
|
||||
PostCard(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.push(
|
||||
PostDetailScreen(post),
|
||||
)
|
||||
},
|
||||
post = post,
|
||||
postLayout = uiState.postLayout,
|
||||
options = buildList {
|
||||
add(stringResource(MR.strings.post_action_share))
|
||||
},
|
||||
blurNsfw = uiState.blurNsfw,
|
||||
onOpenCommunity = { community ->
|
||||
navigator?.push(
|
||||
CommunityDetailScreen(community),
|
||||
)
|
||||
},
|
||||
onOpenCreator = { user ->
|
||||
navigator?.push(
|
||||
UserDetailScreen(user),
|
||||
)
|
||||
},
|
||||
onUpVote = {
|
||||
model.reduce(
|
||||
MultiCommunityMviModel.Intent.UpVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onDownVote = {
|
||||
model.reduce(
|
||||
MultiCommunityMviModel.Intent.DownVotePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onSave = {
|
||||
model.reduce(
|
||||
MultiCommunityMviModel.Intent.SavePost(
|
||||
index = idx,
|
||||
feedback = true,
|
||||
),
|
||||
)
|
||||
},
|
||||
onReply = {
|
||||
val screen = CreateCommentScreen(
|
||||
originalPost = post,
|
||||
)
|
||||
notificationCenter.addObserver(
|
||||
{
|
||||
model.reduce(MultiCommunityMviModel.Intent.Refresh)
|
||||
},
|
||||
key,
|
||||
NotificationCenterContractKeys.CommentCreated
|
||||
)
|
||||
bottomSheetNavigator.show(screen)
|
||||
},
|
||||
onImageClick = { url ->
|
||||
navigator?.push(
|
||||
ZoomableImageScreen(url),
|
||||
)
|
||||
},
|
||||
onOptionSelected = { optionIdx ->
|
||||
when (optionIdx) {
|
||||
else -> model.reduce(
|
||||
MultiCommunityMviModel.Intent.SharePost(idx)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
if (uiState.postLayout != PostLayout.Card) {
|
||||
Divider(modifier = Modifier.padding(vertical = Spacing.s))
|
||||
} else {
|
||||
Spacer(modifier = Modifier.height(Spacing.s))
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
if (!uiState.loading && !uiState.refreshing && uiState.canFetchMore) {
|
||||
model.reduce(MultiCommunityMviModel.Intent.LoadNextPage)
|
||||
}
|
||||
if (uiState.loading && !uiState.refreshing) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxWidth().padding(Spacing.xs),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.size(25.dp),
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(Spacing.xxxl))
|
||||
}
|
||||
}
|
||||
|
||||
PullRefreshIndicator(
|
||||
refreshing = uiState.refreshing,
|
||||
state = pullRefreshState,
|
||||
modifier = Modifier.align(Alignment.TopCenter),
|
||||
backgroundColor = MaterialTheme.colorScheme.background,
|
||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,316 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail
|
||||
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.repository.ThemeRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.SettingsRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.HapticFeedback
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.ShareHelper
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.shareUrl
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.toSortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils.MultiCommunityPaginator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MultiCommunityViewModel(
|
||||
private val mvi: DefaultMviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect>,
|
||||
private val community: MultiCommunityModel,
|
||||
private val postRepository: PostRepository,
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val themeRepository: ThemeRepository,
|
||||
private val shareHelper: ShareHelper,
|
||||
private val settingsRepository: SettingsRepository,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
private val hapticFeedback: HapticFeedback,
|
||||
private val paginator: MultiCommunityPaginator,
|
||||
) : ScreenModel,
|
||||
MviModel<MultiCommunityMviModel.Intent, MultiCommunityMviModel.UiState, MultiCommunityMviModel.Effect> by mvi {
|
||||
|
||||
init {
|
||||
notificationCenter.addObserver({
|
||||
(it as? PostModel)?.also { post ->
|
||||
handlePostUpdate(post)
|
||||
}
|
||||
}, this::class.simpleName.orEmpty(), NotificationCenterContractKeys.PostUpdated)
|
||||
}
|
||||
|
||||
fun finalize() {
|
||||
notificationCenter.removeObserver(this::class.simpleName.orEmpty())
|
||||
}
|
||||
|
||||
override fun onStarted() {
|
||||
mvi.onStarted()
|
||||
|
||||
mvi.scope?.launch {
|
||||
themeRepository.postLayout.onEach { layout ->
|
||||
mvi.updateState { it.copy(postLayout = layout) }
|
||||
}.launchIn(this)
|
||||
|
||||
settingsRepository.currentSettings.onEach { settings ->
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
blurNsfw = settings.blurNsfw,
|
||||
swipeActionsEnabled = settings.enableSwipeActions,
|
||||
)
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
if (uiState.value.posts.isEmpty()) {
|
||||
val settings = settingsRepository.currentSettings.value
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
sortType = settings.defaultPostSortType.toSortType(),
|
||||
)
|
||||
}
|
||||
paginator.setCommunities(community.communityIds)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun reduce(intent: MultiCommunityMviModel.Intent) {
|
||||
when (intent) {
|
||||
is MultiCommunityMviModel.Intent.ChangeSort -> applySortType(intent.value)
|
||||
is MultiCommunityMviModel.Intent.DownVotePost -> toggleDownVote(
|
||||
post = uiState.value.posts[intent.index],
|
||||
feedback = intent.feedback,
|
||||
)
|
||||
|
||||
MultiCommunityMviModel.Intent.HapticIndication -> hapticFeedback.vibrate()
|
||||
MultiCommunityMviModel.Intent.LoadNextPage -> loadNextPage()
|
||||
MultiCommunityMviModel.Intent.Refresh -> refresh()
|
||||
is MultiCommunityMviModel.Intent.SavePost -> toggleSave(
|
||||
post = uiState.value.posts[intent.index],
|
||||
feedback = intent.feedback,
|
||||
)
|
||||
|
||||
is MultiCommunityMviModel.Intent.SharePost -> share(post = uiState.value.posts[intent.index])
|
||||
is MultiCommunityMviModel.Intent.UpVotePost -> toggleUpVote(
|
||||
post = uiState.value.posts[intent.index],
|
||||
feedback = intent.feedback,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun refresh() {
|
||||
paginator.reset()
|
||||
mvi.updateState { it.copy(canFetchMore = true, refreshing = true) }
|
||||
loadNextPage()
|
||||
}
|
||||
|
||||
private fun loadNextPage() {
|
||||
val currentState = mvi.uiState.value
|
||||
if (!currentState.canFetchMore || currentState.loading) {
|
||||
mvi.updateState { it.copy(refreshing = false) }
|
||||
return
|
||||
}
|
||||
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
mvi.updateState { it.copy(loading = true) }
|
||||
val auth = identityRepository.authToken.value
|
||||
val sort = currentState.sortType ?: SortType.Active
|
||||
val refreshing = currentState.refreshing
|
||||
val includeNsfw = settingsRepository.currentSettings.value.includeNsfw
|
||||
|
||||
val postList = paginator.loadNextPage(
|
||||
auth = auth,
|
||||
sort = sort,
|
||||
)
|
||||
val canFetchMore = paginator.canFetchMore
|
||||
mvi.updateState {
|
||||
val newPosts = if (refreshing) {
|
||||
postList
|
||||
} else {
|
||||
// prevents accidental duplication
|
||||
it.posts + postList.filter { p -> it.posts.none { e -> e.id == p.id } }
|
||||
}.filter { post ->
|
||||
if (includeNsfw) {
|
||||
true
|
||||
} else {
|
||||
!post.nsfw
|
||||
}
|
||||
}
|
||||
it.copy(
|
||||
posts = newPosts,
|
||||
loading = false,
|
||||
canFetchMore = canFetchMore,
|
||||
refreshing = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun applySortType(value: SortType) {
|
||||
mvi.updateState { it.copy(sortType = value) }
|
||||
refresh()
|
||||
}
|
||||
|
||||
private fun toggleUpVote(post: PostModel, feedback: Boolean) {
|
||||
val newVote = post.myVote <= 0
|
||||
val newPost = postRepository.asUpVoted(
|
||||
post = post,
|
||||
voted = newVote,
|
||||
)
|
||||
if (feedback) {
|
||||
hapticFeedback.vibrate()
|
||||
}
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
newPost
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val auth = identityRepository.authToken.value.orEmpty()
|
||||
postRepository.upVote(
|
||||
post = post,
|
||||
auth = auth,
|
||||
voted = newVote,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
post
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleDownVote(post: PostModel, feedback: Boolean) {
|
||||
val newValue = post.myVote >= 0
|
||||
val newPost = postRepository.asDownVoted(
|
||||
post = post,
|
||||
downVoted = newValue,
|
||||
)
|
||||
if (feedback) {
|
||||
hapticFeedback.vibrate()
|
||||
}
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
newPost
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val auth = identityRepository.authToken.value.orEmpty()
|
||||
postRepository.downVote(
|
||||
post = post,
|
||||
auth = auth,
|
||||
downVoted = newValue,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
post
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSave(post: PostModel, feedback: Boolean) {
|
||||
val newValue = !post.saved
|
||||
val newPost = postRepository.asSaved(
|
||||
post = post,
|
||||
saved = newValue,
|
||||
)
|
||||
if (feedback) {
|
||||
hapticFeedback.vibrate()
|
||||
}
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
newPost
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val auth = identityRepository.authToken.value.orEmpty()
|
||||
postRepository.save(
|
||||
post = post,
|
||||
auth = auth,
|
||||
saved = newValue,
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
post
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePostUpdate(post: PostModel) {
|
||||
mvi.updateState {
|
||||
it.copy(
|
||||
posts = it.posts.map { p ->
|
||||
if (p.id == post.id) {
|
||||
post
|
||||
} else {
|
||||
p
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun share(post: PostModel) {
|
||||
val url = post.shareUrl
|
||||
if (url.isNotEmpty()) {
|
||||
shareHelper.share(url, "text/plain")
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
import dev.icerock.moko.resources.desc.StringDesc
|
||||
|
||||
interface MultiCommunityEditorMviModel :
|
||||
MviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect> {
|
||||
sealed interface Intent {
|
||||
data class SetName(val value: String) : Intent
|
||||
data class SetSearch(val value: String) : Intent
|
||||
data class SelectImage(val index: Int?) : Intent
|
||||
data class ToggleCommunity(val index: Int) : Intent
|
||||
data object Submit : Intent
|
||||
}
|
||||
|
||||
data class UiState(
|
||||
val name: String = "",
|
||||
val nameError: StringDesc? = null,
|
||||
val icon: String? = null,
|
||||
val availableIcons: List<String> = emptyList(),
|
||||
val communities: List<Pair<CommunityModel, Boolean>> = emptyList(),
|
||||
val searchText: String = "",
|
||||
)
|
||||
|
||||
sealed interface Effect {
|
||||
data object Close : Effect
|
||||
}
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.CheckCircle
|
||||
import androidx.compose.material.icons.filled.Clear
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.core.screen.Screen
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.appearance.theme.Spacing
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.bindToLifecycle
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CommunityItem
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.components.CustomImage
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.commonui.di.getNavigationCoordinator
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.utils.onClick
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.di.getMultiCommunityEditorViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||
import dev.icerock.moko.resources.compose.localized
|
||||
import dev.icerock.moko.resources.compose.stringResource
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
||||
class MultiCommunityEditorScreen(
|
||||
private val editedCommunity: MultiCommunityModel? = null,
|
||||
) : Screen {
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val model = rememberScreenModel { getMultiCommunityEditorViewModel(editedCommunity) }
|
||||
model.bindToLifecycle(key)
|
||||
val uiState by model.uiState.collectAsState()
|
||||
val navigator = remember { getNavigationCoordinator().getRootNavigator() }
|
||||
|
||||
LaunchedEffect(model) {
|
||||
model.effects.onEach {
|
||||
when (it) {
|
||||
MultiCommunityEditorMviModel.Effect.Close -> {
|
||||
navigator?.pop()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(MR.strings.multi_community_editor_title),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
Image(
|
||||
modifier = Modifier.onClick {
|
||||
navigator?.pop()
|
||||
},
|
||||
imageVector = Icons.Default.ArrowBack,
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground),
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
model.reduce(MultiCommunityEditorMviModel.Intent.Submit)
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.CheckCircle,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
val focusManager = LocalFocusManager.current
|
||||
val keyboardScrollConnection = remember {
|
||||
object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource,
|
||||
): Offset {
|
||||
focusManager.clearFocus()
|
||||
return Offset.Zero
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(
|
||||
modifier = Modifier.padding(paddingValues).nestedScroll(keyboardScrollConnection),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.s),
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
maxLines = 1,
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.multi_community_editor_name))
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyMedium,
|
||||
value = uiState.name,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Ascii,
|
||||
autoCorrect = false,
|
||||
imeAction = ImeAction.Next,
|
||||
),
|
||||
onValueChange = { value ->
|
||||
model.reduce(MultiCommunityEditorMviModel.Intent.SetName(value))
|
||||
},
|
||||
isError = uiState.nameError != null,
|
||||
supportingText = {
|
||||
if (uiState.nameError != null) {
|
||||
Text(
|
||||
text = uiState.nameError?.localized().orEmpty(),
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.s))
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = Spacing.m)
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.multi_community_editor_icon))
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(Spacing.s),
|
||||
) {
|
||||
val iconSize = 40.dp
|
||||
itemsIndexed(uiState.availableIcons) { idx, url ->
|
||||
val selected = url == uiState.icon
|
||||
CustomImage(
|
||||
modifier = Modifier
|
||||
.size(iconSize)
|
||||
.clip(RoundedCornerShape(iconSize / 2))
|
||||
.let {
|
||||
if (selected) {
|
||||
it.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
).padding(1.dp).border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
shape = CircleShape,
|
||||
).padding(1.dp).border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.onClick {
|
||||
model.reduce(
|
||||
MultiCommunityEditorMviModel.Intent.SelectImage(
|
||||
idx,
|
||||
)
|
||||
)
|
||||
},
|
||||
url = url,
|
||||
contentScale = ContentScale.FillBounds,
|
||||
)
|
||||
}
|
||||
item {
|
||||
val selected = uiState.icon == null
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(Spacing.xxxs)
|
||||
.size(iconSize)
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = RoundedCornerShape(iconSize / 2),
|
||||
).let {
|
||||
if (selected) {
|
||||
it.border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
).padding(1.dp).border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
shape = CircleShape,
|
||||
).padding(1.dp).border(
|
||||
width = 1.dp,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
shape = CircleShape,
|
||||
)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}.onClick {
|
||||
model.reduce(
|
||||
MultiCommunityEditorMviModel.Intent.SelectImage(
|
||||
null,
|
||||
)
|
||||
)
|
||||
},
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(
|
||||
text = uiState.name.firstOrNull()?.toString().orEmpty()
|
||||
.uppercase(),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(Spacing.s))
|
||||
Column {
|
||||
Text(text = stringResource(MR.strings.multi_community_editor_communities))
|
||||
|
||||
// search field
|
||||
TextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = Color.Transparent,
|
||||
unfocusedContainerColor = Color.Transparent,
|
||||
disabledContainerColor = Color.Transparent,
|
||||
),
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.explore_search_placeholder))
|
||||
},
|
||||
singleLine = true,
|
||||
value = uiState.searchText,
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
),
|
||||
onValueChange = { value ->
|
||||
model.reduce(MultiCommunityEditorMviModel.Intent.SetSearch(value))
|
||||
},
|
||||
trailingIcon = {
|
||||
Icon(
|
||||
modifier = Modifier.onClick {
|
||||
if (uiState.searchText.isNotEmpty()) {
|
||||
model.reduce(
|
||||
MultiCommunityEditorMviModel.Intent.SetSearch("")
|
||||
)
|
||||
}
|
||||
},
|
||||
imageVector = if (uiState.searchText.isEmpty()) Icons.Default.Search else Icons.Default.Clear,
|
||||
contentDescription = null,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.padding(horizontal = Spacing.m)
|
||||
.weight(1f),
|
||||
verticalArrangement = Arrangement.spacedBy(Spacing.xxs),
|
||||
) {
|
||||
itemsIndexed(uiState.communities) { idx, communityItem ->
|
||||
val community = communityItem.first
|
||||
val selected = communityItem.second
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CommunityItem(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
community = community,
|
||||
)
|
||||
Checkbox(
|
||||
checked = selected,
|
||||
onCheckedChange = {
|
||||
model.reduce(
|
||||
MultiCommunityEditorMviModel.Intent.ToggleCommunity(
|
||||
idx
|
||||
)
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor
|
||||
|
||||
import cafe.adriel.voyager.core.model.ScreenModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.DefaultMviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.architecture.MviModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenter
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.notifications.NotificationCenterContractKeys
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.AccountRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.repository.MultiCommunityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.identity.repository.IdentityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.CommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.CommunityRepository
|
||||
import com.github.diegoberaldin.raccoonforlemmy.resources.MR
|
||||
import dev.icerock.moko.resources.desc.desc
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class MultiCommunityEditorViewModel(
|
||||
private val mvi: DefaultMviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect>,
|
||||
private val editedCommunity: MultiCommunityModel? = null,
|
||||
private val identityRepository: IdentityRepository,
|
||||
private val communityRepository: CommunityRepository,
|
||||
private val multiCommunityRepository: MultiCommunityRepository,
|
||||
private val accountRepository: AccountRepository,
|
||||
private val notificationCenter: NotificationCenter,
|
||||
) : ScreenModel,
|
||||
MviModel<MultiCommunityEditorMviModel.Intent, MultiCommunityEditorMviModel.UiState, MultiCommunityEditorMviModel.Effect> by mvi {
|
||||
|
||||
private var communities: List<Pair<CommunityModel, Boolean>> = emptyList()
|
||||
private var debounceJob: Job? = null
|
||||
|
||||
override fun onStarted() {
|
||||
mvi.onStarted()
|
||||
if (communities.isEmpty()) {
|
||||
populate()
|
||||
}
|
||||
}
|
||||
|
||||
override fun reduce(intent: MultiCommunityEditorMviModel.Intent) {
|
||||
when (intent) {
|
||||
is MultiCommunityEditorMviModel.Intent.SelectImage -> selectImage(intent.index)
|
||||
is MultiCommunityEditorMviModel.Intent.SetName -> mvi.updateState { it.copy(name = intent.value) }
|
||||
is MultiCommunityEditorMviModel.Intent.ToggleCommunity -> toggleCommunity(intent.index)
|
||||
is MultiCommunityEditorMviModel.Intent.SetSearch -> setSearch(intent.value)
|
||||
MultiCommunityEditorMviModel.Intent.Submit -> submit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun populate() {
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
val auth = identityRepository.authToken.value
|
||||
communities = communityRepository.getSubscribed(auth).sortedBy { it.name }.map { c ->
|
||||
c to (editedCommunity?.communityIds?.contains(c.id) == true)
|
||||
}
|
||||
mvi.updateState {
|
||||
val newCommunities = communities
|
||||
val availableIcons =
|
||||
newCommunities.filter { i -> i.second }.mapNotNull { i -> i.first.icon }
|
||||
it.copy(
|
||||
communities = newCommunities,
|
||||
name = editedCommunity?.name.orEmpty(),
|
||||
icon = editedCommunity?.icon,
|
||||
availableIcons = availableIcons,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setSearch(value: String) {
|
||||
debounceJob?.cancel()
|
||||
mvi.updateState { it.copy(searchText = value) }
|
||||
debounceJob = mvi.scope?.launch(Dispatchers.IO) {
|
||||
delay(1_000)
|
||||
filterCommunities()
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterCommunities() {
|
||||
val searchText = uiState.value.searchText
|
||||
val filtered = if (searchText.isNotEmpty()) {
|
||||
communities.filter { it.first.name.contains(searchText) }
|
||||
} else {
|
||||
communities
|
||||
}
|
||||
mvi.updateState { it.copy(communities = filtered) }
|
||||
}
|
||||
|
||||
private fun selectImage(index: Int?) {
|
||||
val image = if (index == null) {
|
||||
null
|
||||
} else {
|
||||
uiState.value.availableIcons[index]
|
||||
}
|
||||
mvi.updateState { it.copy(icon = image) }
|
||||
}
|
||||
|
||||
private fun toggleCommunity(index: Int) {
|
||||
mvi.updateState { state ->
|
||||
val newCommunities = state.communities.mapIndexed { idx, item ->
|
||||
if (idx == index) {
|
||||
item.first to !item.second
|
||||
} else {
|
||||
item
|
||||
}
|
||||
}
|
||||
val availableIcons =
|
||||
newCommunities.filter { i -> i.second }.mapNotNull { i -> i.first.icon }
|
||||
state.copy(
|
||||
communities = newCommunities,
|
||||
availableIcons = availableIcons,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun submit() {
|
||||
mvi.updateState { it.copy(nameError = null) }
|
||||
val currentState = uiState.value
|
||||
var valid = true
|
||||
val name = currentState.name
|
||||
if (name.isEmpty()) {
|
||||
mvi.updateState { it.copy(nameError = MR.strings.message_missing_field.desc()) }
|
||||
valid = false
|
||||
}
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
val icon = currentState.icon
|
||||
val communityIds = currentState.communities.filter { it.second }.map { it.first.id }
|
||||
val multiCommunity = editedCommunity?.copy(
|
||||
name = name,
|
||||
icon = icon,
|
||||
communityIds = communityIds,
|
||||
) ?: MultiCommunityModel(
|
||||
name = name,
|
||||
icon = icon,
|
||||
communityIds = communityIds,
|
||||
)
|
||||
|
||||
mvi.scope?.launch(Dispatchers.IO) {
|
||||
val accountId = accountRepository.getActive()?.id ?: return@launch
|
||||
if (multiCommunity.id == null) {
|
||||
val id = multiCommunityRepository.create(multiCommunity, accountId)
|
||||
notificationCenter.getAllObservers(NotificationCenterContractKeys.MultiCommunityCreated)
|
||||
.forEach { it.invoke(multiCommunity.copy(id = id)) }
|
||||
} else {
|
||||
multiCommunityRepository.update(multiCommunity)
|
||||
notificationCenter.getAllObservers(NotificationCenterContractKeys.MultiCommunityCreated)
|
||||
.forEach { it.invoke(multiCommunity) }
|
||||
}
|
||||
mvi.emitEffect(MultiCommunityEditorMviModel.Effect.Close)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,36 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.ListingType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
|
||||
|
||||
internal class CommunityPaginator(
|
||||
private val communityId: Int,
|
||||
private val postRepository: PostRepository,
|
||||
) {
|
||||
private var currentPage: Int = 1
|
||||
var canFetchMore: Boolean = true
|
||||
private set
|
||||
|
||||
fun reset() {
|
||||
currentPage = 1
|
||||
canFetchMore = true
|
||||
}
|
||||
|
||||
suspend fun loadNextPage(
|
||||
auth: String?,
|
||||
sort: SortType,
|
||||
): List<PostModel> {
|
||||
val result = postRepository.getAll(
|
||||
auth = auth,
|
||||
page = currentPage,
|
||||
limit = PostRepository.DEFAULT_PAGE_SIZE,
|
||||
type = ListingType.All,
|
||||
sort = sort,
|
||||
communityId = communityId,
|
||||
)
|
||||
canFetchMore = result.size >= PostRepository.DEFAULT_PAGE_SIZE
|
||||
return result
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.repository.PostRepository
|
||||
|
||||
class DefaultMultiCommunityPaginator(
|
||||
private val postRepository: PostRepository,
|
||||
) : MultiCommunityPaginator {
|
||||
private var paginators = emptyList<CommunityPaginator>()
|
||||
|
||||
override val canFetchMore: Boolean
|
||||
get() = paginators.any { it.canFetchMore }
|
||||
|
||||
override fun setCommunities(ids: List<Int>) {
|
||||
paginators = ids.map {
|
||||
CommunityPaginator(
|
||||
communityId = it,
|
||||
postRepository = postRepository,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
paginators.forEach { it.reset() }
|
||||
}
|
||||
|
||||
override suspend fun loadNextPage(
|
||||
auth: String?,
|
||||
sort: SortType,
|
||||
): List<PostModel> = buildList {
|
||||
for (paginator in paginators) {
|
||||
if (paginator.canFetchMore) {
|
||||
val elements = paginator.loadNextPage(
|
||||
auth = auth,
|
||||
sort = sort,
|
||||
)
|
||||
addAll(elements)
|
||||
}
|
||||
}
|
||||
}.sortedBy { it.publishDate }
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.utils
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.PostModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.domain.lemmy.data.SortType
|
||||
|
||||
interface MultiCommunityPaginator {
|
||||
val canFetchMore: Boolean
|
||||
fun setCommunities(ids: List<Int>)
|
||||
fun reset()
|
||||
|
||||
suspend fun loadNextPage(
|
||||
auth: String? = null,
|
||||
sort: SortType,
|
||||
): List<PostModel>
|
||||
}
|
@ -1,15 +1,35 @@
|
||||
package com.github.diegoberaldin.raccoonforlemmy.feature.search.di
|
||||
|
||||
import com.github.diegoberaldin.raccoonforlemmy.core.persistence.data.MultiCommunityModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.main.ExploreViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.managesubscriptions.ManageSubscriptionsViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.detail.MultiCommunityViewModel
|
||||
import com.github.diegoberaldin.raccoonforlemmy.feature.search.multicommunity.editor.MultiCommunityEditorViewModel
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.koin.core.parameter.parametersOf
|
||||
|
||||
actual fun getExploreViewModel() = SearchScreenModelHelper.model
|
||||
|
||||
actual fun getManageSubscriptionsViewModel() = SearchScreenModelHelper.manageSuscriptionsViewModel
|
||||
|
||||
actual fun getMultiCommunityViewModel(community: MultiCommunityModel) =
|
||||
SearchScreenModelHelper.getMultiCommunityViewModel(community)
|
||||
|
||||
actual fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?) =
|
||||
SearchScreenModelHelper.getMultiCommunityEditorViewModel(editedCommunity)
|
||||
|
||||
object SearchScreenModelHelper : KoinComponent {
|
||||
val model: ExploreViewModel by inject()
|
||||
val manageSuscriptionsViewModel: ManageSubscriptionsViewModel by inject()
|
||||
|
||||
internal fun getMultiCommunityViewModel(community: MultiCommunityModel): MultiCommunityViewModel {
|
||||
val res: MultiCommunityViewModel by inject(parameters = { parametersOf(community) })
|
||||
return res
|
||||
}
|
||||
|
||||
internal fun getMultiCommunityEditorViewModel(editedCommunity: MultiCommunityModel?): MultiCommunityEditorViewModel {
|
||||
val res: MultiCommunityEditorViewModel by inject(parameters = { parametersOf(editedCommunity) })
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
@ -65,7 +65,7 @@
|
||||
<string name="explore_result_type_comments">Comments</string>
|
||||
<string name="explore_result_type_communities">Communities</string>
|
||||
<string name="explore_result_type_users">Users</string>
|
||||
<string name="explore_search_placeholder">Search text</string>
|
||||
<string name="explore_search_placeholder">Search</string>
|
||||
<string name="explore_title_manage_subscriptions">Manage subscriptions</string>
|
||||
|
||||
<string name="profile_not_logged_message">You are currently not logged in.\nPlease add an
|
||||
@ -162,4 +162,12 @@
|
||||
|
||||
<string name="manage_accounts_title">Manage accounts</string>
|
||||
<string name="manage_accounts_button_add">Add account</string>
|
||||
|
||||
<string name="manage_subscriptions_header_multicommunities">Multi-communities</string>
|
||||
<string name="manage_subscriptions_header_subscriptions">Subscriptions</string>
|
||||
|
||||
<string name="multi_community_editor_title">Multi-community editor</string>
|
||||
<string name="multi_community_editor_name">Name</string>
|
||||
<string name="multi_community_editor_icon">Icon</string>
|
||||
<string name="multi_community_editor_communities">Communities</string>
|
||||
</resources>
|
@ -61,7 +61,7 @@
|
||||
<string name="explore_result_type_comments">Comentarios</string>
|
||||
<string name="explore_result_type_communities">Comunidades</string>
|
||||
<string name="explore_result_type_users">Usuarios</string>
|
||||
<string name="explore_search_placeholder">Texto de búsqueda</string>
|
||||
<string name="explore_search_placeholder">Búsqueda</string>
|
||||
<string name="explore_title_manage_subscriptions">Gestionar subscripciones</string>
|
||||
|
||||
<string name="profile_not_logged_message">Acceso no efectuado.\nAñadir una cuenta para
|
||||
@ -159,4 +159,12 @@
|
||||
|
||||
<string name="manage_accounts_title">Gestionar cuentas</string>
|
||||
<string name="manage_accounts_button_add">Nueva cuenta</string>
|
||||
|
||||
<string name="manage_subscriptions_header_multicommunities">Multi-comunidades</string>
|
||||
<string name="manage_subscriptions_header_subscriptions">Subscripciones</string>
|
||||
|
||||
<string name="multi_community_editor_title">Editor multi-comunidad</string>
|
||||
<string name="multi_community_editor_name">Nombre</string>
|
||||
<string name="multi_community_editor_icon">Icono</string>
|
||||
<string name="multi_community_editor_communities">Comunidades</string>
|
||||
</resources>
|
@ -61,7 +61,7 @@
|
||||
<string name="explore_result_type_comments">Commenti</string>
|
||||
<string name="explore_result_type_communities">Comunità</string>
|
||||
<string name="explore_result_type_users">Utenti</string>
|
||||
<string name="explore_search_placeholder">Testo di ricerca</string>
|
||||
<string name="explore_search_placeholder">Ricerca</string>
|
||||
<string name="explore_title_manage_subscriptions">Gestione iscrizioni</string>
|
||||
|
||||
<string name="profile_not_logged_message">Login non effettuato.\nAggiungi un account per
|
||||
@ -159,4 +159,12 @@
|
||||
|
||||
<string name="manage_accounts_title">Gestisci account</string>
|
||||
<string name="manage_accounts_button_add">Aggiungi account</string>
|
||||
|
||||
<string name="manage_subscriptions_header_multicommunities">Multi-comunità</string>
|
||||
<string name="manage_subscriptions_header_subscriptions">Iscrizioni</string>
|
||||
|
||||
<string name="multi_community_editor_title">Editor Multi-comunità</string>
|
||||
<string name="multi_community_editor_name">Nome</string>
|
||||
<string name="multi_community_editor_icon">Icona</string>
|
||||
<string name="multi_community_editor_communities">Comunità</string>
|
||||
</resources>
|
Loading…
x
Reference in New Issue
Block a user