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:
parent
4ec7c832c0
commit
13b7f3b504
37 changed files with 1105 additions and 184 deletions
|
@ -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"
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()))
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -19,6 +19,7 @@ class FileLimitsMiddlewareChannel(
|
|||
val event = message.fileSpaceUsage
|
||||
checkNotNull(event)
|
||||
FileLimitsEvent.SpaceUsage(
|
||||
space = event.spaceId,
|
||||
bytesUsage = event.bytesUsage
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1760,6 +1760,12 @@ suspend fun Analytics.sendSettingsStorageManageEvent() {
|
|||
)
|
||||
}
|
||||
|
||||
suspend fun Analytics.sendSettingsSpaceStorageManageEvent() {
|
||||
sendEvent(
|
||||
eventName = EventsDictionary.screenSettingsSpaceStorageManager
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun Analytics.sendSettingsOffloadEvent() {
|
||||
sendEvent(
|
||||
eventName = EventsDictionary.screenSettingsStorageOffload
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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*/ }) {
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue