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

DROID-1886 Space | Menu with remote storage and limits per account (#548)

This commit is contained in:
Konstantin Ivanov 2023-11-15 17:09:36 +01:00 committed by konstantiniiv
parent 4ec7c832c0
commit 13b7f3b504
37 changed files with 1105 additions and 184 deletions

View file

@ -55,6 +55,7 @@ object EventsDictionary {
const val appearanceScreenShow = "ScreenSettingsAppearance"
const val screenSettingsStorage = "ScreenSettingsStorageIndex"
const val screenSettingsStorageManage = "ScreenSettingsStorageManager"
const val screenSettingsSpaceStorageManager = "ScreenSettingsSpaceStorageManager"
const val screenSettingsStorageOffload = "ScreenFileOffloadWarning"
const val settingsStorageOffload = "SettingsStorageOffload"
const val screenSettingsDelete = "ScreenSettingsDelete"

View file

@ -91,6 +91,7 @@ import com.anytypeio.anytype.di.feature.sets.viewer.ViewerImagePreviewSelectModu
import com.anytypeio.anytype.di.feature.settings.DaggerAboutAppComponent
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
@ -866,6 +867,12 @@ class ComponentManager(
.build()
}
val spacesStorageComponent = Component {
DaggerSpacesStorageComponent.builder()
.withDependencies(findComponentDependencies())
.build()
}
val appearanceComponent = Component {
DaggerAppearanceComponent
.factory()

View file

@ -6,7 +6,6 @@ import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.device.BuildProvider
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.account.DeleteAccount
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
@ -17,7 +16,7 @@ import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.FileSpaceUsage
import com.anytypeio.anytype.domain.workspace.SpacesUsageInfo
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel
@ -81,9 +80,8 @@ object FilesStorageModule {
@PerScreen
fun provideSpaceUsage(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers,
spaceManager: SpaceManager
): FileSpaceUsage = FileSpaceUsage(repo, spaceManager, dispatchers)
dispatchers: AppCoroutineDispatchers
): SpacesUsageInfo = SpacesUsageInfo(repo, dispatchers)
@JvmStatic
@Provides
@ -93,14 +91,6 @@ object FilesStorageModule {
dispatchers: AppCoroutineDispatchers
) : InterceptFileLimitEvents = InterceptFileLimitEvents(channel, dispatchers)
@JvmStatic
@Provides
@PerScreen
fun provideGetAccountUseCase(
repo: AuthRepository,
dispatchers: AppCoroutineDispatchers
): GetAccount = GetAccount(repo = repo, dispatcher = dispatchers)
@JvmStatic
@Provides
@PerScreen

View file

@ -0,0 +1,107 @@
package com.anytypeio.anytype.di.feature.settings
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_utils.di.scope.PerScreen
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.domain.config.ConfigStorage
import com.anytypeio.anytype.domain.debugging.Logger
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.SpacesUsageInfo
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModelFactory
import com.anytypeio.anytype.ui.settings.SpacesStorageFragment
import dagger.Binds
import dagger.Component
import dagger.Module
import dagger.Provides
@PerScreen
@Component(
dependencies = [SpacesStorageDependencies::class],
modules = [
SpacesStorageModule::class,
SpacesStorageModule.Declarations::class
]
)
interface SpacesStorageComponent {
@Component.Builder
interface Builder {
fun withDependencies(dependency: SpacesStorageDependencies): Builder
fun build(): SpacesStorageComponent
}
fun inject(fragment: SpacesStorageFragment)
}
@Module
object SpacesStorageModule {
@JvmStatic
@Provides
@PerScreen
fun provideStoreLessSubscriptionContainer(
repo: BlockRepository,
channel: SubscriptionEventChannel,
dispatchers: AppCoroutineDispatchers,
logger: Logger
): StorelessSubscriptionContainer = StorelessSubscriptionContainer.Impl(
repo = repo,
channel = channel,
dispatchers = dispatchers,
logger = logger
)
@JvmStatic
@Provides
@PerScreen
fun provideSpaceUsage(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): SpacesUsageInfo = SpacesUsageInfo(repo, dispatchers)
@JvmStatic
@Provides
@PerScreen
fun provideFileLimitEvents(
channel: FileLimitsEventChannel,
dispatchers: AppCoroutineDispatchers
) : InterceptFileLimitEvents = InterceptFileLimitEvents(channel, dispatchers)
@JvmStatic
@Provides
@PerScreen
fun provideGetAccountUseCase(
repo: AuthRepository,
dispatchers: AppCoroutineDispatchers
): GetAccount = GetAccount(repo = repo, dispatcher = dispatchers)
@Module
interface Declarations {
@PerScreen
@Binds
fun bindViewModelFactory(factory: SpacesStorageViewModelFactory): ViewModelProvider.Factory
}
}
interface SpacesStorageDependencies : ComponentDependencies {
fun blockRepo(): BlockRepository
fun dispatchers(): AppCoroutineDispatchers
fun analytics(): Analytics
fun configStorage(): ConfigStorage
fun channel(): SubscriptionEventChannel
fun fileEventsChannel(): FileLimitsEventChannel
fun authRepo(): AuthRepository
fun logger(): Logger
fun spaceManager(): SpaceManager
}

View file

@ -38,6 +38,7 @@ 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.spaces.CreateSpaceDependencies
import com.anytypeio.anytype.di.feature.spaces.SelectSpaceDependencies
import com.anytypeio.anytype.di.feature.spaces.SpaceSettingsDependencies
@ -105,7 +106,8 @@ interface MainComponent :
SelectSpaceDependencies,
CreateSpaceDependencies,
SpaceSettingsDependencies,
CreateObjectOfTypeDependencies
CreateObjectOfTypeDependencies,
SpacesStorageDependencies
{
fun inject(app: AndroidApplication)
@ -286,4 +288,9 @@ private abstract class ComponentDependenciesModule private constructor() {
@IntoMap
@ComponentDependenciesKey(CreateObjectOfTypeDependencies::class)
abstract fun provideCreateObjectOfTypeDependencies(component: MainComponent): ComponentDependencies
@Binds
@IntoMap
@ComponentDependenciesKey(SpacesStorageDependencies::class)
abstract fun provideSpacesStorageDependencies(component: MainComponent): ComponentDependencies
}

View file

@ -16,7 +16,7 @@ import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.editor.EditorModalFragment
import com.anytypeio.anytype.ui.home.HomeScreenFragment
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
import com.anytypeio.anytype.ui.settings.RemoteStorageFragment
import com.anytypeio.anytype.ui.settings.RemoteFilesManageFragment
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_EDIT
import com.anytypeio.anytype.ui.templates.EditorTemplateFragment.Companion.TYPE_TEMPLATE_SELECT
import com.anytypeio.anytype.ui.templates.TemplateSelectFragment
@ -305,9 +305,9 @@ class Navigator : AppNavigation {
navController?.navigate(R.id.libraryFragment)
}
override fun openRemoteStorageScreen(subscription: Id) {
override fun openRemoteFilesManageScreen(subscription: Id) {
navController?.navigate(R.id.remoteStorageFragment,
bundleOf(RemoteStorageFragment.SUBSCRIPTION_KEY to subscription))
bundleOf(RemoteFilesManageFragment.SUBSCRIPTION_KEY to subscription))
}
override fun openTemplatesModal(typeId: Id) {

View file

@ -78,7 +78,7 @@ class NavigationRouter(
is AppNavigation.Command.OpenLibrary -> navigation.openLibrary()
is AppNavigation.Command.MigrationErrorScreen -> navigation.migrationErrorScreen()
is AppNavigation.Command.OpenRemoteStorageScreen -> navigation.openRemoteStorageScreen(
is AppNavigation.Command.OpenRemoteFilesManageScreen -> navigation.openRemoteFilesManageScreen(
command.subscription
)

View file

@ -12,16 +12,9 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.safeNavigate
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.intents.SystemAction
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.core_utils.ui.proceed
import com.anytypeio.anytype.di.common.componentManager
@ -30,14 +23,11 @@ import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel.Event
import com.anytypeio.anytype.ui.auth.account.DeleteAccountWarning
import com.anytypeio.anytype.ui.dashboard.ClearCacheAlertFragment
import com.anytypeio.anytype.ui_settings.fstorage.LocalStorageScreen
import com.anytypeio.anytype.ui_settings.fstorage.RemoteStorageScreen
import javax.inject.Inject
import kotlinx.coroutines.launch
class FilesStorageFragment : BaseBottomSheetComposeFragment() {
private val isRemote get() = arg<Boolean>(ARG_STORAGE_TYPE)
@Inject
lateinit var factory: FilesStorageViewModel.Factory
@ -55,19 +45,11 @@ class FilesStorageFragment : BaseBottomSheetComposeFragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
if (isRemote) {
RemoteStorageScreen(
data = vm.state.collectAsStateWithLifecycle().value,
onManageFilesClicked = { throttle { vm.event(Event.OnManageFilesClicked) } },
onGetMoreSpaceClicked = { throttle { vm.event(Event.OnGetMoreSpaceClicked) } },
)
} else {
LocalStorageScreen(
data = vm.state.collectAsStateWithLifecycle().value,
onOffloadFilesClicked = { throttle { vm.event(Event.OnOffloadFilesClicked) } },
onDeleteAccountClicked = { proceedWithAccountDeletion() }
)
}
LocalStorageScreen(
data = vm.state.collectAsStateWithLifecycle().value,
onOffloadFilesClicked = { throttle { vm.event(Event.OnOffloadFilesClicked) } },
onDeleteAccountClicked = { proceedWithAccountDeletion() }
)
}
}
}
@ -101,20 +83,6 @@ class FilesStorageFragment : BaseBottomSheetComposeFragment() {
private fun processCommands(command: FilesStorageViewModel.Command) {
when (command) {
FilesStorageViewModel.Command.OpenOffloadFilesScreen -> showClearCacheDialog()
is FilesStorageViewModel.Command.OpenRemoteStorageScreen -> openRemoteStorageScreen(
subscription = command.subscription
)
is FilesStorageViewModel.Command.SendGetMoreSpaceEmail -> {
proceedWithAction(
SystemAction.MailTo(
generateSupportMail(
account = command.account,
limit = command.limit,
name = command.name
)
)
)
}
}
}
@ -124,14 +92,6 @@ class FilesStorageFragment : BaseBottomSheetComposeFragment() {
dialog.show(childFragmentManager, null)
}
private fun openRemoteStorageScreen(subscription: String) {
findNavController().safeNavigate(
R.id.filesStorageScreen,
R.id.remoteStorageFragment,
bundleOf(RemoteStorageFragment.SUBSCRIPTION_KEY to subscription)
)
}
private fun proceedWithAccountDeletion() {
vm.proceedWithAccountDeletion()
val dialog = DeleteAccountWarning()
@ -142,18 +102,6 @@ class FilesStorageFragment : BaseBottomSheetComposeFragment() {
dialog.show(childFragmentManager, null)
}
private fun generateSupportMail(
account: Id,
name: String,
limit: String,
) : String {
val bodyString = resources.getString(R.string.mail_more_space_body, limit, account, name)
return "storage@anytype.io" +
"?subject=Get%20more%20storage,%20account%20$account" +
"&body=$bodyString"
}
override fun injectDependencies() {
componentManager().filesStorageComponent.get().inject(this)
}

View file

@ -97,11 +97,7 @@ class ProfileSettingsFragment : BaseBottomSheetComposeFragment() {
),
onDataManagementClicked = throttledClick(
onClick = {
findNavController()
.navigate(
R.id.filesStorageScreen,
FilesStorageFragment.args(isRemote = false)
)
findNavController().navigate(R.id.filesStorageScreen)
}
),
onAboutClicked = throttledClick(

View file

@ -21,10 +21,10 @@ import com.anytypeio.anytype.presentation.widgets.collection.Subscription
import com.anytypeio.anytype.presentation.widgets.collection.SubscriptionMapper
import com.anytypeio.anytype.ui.base.navigation
import com.anytypeio.anytype.ui.dashboard.DeleteAlertFragment
import com.anytypeio.anytype.ui.settings.remote.RemoteStorageScreen
import com.anytypeio.anytype.ui.settings.remote.RemoteFilesManageScreen
import javax.inject.Inject
class RemoteStorageFragment : BaseBottomSheetComposeFragment() {
class RemoteFilesManageFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var factory: CollectionViewModel.Factory
@ -47,7 +47,7 @@ class RemoteStorageFragment : BaseBottomSheetComposeFragment() {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
RemoteStorageScreen(vm = vm)
RemoteFilesManageScreen(vm = vm)
}
}
}

View file

@ -0,0 +1,137 @@
package com.anytypeio.anytype.ui.settings
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
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 androidx.navigation.fragment.findNavController
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
import com.anytypeio.anytype.core_utils.ext.safeNavigate
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.intents.SystemAction
import com.anytypeio.anytype.core_utils.intents.proceedWithAction
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.core_utils.ui.proceed
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModelFactory
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel
import com.anytypeio.anytype.ui_settings.space.SpaceStorageScreen
import javax.inject.Inject
import kotlinx.coroutines.launch
class SpacesStorageFragment : BaseBottomSheetComposeFragment() {
@Inject
lateinit var factory: SpacesStorageViewModelFactory
private val vm by viewModels<SpacesStorageViewModel> { factory }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeDialogView(
context = requireContext(),
dialog = requireDialog()
).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
MaterialTheme(typography = typography) {
SpaceStorageScreen(
data = vm.viewState.collectAsStateWithLifecycle().value,
onManageFilesClicked = { throttle { vm.event(SpacesStorageViewModel.Event.OnManageFilesClicked) } },
onGetMoreSpaceClicked = { throttle { vm.event(SpacesStorageViewModel.Event.OnGetMoreSpaceClicked) } },
)
}
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
setupBottomSheetBehavior(PADDING_TOP)
collectCommands()
}
override fun onStart() {
super.onStart()
proceed(vm.toasts) { toast(it) }
vm.onStart()
}
override fun onStop() {
vm.onStop()
super.onStop()
}
private fun collectCommands() {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
vm.commands.collect { command -> processCommands(command) }
}
}
}
private fun processCommands(command: SpacesStorageViewModel.Command) {
when (command) {
is SpacesStorageViewModel.Command.OpenRemoteFilesManageScreen -> {
openRemoteStorageScreen(
subscription = command.subscription
)
}
is SpacesStorageViewModel.Command.SendGetMoreSpaceEmail -> {
proceedWithAction(
SystemAction.MailTo(
generateSupportMail(
account = command.account,
limit = command.limit,
name = command.name
)
)
)
}
}
}
private fun openRemoteStorageScreen(subscription: String) {
findNavController().safeNavigate(
R.id.spacesStorageScreen,
R.id.remoteStorageFragment,
bundleOf(RemoteFilesManageFragment.SUBSCRIPTION_KEY to subscription)
)
}
private fun generateSupportMail(
account: Id,
name: String,
limit: String,
): String {
val bodyString = resources.getString(R.string.mail_more_space_body, limit, account, name)
return "storage@anytype.io" +
"?subject=Get%20more%20storage,%20account%20$account" +
"&body=$bodyString"
}
override fun injectDependencies() {
componentManager().spacesStorageComponent.get().inject(this)
}
override fun releaseDependencies() {
componentManager().spacesStorageComponent.release()
}
}
private const val PADDING_TOP = 54

View file

@ -53,7 +53,7 @@ import kotlinx.coroutines.launch
@ExperimentalMaterialApi
@Composable
fun RemoteStorageScreen(vm: CollectionViewModel) {
fun RemoteFilesManageScreen(vm: CollectionViewModel) {
val uiState by vm.uiState.collectAsStateWithLifecycle()
val showFileAlert by vm.openFileDeleteAlert.collectAsStateWithLifecycle()

View file

@ -48,7 +48,6 @@ import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.presentation.spaces.SpaceSettingsViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceSettingsViewModel.Command
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.ui.settings.FilesStorageFragment
import com.anytypeio.anytype.ui.settings.typography
import com.anytypeio.anytype.ui.spaces.DeleteSpaceWarning
import com.anytypeio.anytype.ui.spaces.Section
@ -96,11 +95,7 @@ class SpaceSettingsFragment : BaseBottomSheetComposeFragment() {
),
onFileStorageClick = throttledClick(
onClick = {
findNavController()
.navigate(
R.id.filesStorageScreen,
FilesStorageFragment.args(isRemote = true)
)
findNavController().navigate(R.id.spacesStorageScreen)
}
),
onPersonalizationClicked = throttledClick(

View file

@ -33,7 +33,7 @@ import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.widgets.WidgetDispatchEvent
import com.anytypeio.anytype.presentation.widgets.collection.CollectionViewModel
import com.anytypeio.anytype.ui.settings.RemoteStorageFragment
import com.anytypeio.anytype.ui.settings.RemoteFilesManageFragment
import dagger.Binds
import dagger.Component
import dagger.Module
@ -53,7 +53,7 @@ interface CollectionComponent {
}
fun inject(fragment: CollectionFragment)
fun inject(fragment: RemoteStorageFragment)
fun inject(fragment: RemoteFilesManageFragment)
}

View file

@ -165,7 +165,7 @@
<dialog
android:id="@+id/remoteStorageFragment"
android:name="com.anytypeio.anytype.ui.settings.RemoteStorageFragment"/>
android:name="com.anytypeio.anytype.ui.settings.RemoteFilesManageFragment"/>
<dialog
android:id="@+id/selectWidgetTypeScreen"
@ -237,6 +237,12 @@
android:label="Files-Storage-Screen">
</dialog>
<dialog
android:id="@+id/spacesStorageScreen"
android:name="com.anytypeio.anytype.ui.settings.SpacesStorageFragment"
android:label="Spaces-Storage-Screen">
</dialog>
<dialog
android:id="@+id/aboutAppScreen"
android:name="com.anytypeio.anytype.ui.settings.AboutAppFragment" />

View file

@ -1,7 +1,10 @@
package com.anytypeio.anytype.core_models
sealed class FileLimitsEvent {
data class SpaceUsage(val bytesUsage: Long) : FileLimitsEvent()
data class SpaceUsage(
val space: Id,
val bytesUsage: Long
) : FileLimitsEvent()
data class LocalUsage(val bytesUsage: Long) : FileLimitsEvent()
data class FileLimitReached(
val spaceId: String,

View file

@ -0,0 +1,33 @@
package com.anytypeio.anytype.core_models
data class NodeUsageInfo(
val nodeUsage: NodeUsage = NodeUsage.empty(),
val spaces: List<SpaceUsage> = emptyList()
)
data class NodeUsage(
var filesCount: Long?,
var cidsCount: Long?,
var bytesUsage: Long?,
var bytesLeft: Long?,
var bytesLimit: Long?,
var localBytesUsage: Long?
) {
companion object {
fun empty() = NodeUsage(
filesCount = null,
cidsCount = null,
bytesUsage = null,
bytesLeft = null,
bytesLimit = null,
localBytesUsage = null
)
}
}
data class SpaceUsage(
var space: Id,
var filesCount: Long,
var cidsCount: Long,
var bytesUsage: Long
)

View file

@ -135,6 +135,8 @@ sealed class ObjectWrapper {
val isValid get() = map.containsKey(Relations.ID)
val notDeletedNorArchived get() = (isDeleted != true && isArchived != true)
val targetSpaceId: String? by default
}
/**

View file

@ -147,8 +147,8 @@ inline fun <T, R> Iterable<T>.allUniqueBy(transform: (T) -> R): Boolean {
}
fun Long.readableFileSize(): String {
if (this <= 0) return "0"
val units = arrayOf("B", "kB", "MB", "GB", "TB")
if (this <= 0) return "Zero KB"
val units = arrayOf("B", "KB", "MB", "GB", "TB")
val digitGroups = (log10(this.toDouble()) / log10(1024.0)).toInt()
return DecimalFormat("#,##0.#").format(this / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
}

View file

@ -13,6 +13,7 @@ import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -873,8 +874,8 @@ class BlockDataRepository(
return remote.setQueryToSet(command)
}
override suspend fun fileSpaceUsage(space: SpaceId): FileLimits {
return remote.fileSpaceUsage(space)
override suspend fun nodeUsage(): NodeUsageInfo {
return remote.nodeUsage()
}
override suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload {

View file

@ -12,6 +12,7 @@ import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -373,7 +374,7 @@ interface BlockRemote {
suspend fun sortDataViewViewRelation(command: Command.SortRelations): Payload
suspend fun addObjectToCollection(command: Command.AddObjectToCollection): Payload
suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload
suspend fun fileSpaceUsage(space: SpaceId): FileLimits
suspend fun nodeUsage(): NodeUsageInfo
suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload

View file

@ -13,6 +13,7 @@ import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -423,7 +424,7 @@ interface BlockRepository {
suspend fun sortDataViewViewRelation(command: Command.SortRelations): Payload
suspend fun addObjectToCollection(command: Command.AddObjectToCollection): Payload
suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload
suspend fun fileSpaceUsage(space: SpaceId): FileLimits
suspend fun nodeUsage(): NodeUsageInfo
suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload
suspend fun duplicateObjectsList(ids: List<Id>): List<Id>
suspend fun createTemplateFromObject(ctx: Id): Id

View file

@ -1,18 +0,0 @@
package com.anytypeio.anytype.domain.workspace
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class FileSpaceUsage(
private val repo: BlockRepository,
private val spaceManager: SpaceManager,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<Unit, FileLimits>(dispatchers.io) {
override suspend fun doWork(params: Unit): FileLimits {
return repo.fileSpaceUsage(space = SpaceId(spaceManager.get()))
}
}

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.domain.workspace
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
class SpacesUsageInfo(
private val repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<Unit, NodeUsageInfo>(dispatchers.io) {
override suspend fun doWork(params: Unit): NodeUsageInfo {
return repo.nodeUsage()
}
}

View file

@ -13,6 +13,7 @@ import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -835,8 +836,8 @@ class BlockMiddleware(
return middleware.setQueryToSet(command)
}
override suspend fun fileSpaceUsage(space: SpaceId): FileLimits {
return middleware.fileSpaceUsage(space)
override suspend fun nodeUsage(): NodeUsageInfo {
return middleware.nodeUsage()
}
override suspend fun setInternalFlags(command: Command.SetInternalFlags): Payload {

View file

@ -19,6 +19,7 @@ class FileLimitsMiddlewareChannel(
val event = message.fileSpaceUsage
checkNotNull(event)
FileLimitsEvent.SpaceUsage(
space = event.spaceId,
bytesUsage = event.bytesUsage
)
}

View file

@ -14,9 +14,9 @@ import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVSort
import com.anytypeio.anytype.core_models.DVViewer
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
import com.anytypeio.anytype.core_models.ObjectWrapper
@ -2295,10 +2295,10 @@ class Middleware @Inject constructor(
}
@Throws(Exception::class)
fun fileSpaceUsage(space: SpaceId): FileLimits {
val request = Rpc.File.SpaceUsage.Request(spaceId = space.id)
fun nodeUsage(): NodeUsageInfo {
val request = Rpc.File.NodeUsage.Request()
if (BuildConfig.DEBUG) logRequest(request)
val response = service.spaceUsage(request)
val response = service.nodeUsageInfo(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.toCoreModel()
}

View file

@ -22,8 +22,9 @@ import com.anytypeio.anytype.core_models.DVViewerCardSize
import com.anytypeio.anytype.core_models.DVViewerRelation
import com.anytypeio.anytype.core_models.DVViewerType
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NodeUsage
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectOrder
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectTypeIds
@ -32,6 +33,7 @@ import com.anytypeio.anytype.core_models.Payload
import com.anytypeio.anytype.core_models.Relation
import com.anytypeio.anytype.core_models.RelationFormat
import com.anytypeio.anytype.core_models.RelationLink
import com.anytypeio.anytype.core_models.SpaceUsage
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
@ -737,11 +739,24 @@ fun MDVViewCardSize.toCodeModels(): DVViewerCardSize = when (this) {
MDVViewCardSize.Large -> DVViewerCardSize.LARGE
}
fun Rpc.File.SpaceUsage.Response.toCoreModel(): FileLimits {
return FileLimits(
bytesUsage = usage?.bytesUsage,
bytesLimit = usage?.bytesLimit,
localBytesUsage = usage?.localBytesUsage
fun Rpc.File.NodeUsage.Response.toCoreModel(): NodeUsageInfo {
return NodeUsageInfo(
nodeUsage = NodeUsage(
filesCount = usage?.filesCount,
cidsCount = usage?.cidsCount,
bytesUsage = usage?.bytesUsage,
bytesLeft = usage?.bytesLeft,
bytesLimit = usage?.bytesLimit,
localBytesUsage = usage?.localBytesUsage
),
spaces = spaces.map {
SpaceUsage(
space = it.spaceId,
filesCount = it.filesCount,
cidsCount = it.cidsCount,
bytesUsage = it.bytesUsage
)
}
)
}

View file

@ -473,4 +473,9 @@ interface MiddlewareService {
fun workspaceObjectListRemove(request: Rpc.Workspace.Object.ListRemove.Request): Rpc.Workspace.Object.ListRemove.Response
//endregion
//region NODE
@Throws(Exception::class)
fun nodeUsageInfo(request: Rpc.File.NodeUsage.Request): Rpc.File.NodeUsage.Response
//endregion
}

View file

@ -1732,4 +1732,17 @@ class MiddlewareServiceImplementation @Inject constructor(
return response
}
}
override fun nodeUsageInfo(request: Rpc.File.NodeUsage.Request): Rpc.File.NodeUsage.Response {
val encoded = Service.fileNodeUsage(
Rpc.File.NodeUsage.Request.ADAPTER.encode(request)
)
val response = Rpc.File.NodeUsage.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.File.NodeUsage.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
}

View file

@ -1760,6 +1760,12 @@ suspend fun Analytics.sendSettingsStorageManageEvent() {
)
}
suspend fun Analytics.sendSettingsSpaceStorageManageEvent() {
sendEvent(
eventName = EventsDictionary.screenSettingsSpaceStorageManager
)
}
suspend fun Analytics.sendSettingsOffloadEvent() {
sendEvent(
eventName = EventsDictionary.screenSettingsStorageOffload

View file

@ -57,7 +57,7 @@ interface AppNavigation {
fun exitToDesktopAndOpenPage(pageId: String)
fun exitToInvitationCodeScreen()
fun openUpdateAppScreen()
fun openRemoteStorageScreen(subscription: Id)
fun openRemoteFilesManageScreen(subscription: Id)
fun deletedAccountScreen(deadline: Long)
@ -131,7 +131,7 @@ interface AppNavigation {
object OpenLibrary: Command()
data class OpenRemoteStorageScreen(val subscription: Id) : Command()
data class OpenRemoteFilesManageScreen(val subscription: Id) : Command()
}
interface Provider {

View file

@ -6,11 +6,8 @@ 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.Account
import com.anytypeio.anytype.core_models.FileLimits
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_utils.ext.bytesToHumanReadableSizeLocal
import com.anytypeio.anytype.core_utils.ext.cancel
@ -28,26 +25,22 @@ 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.search.PROFILE_SUBSCRIPTION_ID
import com.anytypeio.anytype.domain.workspace.FileSpaceUsage
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.domain.workspace.SpacesUsageInfo
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendGetMoreSpaceEvent
import com.anytypeio.anytype.presentation.extension.sendScreenSettingsDeleteEvent
import com.anytypeio.anytype.presentation.extension.sendSettingsOffloadEvent
import com.anytypeio.anytype.presentation.extension.sendSettingsStorageEvent
import com.anytypeio.anytype.presentation.extension.sendSettingsStorageManageEvent
import com.anytypeio.anytype.presentation.extension.sendSettingsStorageOffloadEvent
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.presentation.spaces.spaceIcon
import com.anytypeio.anytype.presentation.widgets.collection.Subscription
import javax.inject.Inject
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
@ -64,10 +57,9 @@ class FilesStorageViewModel(
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider,
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val fileSpaceUsage: FileSpaceUsage,
private val spacesUsageInfo: SpacesUsageInfo,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val buildProvider: BuildProvider,
private val getAccount: GetAccount,
private val deleteAccount: DeleteAccount
) : BaseViewModel() {
@ -103,12 +95,16 @@ class FilesStorageViewModel(
private fun subscribeToFileLimits() {
jobs += viewModelScope.launch {
fileSpaceUsage
spacesUsageInfo
.stream(Unit)
.collect { result ->
result.fold(
onSuccess = {
_fileLimitsState.value = it
_fileLimitsState.value = FileLimits(
bytesUsage = it.nodeUsage.bytesUsage,
bytesLimit = it.nodeUsage.bytesLimit,
localBytesUsage = it.nodeUsage.localBytesUsage
)
},
onFailure = {
Timber.e(it, "Error while getting file space usage")
@ -191,18 +187,10 @@ class FilesStorageViewModel(
private suspend fun dispatchCommand(event: Event) {
when (event) {
Event.OnManageFilesClicked -> {
commands.emit(Command.OpenRemoteStorageScreen(Subscription.Files.id))
analytics.sendSettingsStorageManageEvent()
}
Event.OnOffloadFilesClicked -> {
commands.emit(Command.OpenOffloadFilesScreen)
analytics.sendSettingsOffloadEvent()
}
Event.OnGetMoreSpaceClicked -> {
onGetMoreSpaceClicked()
analytics.sendGetMoreSpaceEvent()
}
}
}
@ -295,30 +283,6 @@ class FilesStorageViewModel(
return percentUsage != null && percentUsage >= WARNING_PERCENT
}
private fun onGetMoreSpaceClicked() {
viewModelScope.launch {
val config = spaceManager.getConfig() ?: return@launch
val params = StoreSearchByIdsParams(
subscription = PROFILE_SUBSCRIPTION_ID,
keys = listOf(Relations.ID, Relations.NAME),
targets = listOf(config.profile)
)
combine(
getAccount.asFlow(Unit),
storelessSubscriptionContainer.subscribe(params)
) { account: Account, profileObj: List<ObjectWrapper.Basic> ->
Command.SendGetMoreSpaceEmail(
account = account.id,
name = profileObj.firstOrNull()?.name.orEmpty(),
limit = _state.value.spaceLimit
)
}
.catch { Timber.e(it, "onGetMoreSpaceClicked error") }
.flowOn(appCoroutineDispatchers.io)
.collect { commands.emit(it) }
}
}
fun proceedWithAccountDeletion() {
viewModelScope.launch {
analytics.sendScreenSettingsDeleteEvent()
@ -346,15 +310,11 @@ class FilesStorageViewModel(
}
sealed class Event {
object OnManageFilesClicked : Event()
object OnOffloadFilesClicked : Event()
object OnGetMoreSpaceClicked : Event()
}
sealed class Command {
object OpenOffloadFilesScreen : Command()
data class OpenRemoteStorageScreen(val subscription: Id) : Command()
data class SendGetMoreSpaceEmail(val account: Id, val name: String, val limit: String) : Command()
}
class Factory @Inject constructor(
@ -365,10 +325,9 @@ class FilesStorageViewModel(
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider,
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val fileSpaceUsage: FileSpaceUsage,
private val spacesUsageInfo: SpacesUsageInfo,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val buildProvider: BuildProvider,
private val getAccount: GetAccount,
private val deleteAccount: DeleteAccount
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -382,10 +341,9 @@ class FilesStorageViewModel(
urlBuilder = urlBuilder,
spaceGradientProvider = spaceGradientProvider,
appCoroutineDispatchers = appCoroutineDispatchers,
fileSpaceUsage = fileSpaceUsage,
spacesUsageInfo = spacesUsageInfo,
interceptFileLimitEvents = interceptFileLimitEvents,
buildProvider = buildProvider,
getAccount = getAccount,
deleteAccount = deleteAccount
) as T
}

View file

@ -0,0 +1,386 @@
package com.anytypeio.anytype.presentation.settings
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.core_models.Account
import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVFilterCondition
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.NodeUsageInfo
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.restrictions.SpaceStatus
import com.anytypeio.anytype.core_utils.ext.cancel
import com.anytypeio.anytype.core_utils.ext.readableFileSize
import com.anytypeio.anytype.core_utils.ext.throttleFirst
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.library.StoreSearchByIdsParams
import com.anytypeio.anytype.domain.library.StoreSearchParams
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.search.PROFILE_SUBSCRIPTION_ID
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.domain.workspace.SpacesUsageInfo
import com.anytypeio.anytype.presentation.common.BaseViewModel
import com.anytypeio.anytype.presentation.extension.sendGetMoreSpaceEvent
import com.anytypeio.anytype.presentation.extension.sendSettingsSpaceStorageManageEvent
import com.anytypeio.anytype.presentation.widgets.collection.Subscription
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
class SpacesStorageViewModel(
private val analytics: Analytics,
private val spaceManager: SpaceManager,
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val spacesUsageInfo: SpacesUsageInfo,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val getAccount: GetAccount,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer
) : BaseViewModel() {
private val _nodeUsageInfo = MutableStateFlow(NodeUsageInfo())
private val _viewState: MutableStateFlow<SpacesStorageScreenState?> = MutableStateFlow(null)
val viewState: StateFlow<SpacesStorageScreenState?> = _viewState
val events = MutableSharedFlow<Event>(replay = 0)
val commands = MutableSharedFlow<Command>(replay = 0)
private val jobs = mutableListOf<Job>()
init {
subscribeToViewEvents()
subscribeToMiddlewareEvents()
proceedWithGettingNodeUsageInfo()
}
private fun subscribeToViewEvents() {
events
.throttleFirst()
.onEach { event ->
dispatchCommand(event)
}
.launchIn(viewModelScope)
}
private suspend fun dispatchCommand(event: Event) {
when (event) {
Event.OnManageFilesClicked -> {
commands.emit(Command.OpenRemoteFilesManageScreen(Subscription.Files.id))
analytics.sendSettingsSpaceStorageManageEvent()
}
Event.OnGetMoreSpaceClicked -> {
onGetMoreSpaceClicked()
analytics.sendGetMoreSpaceEvent()
}
}
}
fun onStart() {
subscribeToSpaces()
}
fun onStop() {
viewModelScope.launch {
storelessSubscriptionContainer.unsubscribe(listOf(SPACES_STORAGE_SUBSCRIPTION_ID))
}
jobs.cancel()
}
private fun subscribeToSpaces() {
jobs += viewModelScope.launch {
val subscribeParams = createStoreSearchParams()
combine(
_nodeUsageInfo,
storelessSubscriptionContainer.subscribe(subscribeParams)
) { nodeUsageInfo, spaces ->
createSpacesStorageScreenState(nodeUsageInfo, spaces)
}
.flowOn(appCoroutineDispatchers.io)
.collect { _viewState.value = it }
}
}
private fun createStoreSearchParams(): StoreSearchParams {
return StoreSearchParams(
subscription = SPACES_STORAGE_SUBSCRIPTION_ID,
keys = listOf(
Relations.ID,
Relations.TARGET_SPACE_ID,
Relations.NAME
),
filters = createFilters()
)
}
private fun createFilters(): List<DVFilter> {
return listOf(
DVFilter(
relation = Relations.LAYOUT,
value = ObjectType.Layout.SPACE_VIEW.code.toDouble(),
condition = DVFilterCondition.EQUAL
),
DVFilter(
relation = Relations.SPACE_ACCOUNT_STATUS,
value = SpaceStatus.SPACE_DELETED.code.toDouble(),
condition = DVFilterCondition.NOT_EQUAL
),
DVFilter(
relation = Relations.SPACE_LOCAL_STATUS,
value = SpaceStatus.OK.code.toDouble(),
condition = DVFilterCondition.EQUAL
)
)
}
private suspend fun createSpacesStorageScreenState(
nodeUsageInfo: NodeUsageInfo,
spaces: List<ObjectWrapper.Basic>
): SpacesStorageScreenState {
val bytesUsage = nodeUsageInfo.nodeUsage.bytesUsage
val bytesLimit = nodeUsageInfo.nodeUsage.bytesLimit
val localUsage = nodeUsageInfo.nodeUsage.localBytesUsage
val percentUsage = calculatePercentUsage(bytesUsage, bytesLimit)
val isShowGetMoreSpace = isNeedToShowGetMoreSpace(percentUsage, localUsage, bytesLimit)
val isShowSpaceUsedWarning = isShowSpaceUsedWarning(percentUsage)
val activeSpaceId = spaceManager.get()
val activeSpace = spaces.firstOrNull { it.targetSpaceId == activeSpaceId }
val segmentLegendItems = getSegmentLegendItems(nodeUsageInfo, activeSpace)
val segmentLineItems = getSegmentLineItems(nodeUsageInfo, activeSpace, spaces)
return SpacesStorageScreenState(
spaceLimit = bytesLimit?.readableFileSize().orEmpty(),
spaceUsage = bytesUsage?.readableFileSize().orEmpty(),
isShowSpaceUsedWarning = isShowSpaceUsedWarning,
isShowGetMoreSpace = isShowGetMoreSpace,
segmentLegendItems = segmentLegendItems,
segmentLineItems = segmentLineItems
)
}
private fun calculatePercentUsage(bytesUsage: Long?, bytesLimit: Long?): Float? {
return if (bytesUsage != null && bytesLimit != null && bytesLimit != 0L) {
(bytesUsage.toFloat() / bytesLimit.toFloat())
} else {
null
}
}
private fun proceedWithGettingNodeUsageInfo() {
viewModelScope.launch {
spacesUsageInfo.async(Unit).fold(
onSuccess = { nodeUsageInfo -> _nodeUsageInfo.value = nodeUsageInfo },
onFailure = { Timber.e(it, "Error while getting file space usage") }
)
}
}
private fun subscribeToMiddlewareEvents() {
jobs += viewModelScope.launch {
interceptFileLimitEvents.run(Unit)
.onEach { events ->
val currentState = _nodeUsageInfo.value
val newState = currentState.updateState(events)
_nodeUsageInfo.value = newState
}
.collect()
}
}
private fun NodeUsageInfo.updateState(events: List<FileLimitsEvent>): NodeUsageInfo {
return events.fold(this) { currentState, event ->
when (event) {
is FileLimitsEvent.LocalUsage -> currentState.copy(
nodeUsage = currentState.nodeUsage.copy(
localBytesUsage = event.bytesUsage
)
)
is FileLimitsEvent.SpaceUsage -> {
val spaceIndex = currentState.spaces.indexOfFirst { it.space == event.space }
if (spaceIndex != -1) {
currentState.copy(
spaces = currentState.spaces.toMutableList().apply {
set(
spaceIndex,
currentState.spaces[spaceIndex].copy(bytesUsage = event.bytesUsage)
)
}
)
} else {
currentState
}
}
else -> currentState
}
}
}
fun event(event: Event) {
Timber.d("Event : [$event]")
viewModelScope.launch { events.emit(event) }
}
private fun isNeedToShowGetMoreSpace(
percentUsage: Float?,
localUsage: Long?,
bytesLimit: Long?
): Boolean {
val localPercentUsage =
if (localUsage != null && bytesLimit != null && bytesLimit != 0L) {
(localUsage.toFloat() / bytesLimit.toFloat())
} else {
null
}
return (percentUsage != null && percentUsage >= FilesStorageViewModel.WARNING_PERCENT)
|| (localPercentUsage != null && localPercentUsage >= FilesStorageViewModel.WARNING_PERCENT)
}
private fun isShowSpaceUsedWarning(
percentUsage: Float?
): Boolean {
return percentUsage != null && percentUsage >= FilesStorageViewModel.WARNING_PERCENT
}
private fun onGetMoreSpaceClicked() {
viewModelScope.launch {
val config = spaceManager.getConfig() ?: return@launch
val params = StoreSearchByIdsParams(
subscription = PROFILE_SUBSCRIPTION_ID,
keys = listOf(Relations.ID, Relations.NAME),
targets = listOf(config.profile)
)
combine(
getAccount.asFlow(Unit),
storelessSubscriptionContainer.subscribe(params)
) { account: Account, profileObj: List<ObjectWrapper.Basic> ->
Command.SendGetMoreSpaceEmail(
account = account.id,
name = profileObj.firstOrNull()?.name.orEmpty(),
limit = _viewState.value?.spaceLimit.orEmpty()
)
}
.catch { Timber.e(it, "onGetMoreSpaceClicked error") }
.flowOn(appCoroutineDispatchers.io)
.collect { commands.emit(it) }
}
}
private suspend fun getSegmentLegendItems(
nodeUsageInfo: NodeUsageInfo,
activeSpace: ObjectWrapper.Basic?
): List<SegmentLegendItem> {
val result = mutableListOf<SegmentLegendItem>()
val currentSpace = nodeUsageInfo.spaces.firstOrNull { it.space == spaceManager.get() }
val otherSpaces = nodeUsageInfo.spaces.filter { it.space != spaceManager.get() }
var otherSpacesUsages = 0L
otherSpaces.forEach { spaceUsage ->
otherSpacesUsages += spaceUsage.bytesUsage
}
result.add(
SegmentLegendItem.Active(
name = activeSpace?.name.orEmpty(),
usage = currentSpace?.bytesUsage?.readableFileSize().orEmpty(),
)
)
result.add(
SegmentLegendItem.Other(
legend = otherSpacesUsages.readableFileSize()
)
)
val freeSpace = nodeUsageInfo.nodeUsage.bytesLeft?.readableFileSize()
result.add(
SegmentLegendItem.Free(
legend = freeSpace.orEmpty()
)
)
return result
}
private fun getSegmentLineItems(
nodeUsageInfo: NodeUsageInfo,
activeSpace: ObjectWrapper.Basic?,
allSpaces: List<ObjectWrapper.Basic> = emptyList()
): List<SegmentLineItem> {
val result = mutableListOf<SegmentLineItem>()
val bytesLimit = nodeUsageInfo.nodeUsage.bytesLimit?.toFloat()
if (activeSpace == null || bytesLimit == null || bytesLimit == 0F) {
Timber.e("SpacesStorage, Space Id or Node bytesLimit is null or 0")
return result
}
val nodeSpaces = nodeUsageInfo.spaces
val items = allSpaces.map { s ->
val space = nodeSpaces.firstOrNull { it.space == s.targetSpaceId }
if (space == null) {
SegmentLineItem.Other(0F)
} else {
val value = space.bytesUsage.toFloat() / bytesLimit
if (space.space == activeSpace.targetSpaceId) {
SegmentLineItem.Active(value)
} else {
SegmentLineItem.Other(value)
}
}
}.sortedByDescending { it is SegmentLineItem.Active }
return buildList {
addAll(items)
val freeSpacesLeft = nodeUsageInfo.nodeUsage.bytesLeft?.toFloat()?.div(bytesLimit) ?: 0F
add(
SegmentLineItem.Free(freeSpacesLeft)
)
}
}
data class SpacesStorageScreenState(
val spaceLimit: String,
val spaceUsage: String,
val isShowGetMoreSpace: Boolean,
val isShowSpaceUsedWarning: Boolean,
val segmentLegendItems: List<SegmentLegendItem> = emptyList(),
val segmentLineItems: List<SegmentLineItem> = emptyList()
)
sealed class SegmentLegendItem {
data class Active(val name: String, val usage: String) : SegmentLegendItem()
data class Other(val legend: String) : SegmentLegendItem()
data class Free(val legend: String) : SegmentLegendItem()
}
sealed class SegmentLineItem {
abstract val value: Float
data class Active(override val value: Float) : SegmentLineItem()
data class Other(override val value: Float) : SegmentLineItem()
data class Free(override val value: Float) : SegmentLineItem()
}
sealed class Event {
object OnManageFilesClicked : Event()
object OnGetMoreSpaceClicked : Event()
}
sealed class Command {
data class OpenRemoteFilesManageScreen(val subscription: Id) : Command()
data class SendGetMoreSpaceEmail(val account: Id, val name: String, val limit: String) :
Command()
}
companion object {
private const val SPACES_STORAGE_SUBSCRIPTION_ID = "spaces_storage_view_model_subscription"
}
}

View file

@ -0,0 +1,35 @@
package com.anytypeio.anytype.presentation.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.domain.auth.interactor.GetAccount
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.library.StorelessSubscriptionContainer
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.SpaceManager
import com.anytypeio.anytype.domain.workspace.SpacesUsageInfo
import javax.inject.Inject
class SpacesStorageViewModelFactory @Inject constructor(
private val analytics: Analytics,
private val spaceManager: SpaceManager,
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val spacesUsageInfo: SpacesUsageInfo,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val storelessSubscriptionContainer: StorelessSubscriptionContainer,
private val getAccount: GetAccount,
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
modelClass: Class<T>
): T = SpacesStorageViewModel(
analytics = analytics,
spaceManager = spaceManager,
appCoroutineDispatchers = appCoroutineDispatchers,
spacesUsageInfo = spacesUsageInfo,
interceptFileLimitEvents = interceptFileLimitEvents,
storelessSubscriptionContainer = storelessSubscriptionContainer,
getAccount = getAccount,
) as T
}

View file

@ -0,0 +1,73 @@
package com.anytypeio.anytype.ui_settings.space
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.MaterialTheme
import androidx.compose.material.ripple.LocalRippleTheme
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.core_ui.views.NoRippleTheme
import com.anytypeio.anytype.presentation.settings.SpacesStorageViewModel
import com.anytypeio.anytype.ui_settings.R
@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(LocalRippleTheme provides NoRippleTheme) {
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_tertiary)
}
is SpacesStorageViewModel.SegmentLineItem.Other -> {
colorResource(id = R.color.palette_system_amber_50)
}
}
Box(
modifier = Modifier
.width(maxOf(item.value.times(oneValueWidth), 4f.dp))
.height(27.dp)
.clip(MaterialTheme.shapes.medium.copy(CornerSize(5.dp)))
.background(color)
)
Spacer(modifier = Modifier.width(2.dp))
}
}
}
}
}

View file

@ -0,0 +1,195 @@
package com.anytypeio.anytype.ui_settings.space
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.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.shape.CircleShape
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.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.BodyCalloutMedium
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.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) {
Text(
text = stringResource(id = R.string.get_more_space),
color = colorResource(R.color.palette_system_red),
style = BodyCalloutMedium,
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp)
.clickable { onGetMoreSpaceClicked() }
)
}
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_tertiary) 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*/ }) {
}
}