diff --git a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt index a9b080be01..30c5c524a0 100644 --- a/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt +++ b/analytics/src/main/java/com/anytypeio/anytype/analytics/base/EventsDictionary.kt @@ -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" diff --git a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt index 0af94121c4..e0d65499cd 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/common/ComponentManager.kt @@ -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() diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/FilesStorageDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/FilesStorageDI.kt index 30db004d74..93b4381971 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/FilesStorageDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/FilesStorageDI.kt @@ -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 diff --git a/app/src/main/java/com/anytypeio/anytype/di/feature/settings/SpacesStorageDI.kt b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/SpacesStorageDI.kt new file mode 100644 index 0000000000..df9ef72138 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/di/feature/settings/SpacesStorageDI.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt index 1a253f90e3..934c43c022 100644 --- a/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/anytypeio/anytype/di/main/MainComponent.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt index 43eca7b0cf..7523d9a7e2 100644 --- a/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt +++ b/app/src/main/java/com/anytypeio/anytype/navigation/Navigator.kt @@ -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) { diff --git a/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt index 17beccaa99..378a9eee33 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/base/NavigationRouter.kt @@ -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 ) diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/FilesStorageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/FilesStorageFragment.kt index efd7f8cc65..5a45f9a545 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/FilesStorageFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/FilesStorageFragment.kt @@ -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(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) } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt index 21ec60c3f5..6d168ca259 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/ProfileSettingsFragment.kt @@ -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( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteStorageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteFilesManageFragment.kt similarity index 95% rename from app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteStorageFragment.kt rename to app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteFilesManageFragment.kt index 7fa1e4cc30..2c1abc5af7 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteStorageFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/RemoteFilesManageFragment.kt @@ -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) } } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/SpacesStorageFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/SpacesStorageFragment.kt new file mode 100644 index 0000000000..53f636ff88 --- /dev/null +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/SpacesStorageFragment.kt @@ -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 { 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 diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteStorageScreen.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteFilesManageScreen.kt similarity index 99% rename from app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteStorageScreen.kt rename to app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteFilesManageScreen.kt index 6f6365b0fa..e86d8803b8 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteStorageScreen.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/remote/RemoteFilesManageScreen.kt @@ -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() diff --git a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt index 9ef0c0069c..99797dfecc 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/settings/space/SpaceSettingsFragment.kt @@ -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( diff --git a/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionDI.kt b/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionDI.kt index 0264b48477..62542d07bf 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionDI.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/widgets/collection/CollectionDI.kt @@ -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) } diff --git a/app/src/main/res/navigation/graph.xml b/app/src/main/res/navigation/graph.xml index 470b8c3140..59820e56f2 100644 --- a/app/src/main/res/navigation/graph.xml +++ b/app/src/main/res/navigation/graph.xml @@ -165,7 +165,7 @@ + android:name="com.anytypeio.anytype.ui.settings.RemoteFilesManageFragment"/> + + + diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/FileLimitsEvent.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/FileLimitsEvent.kt index af25a60406..355b9d54d6 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/FileLimitsEvent.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/FileLimitsEvent.kt @@ -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, diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/NodeUsageInfo.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/NodeUsageInfo.kt new file mode 100644 index 0000000000..2f8f09856e --- /dev/null +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/NodeUsageInfo.kt @@ -0,0 +1,33 @@ +package com.anytypeio.anytype.core_models + +data class NodeUsageInfo( + val nodeUsage: NodeUsage = NodeUsage.empty(), + val spaces: List = 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 +) \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt index cb44b4f383..69181c5eaa 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/ObjectWrapper.kt @@ -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 } /** diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt index 1431512c6c..5325055852 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Extensions.kt @@ -147,8 +147,8 @@ inline fun Iterable.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] } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index edc003fc52..8a7cd80085 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -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 { diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index a579a37631..53b09557e2 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -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 diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index 87b60b1cd6..92dabb7015 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -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): List suspend fun createTemplateFromObject(ctx: Id): Id diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/FileSpaceUsage.kt b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/FileSpaceUsage.kt deleted file mode 100644 index 2d7bbd0676..0000000000 --- a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/FileSpaceUsage.kt +++ /dev/null @@ -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(dispatchers.io) { - - override suspend fun doWork(params: Unit): FileLimits { - return repo.fileSpaceUsage(space = SpaceId(spaceManager.get())) - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/workspace/SpacesUsageInfo.kt b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/SpacesUsageInfo.kt new file mode 100644 index 0000000000..cc468b3909 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/workspace/SpacesUsageInfo.kt @@ -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(dispatchers.io) { + + override suspend fun doWork(params: Unit): NodeUsageInfo { + return repo.nodeUsage() + } +} \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt index 4e8d74b419..ca236a7386 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/block/BlockMiddleware.kt @@ -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 { diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/FileLimitsMiddlewareChannel.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/FileLimitsMiddlewareChannel.kt index c997fdc632..2340fc2955 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/FileLimitsMiddlewareChannel.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/FileLimitsMiddlewareChannel.kt @@ -19,6 +19,7 @@ class FileLimitsMiddlewareChannel( val event = message.fileSpaceUsage checkNotNull(event) FileLimitsEvent.SpaceUsage( + space = event.spaceId, bytesUsage = event.bytesUsage ) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index 73c38ee870..aac5b55a84 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -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() } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt index 634f53da16..b75384b60b 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/mappers/ToCoreModelMappers.kt @@ -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 + ) + } ) } diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt index fe7c8afe97..1b1da8176e 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareService.kt @@ -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 } \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt index e5566b6487..c4194207a0 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/service/MiddlewareServiceImplementation.kt @@ -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 + } + } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt index 7ba1e486f3..5d975cb7cd 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/extension/AnalyticsExt.kt @@ -1760,6 +1760,12 @@ suspend fun Analytics.sendSettingsStorageManageEvent() { ) } +suspend fun Analytics.sendSettingsSpaceStorageManageEvent() { + sendEvent( + eventName = EventsDictionary.screenSettingsSpaceStorageManager + ) +} + suspend fun Analytics.sendSettingsOffloadEvent() { sendEvent( eventName = EventsDictionary.screenSettingsStorageOffload diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt index d8616f1e3f..f64e236953 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/navigation/AppNavigation.kt @@ -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 { diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/FilesStorageViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/FilesStorageViewModel.kt index be6912c026..338030458a 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/FilesStorageViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/FilesStorageViewModel.kt @@ -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 -> - 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 } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModel.kt new file mode 100644 index 0000000000..6e3a02b6b3 --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModel.kt @@ -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 = MutableStateFlow(null) + val viewState: StateFlow = _viewState + val events = MutableSharedFlow(replay = 0) + val commands = MutableSharedFlow(replay = 0) + private val jobs = mutableListOf() + + 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 { + 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 + ): 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): 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 -> + 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 { + val result = mutableListOf() + 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 = emptyList() + ): List { + val result = mutableListOf() + 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 = emptyList(), + val segmentLineItems: List = 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" + } +} \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModelFactory.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModelFactory.kt new file mode 100644 index 0000000000..89361f4f0b --- /dev/null +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/settings/SpacesStorageViewModelFactory.kt @@ -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 create( + modelClass: Class + ): T = SpacesStorageViewModel( + analytics = analytics, + spaceManager = spaceManager, + appCoroutineDispatchers = appCoroutineDispatchers, + spacesUsageInfo = spacesUsageInfo, + interceptFileLimitEvents = interceptFileLimitEvents, + storelessSubscriptionContainer = storelessSubscriptionContainer, + getAccount = getAccount, + ) as T +} \ No newline at end of file diff --git a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SegmentLine.kt b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SegmentLine.kt new file mode 100644 index 0000000000..bfc504e6a1 --- /dev/null +++ b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SegmentLine.kt @@ -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) { + 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)) + } + } + } + } +} diff --git a/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SpaceStorageScreen.kt b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SpaceStorageScreen.kt new file mode 100644 index 0000000000..35011cc057 --- /dev/null +++ b/ui-settings/src/main/java/com/anytypeio/anytype/ui_settings/space/SpaceStorageScreen.kt @@ -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 +) { + 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*/ }) { + } +} \ No newline at end of file