DROID-2173 Multispaces | Enhancement | Allow changing space gradient icon in space settings (#803)

This commit is contained in:
Evgenii Kozlov 2024-01-26 18:03:39 +01:00 committed by GitHub
parent c0a76ad36e
commit c3d6882e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 72 additions and 893 deletions

View File

@ -86,7 +86,6 @@ import com.anytypeio.anytype.di.feature.settings.DaggerAppearanceComponent
import com.anytypeio.anytype.di.feature.settings.DaggerFilesStorageComponent
import com.anytypeio.anytype.di.feature.settings.DaggerSpacesStorageComponent
import com.anytypeio.anytype.di.feature.settings.LogoutWarningModule
import com.anytypeio.anytype.di.feature.settings.MainSettingsModule
import com.anytypeio.anytype.di.feature.settings.ProfileModule
import com.anytypeio.anytype.di.feature.sharing.DaggerAddToAnytypeComponent
import com.anytypeio.anytype.di.feature.spaces.DaggerCreateSpaceComponent
@ -800,10 +799,6 @@ class ComponentManager(
main.logoutWarningComponent().module(LogoutWarningModule).build()
}
val mainSettingsComponent = Component {
main.mainSettingsComponent().module(MainSettingsModule).build()
}
val filesStorageComponent = Component {
DaggerFilesStorageComponent.builder()
.withDependencies(findComponentDependencies())

View File

@ -1,97 +0,0 @@
package com.anytypeio.anytype.di.feature.settings
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.device.share.debug.DebugSpaceDeviceFileContentSaver
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.debugging.DebugSpaceContentSaver
import com.anytypeio.anytype.domain.debugging.DebugSpaceShareDownloader
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.spaces.SetSpaceDetails
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import com.anytypeio.anytype.ui.settings.MainSettingFragment
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
@Subcomponent(
modules = [
MainSettingsModule::class,
MainSettingsModule.Bindings::class
]
)
@PerScreen
interface MainSettingsSubComponent {
@Subcomponent.Builder
interface Builder {
fun module(module: MainSettingsModule): Builder
fun build(): MainSettingsSubComponent
}
fun inject(fragment: MainSettingFragment)
}
@Module
object MainSettingsModule {
@JvmStatic
@Provides
@PerScreen
fun provideSetObjectDetails(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): SetObjectDetails = SetObjectDetails(
repo = repo,
dispatchers = dispatchers
)
@JvmStatic
@Provides
@PerScreen
fun provideSpaceGradientProvider(): SpaceGradientProvider = SpaceGradientProvider.Default
@JvmStatic
@Provides
@PerScreen
fun provideViewModelFactory(
analytics: Analytics,
storelessSubscriptionContainer: StorelessSubscriptionContainer,
urlBuilder: UrlBuilder,
spaceGradientProvider: SpaceGradientProvider,
debugSpaceShareDownloader: DebugSpaceShareDownloader,
spaceManager: SpaceManager,
setSpaceDetails: SetSpaceDetails
): MainSettingsViewModel.Factory = MainSettingsViewModel.Factory(
analytics = analytics,
storelessSubscriptionContainer = storelessSubscriptionContainer,
urlBuilder = urlBuilder,
setSpaceDetails = setSpaceDetails,
spaceGradientProvider = spaceGradientProvider,
debugSpaceShareDownloader = debugSpaceShareDownloader,
spaceManager = spaceManager
)
@Module
interface Bindings {
@PerScreen
@Binds
fun bindUriFileProvider(
defaultProvider: DefaultUriFileProvider
): UriFileProvider
@PerScreen
@Binds
fun bindSpaceDebugDeviceSharer(
saver: DebugSpaceDeviceFileContentSaver
): DebugSpaceContentSaver
}
}

View File

@ -1,6 +1,6 @@
package com.anytypeio.anytype.di.main
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.app.AndroidApplication
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.di.common.ComponentDependenciesKey
import com.anytypeio.anytype.di.feature.AppPreferencesDependencies
@ -33,7 +33,6 @@ import com.anytypeio.anytype.di.feature.settings.AboutAppDependencies
import com.anytypeio.anytype.di.feature.settings.AppearanceDependencies
import com.anytypeio.anytype.di.feature.settings.FilesStorageDependencies
import com.anytypeio.anytype.di.feature.settings.LogoutWarningSubComponent
import com.anytypeio.anytype.di.feature.settings.MainSettingsSubComponent
import com.anytypeio.anytype.di.feature.settings.ProfileSubComponent
import com.anytypeio.anytype.di.feature.settings.SpacesStorageDependencies
import com.anytypeio.anytype.di.feature.sharing.AddToAnytypeDependencies
@ -129,7 +128,6 @@ interface MainComponent :
fun keychainPhraseComponentBuilder(): KeychainPhraseSubComponent.Builder
fun personalizationSettingsComponentBuilder(): PersonalizationSettingsSubComponent.Builder
fun logoutWarningComponent(): LogoutWarningSubComponent.Builder
fun mainSettingsComponent(): MainSettingsSubComponent.Builder
//endregion
}

View File

@ -1,182 +0,0 @@
package com.anytypeio.anytype.ui.settings
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
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.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.ext.shareFile
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.presentation.settings.MainSettingsViewModel
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel.Command
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel.Event
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.ui.editor.modals.IconPickerFragmentBase.Companion.ARG_CONTEXT_ID_KEY
import com.anytypeio.anytype.ui.sets.ARG_SHOW_REMOVE_BUTTON
import com.anytypeio.anytype.ui.settings.system.SettingsActivity
import com.anytypeio.anytype.ui_settings.main.MainSettingScreen
import java.io.File
import javax.inject.Inject
import kotlinx.coroutines.launch
import timber.log.Timber
@Deprecated("To be deleted")
class MainSettingFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var factory: MainSettingsViewModel.Factory
@Inject
lateinit var featureToggles: FeatureToggles
@Inject
lateinit var uriFileProvider: UriFileProvider
private val vm by viewModels<MainSettingsViewModel> { factory }
private val onProfileClicked = {
vm.onOptionClicked(Event.OnProfileClicked)
}
private val onAboutAppClicked = {
vm.onOptionClicked(Event.OnAboutClicked)
}
private val onPersonalizationClicked = {
vm.onOptionClicked(Event.OnPersonalizationClicked)
}
private val onAppearanceClicked = {
vm.onOptionClicked(Event.OnAppearanceClicked)
}
private val onDebugClicked = {
vm.onOptionClicked(Event.OnDebugClicked)
}
private val onSpaceImageClicked = {
vm.onOptionClicked(Event.OnSpaceImageClicked)
}
private val onNameSet = { name: String ->
vm.onNameSet(name)
}
private val onFileStorageClicked = {
vm.onOptionClicked(Event.OnFilesStorageClicked)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
) = ComposeDialogView(
context = requireContext(),
dialog = requireDialog()
).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
MainSettingScreen(
workspace = vm.workspaceAndAccount.collectAsStateWithLifecycle().value,
onProfileClicked = onProfileClicked,
onAboutAppClicked = onAboutAppClicked,
onAppearanceClicked = onAppearanceClicked,
onDebugClicked = onDebugClicked,
onPersonalizationClicked = onPersonalizationClicked,
onSpaceIconClick = onSpaceImageClicked,
onNameSet = onNameSet,
onFileStorageClick = onFileStorageClicked
)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupBottomSheetBehavior(PADDING_TOP)
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect { command -> processCommands(command) }
}
}
}
private fun processCommands(command: Command) {
when (command) {
is Command.OpenAboutScreen -> {
safeNavigate(R.id.actionOpenAboutAppScreen)
}
is Command.OpenProfileScreen -> {
safeNavigate(R.id.actionOpenProfileScreen)
}
is Command.OpenAppearanceScreen -> {
safeNavigate(R.id.actionOpenAppearanceScreen)
}
is Command.OpenPersonalizationScreen -> {
safeNavigate(R.id.actionOpenPersonalizationScreen)
}
is Command.OpenDebugScreen -> {
startActivity(Intent(requireActivity(), SettingsActivity::class.java))
}
is Command.OpenSpaceImageSet -> {
safeNavigate(
R.id.actionOpenImagePickerScreen, bundleOf(
ARG_CONTEXT_ID_KEY to command.id,
ARG_SHOW_REMOVE_BUTTON to command.showRemoveButton
)
)
}
is Command.OpenFilesStorageScreen -> {
safeNavigate(R.id.actionOpenFilesStorageScreen)
}
is Command.Toast -> {
toast(
msg = command.msg,
duration = if (command.isLongDuration)
Toast.LENGTH_LONG
else
Toast.LENGTH_SHORT
)
}
is Command.ShareSpaceDebug -> {
try {
shareFile(
uriFileProvider.getUriForFile(File(command.path))
)
} catch (e: Exception) {
Timber.e(e, "Error while sharing space debug").also {
toast("Error while sharing space debug. Please try again later.")
}
}
}
}
}
override fun injectDependencies() {
componentManager().mainSettingsComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().mainSettingsComponent.release()
}
}
private const val PADDING_TOP = 54

View File

@ -75,9 +75,13 @@ class SpaceSettingsFragment : BaseBottomSheetComposeFragment() {
).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
MaterialTheme(
typography = typography,
colors = MaterialTheme.colors.copy(
surface = colorResource(id = R.color.context_menu_background)
)
) {
SpaceSettingsScreen(
onSpaceIconClick = {},
onNameSet = vm::onNameSet,
spaceData = vm.spaceViewState.collectAsStateWithLifecycle().value,
onDeleteSpaceClicked = throttledClick(
@ -125,7 +129,8 @@ class SpaceSettingsFragment : BaseBottomSheetComposeFragment() {
successToast = context.getString(R.string.created_by_id_copied_toast_msg)
)
},
onDebugClicked = vm::onSpaceDebugClicked
onDebugClicked = vm::onSpaceDebugClicked,
onRandomGradientClicked = vm::onRandomSpaceGradientClicked
)
LaunchedEffect(Unit) { vm.toasts.collect { toast(it) } }
LaunchedEffect(Unit) {
@ -176,7 +181,6 @@ class SpaceSettingsFragment : BaseBottomSheetComposeFragment() {
@Composable
fun SpaceSettingsScreen(
spaceData: ViewState<SpaceSettingsViewModel.SpaceData>,
onSpaceIconClick: () -> Unit,
onNameSet: (String) -> Unit,
onDeleteSpaceClicked: () -> Unit,
onFileStorageClick: () -> Unit,
@ -184,7 +188,8 @@ fun SpaceSettingsScreen(
onSpaceIdClicked: (Id) -> Unit,
onNetworkIdClicked: (Id) -> Unit,
onCreatedByClicked: (Id) -> Unit,
onDebugClicked: () -> Unit
onDebugClicked: () -> Unit,
onRandomGradientClicked: () -> Unit
) {
LazyColumn(
modifier = Modifier
@ -205,8 +210,8 @@ fun SpaceSettingsScreen(
is ViewState.Success -> spaceData.data.icon
else -> null
},
onSpaceIconClick = onSpaceIconClick,
onNameSet = onNameSet
onNameSet = onNameSet,
onRandomGradientClicked = onRandomGradientClicked
)
}
item { Divider() }

View File

@ -59,6 +59,7 @@
<string name="space">Space</string>
<string name="space_name">Space name</string>
<string name="personal_space">Personal Space</string>
<string name="space_settings_apply_random_gradient">Apply random gradient</string>
<string name="delete_space">Delete space</string>
<string name="you_can_store">You can store up to %1$s of your files on our encrypted backup node for free. If you reach the limit, files will be stored only locally.</string>
<string name="in_order_to_save">In order to save space on your local device, you can offload all your files to our encrypted backup node. The files will be loaded back when you open them.</string>

View File

@ -377,4 +377,6 @@ class FilesStorageViewModel(
companion object {
const val WARNING_PERCENT = 0.9f
}
}
}
const val SPACE_STORAGE_SUBSCRIPTION_ID = "settings_space_storage_subscription"

View File

@ -1,301 +0,0 @@
package com.anytypeio.anytype.presentation.settings
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.Filepath
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_utils.ext.throttleFirst
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.debugging.DebugSpaceShareDownloader
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.spaces.SetSpaceDetails
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.profile.ProfileIconView
import com.anytypeio.anytype.presentation.profile.profileIcon
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.spaces.spaceIcon
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import timber.log.Timber
@Deprecated("Legacy. To be deleted")
class MainSettingsViewModel(
private val analytics: Analytics,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val urlBuilder: UrlBuilder,
private val setSpaceDetails: SetSpaceDetails,
private val spaceGradientProvider: SpaceGradientProvider,
private val debugSpaceShareDownloader: DebugSpaceShareDownloader,
private val spaceManager: SpaceManager
) : ViewModel() {
val events = MutableSharedFlow<Event>(replay = 0)
val commands = MutableSharedFlow<Command>(replay = 0)
val workspaceAndAccount = spaceManager
.observe()
.flatMapLatest { config ->
storelessSubscriptionContainer.subscribe(
StoreSearchByIdsParams(
subscription = SPACE_STORAGE_SUBSCRIPTION_ID,
targets = listOf(config.spaceView, config.profile),
keys = listOf(
Relations.ID,
Relations.SPACE_ID,
Relations.NAME,
Relations.ICON_EMOJI,
Relations.ICON_IMAGE,
Relations.ICON_OPTION
)
)
).map { result ->
val workspace = result.find { it.id == config.spaceView }
if (workspace == null) Timber.w("Workspace not found")
val profile = result.find { it.id == config.profile }
if (profile == null) Timber.w("Profile not found")
WorkspaceAndAccount.Account(
space = workspace?.let {
WorkspaceAndAccount.SpaceData(
name = workspace.name.orEmpty(),
icon = workspace.spaceIcon(urlBuilder, spaceGradientProvider)
)
},
profile = profile?.let {
WorkspaceAndAccount.ProfileData(
name = profile.name.orEmpty(),
icon = profile.profileIcon(urlBuilder)
)
}
)
}.catch { Timber.e(it, "Error while observing space and account") }
}.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(STOP_SUBSCRIPTION_TIMEOUT),
WorkspaceAndAccount.Idle
)
init {
events
.throttleFirst()
.onEach { event -> dispatchAnalyticEvent(event) }
.onEach { event -> dispatchCommand(event) }
.launchIn(viewModelScope)
viewModelScope.launch {
analytics.sendEvent(
eventName = EventsDictionary.screenSettingSpacesSpaceIndex
)
}
}
fun onOptionClicked(event: Event) {
viewModelScope.launch { events.emit(event) }
}
private suspend fun dispatchCommand(event: Event) {
when (event) {
Event.OnAboutClicked -> commands.emit(Command.OpenAboutScreen)
Event.OnProfileClicked -> commands.emit(Command.OpenProfileScreen)
Event.OnAppearanceClicked -> commands.emit(Command.OpenAppearanceScreen)
Event.OnPersonalizationClicked -> commands.emit(Command.OpenPersonalizationScreen)
Event.OnDebugClicked -> {
proceedWithSpaceDebug()
}
Event.OnSpaceImageClicked -> {
val config = spaceManager.getConfig()
if (config != null) {
commands.emit(
Command.OpenSpaceImageSet(
id = config.spaceView,
showRemoveButton = isShowRemoveButton()
)
)
} else {
commands.emit(Command.Toast(COULD_NOT_GET_CONFIG_ERROR))
}
}
Event.OnFilesStorageClicked -> {
commands.emit(Command.OpenFilesStorageScreen)
}
}
}
private fun isShowRemoveButton() : Boolean {
return (workspaceAndAccount.value as? WorkspaceAndAccount.Account)
?.space?.icon !is SpaceIconView.Gradient
}
private fun proceedWithSpaceDebug() {
viewModelScope.launch {
debugSpaceShareDownloader
.stream(Unit)
.collect { result ->
result.fold(
onLoading = {
commands.emit(
Command.Toast(SPACE_DEBUG_MSG, isLongDuration = true)
)
},
onSuccess = { path ->
commands.emit(
Command.ShareSpaceDebug(path)
)
}
)
}
}
}
private fun dispatchAnalyticEvent(event: Event) {
when (event) {
Event.OnAboutClicked -> {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.MENU_HELP
)
}
Event.OnProfileClicked -> {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.accountDataSettingsShow
)
}
Event.OnAppearanceClicked -> {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.appearanceScreenShow
)
}
Event.OnPersonalizationClicked -> {
viewModelScope.sendEvent(
analytics = analytics,
eventName = EventsDictionary.personalisationSettingsShow
)
}
Event.OnSpaceImageClicked -> {}
Event.OnDebugClicked -> {}
Event.OnFilesStorageClicked -> {}
}
}
override fun onCleared() {
super.onCleared()
viewModelScope.launch {
storelessSubscriptionContainer.unsubscribe(
listOf(SPACE_STORAGE_SUBSCRIPTION_ID)
)
}
}
fun onNameSet(name: String) {
Timber.d("onNameSet")
viewModelScope.launch {
val config = spaceManager.getConfig()
if (config != null) {
setSpaceDetails.async(
SetSpaceDetails.Params(
space = SpaceId(config.space),
details = mapOf(Relations.NAME to name)
)
).fold(
onFailure = {
Timber.e(it, "Error while updating object details")
},
onSuccess = {
Timber.d("Name successfully set for current space: ${config.space}")
}
)
} else {
Timber.w("Something went wrong: config is empty")
}
}
}
class Factory @Inject constructor(
private val analytics: Analytics,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val urlBuilder: UrlBuilder,
private val setSpaceDetails: SetSpaceDetails,
private val spaceGradientProvider: SpaceGradientProvider,
private val debugSpaceShareDownloader: DebugSpaceShareDownloader,
private val spaceManager: SpaceManager
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>
): T = MainSettingsViewModel(
analytics = analytics,
storelessSubscriptionContainer = storelessSubscriptionContainer,
urlBuilder = urlBuilder,
spaceGradientProvider = spaceGradientProvider,
debugSpaceShareDownloader = debugSpaceShareDownloader,
spaceManager = spaceManager,
setSpaceDetails = setSpaceDetails
) as T
}
sealed class Event {
object OnAboutClicked : Event()
object OnAppearanceClicked : Event()
object OnProfileClicked : Event()
object OnPersonalizationClicked : Event()
object OnDebugClicked : Event()
object OnSpaceImageClicked : Event()
object OnFilesStorageClicked : Event()
}
sealed class Command {
object OpenAboutScreen : Command()
object OpenAppearanceScreen : Command()
object OpenProfileScreen : Command()
object OpenPersonalizationScreen : Command()
object OpenDebugScreen : Command()
class OpenSpaceImageSet(val id: Id, val showRemoveButton: Boolean) : Command()
object OpenFilesStorageScreen : Command()
data class Toast(val msg: String, val isLongDuration: Boolean = false) : Command()
data class ShareSpaceDebug(val path: Filepath): Command()
}
sealed class WorkspaceAndAccount {
object Idle : WorkspaceAndAccount()
class Account(
val space: SpaceData?,
val profile: ProfileData?
) : WorkspaceAndAccount()
data class SpaceData(
val name: String,
val icon: SpaceIconView
)
data class ProfileData(
val name: String,
val icon: ProfileIconView,
)
}
companion object {
const val SPACE_DEBUG_MSG = "Kindly share this debug logs with Anytype developers."
const val COULD_NOT_GET_CONFIG_ERROR = "Could not get config. Please, try again later."
}
}
const val SPACE_STORAGE_SUBSCRIPTION_ID = "settings_space_storage_subscription"
const val STOP_SUBSCRIPTION_TIMEOUT = 1_000L

View File

@ -46,7 +46,8 @@ class SpaceSettingsViewModel(
private val urlBuilder: UrlBuilder,
private val deleteSpace: DeleteSpace,
private val configStorage: ConfigStorage,
private val debugSpaceShareDownloader: DebugSpaceShareDownloader
private val debugSpaceShareDownloader: DebugSpaceShareDownloader,
private val spaceGradientProvider: SpaceGradientProvider
): BaseViewModel() {
val commands = MutableSharedFlow<Command>()
@ -165,6 +166,22 @@ class SpaceSettingsViewModel(
proceedWithSpaceDebug()
}
fun onRandomSpaceGradientClicked() {
viewModelScope.launch {
val config = spaceConfig
if (config != null) {
setSpaceDetails.async(
SetSpaceDetails.Params(
space = SpaceId(config.space),
details = mapOf(
Relations.ICON_OPTION to spaceGradientProvider.randomId().toDouble()
)
)
)
}
}
}
fun onDeleteSpaceClicked() {
viewModelScope.launch {
analytics.sendEvent(
@ -274,7 +291,8 @@ class SpaceSettingsViewModel(
private val spaceManager: SpaceManager,
private val deleteSpace: DeleteSpace,
private val configStorage: ConfigStorage,
private val debugFileShareDownloader: DebugSpaceShareDownloader
private val debugFileShareDownloader: DebugSpaceShareDownloader,
private val spaceGradientProvider: SpaceGradientProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
@ -288,7 +306,8 @@ class SpaceSettingsViewModel(
analytics = analytics,
deleteSpace = deleteSpace,
configStorage = configStorage,
debugSpaceShareDownloader = debugFileShareDownloader
debugSpaceShareDownloader = debugFileShareDownloader,
spaceGradientProvider = spaceGradientProvider
) as T
}

View File

@ -1,179 +1,42 @@
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.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
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.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.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.graphics.toColorInt
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.views.BodyRegular
import com.anytypeio.anytype.presentation.profile.ProfileIconView
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.ui_settings.R
@Deprecated("To be deleted")
@Composable
fun MainSettingScreen(
workspace: MainSettingsViewModel.WorkspaceAndAccount,
onSpaceIconClick: () -> Unit,
onProfileClicked: () -> Unit,
onAboutAppClicked: () -> Unit,
onDebugClicked: () -> Unit,
onPersonalizationClicked: () -> Unit,
onAppearanceClicked: () -> Unit,
onNameSet: (String) -> Unit,
onFileStorageClick: () -> Unit
) {
Column(Modifier.fillMaxSize()) {
SpaceHeader(
modifier = Modifier.align(Alignment.CenterHorizontally),
workspace = workspace,
onSpaceIconClick = onSpaceIconClick,
onNameSet = onNameSet
)
Spacer(
modifier = Modifier
.height(10.dp)
.padding(top = 4.dp)
)
Divider()
Spacer(modifier = Modifier.height(26.dp))
Settings(
onProfileClicked = onProfileClicked,
onPersonalizationClicked = onPersonalizationClicked,
onAppearanceClicked = onAppearanceClicked,
onAboutAppClicked = onAboutAppClicked,
onDebugClicked = onDebugClicked,
accountData = workspace,
onFileStorageClick = onFileStorageClick
)
Box(modifier = Modifier.height(16.dp))
}
}
@Composable
private fun Settings(
onProfileClicked: () -> Unit,
onPersonalizationClicked: () -> Unit,
onAppearanceClicked: () -> Unit,
onAboutAppClicked: () -> Unit,
onDebugClicked: () -> Unit,
accountData: MainSettingsViewModel.WorkspaceAndAccount,
onFileStorageClick: () -> Unit
) {
Section(
modifier = Modifier.padding(start = 20.dp, bottom = 4.dp),
title = stringResource(id = R.string.settings)
)
ProfileOption(
data = (accountData as? MainSettingsViewModel.WorkspaceAndAccount.Account)?.profile,
text = stringResource(R.string.profile),
onClick = onProfileClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_personalization,
text = stringResource(R.string.personalization),
onClick = onPersonalizationClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_appearance,
text = stringResource(R.string.appearance),
onClick = onAppearanceClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_file_storage,
text = stringResource(R.string.file_storage),
onClick = onFileStorageClick
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_debug,
text = stringResource(R.string.space_debug),
onClick = onDebugClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_about,
text = stringResource(R.string.about),
onClick = onAboutAppClicked
)
Divider(paddingStart = 60.dp)
}
@Composable
fun SpaceHeader(
modifier: Modifier = Modifier,
workspace: MainSettingsViewModel.WorkspaceAndAccount,
onSpaceIconClick: () -> Unit,
onNameSet: (String) -> Unit
) {
when (workspace) {
is MainSettingsViewModel.WorkspaceAndAccount.Account -> {
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)) {
workspace.space?.icon?.let {
SpaceImageBlock(
icon = it,
onSpaceIconClick = onSpaceIconClick
)
}
}
workspace.space?.name?.let {
// SpaceNameBlock(
// name = it,
// onNameSet = onNameSet
// )
}
}
is MainSettingsViewModel.WorkspaceAndAccount.Idle -> {}
}
}
@Composable
fun SpaceHeader(
name: String?,
icon: SpaceIconView?,
modifier: Modifier = Modifier,
onSpaceIconClick: () -> Unit,
onNameSet: (String) -> Unit
onNameSet: (String) -> Unit,
onRandomGradientClicked: () -> Unit
) {
val isSpaceIconMenuExpanded = remember {
mutableStateOf(false)
}
Box(modifier = modifier.padding(vertical = 6.dp)) {
Dragger()
}
@ -184,9 +47,31 @@ fun SpaceHeader(
if (icon != null) {
SpaceImageBlock(
icon = icon,
onSpaceIconClick = onSpaceIconClick,
onSpaceIconClick = {
isSpaceIconMenuExpanded.value = !isSpaceIconMenuExpanded.value
},
gradientCornerRadius = 4.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 (name != null) {
@ -198,87 +83,6 @@ fun SpaceHeader(
}
}
@Composable
fun ProfileOption(
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(R.color.text_primary),
modifier = Modifier.padding(
start = 12.dp
),
style = BodyRegular
)
Box(
modifier = Modifier.weight(1.0f, true),
contentAlignment = Alignment.CenterEnd
) {
Arrow()
}
}
}
@Composable
fun GradientComposeView(
modifier: Modifier,

View File

@ -1,65 +0,0 @@
package com.anytypeio.anytype.ui_settings.space
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.anytypeio.anytype.core_ui.foundation.Divider
import com.anytypeio.anytype.core_ui.foundation.Option
import com.anytypeio.anytype.presentation.settings.MainSettingsViewModel
import com.anytypeio.anytype.ui_settings.R
import com.anytypeio.anytype.ui_settings.main.ProfileOption
import com.anytypeio.anytype.ui_settings.main.Section
@Composable
private fun Settings(
onProfileClicked: () -> Unit,
onPersonalizationClicked: () -> Unit,
onAppearanceClicked: () -> Unit,
onAboutAppClicked: () -> Unit,
onDebugClicked: () -> Unit,
accountData: MainSettingsViewModel.WorkspaceAndAccount,
onFileStorageClick: () -> Unit
) {
Section(
modifier = Modifier.padding(start = 20.dp, bottom = 4.dp),
title = stringResource(id = R.string.space_settings)
)
ProfileOption(
data = (accountData as? MainSettingsViewModel.WorkspaceAndAccount.Account)?.profile,
text = stringResource(R.string.profile),
onClick = onProfileClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_personalization,
text = stringResource(R.string.personalization),
onClick = onPersonalizationClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_appearance,
text = stringResource(R.string.appearance),
onClick = onAppearanceClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_file_storage,
text = stringResource(R.string.file_storage),
onClick = onFileStorageClick
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_debug,
text = stringResource(R.string.space_debug),
onClick = onDebugClicked
)
Divider(paddingStart = 60.dp)
Option(
image = R.drawable.ic_about,
text = stringResource(R.string.about),
onClick = onAboutAppClicked
)
Divider(paddingStart = 60.dp)
}