mirror of
https://github.com/anyproto/anytype-kotlin.git
synced 2025-06-08 05:47:05 +09:00
DROID-1014 Account | Enhancement | Account name & image icon editing (#3062)
DROID-1014 Account | Enhancement | Account name & image icon editing
This commit is contained in:
parent
85d579f24b
commit
776551ac12
10 changed files with 636 additions and 50 deletions
|
@ -12,6 +12,11 @@ import com.anytypeio.anytype.domain.config.ConfigStorage
|
|||
import com.anytypeio.anytype.domain.config.UserSettingsRepository
|
||||
import com.anytypeio.anytype.domain.debugging.DebugSync
|
||||
import com.anytypeio.anytype.domain.device.ClearFileCache
|
||||
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
|
||||
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
import com.anytypeio.anytype.domain.`object`.SetObjectDetails
|
||||
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
|
||||
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
|
||||
import com.anytypeio.anytype.providers.DefaultUriFileProvider
|
||||
import com.anytypeio.anytype.ui.settings.AccountAndDataFragment
|
||||
|
@ -46,12 +51,22 @@ object AccountAndDataModule {
|
|||
clearFileCache: ClearFileCache,
|
||||
deleteAccount: DeleteAccount,
|
||||
debugSyncShareDownloader: DebugSyncShareDownloader,
|
||||
analytics: Analytics
|
||||
analytics: Analytics,
|
||||
storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
setObjectDetails: SetObjectDetails,
|
||||
configStorage: ConfigStorage,
|
||||
urlBuilder: UrlBuilder,
|
||||
setDocumentImageIcon: SetDocumentImageIcon
|
||||
): AccountAndDataViewModel.Factory = AccountAndDataViewModel.Factory(
|
||||
clearFileCache = clearFileCache,
|
||||
deleteAccount = deleteAccount,
|
||||
debugSyncShareDownloader = debugSyncShareDownloader,
|
||||
analytics = analytics
|
||||
analytics = analytics,
|
||||
storelessSubscriptionContainer = storelessSubscriptionContainer,
|
||||
setObjectDetails = setObjectDetails,
|
||||
configStorage = configStorage,
|
||||
urlBuilder = urlBuilder,
|
||||
setDocumentImageIcon = setDocumentImageIcon
|
||||
)
|
||||
|
||||
@Provides
|
||||
|
@ -102,6 +117,37 @@ object AccountAndDataModule {
|
|||
@PerScreen
|
||||
fun deleteAccount(repo: AuthRepository): DeleteAccount = DeleteAccount(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideSetObjectDetails(
|
||||
repo: BlockRepository,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
): SetObjectDetails = SetObjectDetails(
|
||||
repo,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideSetDocumentImageIcon(
|
||||
repo: BlockRepository
|
||||
): SetDocumentImageIcon = SetDocumentImageIcon(repo)
|
||||
|
||||
@JvmStatic
|
||||
@Provides
|
||||
@PerScreen
|
||||
fun provideStoreLessSubscriptionContainer(
|
||||
repo: BlockRepository,
|
||||
channel: SubscriptionEventChannel,
|
||||
dispatchers: AppCoroutineDispatchers
|
||||
): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl(
|
||||
repo = repo,
|
||||
channel = channel,
|
||||
dispatchers = dispatchers
|
||||
)
|
||||
|
||||
@Module
|
||||
interface Bindings {
|
||||
|
||||
|
|
|
@ -1,29 +1,43 @@
|
|||
package com.anytypeio.anytype.ui.settings
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.analytics.base.EventsDictionary
|
||||
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
|
||||
import com.anytypeio.anytype.core_utils.ext.GetImageContract
|
||||
import com.anytypeio.anytype.core_utils.ext.parseImagePath
|
||||
import com.anytypeio.anytype.core_utils.ext.shareFile
|
||||
import com.anytypeio.anytype.core_utils.ext.subscribe
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
import com.anytypeio.anytype.ui.auth.account.DeleteAccountWarning
|
||||
import com.anytypeio.anytype.ui.dashboard.ClearCacheAlertFragment
|
||||
import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase
|
||||
import com.anytypeio.anytype.ui.profile.KeychainPhraseDialog
|
||||
import com.anytypeio.anytype.ui.sets.ARG_SHOW_REMOVE_BUTTON
|
||||
import com.anytypeio.anytype.ui_settings.account.AccountAndDataScreen
|
||||
import com.anytypeio.anytype.ui_settings.account.AccountAndDataViewModel
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import javax.inject.Inject
|
||||
import timber.log.Timber
|
||||
|
||||
class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
|
||||
|
||||
|
@ -55,7 +69,10 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
|
|||
shareFile(uri)
|
||||
}
|
||||
}
|
||||
return ComposeView(requireContext()).apply {
|
||||
return ComposeDialogView(
|
||||
context = requireContext(),
|
||||
dialog = requireDialog()
|
||||
).apply {
|
||||
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
|
||||
setContent {
|
||||
MaterialTheme(typography = typography) {
|
||||
|
@ -68,13 +85,28 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
|
|||
isLogoutInProgress = vm.isLoggingOut.collectAsState().value,
|
||||
isClearCacheInProgress = vm.isClearFileCacheInProgress.collectAsState().value,
|
||||
isDebugSyncReportInProgress = vm.isDebugSyncReportInProgress.collectAsState().value,
|
||||
isShowDebug = toggles.isTroubleshootingMode
|
||||
isShowDebug = toggles.isTroubleshootingMode,
|
||||
onNameChange = { vm.onNameChange(it) },
|
||||
onProfileIconClick = { proceedWithIconClick() },
|
||||
vm.accountData.collectAsStateWithLifecycle().value
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
val offsetFromTop = PADDING_TOP
|
||||
(dialog as? BottomSheetDialog)?.behavior?.apply {
|
||||
isFitToContents = false
|
||||
expandedOffset = offsetFromTop
|
||||
state = BottomSheetBehavior.STATE_EXPANDED
|
||||
skipCollapsed = true
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithClearFileCacheWarning() {
|
||||
vm.onClearCacheButtonClicked()
|
||||
val dialog = ClearCacheAlertFragment.new()
|
||||
|
@ -91,6 +123,48 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
|
|||
dialog.show(childFragmentManager, null)
|
||||
}
|
||||
|
||||
private fun proceedWithIconClick() {
|
||||
if (!hasExternalStoragePermission()) {
|
||||
permissionReadStorage.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE))
|
||||
} else {
|
||||
openGallery()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hasExternalStoragePermission() = ContextCompat.checkSelfPermission(
|
||||
requireActivity(),
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
).let { result -> result == PackageManager.PERMISSION_GRANTED }
|
||||
|
||||
private val permissionReadStorage =
|
||||
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
|
||||
val readResult = grantResults[Manifest.permission.READ_EXTERNAL_STORAGE]
|
||||
if (readResult == true) {
|
||||
openGallery()
|
||||
} else {
|
||||
toast(R.string.permission_read_denied)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openGallery() {
|
||||
getContent.launch(SELECT_IMAGE_CODE)
|
||||
}
|
||||
|
||||
private val getContent = registerForActivityResult(GetImageContract()) { uri: Uri? ->
|
||||
if (uri != null) {
|
||||
try {
|
||||
val path = uri.parseImagePath(requireContext())
|
||||
vm.onPickedImageFromDevice(path = path)
|
||||
} catch (e: Exception) {
|
||||
toast("Error while parsing path for cover image")
|
||||
Timber.d(e, "Error while parsing path for cover image")
|
||||
}
|
||||
} else {
|
||||
toast("Error while upload cover image, URI is null")
|
||||
Timber.e("Error while upload cover image, URI is null")
|
||||
}
|
||||
}
|
||||
|
||||
override fun injectDependencies() {
|
||||
componentManager().accountAndDataComponent.get().inject(this)
|
||||
}
|
||||
|
@ -100,3 +174,6 @@ class AccountAndDataFragment : BaseBottomSheetComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private const val PADDING_TOP = 54
|
||||
|
||||
private const val SELECT_IMAGE_CODE = 1
|
||||
|
|
|
@ -6,7 +6,6 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.viewModels
|
||||
|
@ -16,7 +15,6 @@ import androidx.lifecycle.lifecycleScope
|
|||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.anytypeio.anytype.R
|
||||
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
|
||||
import com.anytypeio.anytype.core_utils.ext.toast
|
||||
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
|
||||
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
|
||||
import com.anytypeio.anytype.di.common.componentManager
|
||||
|
@ -82,7 +80,7 @@ class MainSettingFragment : BaseBottomSheetComposeFragment() {
|
|||
setContent {
|
||||
MaterialTheme(typography = typography) {
|
||||
MainSettingScreen(
|
||||
workspace = vm.workspaceData.collectAsStateWithLifecycle().value,
|
||||
workspace = vm.workspaceAndAccount.collectAsStateWithLifecycle().value,
|
||||
onAccountAndDataClicked = onAccountAndDataClicked,
|
||||
onAboutAppClicked = onAboutAppClicked,
|
||||
onAppearanceClicked = onAppearanceClicked,
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.anytypeio.anytype.core_utils.ext
|
|||
|
||||
import android.content.Context
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
|
||||
|
@ -65,6 +66,9 @@ inline fun <T> List<T>.replace(replacement: (T) -> T, target: (T) -> Boolean): L
|
|||
|
||||
fun Context.toast(msg: CharSequence) = Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
|
||||
fun Fragment.toast(msg: CharSequence) = requireActivity().toast(msg)
|
||||
|
||||
fun Fragment.toast(@StringRes msgId: Int) = requireActivity().toast(requireActivity().getString(msgId))
|
||||
|
||||
fun Fragment.dismissInnerDialog(tag: String) {
|
||||
val fragment = childFragmentManager.findFragmentByTag(tag)
|
||||
(fragment as? DialogFragment)?.dismiss()
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package com.anytypeio.anytype.presentation.profile
|
||||
|
||||
import com.anytypeio.anytype.core_models.ObjectWrapper
|
||||
import com.anytypeio.anytype.core_models.Url
|
||||
import com.anytypeio.anytype.domain.misc.UrlBuilder
|
||||
|
||||
sealed class ProfileIconView {
|
||||
object Loading : ProfileIconView()
|
||||
object Placeholder : ProfileIconView()
|
||||
data class Emoji(val unicode: String) : ProfileIconView()
|
||||
data class Image(val url: Url) : ProfileIconView()
|
||||
}
|
||||
|
||||
fun ObjectWrapper.Basic.profileIcon(builder: UrlBuilder): ProfileIconView = when {
|
||||
!iconImage.isNullOrEmpty() -> {
|
||||
val hash = checkNotNull(iconImage)
|
||||
ProfileIconView.Image(builder.thumbnail(hash))
|
||||
}
|
||||
else -> ProfileIconView.Placeholder
|
||||
}
|
|
@ -15,6 +15,8 @@ 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.`object`.SetObjectDetails
|
||||
import com.anytypeio.anytype.presentation.profile.ProfileIconView
|
||||
import com.anytypeio.anytype.presentation.profile.profileIcon
|
||||
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
|
||||
import com.anytypeio.anytype.presentation.spaces.spaceIcon
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
|
@ -37,10 +39,13 @@ class MainSettingsViewModel(
|
|||
val events = MutableSharedFlow<Event>(replay = 0)
|
||||
val commands = MutableSharedFlow<Command>(replay = 0)
|
||||
|
||||
val workspaceData = storelessSubscriptionContainer.subscribe(
|
||||
private val profileId = configStorage.get().profile
|
||||
private val workspaceId = configStorage.get().workspace
|
||||
|
||||
val workspaceAndAccount = storelessSubscriptionContainer.subscribe(
|
||||
StoreSearchByIdsParams(
|
||||
subscription = SPACE_SUBSCRIPTION_ID,
|
||||
targets = listOf(configStorage.get().workspace),
|
||||
targets = listOf(workspaceId, profileId),
|
||||
keys = listOf(
|
||||
Relations.ID,
|
||||
Relations.NAME,
|
||||
|
@ -49,15 +54,26 @@ class MainSettingsViewModel(
|
|||
)
|
||||
)
|
||||
).map { result ->
|
||||
val obj = result.firstOrNull()
|
||||
WorkspaceData.Data(
|
||||
name = obj?.name ?: "",
|
||||
icon = obj?.spaceIcon(urlBuilder) ?: SpaceIconView.Placeholder
|
||||
val workspace = result.find { it.id == workspaceId }
|
||||
val profile = result.find { it.id == profileId }
|
||||
WorkspaceAndAccount.Account(
|
||||
space = workspace?.let {
|
||||
WorkspaceAndAccount.SpaceData(
|
||||
name = workspace.name ?: "",
|
||||
icon = workspace.spaceIcon(urlBuilder)
|
||||
)
|
||||
},
|
||||
profile = profile?.let {
|
||||
WorkspaceAndAccount.ProfileData(
|
||||
name = profile.name ?: "",
|
||||
icon = profile.profileIcon(urlBuilder)
|
||||
)
|
||||
}
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT),
|
||||
WorkspaceData.Idle
|
||||
WorkspaceAndAccount.Idle
|
||||
)
|
||||
|
||||
init {
|
||||
|
@ -79,9 +95,11 @@ class MainSettingsViewModel(
|
|||
Event.OnAppearanceClicked -> commands.emit(Command.OpenAppearanceScreen)
|
||||
Event.OnPersonalizationClicked -> commands.emit(Command.OpenPersonalizationScreen)
|
||||
Event.OnDebugClicked -> commands.emit(Command.OpenDebugScreen)
|
||||
Event.OnSpaceImageClicked -> commands.emit(Command.OpenSpaceImageSet(
|
||||
configStorage.get().workspace
|
||||
))
|
||||
Event.OnSpaceImageClicked -> commands.emit(
|
||||
Command.OpenSpaceImageSet(
|
||||
configStorage.get().workspace
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -125,7 +143,7 @@ class MainSettingsViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onNameSet(name: String) {
|
||||
fun onNameSet(name: String) {
|
||||
viewModelScope.launch {
|
||||
setObjectDetails.execute(
|
||||
SetObjectDetails.Params(
|
||||
|
@ -182,14 +200,26 @@ class MainSettingsViewModel(
|
|||
class OpenSpaceImageSet(val id: Id) : Command()
|
||||
}
|
||||
|
||||
sealed class WorkspaceData {
|
||||
object Idle : WorkspaceData()
|
||||
class Data(
|
||||
sealed class WorkspaceAndAccount {
|
||||
object Idle : WorkspaceAndAccount()
|
||||
class Account(
|
||||
val space: SpaceData?,
|
||||
val profile: ProfileData?
|
||||
) : WorkspaceAndAccount()
|
||||
|
||||
data class SpaceData(
|
||||
val name: String,
|
||||
val icon: SpaceIconView
|
||||
) : WorkspaceData()
|
||||
)
|
||||
|
||||
data class ProfileData(
|
||||
val name: String,
|
||||
val icon: ProfileIconView,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private const val SPACE_SUBSCRIPTION_ID = "settings_space_subscription"
|
||||
|
|
|
@ -1,31 +1,56 @@
|
|||
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.fillMaxHeight
|
||||
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.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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
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.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.Toolbar
|
||||
import com.anytypeio.anytype.core_ui.foundation.noRippleClickable
|
||||
import com.anytypeio.anytype.presentation.profile.ProfileIconView
|
||||
import com.anytypeio.anytype.ui_settings.R
|
||||
import com.anytypeio.anytype.ui_settings.main.NameBlock
|
||||
|
||||
@Composable
|
||||
fun AccountAndDataScreen(
|
||||
|
@ -38,15 +63,23 @@ fun AccountAndDataScreen(
|
|||
isClearCacheInProgress: Boolean,
|
||||
isDebugSyncReportInProgress: Boolean,
|
||||
isShowDebug: Boolean,
|
||||
onNameChange: (String) -> Unit,
|
||||
onProfileIconClick: () -> Unit,
|
||||
account: AccountAndDataViewModel.AccountProfile
|
||||
) {
|
||||
Column {
|
||||
Box(
|
||||
Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.align(Alignment.CenterHorizontally)) {
|
||||
Dragger()
|
||||
}
|
||||
Toolbar(stringResource(R.string.account_and_data))
|
||||
Column(modifier = Modifier.fillMaxHeight()) {
|
||||
Header(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally),
|
||||
account = account,
|
||||
onNameSet = onNameChange,
|
||||
onProfileIconClick = onProfileIconClick
|
||||
)
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(10.dp)
|
||||
.padding(top = 4.dp)
|
||||
)
|
||||
Divider()
|
||||
Section(stringResource(R.string.access))
|
||||
Option(
|
||||
image = R.drawable.ic_keychain_phrase,
|
||||
|
@ -206,4 +239,169 @@ fun ActionWithProgressBar(
|
|||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Header(
|
||||
modifier: Modifier = Modifier,
|
||||
account: AccountAndDataViewModel.AccountProfile,
|
||||
onProfileIconClick: () -> Unit,
|
||||
onNameSet: (String) -> Unit
|
||||
) {
|
||||
when (account) {
|
||||
is AccountAndDataViewModel.AccountProfile.Data -> {
|
||||
Box(modifier = modifier.padding(vertical = 6.dp)) {
|
||||
Dragger()
|
||||
}
|
||||
Box(modifier = modifier.padding(top = 12.dp, bottom = 28.dp)) {
|
||||
ProfileNameBlock()
|
||||
}
|
||||
Box(modifier = modifier.padding(bottom = 16.dp)) {
|
||||
ProfileImageBlock(
|
||||
name = account.name,
|
||||
icon = account.icon,
|
||||
onProfileIconClick = onProfileIconClick
|
||||
)
|
||||
}
|
||||
NameBlock(name = account.name, onNameSet = onNameSet)
|
||||
}
|
||||
is AccountAndDataViewModel.AccountProfile.Idle -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun NameBlock(
|
||||
modifier: Modifier = Modifier,
|
||||
name: String,
|
||||
onNameSet: (String) -> Unit
|
||||
) {
|
||||
|
||||
val nameValue = remember { mutableStateOf(name) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
Column(modifier = modifier.padding(start = 20.dp)) {
|
||||
Text(
|
||||
text = "Name",
|
||||
color = colorResource(id = R.color.text_secondary),
|
||||
fontSize = 13.sp
|
||||
)
|
||||
BasicTextField(
|
||||
value = nameValue.value,
|
||||
onValueChange = {
|
||||
nameValue.value = it
|
||||
},
|
||||
modifier = Modifier.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 = {
|
||||
onNameSet.invoke(nameValue.value)
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = @Composable { innerTextField ->
|
||||
TextFieldDefaults.OutlinedTextFieldDecorationBox(
|
||||
value = nameValue.value,
|
||||
innerTextField = innerTextField,
|
||||
singleLine = true,
|
||||
enabled = true,
|
||||
isError = false,
|
||||
placeholder = {
|
||||
Text(text = "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),
|
||||
),
|
||||
contentPadding = PaddingValues(
|
||||
start = 0.dp,
|
||||
top = 0.dp,
|
||||
end = 0.dp,
|
||||
bottom = 0.dp
|
||||
),
|
||||
border = {},
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
visualTransformation = VisualTransformation.None
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ProfileNameBlock(modifier: Modifier = Modifier) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_and_data),
|
||||
style = MaterialTheme.typography.h3,
|
||||
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,
|
||||
error = painterResource(id = R.drawable.ic_home_widget_space)
|
||||
),
|
||||
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.shape_primary))
|
||||
.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)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,14 +7,27 @@ 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.Relations
|
||||
import com.anytypeio.anytype.domain.account.DeleteAccount
|
||||
import com.anytypeio.anytype.domain.base.BaseUseCase
|
||||
import com.anytypeio.anytype.domain.base.Interactor
|
||||
import com.anytypeio.anytype.domain.base.fold
|
||||
import com.anytypeio.anytype.domain.config.ConfigStorage
|
||||
import com.anytypeio.anytype.domain.device.ClearFileCache
|
||||
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.`object`.SetObjectDetails
|
||||
import com.anytypeio.anytype.presentation.profile.ProfileIconView
|
||||
import com.anytypeio.anytype.presentation.profile.profileIcon
|
||||
import com.anytypeio.anytype.ui_settings.account.repo.DebugSyncShareDownloader
|
||||
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
|
||||
|
||||
|
@ -23,6 +36,11 @@ class AccountAndDataViewModel(
|
|||
private val analytics: Analytics,
|
||||
private val deleteAccount: DeleteAccount,
|
||||
private val debugSyncShareDownloader: DebugSyncShareDownloader,
|
||||
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
private val setObjectDetails: SetObjectDetails,
|
||||
private val configStorage: ConfigStorage,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val setImageIcon: SetDocumentImageIcon
|
||||
) : ViewModel() {
|
||||
|
||||
private val jobs = mutableListOf<Job>()
|
||||
|
@ -32,6 +50,31 @@ class AccountAndDataViewModel(
|
|||
val isLoggingOut = MutableStateFlow(false)
|
||||
val debugSyncReportUri = MutableStateFlow<Uri?>(null)
|
||||
|
||||
private val profileId = configStorage.get().profile
|
||||
|
||||
val accountData = storelessSubscriptionContainer.subscribe(
|
||||
StoreSearchByIdsParams(
|
||||
subscription = ACCOUNT_AND_DATA_SUBSCRIPTION_ID,
|
||||
keys = listOf(
|
||||
Relations.ID,
|
||||
Relations.NAME,
|
||||
Relations.ICON_IMAGE,
|
||||
Relations.ICON_EMOJI
|
||||
),
|
||||
targets = listOf(profileId)
|
||||
)
|
||||
).map { result ->
|
||||
val obj = result.firstOrNull()
|
||||
AccountProfile.Data(
|
||||
name = obj?.name ?: "",
|
||||
icon = obj?.profileIcon(urlBuilder) ?: ProfileIconView.Placeholder
|
||||
)
|
||||
}.stateIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT),
|
||||
AccountProfile.Idle
|
||||
)
|
||||
|
||||
fun onClearFileCacheAccepted() {
|
||||
Timber.d("onClearFileCacheAccepted, ")
|
||||
jobs += viewModelScope.launch {
|
||||
|
@ -57,6 +100,24 @@ class AccountAndDataViewModel(
|
|||
}
|
||||
}
|
||||
|
||||
fun onNameChange(name: String) {
|
||||
viewModelScope.launch {
|
||||
setObjectDetails.execute(
|
||||
SetObjectDetails.Params(
|
||||
ctx = profileId,
|
||||
details = mapOf(Relations.NAME to name)
|
||||
)
|
||||
).fold(
|
||||
onFailure = {
|
||||
Timber.e(it, "Error while updating object details")
|
||||
},
|
||||
onSuccess = {
|
||||
// do nothing
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onClearCacheButtonClicked() {
|
||||
jobs += viewModelScope.sendEvent(
|
||||
analytics = analytics,
|
||||
|
@ -108,6 +169,35 @@ class AccountAndDataViewModel(
|
|||
forEach { it.cancel() }
|
||||
clear()
|
||||
}
|
||||
viewModelScope.launch {
|
||||
storelessSubscriptionContainer.unsubscribe(
|
||||
listOf(ACCOUNT_AND_DATA_SUBSCRIPTION_ID)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun onPickedImageFromDevice(path: String) {
|
||||
viewModelScope.launch {
|
||||
setImageIcon(
|
||||
SetImageIcon.Params(target = profileId, path = path)
|
||||
).process(
|
||||
failure = {
|
||||
Timber.e("Error while setting image icon")
|
||||
},
|
||||
success = { (payload, _) ->
|
||||
// do nothing
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class AccountProfile {
|
||||
object Idle: AccountProfile()
|
||||
|
||||
class Data(
|
||||
val name: String,
|
||||
val icon: ProfileIconView
|
||||
): AccountProfile()
|
||||
}
|
||||
|
||||
class Factory(
|
||||
|
@ -115,6 +205,11 @@ class AccountAndDataViewModel(
|
|||
private val deleteAccount: DeleteAccount,
|
||||
private val debugSyncShareDownloader: DebugSyncShareDownloader,
|
||||
private val analytics: Analytics,
|
||||
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
|
||||
private val setObjectDetails: SetObjectDetails,
|
||||
private val configStorage: ConfigStorage,
|
||||
private val urlBuilder: UrlBuilder,
|
||||
private val setDocumentImageIcon: SetDocumentImageIcon
|
||||
) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
|
@ -122,8 +217,16 @@ class AccountAndDataViewModel(
|
|||
clearFileCache = clearFileCache,
|
||||
deleteAccount = deleteAccount,
|
||||
debugSyncShareDownloader = debugSyncShareDownloader,
|
||||
analytics = analytics
|
||||
analytics = analytics,
|
||||
storelessSubscriptionContainer = storelessSubscriptionContainer,
|
||||
setObjectDetails = setObjectDetails,
|
||||
configStorage = configStorage,
|
||||
urlBuilder = urlBuilder,
|
||||
setImageIcon = setDocumentImageIcon
|
||||
) as T
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val STOP_SUBSCRIPTION_TIMEOUT = 1_000L
|
||||
private const val ACCOUNT_AND_DATA_SUBSCRIPTION_ID = "account_and_data_subscription"
|
|
@ -1,25 +1,41 @@
|
|||
package com.anytypeio.anytype.ui_settings.main
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
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.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
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.presentation.profile.ProfileIconView
|
||||
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel
|
||||
import com.anytypeio.anytype.ui_settings.R
|
||||
|
||||
@Composable
|
||||
fun MainSettingScreen(
|
||||
workspace: MainSettingsViewModel.WorkspaceData,
|
||||
workspace: MainSettingsViewModel.WorkspaceAndAccount,
|
||||
onSpaceIconClick: () -> Unit,
|
||||
onAccountAndDataClicked: () -> Unit,
|
||||
onAboutAppClicked: () -> Unit,
|
||||
|
@ -27,7 +43,7 @@ fun MainSettingScreen(
|
|||
onPersonalizationClicked: () -> Unit,
|
||||
onAppearanceClicked: () -> Unit,
|
||||
onNameSet: (String) -> Unit,
|
||||
showDebugMenu: Boolean
|
||||
showDebugMenu: Boolean,
|
||||
) {
|
||||
Column(Modifier.fillMaxSize()) {
|
||||
Header(
|
||||
|
@ -36,7 +52,11 @@ fun MainSettingScreen(
|
|||
onSpaceIconClick = onSpaceIconClick,
|
||||
onNameSet = onNameSet
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp).padding(top = 4.dp))
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.height(10.dp)
|
||||
.padding(top = 4.dp)
|
||||
)
|
||||
Divider()
|
||||
Spacer(modifier = Modifier.height(26.dp))
|
||||
Settings(
|
||||
|
@ -45,7 +65,8 @@ fun MainSettingScreen(
|
|||
onAppearanceClicked = onAppearanceClicked,
|
||||
onAboutAppClicked = onAboutAppClicked,
|
||||
showDebugMenu = showDebugMenu,
|
||||
onDebugClicked = onDebugClicked
|
||||
onDebugClicked = onDebugClicked,
|
||||
accountData = workspace
|
||||
)
|
||||
Box(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
@ -58,14 +79,15 @@ private fun Settings(
|
|||
onAppearanceClicked: () -> Unit,
|
||||
onAboutAppClicked: () -> Unit,
|
||||
showDebugMenu: Boolean,
|
||||
onDebugClicked: () -> Unit
|
||||
onDebugClicked: () -> Unit,
|
||||
accountData: MainSettingsViewModel.WorkspaceAndAccount
|
||||
) {
|
||||
Section(
|
||||
modifier = Modifier.padding(start = 20.dp, bottom = 4.dp),
|
||||
title = "Settings"
|
||||
title = stringResource(id = R.string.settings)
|
||||
)
|
||||
Option(
|
||||
image = R.drawable.ic_account_and_data,
|
||||
AccountOption(
|
||||
data = (accountData as? MainSettingsViewModel.WorkspaceAndAccount.Account)?.profile,
|
||||
text = stringResource(R.string.account_and_data),
|
||||
onClick = onAccountAndDataClicked
|
||||
)
|
||||
|
@ -101,12 +123,12 @@ private fun Settings(
|
|||
@Composable
|
||||
private fun Header(
|
||||
modifier: Modifier = Modifier,
|
||||
workspace: MainSettingsViewModel.WorkspaceData,
|
||||
workspace: MainSettingsViewModel.WorkspaceAndAccount,
|
||||
onSpaceIconClick: () -> Unit,
|
||||
onNameSet: (String) -> Unit
|
||||
) {
|
||||
when (workspace) {
|
||||
is MainSettingsViewModel.WorkspaceData.Data -> {
|
||||
is MainSettingsViewModel.WorkspaceAndAccount.Account -> {
|
||||
Box(modifier = modifier.padding(vertical = 6.dp)) {
|
||||
Dragger()
|
||||
}
|
||||
|
@ -114,13 +136,97 @@ private fun Header(
|
|||
SpaceNameBlock()
|
||||
}
|
||||
Box(modifier = modifier.padding(bottom = 16.dp)) {
|
||||
SpaceImageBlock(
|
||||
icon = workspace.icon,
|
||||
onSpaceIconClick = onSpaceIconClick
|
||||
)
|
||||
workspace.space?.icon?.let {
|
||||
SpaceImageBlock(
|
||||
icon = it,
|
||||
onSpaceIconClick = onSpaceIconClick
|
||||
)
|
||||
}
|
||||
}
|
||||
workspace.space?.name?.let {
|
||||
NameBlock(name = it, onNameSet = onNameSet)
|
||||
}
|
||||
NameBlock(name = workspace.name, onNameSet = onNameSet)
|
||||
}
|
||||
is MainSettingsViewModel.WorkspaceData.Idle -> {}
|
||||
is MainSettingsViewModel.WorkspaceAndAccount.Idle -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountOption(
|
||||
data: MainSettingsViewModel.WorkspaceAndAccount.ProfileData?,
|
||||
text: String,
|
||||
onClick: () -> Unit = {}
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.height(52.dp)
|
||||
.clickable(onClick = onClick)
|
||||
|
||||
) {
|
||||
data?.let {
|
||||
when (val icon = it.icon) {
|
||||
is ProfileIconView.Image -> {
|
||||
Image(
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = icon.url,
|
||||
error = painterResource(id = R.drawable.ic_home_widget_space)
|
||||
),
|
||||
contentDescription = "Custom image profile",
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp)
|
||||
.size(28.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val nameFirstChar = if (data.name.isEmpty()) {
|
||||
stringResource(id = R.string.account_default_name)
|
||||
} else {
|
||||
data.name.first().uppercaseChar().toString()
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(start = 20.dp)
|
||||
.size(28.dp)
|
||||
.clip(RoundedCornerShape(14.dp))
|
||||
.background(colorResource(id = R.color.shape_primary))
|
||||
) {
|
||||
Text(
|
||||
text = nameFirstChar,
|
||||
style = MaterialTheme.typography.h3.copy(
|
||||
color = colorResource(id = R.color.text_white),
|
||||
fontSize = 14.sp
|
||||
),
|
||||
modifier = Modifier.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
} ?: kotlin.run {
|
||||
Image(
|
||||
painterResource(R.drawable.ic_account_and_data),
|
||||
contentDescription = "Option icon",
|
||||
modifier = Modifier.padding(
|
||||
start = 20.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = text,
|
||||
color = colorResource(com.anytypeio.anytype.core_ui.R.color.text_primary),
|
||||
modifier = Modifier.padding(
|
||||
start = 12.dp
|
||||
)
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1.0f, true),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
Arrow()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -26,6 +26,10 @@
|
|||
<string name="dark">Dark</string>
|
||||
<string name="system">System</string>
|
||||
|
||||
<string name="account_default_name">U</string>
|
||||
|
||||
<string name="settings">Settings</string>
|
||||
|
||||
<string name="have_you_back_up_your_recovery_phrase">Have you backed up your recovery phrase?</string>
|
||||
<string name="you_will_need_to_sign_in">You will need it to sign in. Keep it in a safe place. If you lose it, you can no longer access your account.</string>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue