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:
parent
28bc323b73
commit
9ded869c33
59 changed files with 950 additions and 69 deletions
48
feature-ui-settings/build.gradle
Normal file
48
feature-ui-settings/build.gradle
Normal 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
|
||||
}
|
1
feature-ui-settings/src/main/AndroidManifest.xml
Normal file
1
feature-ui-settings/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1 @@
|
|||
<manifest />
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 = {}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
|
@ -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
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
|
@ -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*/ }) {
|
||||
}
|
||||
}
|
27
feature-ui-settings/src/main/res/drawable/ic_about.xml
Normal file
27
feature-ui-settings/src/main/res/drawable/ic_about.xml
Normal 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>
|
22
feature-ui-settings/src/main/res/drawable/ic_appearance.xml
Normal file
22
feature-ui-settings/src/main/res/drawable/ic_appearance.xml
Normal 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>
|
21
feature-ui-settings/src/main/res/drawable/ic_debug.xml
Normal file
21
feature-ui-settings/src/main/res/drawable/ic_debug.xml
Normal 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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
2
feature-ui-settings/src/main/res/values/strings.xml
Normal file
2
feature-ui-settings/src/main/res/values/strings.xml
Normal file
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
Loading…
Add table
Add a link
Reference in a new issue