1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-3239 Participant card | UI + logic (#1992)

This commit is contained in:
Konstantin Ivanov 2025-01-17 14:05:17 +01:00 committed by GitHub
parent 28bc323b73
commit 9ded869c33
Signed by: github
GPG key ID: B5690EEEBB952194
59 changed files with 950 additions and 69 deletions

View file

@ -0,0 +1,48 @@
plugins {
id "com.android.library"
id "kotlin-android"
alias(libs.plugins.compose.compiler)
}
android {
def config = rootProject.extensions.getByName("ext")
buildFeatures {
compose true
}
namespace 'com.anytypeio.anytype.ui_settings'
}
dependencies {
implementation project(':domain')
implementation project(':core-ui')
implementation project(':analytics')
implementation project(':core-models')
implementation project(':core-utils')
implementation project(':localization')
implementation project(':presentation')
implementation project(':library-emojifier')
compileOnly libs.javaxInject
implementation libs.lifecycleViewModel
implementation libs.lifecycleRuntime
implementation libs.appcompat
implementation libs.compose
implementation libs.composeFoundation
implementation libs.composeMaterial
implementation libs.composeToolingPreview
implementation libs.activityCompose
implementation libs.coilCompose
debugImplementation libs.composeTooling
implementation libs.timber
testImplementation libs.junit
testImplementation libs.kotlinTest
}

View file

@ -0,0 +1 @@
<manifest />

View file

@ -0,0 +1,178 @@
package com.anytypeio.anytype.ui_settings.about
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Caption2Regular
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_ui.views.UXBody
import com.anytypeio.anytype.ui_settings.R
@Composable
fun AboutAppScreen(
libraryVersion: String,
accountId: String,
analyticsId: String,
deviceId: String,
version: String,
buildNumber: Int,
onMetaClicked: () -> Unit,
onContactUsClicked: () -> Unit,
onExternalLinkClicked: (AboutAppViewModel.ExternalLink) -> Unit,
) {
Column(Modifier.verticalScroll(rememberScrollState())) {
Box(
modifier = Modifier
.padding(top = 6.dp)
.align(Alignment.CenterHorizontally)
) {
Dragger()
}
Box(
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(
top = 18.dp,
bottom = 12.dp
)
) {
Text(
text = stringResource(R.string.about),
style = Title1,
color = colorResource(R.color.text_primary)
)
}
Section(title = stringResource(id = R.string.about_help_and_community))
Option(title = stringResource(id = R.string.about_what_is_new)) {
onExternalLinkClicked(AboutAppViewModel.ExternalLink.WhatIsNew)
}
Divider()
Option(title = stringResource(id = R.string.about_anytype_community)) {
onExternalLinkClicked(AboutAppViewModel.ExternalLink.AnytypeCommunity)
}
Divider()
Option(title = stringResource(id = R.string.about_help_and_tutorials)) {
onExternalLinkClicked(AboutAppViewModel.ExternalLink.HelpAndTutorials)
}
Divider()
Option(title = stringResource(id = R.string.contact_us)) {
onContactUsClicked()
}
Divider()
Section(title = stringResource(id = R.string.about_legal))
Option(title = stringResource(id = R.string.about_terms_of_use)) {
onExternalLinkClicked(AboutAppViewModel.ExternalLink.TermsOfUse)
}
Divider()
Option(title = stringResource(id = R.string.about_privacy_policy)) {
onExternalLinkClicked(AboutAppViewModel.ExternalLink.PrivacyPolicy)
}
Divider()
Text(
text = stringResource(R.string.tech_info),
style = Caption1Regular,
color = colorResource(R.color.text_secondary),
modifier = Modifier.padding(
top = 26.dp,
start = 20.dp
)
)
Box(
modifier = Modifier
.clickable {
onMetaClicked()
}
.padding(
top = 16.dp,
start = 20.dp,
end = 20.dp,
bottom = 20.dp
)
) {
Text(
text = stringResource(
id = R.string.about_meta_info,
version,
buildNumber,
libraryVersion,
accountId,
deviceId,
analyticsId
),
style = Caption2Regular.copy(
color = colorResource(id = R.color.text_secondary)
)
)
}
}
}
@Composable
fun Option(
modifier: Modifier = Modifier,
title: String,
onClick: () -> Unit
) {
Box(
modifier = modifier
.fillMaxWidth()
.clickable { onClick.invoke() }
.padding(horizontal = 20.dp, vertical = 14.dp)
) {
Text(
text = title,
style = UXBody.copy(color = colorResource(id = R.color.text_primary))
)
Image(
painter = painterResource(id = R.drawable.ic_arrow_forward),
contentDescription = "Arrow Forward",
modifier = Modifier.align(Alignment.CenterEnd)
)
}
}
@Composable
fun Section(
modifier: Modifier = Modifier,
title: String
) {
Box(modifier = modifier.padding(start = 20.dp, end = 20.dp, top = 26.dp, bottom = 8.dp)) {
Text(
text = title,
style = Caption1Regular.copy(color = colorResource(id = R.color.text_secondary))
)
}
}
@Preview
@Composable
fun PreviewAboutAppScreen() {
AboutAppScreen(
libraryVersion = "1.0.0",
accountId = "1234567890",
analyticsId = "1234567890",
deviceId = "123132323",
version = "1.0.0",
buildNumber = 1,
onMetaClicked = {},
onExternalLinkClicked = {},
onContactUsClicked = {}
)
}

View file

@ -0,0 +1,107 @@
package com.anytypeio.anytype.ui_settings.about
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.auth.interactor.GetLibraryVersion
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.config.ConfigStorage
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class AboutAppViewModel(
private val getAccount: GetAccount,
private val getLibraryVersion: GetLibraryVersion,
private val analytics: Analytics,
private val configStorage: ConfigStorage
) : ViewModel() {
val navigation = MutableSharedFlow<Navigation>()
fun onExternalLinkClicked(link: ExternalLink) {
proceedWithAnalytics(link)
viewModelScope.launch {
navigation.emit(Navigation.OpenExternalLink(link))
}
}
fun onContactUsClicked() {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.MENU_HELP_CONTACT_US
)
}
private fun proceedWithAnalytics(link: ExternalLink) {
viewModelScope.sendEvent(
analytics = analytics,
eventName = link.eventName
)
}
val libraryVersion = MutableStateFlow("")
val accountId = MutableStateFlow("")
val analyticsId = MutableStateFlow("")
val deviceId = MutableStateFlow("")
init {
viewModelScope.launch {
getAccount.stream(Unit).collect { result ->
result.fold(
onSuccess = { accountId.value = it.id },
onFailure = { Timber.e(it, "getAccount error") }
)
}
}
viewModelScope.launch {
val config = configStorage.get()
analyticsId.value = config.analytics
deviceId.value = config.device
}
viewModelScope.launch {
getLibraryVersion(BaseUseCase.None).process(
failure = {},
success = { version ->
libraryVersion.value = version
}
)
}
}
sealed interface Navigation {
class OpenExternalLink(val link: ExternalLink) : Navigation
}
sealed class ExternalLink(val eventName: String) {
object WhatIsNew : ExternalLink(EventsDictionary.MENU_HELP_WHAT_IS_NEW)
object AnytypeCommunity : ExternalLink(EventsDictionary.MENU_HELP_COMMUNITY)
object HelpAndTutorials : ExternalLink(EventsDictionary.MENU_HELP_TUTORIAL)
object TermsOfUse : ExternalLink(EventsDictionary.MENU_HELP_TERMS)
object PrivacyPolicy : ExternalLink(EventsDictionary.MENU_HELP_PRIVACY)
}
class Factory @Inject constructor(
private val getAccount: GetAccount,
private val getLibraryVersion: GetLibraryVersion,
private val analytics: Analytics,
private val configStorage: ConfigStorage
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AboutAppViewModel(
getAccount = getAccount,
getLibraryVersion = getLibraryVersion,
analytics = analytics,
configStorage = configStorage
) as T
}
}
}

View file

@ -0,0 +1,103 @@
package com.anytypeio.anytype.ui_settings.account
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.EventsPropertiesKey
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.analytics.props.Props
import com.anytypeio.anytype.domain.auth.interactor.Logout
import com.anytypeio.anytype.domain.base.Interactor
import com.anytypeio.anytype.domain.misc.AppActionManager
import com.anytypeio.anytype.domain.subscriptions.GlobalSubscriptionManager
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
class LogoutWarningViewModel(
private val logout: Logout,
private val analytics: Analytics,
private val appActionManager: AppActionManager,
private val globalSubscriptionManager: GlobalSubscriptionManager
) : ViewModel() {
val commands = MutableSharedFlow<Command>(replay = 0)
val isLoggingOut = MutableStateFlow(false)
fun onLogoutClicked() {
val startTime = System.currentTimeMillis()
viewModelScope.launch {
logout(
params = Logout.Params(clearLocalRepositoryData = false)
).collect { status ->
when (status) {
is Interactor.Status.Started -> {
isLoggingOut.value = true
}
is Interactor.Status.Success -> {
viewModelScope.sendEvent(
analytics = analytics,
startTime = startTime,
middleTime = System.currentTimeMillis(),
eventName = EventsDictionary.logout,
props = Props(
mapOf(
EventsPropertiesKey.route to EventsDictionary.Routes.screenSettings
)
)
)
appActionManager.setup(AppActionManager.Action.ClearAll)
unsubscribeFromGlobalSubscriptions()
isLoggingOut.value = false
commands.emit(Command.Logout)
}
is Interactor.Status.Error -> {
isLoggingOut.value = false
commands.emit(Command.ShowError(status.throwable.message ?: ""))
Timber.e(status.throwable.message, "Error while logging out")
}
}
}
}
}
private fun unsubscribeFromGlobalSubscriptions() {
globalSubscriptionManager.onStop()
}
fun onBackupClicked() {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.keychainPhraseScreenShow,
props = Props(
mapOf(EventsPropertiesKey.type to EventsDictionary.Type.beforeLogout)
)
)
}
class Factory @Inject constructor(
private val logout: Logout,
private val analytics: Analytics,
private val appActionManager: AppActionManager,
private val globalSubscriptionManager: GlobalSubscriptionManager
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return LogoutWarningViewModel(
logout = logout,
analytics = analytics,
appActionManager = appActionManager,
globalSubscriptionManager = globalSubscriptionManager
) as T
}
}
sealed class Command {
data object Logout : Command()
data class ShowError(val msg: String) : Command()
}
}

View file

@ -0,0 +1,465 @@
package com.anytypeio.anytype.ui_settings.account
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.compose.rememberAsyncImagePainter
import com.anytypeio.anytype.core_ui.foundation.Arrow
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.Option
import com.anytypeio.anytype.core_ui.foundation.OptionMembership
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.core_models.membership.MembershipStatus
import com.anytypeio.anytype.presentation.profile.ProfileIconView
import com.anytypeio.anytype.ui_settings.R
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
@Composable
fun ProfileSettingsScreen(
onKeychainPhraseClicked: () -> Unit,
onLogoutClicked: () -> Unit,
isLogoutInProgress: Boolean,
onNameChange: (String) -> Unit,
onProfileIconClick: () -> Unit,
account: ProfileSettingsViewModel.AccountProfile,
onAppearanceClicked: () -> Unit,
onDataManagementClicked: () -> Unit,
onAboutClicked: () -> Unit,
onSpacesClicked: () -> Unit,
onMembershipClicked: () -> Unit,
membershipStatus: MembershipStatus?,
showMembership: ShowMembership?
) {
LazyColumn(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
Header(
account = account,
onNameSet = onNameChange,
onProfileIconClick = onProfileIconClick
)
}
item {
Spacer(
modifier = Modifier
.height(10.dp)
.padding(top = 4.dp)
)
}
item {
Divider()
}
item {
Section(stringResource(R.string.settings))
}
item {
Option(
image = R.drawable.ic_appearance,
text = stringResource(R.string.appearance),
onClick = onAppearanceClicked
)
}
item {
Divider(paddingStart = 60.dp)
}
item {
Option(
image = R.drawable.ic_file_storage,
text = stringResource(R.string.data_management),
onClick = onDataManagementClicked
)
}
if (showMembership?.isShowing == true) {
item {
Divider(paddingStart = 60.dp)
}
item {
OptionMembership(
image = R.drawable.ic_membership,
text = stringResource(R.string.settings_membership),
onClick = onMembershipClicked,
membershipStatus = membershipStatus
)
}
}
item {
Divider(paddingStart = 60.dp)
}
item {
Option(
image = R.drawable.ic_settings_spaces,
text = stringResource(R.string.multiplayer_spaces),
onClick = onSpacesClicked
)
}
item {
Divider(paddingStart = 60.dp)
}
item {
Option(
image = R.drawable.ic_about,
text = stringResource(R.string.about),
onClick = onAboutClicked
)
}
item {
Divider(paddingStart = 60.dp)
}
item {
Section(stringResource(R.string.access))
}
item {
Option(
image = R.drawable.ic_keychain_phrase,
text = stringResource(R.string.key),
onClick = onKeychainPhraseClicked
)
}
item {
Divider(paddingStart = 60.dp)
}
item {
ActionWithProgressBar(
name = stringResource(R.string.log_out),
color = colorResource(R.color.palette_dark_red),
onClick = onLogoutClicked,
isInProgress = isLogoutInProgress
)
}
item {
Box(Modifier.height(54.dp))
}
}
}
@Composable
fun Section(name: String) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth(),
contentAlignment = Alignment.BottomStart
) {
Text(
text = name,
modifier = Modifier.padding(
start = 20.dp,
bottom = 8.dp
),
color = colorResource(R.color.text_secondary),
style = Caption1Regular
)
}
}
@Composable
fun Action(
name: String,
color: Color = Color.Unspecified,
onClick: () -> Unit = {}
) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.clickable(onClick = onClick),
contentAlignment = Alignment.CenterStart
) {
Text(
text = name,
color = color,
style = BodyRegular,
modifier = Modifier.padding(
start = 20.dp
)
)
}
}
@Composable
fun ActionWithProgressBar(
name: String,
color: Color = Color.Unspecified,
onClick: () -> Unit = {},
isInProgress: Boolean
) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.clickable(onClick = onClick),
contentAlignment = Alignment.CenterStart
) {
Text(
text = name,
color = color,
style = BodyRegular,
modifier = Modifier.padding(
start = 20.dp
)
)
if (isInProgress) {
CircularProgressIndicator(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 20.dp)
.size(24.dp),
color = colorResource(R.color.shape_secondary)
)
}
}
}
@Composable
private fun Header(
modifier: Modifier = Modifier,
account: ProfileSettingsViewModel.AccountProfile,
onProfileIconClick: () -> Unit,
onNameSet: (String) -> Unit
) {
when (account) {
is ProfileSettingsViewModel.AccountProfile.Data -> {
Box(modifier = modifier.padding(vertical = 6.dp)) {
Dragger()
}
Box(modifier = modifier.padding(top = 12.dp, bottom = 28.dp)) {
ProfileTitleBlock()
}
Box(modifier = modifier.padding(bottom = 16.dp)) {
ProfileImageBlock(
name = account.name,
icon = account.icon,
onProfileIconClick = onProfileIconClick
)
}
ProfileNameBlock(name = account.name, onNameSet = onNameSet)
}
is ProfileSettingsViewModel.AccountProfile.Idle -> {}
}
}
@OptIn(ExperimentalMaterialApi::class, FlowPreview::class)
@Composable
fun ProfileNameBlock(
modifier: Modifier = Modifier,
name: String,
onNameSet: (String) -> Unit
) {
val nameValue = remember { mutableStateOf(name) }
val focusManager = LocalFocusManager.current
LaunchedEffect(nameValue.value) {
snapshotFlow { nameValue.value }
.debounce(PROFILE_NAME_CHANGE_DELAY)
.distinctUntilChanged()
.filter { it.isNotEmpty() }
.collect { query ->
onNameSet(query)
}
}
Column(modifier = modifier.padding(start = 20.dp)) {
Text(
text = stringResource(id = R.string.name),
color = colorResource(id = R.color.text_secondary),
fontSize = 13.sp
)
BasicTextField(
value = nameValue.value,
onValueChange = {
nameValue.value = it
},
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp, end = 20.dp),
enabled = true,
textStyle = TextStyle(
fontSize = 22.sp,
fontWeight = FontWeight.SemiBold,
color = colorResource(id = R.color.text_primary)
),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
),
singleLine = true,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = nameValue.value,
innerTextField = innerTextField,
singleLine = true,
enabled = true,
isError = false,
placeholder = {
Text(text = stringResource(R.string.account_name))
},
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = colorResource(id = R.color.text_primary),
backgroundColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
placeholderColor = colorResource(id = R.color.text_tertiary),
cursorColor = colorResource(id = R.color.orange)
),
contentPadding = PaddingValues(
start = 0.dp,
top = 0.dp,
end = 0.dp,
bottom = 0.dp
),
border = {},
interactionSource = remember { MutableInteractionSource() },
visualTransformation = VisualTransformation.None
)
}
)
}
}
@Composable
fun ProfileTitleBlock() {
Text(
text = stringResource(R.string.profile),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
@Composable
fun ProfileImageBlock(
name: String,
icon: ProfileIconView,
onProfileIconClick: () -> Unit
) {
when (icon) {
is ProfileIconView.Image -> {
Image(
painter = rememberAsyncImagePainter(model = icon.url),
contentDescription = "Custom image profile",
contentScale = ContentScale.Crop,
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(48.dp))
.noRippleClickable {
onProfileIconClick.invoke()
}
)
}
else -> {
val nameFirstChar = if (name.isEmpty()) {
stringResource(id = R.string.account_default_name)
} else {
name.first().uppercaseChar().toString()
}
Box(
modifier = Modifier
.size(96.dp)
.clip(RoundedCornerShape(48.dp))
.background(colorResource(id = R.color.text_tertiary))
.noRippleClickable {
onProfileIconClick.invoke()
}
) {
Text(
text = nameFirstChar,
style = MaterialTheme.typography.h3.copy(
color = colorResource(id = R.color.text_white),
fontSize = 64.sp
),
modifier = Modifier.align(Alignment.Center)
)
}
}
}
}
@Preview
@Composable
private fun ProfileSettingPreview() {
ProfileSettingsScreen(
onKeychainPhraseClicked = {},
onLogoutClicked = {},
isLogoutInProgress = false,
onNameChange = {},
onProfileIconClick = {},
account = ProfileSettingsViewModel.AccountProfile.Data(
"Walter",
icon = ProfileIconView.Placeholder("Walter")
),
onAppearanceClicked = {},
onDataManagementClicked = {},
onAboutClicked = {},
onSpacesClicked = {},
onMembershipClicked = {},
membershipStatus = null,
showMembership = ShowMembership(true)
)
}
private const val PROFILE_NAME_CHANGE_DELAY = 300L

View file

@ -0,0 +1,190 @@
package com.anytypeio.anytype.ui_settings.account
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NetworkMode
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.account.DeleteAccount
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.domain.icon.SetImageIcon
import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.networkmode.GetNetworkMode
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
import com.anytypeio.anytype.domain.search.PROFILE_SUBSCRIPTION_ID
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendScreenSettingsDeleteEvent
import com.anytypeio.anytype.core_models.membership.MembershipStatus
import com.anytypeio.anytype.domain.search.ProfileSubscriptionManager
import com.anytypeio.anytype.presentation.membership.provider.MembershipProvider
import com.anytypeio.anytype.presentation.profile.ProfileIconView
import com.anytypeio.anytype.presentation.profile.profileIcon
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
class ProfileSettingsViewModel(
private val analytics: Analytics,
private val container: StorelessSubscriptionContainer,
private val setObjectDetails: SetObjectDetails,
private val configStorage: ConfigStorage,
private val urlBuilder: UrlBuilder,
private val setImageIcon: SetDocumentImageIcon,
private val membershipProvider: MembershipProvider,
private val getNetworkMode: GetNetworkMode,
private val profileContainer: ProfileSubscriptionManager
) : BaseViewModel() {
private val jobs = mutableListOf<Job>()
val isLoggingOut = MutableStateFlow(false)
val debugSyncReportUri = MutableStateFlow<Uri?>(null)
val membershipStatusState = MutableStateFlow<MembershipStatus?>(null)
val showMembershipState = MutableStateFlow<ShowMembership?>(null)
val profileData = profileContainer.observe().map { obj ->
AccountProfile.Data(
name = obj.name.orEmpty(),
icon = obj.profileIcon(urlBuilder)
)
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT),
AccountProfile.Idle
)
init {
viewModelScope.launch {
analytics.sendEvent(
eventName = EventsDictionary.screenSettingsAccount
)
}
viewModelScope.launch {
getNetworkMode.async(Unit).fold(
onSuccess = { result ->
showMembershipState.value = when (result.networkMode) {
NetworkMode.DEFAULT -> ShowMembership(true)
NetworkMode.LOCAL -> ShowMembership(false)
NetworkMode.CUSTOM -> ShowMembership(true)
}
},
onFailure = { Timber.e(it, "Error while getting network mode") }
)
}
viewModelScope.launch {
membershipProvider.status().collect { status ->
membershipStatusState.value = status
}
}
}
fun onNameChange(name: String) {
Timber.d("onNameChange, name:[$name]")
viewModelScope.launch {
val profile = configStorage.getOrNull()?.profile
if (profile != null) {
setObjectDetails.execute(
SetObjectDetails.Params(
ctx = profile,
details = mapOf(Relations.NAME to name)
)
).fold(
onFailure = {
Timber.e(it, "Error while updating object details")
},
onSuccess = {
// do nothing
}
)
} else {
Timber.w("Config storage missing")
}
}
}
fun onStop() {
Timber.d("onStop")
jobs.apply {
forEach { it.cancel() }
clear()
}
}
fun onPickedImageFromDevice(path: String) {
viewModelScope.launch {
val config = configStorage.getOrNull()
if (config != null) {
setImageIcon(
SetImageIcon.Params(
target = config.profile,
path = path,
spaceId = SpaceId(config.techSpace)
)
).process(
failure = {
Timber.e("Error while setting image icon")
},
success = {
// do nothing
}
)
} else {
Timber.e("Missing config while trying to set profile image")
}
}
}
sealed class AccountProfile {
data object Idle: AccountProfile()
class Data(
val name: String,
val icon: ProfileIconView
): AccountProfile()
}
class Factory(
private val analytics: Analytics,
private val container: StorelessSubscriptionContainer,
private val setObjectDetails: SetObjectDetails,
private val configStorage: ConfigStorage,
private val urlBuilder: UrlBuilder,
private val setDocumentImageIcon: SetDocumentImageIcon,
private val membershipProvider: MembershipProvider,
private val getNetworkMode: GetNetworkMode,
private val profileSubscriptionManager: ProfileSubscriptionManager
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return ProfileSettingsViewModel(
analytics = analytics,
container = container,
setObjectDetails = setObjectDetails,
configStorage = configStorage,
urlBuilder = urlBuilder,
setImageIcon = setDocumentImageIcon,
membershipProvider = membershipProvider,
getNetworkMode = getNetworkMode,
profileContainer = profileSubscriptionManager
) as T
}
}
}
private const val STOP_SUBSCRIPTION_TIMEOUT = 1_000L
data class ShowMembership(val isShowing: Boolean)

View file

@ -0,0 +1,372 @@
package com.anytypeio.anytype.ui_settings.appearance
import androidx.annotation.StringRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement.SpaceEvenly
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
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.layout.wrapContentSize
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Outline
import androidx.compose.ui.graphics.Paint
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawOutline
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.ThemeMode
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.foundation.Toolbar
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Caption2Regular
import com.anytypeio.anytype.ui_settings.R
private val buttonSize = 60.dp
private const val firstQuarterFactor = 0.5f
private const val thirdQuartersFactor = 1.5f
@Composable
fun AppearanceScreen(
light: () -> Unit,
dark: () -> Unit,
system: () -> Unit,
selectedMode: ThemeMode
) {
Column {
Box(
Modifier
.padding(vertical = 6.dp)
.align(Alignment.CenterHorizontally)
) {
Dragger()
}
Toolbar(stringResource(R.string.appearance))
Text(
modifier = Modifier
.fillMaxWidth()
.padding(top = 18.dp, bottom = 12.dp),
textAlign = TextAlign.Center,
text = stringResource(R.string.mode),
style = Caption1Medium,
color = colorResource(R.color.text_secondary),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 20.dp),
horizontalArrangement = SpaceEvenly
) {
LightModeButton(
onClick = light,
selectedMode == ThemeMode.Light
)
DarkModeButton(
onClick = dark,
selectedMode == ThemeMode.Night
)
SystemModeButton(
onClick = system,
selectedMode == ThemeMode.System
)
}
Box(Modifier.height(16.dp))
}
}
@Composable
fun LightModeButton(
onClick: () -> Unit = {},
isSelected: Boolean
) {
ButtonColumn(onClick = onClick) {
SelectionBox(isSelected = isSelected) {
Text(
text = "Aa",
style = MaterialTheme.typography.h1,
modifier = Modifier
.size(buttonSize)
.background(
color = Color.White,
shape = RoundedCornerShape(14.dp)
)
.border(
width = 1.dp,
color = colorResource(id = R.color.shape_primary),
shape = RoundedCornerShape(14.dp)
)
.wrapContentSize(align = Alignment.Center)
)
}
ModeNameText(id = R.string.light)
}
}
@Composable
fun DarkModeButton(
onClick: () -> Unit = {},
isSelected: Boolean
) {
ButtonColumn(onClick = onClick) {
SelectionBox(isSelected = isSelected) {
Text(
text = "Aa",
style = MaterialTheme.typography.h1,
color = colorResource(id = R.color.text_white),
modifier = Modifier
.size(buttonSize)
.background(
color = Color.Black,
shape = RoundedCornerShape(14.dp)
)
.wrapContentSize(align = Alignment.Center)
)
}
ModeNameText(id = R.string.dark)
}
}
@Composable
fun SystemModeButton(
onClick: () -> Unit = {},
isSelected: Boolean
) {
ButtonColumn(onClick = onClick) {
SelectionBox(isSelected = isSelected) {
val cornersRadius = with(LocalDensity.current) { 14.dp.toPx() }
val greyCornerRadius = with(LocalDensity.current) { 15.dp.toPx() }
val greyBorderSize = with(LocalDensity.current) { 1.dp.toPx() }
val greyColor = colorResource(id = R.color.shape_primary)
Canvas(
modifier = Modifier.size(buttonSize)
) {
val rect = Rect(Offset.Zero, size)
drawWholeViewGreyRoundedRectangle(this, rect, greyColor, greyCornerRadius)
drawFirstQuarterWhiteRoundedRectangle(this, rect, cornersRadius, greyBorderSize)
drawSecondQuarterWhiteRectangle(this, rect, greyBorderSize)
drawThirdQuarterBlackRectangle(this, rect)
drawFourthQuarterBlackRoundedRectangle(this, rect, cornersRadius)
}
val annotatedString = buildAnnotatedString {
withStyle(style = SpanStyle(Color.Black)) {
append("A")
}
withStyle(style = SpanStyle(Color.White)) {
append("a")
}
}
Text(
text = annotatedString,
style = MaterialTheme.typography.h1,
modifier = Modifier
.size(buttonSize)
.wrapContentSize(align = Alignment.Center)
)
}
ModeNameText(id = R.string.system)
}
}
@Composable
fun SelectionBox(
isSelected: Boolean,
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier = Modifier
.let {
if (isSelected) it.border(
width = 2.dp,
color = colorResource(id = R.color.amber25),
shape = RoundedCornerShape(18.dp)
) else {
it
}
}
.padding(4.dp),
content = content
)
}
@Composable
fun ButtonColumn(
onClick: () -> Unit,
content: @Composable ColumnScope.() -> Unit
) {
Column(
modifier = Modifier
.wrapContentWidth(Alignment.CenterHorizontally)
.clickable(onClick = onClick),
horizontalAlignment = Alignment.CenterHorizontally,
content = content
)
}
@Composable
fun ModeNameText(
@StringRes id: Int
) {
Text(
text = stringResource(id = id),
style = Caption2Regular,
color = colorResource(R.color.text_secondary),
modifier = Modifier.padding(top = 4.dp)
)
}
@Preview()
@Composable
fun ComposablePreview() {
AppearanceScreen({}, {}, {}, ThemeMode.Light)
}
fun drawThirdQuarterBlackRectangle(
drawScope: DrawScope,
rect: Rect
) {
with(drawScope) {
val path = Path().apply {
moveTo(rect.topCenter.x, rect.topCenter.y)
lineTo(rect.topCenter.x * thirdQuartersFactor, rect.topCenter.y)
lineTo(rect.bottomCenter.x * thirdQuartersFactor, rect.bottomCenter.y)
lineTo(rect.bottomCenter.x, rect.bottomCenter.y)
close()
}
drawPathIntoCanvas(this, path, Color.Black)
}
}
fun drawFourthQuarterBlackRoundedRectangle(
drawScope: DrawScope,
rect: Rect,
cornersRadius: Float
) {
with(drawScope) {
val path = Path().apply {
moveTo(rect.topCenter.x * thirdQuartersFactor - cornersRadius, rect.topCenter.y)
lineTo(rect.topRight)
lineTo(rect.bottomRight)
lineTo(rect.bottomCenter.x * thirdQuartersFactor - cornersRadius, rect.bottomCenter.y)
close()
}
drawPathIntoCanvas(this, path, Color.Black, PathEffect.cornerPathEffect(cornersRadius))
}
}
fun drawSecondQuarterWhiteRectangle(
drawScope: DrawScope,
rect: Rect,
greyBorderSize: Float
) {
with(drawScope) {
val path = Path().apply {
moveTo(rect.topCenter.x, rect.topCenter.y + greyBorderSize)
lineTo(rect.topCenter.x * firstQuarterFactor, rect.topCenter.y + greyBorderSize)
lineTo(rect.bottomCenter.x * firstQuarterFactor, rect.bottomCenter.y - greyBorderSize)
lineTo(rect.bottomCenter.x, rect.bottomCenter.y - greyBorderSize)
close()
}
drawPathIntoCanvas(this, path, Color.White, null)
}
}
fun drawFirstQuarterWhiteRoundedRectangle(
drawScope: DrawScope,
rect: Rect,
cornersRadius: Float,
greyBorderSize: Float
) {
with(drawScope) {
val path = Path().apply {
moveTo(
rect.topCenter.x * firstQuarterFactor + cornersRadius,
rect.topCenter.y + greyBorderSize
)
lineTo(rect.topLeft.x + greyBorderSize, rect.topLeft.y + greyBorderSize)
lineTo(rect.bottomLeft.x + greyBorderSize, rect.bottomLeft.y - greyBorderSize)
lineTo(
rect.bottomCenter.x * firstQuarterFactor + cornersRadius,
rect.bottomCenter.y - greyBorderSize
)
close()
}
drawPathIntoCanvas(this, path, Color.White, PathEffect.cornerPathEffect(cornersRadius))
}
}
fun drawWholeViewGreyRoundedRectangle(
drawScope: DrawScope,
rect: Rect,
greyColor: Color,
greyCornerRadius: Float
) {
with(drawScope) {
val path = Path().apply {
moveTo(rect.topLeft.x, rect.topLeft.y)
lineTo(rect.topRight.x, rect.topRight.y)
lineTo(rect.bottomRight.x, rect.bottomRight.y)
lineTo(rect.bottomLeft.x, rect.bottomLeft.y)
close()
}
drawPathIntoCanvas(this, path, greyColor, PathEffect.cornerPathEffect(greyCornerRadius))
}
}
fun drawPathIntoCanvas(
drawScope: DrawScope,
path: Path,
toColor: Color,
toPathEffect: PathEffect? = null
) {
with(drawScope) {
drawIntoCanvas { canvas ->
canvas.drawOutline(
outline = Outline.Generic(path),
paint = Paint().apply {
color = toColor
pathEffect = toPathEffect
}
)
}
}
}
private fun Path.lineTo(offset: Offset) = lineTo(offset.x, offset.y)

View file

@ -0,0 +1,95 @@
package com.anytypeio.anytype.ui_settings.appearance
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.ThemeMode
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.theme.GetTheme
import com.anytypeio.anytype.domain.theme.SetTheme
import com.anytypeio.anytype.presentation.analytics.AnalyticSpaceHelperDelegate
import com.anytypeio.anytype.presentation.extension.sendChangeThemeEvent
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
import timber.log.Timber
import javax.inject.Inject
class AppearanceViewModel(
private val getTheme: GetTheme,
private val setTheme: SetTheme,
private val themeApplicator: ThemeApplicator,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
) : ViewModel(), AnalyticSpaceHelperDelegate by analyticSpaceHelperDelegate {
val selectedTheme = MutableStateFlow<ThemeMode>(ThemeMode.System)
init {
viewModelScope.launch {
getTheme(BaseUseCase.None).proceed(
success = {
proceedWithUpdatingSelectedTheme(it)
},
failure = {
Timber.e(it, "Error while getting current app theme")
})
}
}
private fun saveTheme(mode: ThemeMode) {
viewModelScope.launch {
analytics.sendChangeThemeEvent(mode)
}
viewModelScope.launch {
setTheme(params = mode).proceed(
success = {
proceedWithUpdatingTheme(mode)
},
failure = {
Timber.e(it, "Error while setting current app theme")
}
)
}
}
fun onLight() {
saveTheme(ThemeMode.Light)
}
fun onDark() {
saveTheme(ThemeMode.Night)
}
fun onSystem() {
saveTheme(ThemeMode.System)
}
private fun proceedWithUpdatingTheme(themeMode: ThemeMode) {
themeApplicator.apply(themeMode)
proceedWithUpdatingSelectedTheme(themeMode)
}
private fun proceedWithUpdatingSelectedTheme(themeMode: ThemeMode) {
selectedTheme.value = themeMode
}
class Factory @Inject constructor(
private val getTheme: GetTheme,
private val setTheme: SetTheme,
private val themeApplicator: ThemeApplicator,
private val analytics: Analytics,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return AppearanceViewModel(
getTheme = getTheme,
setTheme = setTheme,
themeApplicator = themeApplicator,
analytics = analytics,
analyticSpaceHelperDelegate = analyticSpaceHelperDelegate
) as T
}
}
}

View file

@ -0,0 +1,27 @@
package com.anytypeio.anytype.ui_settings.appearance
import androidx.appcompat.app.AppCompatDelegate
import com.anytypeio.anytype.core_models.ThemeMode
import javax.inject.Inject
interface ThemeApplicator {
fun apply(theme: ThemeMode)
}
class ThemeApplicatorImpl @Inject constructor(): ThemeApplicator {
override fun apply(theme: ThemeMode) {
when(theme) {
ThemeMode.Light -> apply(AppCompatDelegate.MODE_NIGHT_NO)
ThemeMode.Night -> apply(AppCompatDelegate.MODE_NIGHT_YES)
ThemeMode.System -> apply(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
}
}
private fun apply(mode: Int) {
if (AppCompatDelegate.getDefaultNightMode() != mode) {
AppCompatDelegate.setDefaultNightMode(mode)
}
}
}

View file

@ -0,0 +1,202 @@
package com.anytypeio.anytype.ui_settings.fstorage
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.PaddingValues
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.anytypeio.anytype.core_ui.common.DefaultPreviews
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.ButtonWarning
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel.ScreenState
import com.anytypeio.anytype.ui_settings.R
import com.anytypeio.anytype.ui_settings.fstorage.MockFileStorage.mockData
@Composable
fun LocalStorageScreen(
data: ScreenState,
onOffloadFilesClicked: () -> Unit,
onDeleteAccountClicked: () -> Unit
) {
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(16.dp),
backgroundColor = colorResource(id = R.color.background_secondary)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp),
) {
Box(
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Dragger()
}
Header(stringResource(id = R.string.data_management))
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.local_storage),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.in_order_to_save),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(4.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = "\uD83D\uDCF1",
style = TextStyle(fontSize = 28.sp)
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = data.device.orEmpty(),
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(
id = R.string.local_storage_used,
data.localUsage
),
style = Relations3,
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(18.dp))
ButtonSecondary(
text = stringResource(id = R.string.offload_files),
onClick = onOffloadFilesClicked,
size = ButtonSize.SmallSecondary.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.danger_zone),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = stringResource(id = R.string.deleted_account_danger_zone_msg),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(12.dp))
ButtonWarning(
text = stringResource(id = R.string.delete_account),
onClick = onDeleteAccountClicked,
size = ButtonSize.SmallSecondary.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
Spacer(modifier = Modifier.height(24.dp))
}
}
}
@Composable
private fun Header(
text: String,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.padding(top = 12.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = text,
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
}
object MockFileStorage {
val mockSpaceInfraUsage = "212 MB of 1 GB used"
val mockSpaceInfraPercent = 0.9F
val mockDevice = "iPhone 13 Pro"
val mockSpaceLocalUsage = "518 MB used"
val mockInfraMax = "14 GB"
val mockData = ScreenState(
spaceUsage = mockSpaceInfraUsage,
percentUsage = mockSpaceInfraPercent,
device = mockDevice,
localUsage = mockSpaceLocalUsage,
spaceLimit = mockInfraMax,
isShowGetMoreSpace = true,
isShowSpaceUsedWarning = true
)
}
@Composable
@DefaultPreviews
fun PreviewLocalStorageScreen() {
LocalStorageScreen(
data = mockData,
onOffloadFilesClicked = {},
onDeleteAccountClicked = {}
)
}

View file

@ -0,0 +1,156 @@
package com.anytypeio.anytype.ui_settings.main
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Divider
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.core.graphics.toColorInt
import com.anytypeio.anytype.core_ui.features.SpaceIconView
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.ui_settings.R
import timber.log.Timber
@Composable
fun SpaceHeader(
name: String?,
icon: SpaceIconView?,
modifier: Modifier = Modifier,
onNameSet: (String) -> Unit,
onRandomGradientClicked: () -> Unit,
isEditEnabled: Boolean,
onSpaceImagePicked: (Uri) -> Unit
) {
val context = LocalContext.current
val singlePhotoPickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.PickVisualMedia(),
onResult = { uri ->
if (uri != null) {
onSpaceImagePicked(uri)
} else {
Timber.w("Uri was null after picking image")
}
}
)
val isSpaceIconMenuExpanded = remember {
mutableStateOf(false)
}
Box(modifier = modifier.padding(vertical = 6.dp)) {
Dragger()
}
Box(modifier = modifier.padding(top = 12.dp, bottom = 28.dp)) {
SpaceNameBlock()
}
Box(modifier = modifier.padding(bottom = 16.dp)) {
if (icon != null) {
SpaceIconView(
icon = icon,
onSpaceIconClick = {
if (isEditEnabled) {
isSpaceIconMenuExpanded.value = !isSpaceIconMenuExpanded.value
}
}
)
MaterialTheme(
shapes = MaterialTheme.shapes.copy(medium = RoundedCornerShape(16.dp))
) {
DropdownMenu(
expanded = isSpaceIconMenuExpanded.value,
offset = DpOffset(x = 0.dp, y = 6.dp),
onDismissRequest = {
isSpaceIconMenuExpanded.value = false
}
) {
DropdownMenuItem(
onClick = {
onRandomGradientClicked()
isSpaceIconMenuExpanded.value = false
},
) {
Text(
text = stringResource(R.string.space_settings_apply_random_gradient),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
if (ActivityResultContracts.PickVisualMedia.isPhotoPickerAvailable(context)) {
Divider(
thickness = 0.5.dp,
color = colorResource(id = R.color.shape_primary)
)
DropdownMenuItem(
onClick = {
singlePhotoPickerLauncher.launch(
PickVisualMediaRequest(
ActivityResultContracts.PickVisualMedia.ImageOnly
)
)
isSpaceIconMenuExpanded.value = false
},
) {
Text(
text = stringResource(R.string.space_settings_apply_upload_image),
style = BodyRegular,
color = colorResource(id = R.color.text_primary)
)
}
}
}
}
}
}
if (name != null) {
SpaceNameBlock(
modifier = Modifier,
name = name,
onNameSet = onNameSet,
isEditEnabled = isEditEnabled
)
}
}
@Composable
fun GradientComposeView(
modifier: Modifier,
from: String,
to: String,
size: Dp
) {
val gradient = Brush.radialGradient(
colors = listOf(
Color(from.toColorInt()),
Color(to.toColorInt())
)
)
Box(
modifier = modifier
.size(size)
.clip(CircleShape)
.background(gradient)
)
}

View file

@ -0,0 +1,162 @@
package com.anytypeio.anytype.ui_settings.main
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.Text
import androidx.compose.material.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.HeadlineHeading
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.ui_settings.R
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.dropWhile
import kotlinx.coroutines.flow.filter
@Composable
fun Section(modifier: Modifier = Modifier, title: String) {
Text(
modifier = modifier,
text = title,
color = colorResource(id = R.color.text_secondary),
style = Caption1Regular
)
}
@OptIn(FlowPreview::class)
@Composable
fun SpaceNameBlock(
modifier: Modifier = Modifier,
name: String,
onNameSet: (String) -> Unit,
isEditEnabled: Boolean
) {
val nameValue = remember { mutableStateOf(name) }
val focusManager = LocalFocusManager.current
LaunchedEffect(nameValue.value) {
snapshotFlow { nameValue.value }
.debounce(SPACE_NAME_CHANGE_DELAY)
.dropWhile { input -> input == name }
.distinctUntilChanged()
.filter { it.isNotEmpty() }
.collect { query ->
onNameSet(query)
}
}
Column(modifier = modifier.padding(start = 20.dp)) {
Text(
text = stringResource(id = R.string.space_name),
style = Caption1Regular,
color = colorResource(id = R.color.text_secondary)
)
SettingsTextField(
value = nameValue.value,
onValueChange = {
nameValue.value = it
},
keyboardActions = KeyboardActions(
onDone = {
focusManager.clearFocus()
}
),
isEditEnabled = isEditEnabled
)
}
}
@Composable
fun SpaceNameBlock() {
Text(
text = stringResource(id = R.string.space_settings),
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SettingsTextField(
value: String,
onValueChange: (String) -> Unit,
visualTransformation: VisualTransformation = VisualTransformation.None,
keyboardActions: KeyboardActions = KeyboardActions.Default,
isEditEnabled: Boolean
) {
BasicTextField(
value = value,
modifier = Modifier
.padding(top = 4.dp, end = 20.dp)
.fillMaxWidth(),
onValueChange = onValueChange,
enabled = isEditEnabled,
readOnly = !isEditEnabled,
textStyle = HeadlineHeading.copy(color = colorResource(id = R.color.text_primary)),
cursorBrush = SolidColor(colorResource(id = R.color.orange)),
visualTransformation = visualTransformation,
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Text,
imeAction = ImeAction.Done
),
keyboardActions = keyboardActions,
interactionSource = remember { MutableInteractionSource() },
singleLine = true,
maxLines = 1,
decorationBox = @Composable { innerTextField ->
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = value,
visualTransformation = visualTransformation,
innerTextField = innerTextField,
label = null,
leadingIcon = null,
trailingIcon = null,
singleLine = true,
enabled = true,
isError = false,
placeholder = {
Text(text = stringResource(id = R.string.space_name))
},
interactionSource = remember { MutableInteractionSource() },
colors = TextFieldDefaults.outlinedTextFieldColors(
textColor = colorResource(id = R.color.text_primary),
backgroundColor = Color.Transparent,
disabledBorderColor = Color.Transparent,
errorBorderColor = Color.Transparent,
focusedBorderColor = Color.Transparent,
unfocusedBorderColor = Color.Transparent,
placeholderColor = colorResource(id = R.color.text_tertiary),
cursorColor = colorResource(id = R.color.orange)
),
contentPadding = PaddingValues(),
border = {}
)
}
)
}
private const val SPACE_NAME_CHANGE_DELAY = 300L

View file

@ -0,0 +1,80 @@
package com.anytypeio.anytype.ui_settings.space
import androidx.compose.foundation.background
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.width
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.LocalRippleConfiguration
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel
import com.anytypeio.anytype.ui_settings.R
@OptIn(ExperimentalMaterialApi::class)
@Composable
fun SegmentLine(items: List<SpacesStorageViewModel.SegmentLineItem>) {
var size by remember { mutableStateOf(IntSize.Zero) }
Column(
modifier = Modifier
.height(27.dp)
.fillMaxWidth()
.onSizeChanged { size = it }
) {
CompositionLocalProvider(LocalRippleConfiguration provides null) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(27.dp),
verticalAlignment = Alignment.CenterVertically
) {
val freeWidth = with(LocalDensity.current) {
size.width.toDp() - (items.size - 1).dp * 2
}
val values = items.sumOf { it.value.toDouble() }
val oneValueWidth = freeWidth / maxOf(values.toFloat(), 1f)
items.forEach { item ->
val color = when (item) {
is SpacesStorageViewModel.SegmentLineItem.Active -> {
colorResource(id = R.color.palette_system_amber_125)
}
is SpacesStorageViewModel.SegmentLineItem.Free -> {
colorResource(id = R.color.shape_secondary)
}
is SpacesStorageViewModel.SegmentLineItem.Other -> {
colorResource(id = R.color.palette_system_amber_50)
}
}
Box(
modifier = Modifier
.width(maxOf(item.value.times(oneValueWidth), 4f.dp))
.height(27.dp)
.clip(MaterialTheme.shapes.medium.copy(CornerSize(5.dp)))
.background(color)
)
Spacer(modifier = Modifier.width(2.dp))
}
}
}
}
}

View file

@ -0,0 +1,519 @@
package com.anytypeio.anytype.ui_settings.space
import android.net.Uri
import androidx.compose.foundation.Image
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_models.DEFAULT_SPACE_TYPE
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.PRIVATE_SPACE_TYPE
import com.anytypeio.anytype.core_models.SHARED_SPACE_TYPE
import com.anytypeio.anytype.core_models.SpaceType
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
import com.anytypeio.anytype.core_models.multiplayer.SpaceMemberPermissions
import com.anytypeio.anytype.core_ui.extensions.throttledClick
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Option
import com.anytypeio.anytype.core_ui.foundation.Section
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
import com.anytypeio.anytype.core_ui.views.BodyCalloutMedium
import com.anytypeio.anytype.core_ui.views.BodyRegular
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.ButtonUpgrade
import com.anytypeio.anytype.core_ui.views.ButtonWarning
import com.anytypeio.anytype.core_ui.views.Caption1Regular
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Regular
import com.anytypeio.anytype.core_utils.const.DateConst
import com.anytypeio.anytype.core_utils.ext.formatTimeInMillis
import com.anytypeio.anytype.core_utils.ui.ViewState
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.spaces.SpaceSettingsViewModel
import com.anytypeio.anytype.ui_settings.R
import com.anytypeio.anytype.ui_settings.main.SpaceHeader
@Composable
fun SpaceSettingsScreen(
state: ViewState<SpaceSettingsViewModel.SpaceData>,
onNameSet: (String) -> Unit,
onDeleteSpaceClicked: () -> Unit,
onFileStorageClick: () -> Unit,
onPersonalizationClicked: () -> Unit,
onSpaceIdClicked: (Id) -> Unit,
onNetworkIdClicked: (Id) -> Unit,
onCreatedByClicked: (Id) -> Unit,
onDebugClicked: () -> Unit,
onRandomGradientClicked: () -> Unit,
onSharePrivateSpaceClicked: () -> Unit,
onManageSharedSpaceClicked: () -> Unit,
onAddMoreSpacesClicked: () -> Unit,
onSpaceImagePicked: (Uri) -> Unit
) {
LazyColumn(
modifier = Modifier
.nestedScroll(rememberNestedScrollInteropConnection())
.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally
) {
item {
SpaceHeader(
modifier = Modifier,
name = when (state) {
is ViewState.Success -> state.data.name.ifEmpty {
stringResource(id = R.string.untitled)
}
else -> null
},
icon = when (state) {
is ViewState.Success -> state.data.icon
else -> null
},
onNameSet = onNameSet,
onRandomGradientClicked = onRandomGradientClicked,
isEditEnabled = when(state) {
is ViewState.Error -> false
ViewState.Init -> false
ViewState.Loading -> false
is ViewState.Success -> state.data.permissions.isOwnerOrEditor()
},
onSpaceImagePicked = onSpaceImagePicked
)
}
item { Divider() }
item {
if (state is ViewState.Success) {
Section(title = stringResource(id = R.string.multiplayer_space_type))
} else {
Section(title = EMPTY_STRING_VALUE)
}
}
item {
if (state is ViewState.Success) {
when(state.data.spaceType) {
DEFAULT_SPACE_TYPE -> {
TypeOfSpace(state.data.spaceType)
}
PRIVATE_SPACE_TYPE -> {
PrivateSpaceSharing(
onSharePrivateSpaceClicked = onSharePrivateSpaceClicked,
shareLimitStateState = state.data.shareLimitReached,
onAddMoreSpacesClicked = onAddMoreSpacesClicked
)
}
SHARED_SPACE_TYPE -> {
SharedSpaceSharing(
onManageSharedSpaceClicked = onManageSharedSpaceClicked,
isUserOwner = state.data.permissions == SpaceMemberPermissions.OWNER,
requests = state.data.requests
)
}
}
}
}
item {
Divider()
}
item {
Section(title = stringResource(id = R.string.settings))
}
when (state) {
is ViewState.Success -> {
if (state.data.permissions.isOwner()) {
item {
Option(
image = R.drawable.ic_file_storage,
text = stringResource(R.string.remote_storage),
onClick = throttledClick(onFileStorageClick)
)
}
item {
Divider(paddingStart = 60.dp)
}
}
item {
Option(
image = R.drawable.ic_personalization,
text = stringResource(R.string.personalization),
onClick = throttledClick(onPersonalizationClicked)
)
}
item {
Divider(paddingStart = 60.dp)
}
}
else -> {
// Do nothing.
}
}
item {
Option(image = R.drawable.ic_debug,
text = stringResource(R.string.debug),
onClick = throttledClick(onDebugClicked)
)
}
item {
Divider(
paddingStart = 60.dp
)
}
item {
Section(title = stringResource(id = R.string.space_info))
}
if (state is ViewState.Success) {
item {
SettingsItem(
title = stringResource(id = R.string.space_id),
value = state.data.spaceId.orEmpty().ifEmpty {
stringResource(id = R.string.unknown)
},
onClick = { onSpaceIdClicked(it) }
)
}
item {
SettingsItem(
title = stringResource(id = R.string.network_id),
value = state.data.network.orEmpty().ifEmpty {
stringResource(id = R.string.unknown)
},
onClick = { onNetworkIdClicked(it) }
)
}
item {
SettingsItem(
title = stringResource(id = R.string.created_by),
value = state.data.createdBy.orEmpty().ifEmpty {
stringResource(id = R.string.unknown)
},
onClick = { onCreatedByClicked(it) }
)
}
item {
SettingsItem(
title = stringResource(id = R.string.creation_date),
value = state.data.createdDateInMillis?.formatTimeInMillis(
DateConst.DEFAULT_DATE_FORMAT
) ?: stringResource(id = R.string.unknown),
onClick = {},
showIcon = false
)
}
}
if (state is ViewState.Success && state.data.isDeletable) {
item {
val label = when(state.data.permissions) {
SpaceMemberPermissions.OWNER -> stringResource(R.string.delete_space)
else -> stringResource(R.string.multiplayer_leave_space)
}
Box(modifier = Modifier.height(78.dp)) {
ButtonWarning(
onClick = { onDeleteSpaceClicked() },
text = label,
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, bottom = 10.dp)
.fillMaxWidth()
.align(Alignment.BottomCenter),
size = ButtonSize.Large
)
}
}
}
item {
Spacer(modifier = Modifier.height(16.dp))
}
}
}
@Composable
private fun SettingsItem(
title: String,
value: String?,
onClick: (String) -> Unit,
showIcon: Boolean = true
) {
Column(
modifier = Modifier
.height(90.dp)
.padding(horizontal = 20.dp)
.fillMaxWidth()
.noRippleClickable {
if (showIcon) onClick(value.orEmpty())
}
) {
Text(
text = title,
style = BodyCalloutMedium,
modifier = Modifier.padding(top = 12.dp),
color = colorResource(id = R.color.text_secondary)
)
Row(
modifier = Modifier
.wrapContentHeight()
.padding(top = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier
.wrapContentHeight()
.weight(1.0f, true)
.padding(top = 4.dp, end = 27.dp),
text = value ?: stringResource(id = R.string.unknown),
style = PreviewTitle2Regular,
maxLines = 2,
minLines = 2,
color = colorResource(id = R.color.text_primary),
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start
)
if (showIcon) {
Image(
painterResource(id = R.drawable.ic_copy_24),
contentDescription = "Option icon",
)
}
}
}
}
@Composable
@Preview(showBackground = true)
fun SpaceSettingsScreenPreview() {
SpaceSettingsScreen(
state = ViewState.Success(
data = SpaceSettingsViewModel.SpaceData(
spaceId = "IDdflkdsl;kfldsklfkdslakfl;sdkalfkldskfl;dskal;fklflsdkl;fkdsl;akfl;dskal;fks",
createdDateInMillis = null,
createdBy = "1235",
network = "332311313131flsdklfksdlkfksdlkfksdlkflasd324213432432",
name = "Dream team",
icon = SpaceIconView.Placeholder(),
isDeletable = true,
spaceType = DEFAULT_SPACE_TYPE,
permissions = SpaceMemberPermissions.OWNER,
shareLimitReached = SpaceSettingsViewModel.ShareLimitsState(
shareLimitReached = false,
sharedSpacesLimit = 0
)
)
),
onNameSet = {},
onDeleteSpaceClicked = {},
onFileStorageClick = {},
onPersonalizationClicked = {},
onSpaceIdClicked = {},
onNetworkIdClicked = {} ,
onCreatedByClicked = {},
onDebugClicked = {},
onRandomGradientClicked = {},
onManageSharedSpaceClicked = {},
onSharePrivateSpaceClicked = {},
onAddMoreSpacesClicked = {},
onSpaceImagePicked = {}
)
}
@Composable
fun PrivateSpaceSharing(
onSharePrivateSpaceClicked: () -> Unit,
onAddMoreSpacesClicked: () -> Unit,
shareLimitStateState: SpaceSettingsViewModel.ShareLimitsState
) {
Column {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.noRippleClickable(
onClick = throttledClick(
onClick = { onSharePrivateSpaceClicked() }
)
)
) {
Text(
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.CenterStart),
text = stringResource(id = R.string.space_type_private_space),
color = if (shareLimitStateState.shareLimitReached)
colorResource(id = R.color.text_secondary)
else
colorResource(id = R.color.text_primary),
style = BodyRegular
)
Row(
modifier = Modifier.align(Alignment.CenterEnd)
) {
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = stringResource(id = R.string.multiplayer_share),
color = colorResource(id = R.color.text_secondary),
style = BodyRegular
)
Spacer(Modifier.width(10.dp))
Image(
painter = painterResource(R.drawable.ic_arrow_forward),
contentDescription = "Arrow forward",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(end = 20.dp)
)
}
}
if (shareLimitStateState.shareLimitReached) {
Text(
modifier = Modifier
.fillMaxWidth()
.padding(start = 20.dp, end = 20.dp),
text = stringResource(
id = R.string.membership_space_settings_share_limit,
shareLimitStateState.sharedSpacesLimit
),
color = colorResource(id = R.color.text_primary),
style = Caption1Regular
)
ButtonUpgrade(
modifier = Modifier
.padding(start = 20.dp, end = 20.dp, top = 10.dp)
.height(32.dp),
onClick = { onAddMoreSpacesClicked() },
text = stringResource(id = R.string.multiplayer_upgrade_spaces_button)
)
}
}
}
@Composable
fun SharedSpaceSharing(
onManageSharedSpaceClicked: () -> Unit,
isUserOwner: Boolean,
requests: Int = 0
) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
.noRippleClickable(
onClick = throttledClick(
onClick = { onManageSharedSpaceClicked() }
)
)
) {
Text(
modifier = Modifier
.padding(start = 20.dp)
.align(Alignment.CenterStart),
text = stringResource(id = R.string.space_type_shared_space),
color = colorResource(id = R.color.text_primary),
style = BodyRegular
)
Row(
modifier = Modifier.align(Alignment.CenterEnd)
) {
Text(
modifier = Modifier.align(Alignment.CenterVertically),
text = if (isUserOwner) {
if (requests > 0) {
pluralStringResource(
R.plurals.multiplayer_number_of_join_requests,
requests,
requests,
requests
)
} else {
stringResource(id = R.string.multiplayer_manage)
}
} else {
stringResource(id = R.string.multiplayer_members)
},
color = colorResource(id = R.color.text_secondary),
style = BodyRegular
)
Spacer(Modifier.width(10.dp))
Image(
painter = painterResource(R.drawable.ic_arrow_forward),
contentDescription = "Arrow forward",
modifier = Modifier
.align(Alignment.CenterVertically)
.padding(end = 20.dp)
)
}
}
}
@Composable
fun TypeOfSpace(spaceType: SpaceType?) {
Box(
modifier = Modifier
.height(52.dp)
.fillMaxWidth()
) {
Image(
modifier = Modifier
.align(Alignment.CenterStart)
.padding(start = 18.dp),
painter = painterResource(id = R.drawable.ic_space_type_private),
contentDescription = "Private space icon"
)
if (spaceType != null) {
val spaceTypeName = when (spaceType) {
DEFAULT_SPACE_TYPE -> stringResource(id = R.string.space_type_default_space)
PRIVATE_SPACE_TYPE -> stringResource(id = R.string.space_type_private_space)
SHARED_SPACE_TYPE -> stringResource(id = R.string.space_type_shared_space)
else -> stringResource(id = R.string.space_type_unknown)
}
Text(
modifier = Modifier
.padding(start = 42.dp)
.align(Alignment.CenterStart),
text = spaceTypeName,
color = colorResource(id = R.color.text_primary),
style = BodyRegular
)
}
}
}
@Preview
@Composable
private fun PrivateSpaceSharingPreview() {
PrivateSpaceSharing(
onSharePrivateSpaceClicked = {},
shareLimitStateState = SpaceSettingsViewModel.ShareLimitsState(
shareLimitReached = true,
sharedSpacesLimit = 5
),
onAddMoreSpacesClicked = {}
)
}
@Preview
@Composable
private fun SharedSpaceSharingPreview() {
SharedSpaceSharing(
onManageSharedSpaceClicked = {},
isUserOwner = true
)
}

View file

@ -0,0 +1,195 @@
package com.anytypeio.anytype.ui_settings.space
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
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.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Card
import androidx.compose.material.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.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.foundation.Dragger
import com.anytypeio.anytype.core_ui.views.BodyCalloutRegular
import com.anytypeio.anytype.core_ui.views.ButtonSecondary
import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.ButtonUpgrade
import com.anytypeio.anytype.core_ui.views.Caption1Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel.SegmentLegendItem.Active
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel.SegmentLegendItem.Free
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel.SegmentLegendItem.Other
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel.SpacesStorageScreenState
import com.anytypeio.anytype.ui_settings.R
@Composable
fun SpaceStorageScreen(
data: SpacesStorageScreenState?,
onManageFilesClicked: () -> Unit,
onGetMoreSpaceClicked: () -> Unit
) {
data?.let { currentData ->
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(16.dp),
backgroundColor = colorResource(id = R.color.background_secondary)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp),
) {
Box(
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Dragger()
}
Header(text = stringResource(id = R.string.remote_storage))
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(id = R.string.you_can_store, data.spaceLimit),
modifier = Modifier.fillMaxWidth(),
color = colorResource(R.color.text_primary),
style = BodyCalloutRegular
)
if (data.isShowGetMoreSpace) {
ButtonUpgrade(
modifier = Modifier
.padding(top = 16.dp, bottom = 20.dp)
.height(36.dp)
.verticalScroll(rememberScrollState()),
onClick = { onGetMoreSpaceClicked() },
text = stringResource(id = com.anytypeio.anytype.core_ui.R.string.multiplayer_upgrade_button)
)
}
Spacer(modifier = Modifier.height(20.dp))
Text(
text = stringResource(
id = R.string.space_usage,
data.spaceUsage,
data.spaceLimit
),
style = Relations3,
color = if (data.isShowSpaceUsedWarning) {
colorResource(id = R.color.palette_system_red)
} else {
colorResource(id = R.color.text_secondary)
},
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(8.dp))
SegmentLine(items = currentData.segmentLineItems)
Spacer(modifier = Modifier.height(16.dp))
SegmentLegend(items = currentData.segmentLegendItems)
ButtonSecondary(
text = stringResource(id = R.string.manage_files),
onClick = onManageFilesClicked,
size = ButtonSize.SmallSecondary.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
Spacer(modifier = Modifier.height(44.dp))
}
}
}
}
@Composable
private fun Header(
text: String,
modifier: Modifier = Modifier
) {
Box(
modifier = modifier
.padding(top = 12.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Text(
text = text,
style = Title1,
color = colorResource(id = R.color.text_primary)
)
}
}
@Composable
private fun SegmentLegend(
items: List<SpacesStorageViewModel.SegmentLegendItem>
) {
Column(
modifier = Modifier.fillMaxWidth()
) {
items.forEach { item ->
Row(
modifier = Modifier
.fillMaxWidth()
.height(20.dp),
verticalAlignment = Alignment.CenterVertically
) {
val (color, text) = when (item) {
is Active -> {
colorResource(id = R.color.palette_system_amber_125) to "${item.name} | ${item.usage}"
}
is Free -> {
colorResource(id = R.color.shape_secondary) to "Free | ${item.legend}"
}
is Other -> {
colorResource(id = R.color.palette_system_amber_50) to "Other spaces | ${item.legend}"
}
}
Box(
modifier = Modifier
.size(16.dp)
.clip(CircleShape)
.background(color)
)
Text(
modifier = Modifier
.padding(start = 10.dp),
text = text,
style = Caption1Medium,
color = colorResource(id = R.color.text_primary)
)
}
Spacer(modifier = Modifier.height(8.dp))
}
}
}
@Preview
@Composable
fun PreviewSpaceStorageScreen() {
SpaceStorageScreen(data = SpacesStorageScreenState(
spaceLimit = "sociosqu",
spaceUsage = "error",
isShowGetMoreSpace = false,
isShowSpaceUsedWarning = false,
segmentLegendItems = listOf(),
segmentLineItems = listOf()
), onManageFilesClicked = { /*TODO*/ }) {
}
}

View file

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M6.025,0L22.025,0A6,6 0,0 1,28.025 6L28.025,22A6,6 0,0 1,22.025 28L6.025,28A6,6 0,0 1,0.025 22L0.025,6A6,6 0,0 1,6.025 0z"
android:fillColor="@color/palette_system_amber_100"/>
<path
android:pathData="M13.275,5.75C13.275,5.336 13.611,5 14.025,5C14.44,5 14.775,5.336 14.775,5.75V22.25C14.775,22.664 14.44,23 14.025,23C13.611,23 13.275,22.664 13.275,22.25V5.75Z"
android:fillColor="@color/white"/>
<path
android:pathData="M5.775,14.75C5.361,14.75 5.025,14.414 5.025,14C5.025,13.586 5.361,13.25 5.775,13.25L22.275,13.25C22.69,13.25 23.025,13.586 23.025,14C23.025,14.414 22.69,14.75 22.275,14.75L5.775,14.75Z"
android:fillColor="@color/white"/>
<path
android:pathData="M9.251,7.23C9.044,6.871 9.167,6.413 9.526,6.206C9.884,5.998 10.343,6.121 10.55,6.48L18.8,20.77C19.007,21.128 18.884,21.587 18.525,21.794C18.167,22.001 17.708,21.878 17.501,21.52L9.251,7.23Z"
android:fillColor="@color/white"/>
<path
android:pathData="M7.256,18.774C6.897,18.982 6.439,18.859 6.231,18.5C6.024,18.141 6.147,17.683 6.506,17.475L20.795,9.225C21.154,9.018 21.613,9.141 21.82,9.5C22.027,9.859 21.904,10.317 21.545,10.524L7.256,18.774Z"
android:fillColor="@color/white"/>
<path
android:pathData="M6.506,10.524C6.147,10.317 6.024,9.859 6.231,9.5C6.439,9.141 6.897,9.018 7.256,9.225L21.545,17.475C21.904,17.683 22.027,18.141 21.82,18.5C21.613,18.859 21.154,18.982 20.795,18.774L6.506,10.524Z"
android:fillColor="@color/white"/>
<path
android:pathData="M10.55,21.52C10.343,21.879 9.884,22.001 9.525,21.794C9.167,21.587 9.044,21.129 9.251,20.77L17.501,6.481C17.708,6.122 18.167,5.999 18.525,6.206C18.884,6.413 19.007,6.872 18.8,7.231L10.55,21.52Z"
android:fillColor="@color/white"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M6,0L22,0A6,6 0,0 1,28 6L28,22A6,6 0,0 1,22 28L6,28A6,6 0,0 1,0 22L0,6A6,6 0,0 1,6 0z"
android:fillColor="@color/palette_system_red"/>
<path
android:pathData="M19,11.5C19,10.1193 17.8807,9 16.5,9H11.5C10.2905,9 9.2816,9.8589 9.05,11H13.5C15.433,11 17,12.567 17,14.5V18.95C18.1411,18.7184 19,17.7095 19,16.5V11.5Z"
android:fillColor="@color/white"
android:fillType="evenOdd"/>
<path
android:pathData="M21.9998,8.5C21.9998,7.1193 20.8805,6 19.4998,6H14.4998C13.2903,6 12.2814,6.8589 12.0498,8H16.4998C18.4328,8 19.9998,9.567 19.9998,11.5V15.95C21.1409,15.7184 21.9998,14.7095 21.9998,13.5V8.5Z"
android:fillColor="@color/white"
android:fillType="evenOdd"/>
<path
android:strokeWidth="1"
android:pathData="M16.5,14.5L16.5,19.5A3,3 0,0 1,13.5 22.5L8.5,22.5A3,3 0,0 1,5.5 19.5L5.5,14.5A3,3 0,0 1,8.5 11.5L13.5,11.5A3,3 0,0 1,16.5 14.5z"
android:fillColor="@color/white"
android:strokeColor="@color/palette_system_red"/>
</vector>

View file

@ -0,0 +1,21 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:fillColor="@color/chapter_yellow"
android:pathData="M6,0L22,0A6,6 0,0 1,28 6L28,22A6,6 0,0 1,22 28L6,28A6,6 0,0 1,0 22L0,6A6,6 0,0 1,6 0z" />
<path
android:fillColor="@color/white"
android:pathData="M14,4C13,10 10,13 4,14H14V4Z" />
<path
android:fillColor="@color/white"
android:pathData="M14,24C13,18 10,15 4,14H14V24Z" />
<path
android:fillColor="@color/white"
android:pathData="M14,4C15,10 18,13 24,14H14V4Z" />
<path
android:fillColor="@color/white"
android:pathData="M14,24C15,18 18,15 24,14H14V24Z" />
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M6,0L22,0A6,6 0,0 1,28 6L28,22A6,6 0,0 1,22 28L6,28A6,6 0,0 1,0 22L0,6A6,6 0,0 1,6 0z"
android:fillColor="@color/palette_system_blue"/>
<path
android:pathData="M6,10.5C6,9.119 7.119,8 8.5,8H19.5C20.881,8 22,9.119 22,10.5C22,11.881 20.881,13 19.5,13H8.5C7.119,13 6,11.881 6,10.5ZM10,10.5C10,11.052 9.552,11.5 9,11.5C8.448,11.5 8,11.052 8,10.5C8,9.948 8.448,9.5 9,9.5C9.552,9.5 10,9.948 10,10.5ZM6,17.5C6,16.119 7.119,15 8.5,15H19.5C20.881,15 22,16.119 22,17.5C22,18.881 20.881,20 19.5,20H8.5C7.119,20 6,18.881 6,17.5ZM10,17.5C10,18.052 9.552,18.5 9,18.5C8.448,18.5 8,18.052 8,17.5C8,16.948 8.448,16.5 9,16.5C9.552,16.5 10,16.948 10,17.5Z"
android:fillColor="@color/white"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M6,0L22,0A6,6 0,0 1,28 6L28,22A6,6 0,0 1,22 28L6,28A6,6 0,0 1,0 22L0,6A6,6 0,0 1,6 0z"
android:fillColor="@color/palette_system_sky"/>
<path
android:pathData="M12.0015,13H16.0015V16L14.5015,17.5L16.0015,19L14.5015,20.5L16.0015,22V23L14.0015,25.0312L12.0015,23V13Z"
android:fillColor="@color/white"/>
<path
android:pathData="M14,9m-5,0a5,5 0,1 1,10 0a5,5 0,1 1,-10 0"
android:fillColor="@color/white"/>
<path
android:pathData="M14,7.5m-1.5,0a1.5,1.5 0,1 1,3 0a1.5,1.5 0,1 1,-3 0"
android:fillColor="@color/palette_system_sky"/>
</vector>

View file

@ -0,0 +1,33 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M6,0L22,0A6,6 0,0 1,28 6L28,22A6,6 0,0 1,22 28L6,28A6,6 0,0 1,0 22L0,6A6,6 0,0 1,6 0z"
android:fillColor="@color/palette_system_purple"/>
<path
android:pathData="M5.9707,20L5.9707,20A0.75,0.75 0,0 1,6.7207 19.25L21.2207,19.25A0.75,0.75 0,0 1,21.9707 20L21.9707,20A0.75,0.75 0,0 1,21.2207 20.75L6.7207,20.75A0.75,0.75 0,0 1,5.9707 20z"
android:fillColor="@color/white"/>
<path
android:strokeWidth="1"
android:pathData="M15.4707,20L15.4707,20A2,2 0,0 1,17.4707 18L17.4707,18A2,2 0,0 1,19.4707 20L19.4707,20A2,2 0,0 1,17.4707 22L17.4707,22A2,2 0,0 1,15.4707 20z"
android:fillColor="@color/personalization_background_purple"
android:strokeColor="@color/white"/>
<path
android:pathData="M5.9707,14L5.9707,14A0.75,0.75 0,0 1,6.7207 13.25L21.2207,13.25A0.75,0.75 0,0 1,21.9707 14L21.9707,14A0.75,0.75 0,0 1,21.2207 14.75L6.7207,14.75A0.75,0.75 0,0 1,5.9707 14z"
android:fillColor="@color/white"/>
<path
android:pathData="M5.9707,8L5.9707,8A0.75,0.75 0,0 1,6.7207 7.25L21.2207,7.25A0.75,0.75 0,0 1,21.9707 8L21.9707,8A0.75,0.75 0,0 1,21.2207 8.75L6.7207,8.75A0.75,0.75 0,0 1,5.9707 8z"
android:fillColor="@color/white"/>
<path
android:strokeWidth="1"
android:pathData="M8.4707,14L8.4707,14A2,2 0,0 1,10.4707 12L10.4707,12A2,2 0,0 1,12.4707 14L12.4707,14A2,2 0,0 1,10.4707 16L10.4707,16A2,2 0,0 1,8.4707 14z"
android:fillColor="@color/personalization_background_purple"
android:strokeColor="@color/white"/>
<path
android:strokeWidth="1"
android:pathData="M15.4707,8L15.4707,8A2,2 0,0 1,17.4707 6L17.4707,6A2,2 0,0 1,19.4707 8L19.4707,8A2,2 0,0 1,17.4707 10L17.4707,10A2,2 0,0 1,15.4707 8z"
android:fillColor="@color/personalization_background_purple"
android:strokeColor="@color/white"/>
</vector>

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>