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

DROID-1248 App | Enhancement | Storage limits (#3199)

* DROID-1248 mw

* DROID-1246 Settings | Enhancement | Space usage and file limits (#3177)

* DROID-1246 space usage command

* DROID-1246 space usage response

* DROID-1246 space usage useCase

* DROID-1246 space storage

* DROID-1246 use case di

* DROID-1246 filelimit core model

* DROID-1246 build provider

* DROID-1246 subscribeToSpace

* DROID-1246 file limits events

* DROID-1246 screen state

* DROID-1246 rename

* DROID-1246 pr fix

* DROID-1246 jobs added

* DROID-1246 name fix

* DROID-1247 App | Enhancement | Sync status filter (#3179)

* DROID-1247 fileSyncStatus

* DROID-1247 sync status relation

* DROID-1247 search file objects by sync status

* DROID-1248 Editor | Enhancement | Storage limits toast (#3181)

* DROID-1248 file storage, unsubscribe

* DROID-1248 show toast on file storage limit

* DROID-1248 tests

* DROID-1248 code style

* DROID-1248 fix

* DROID-1248 use case naming

* DROID-1248 rename const

* DROID-1248 jobs cancel

* DROID-1248 rename

* DROID-1248 tests

* DROID-1248 mw version

* DROID-1248 revert version
This commit is contained in:
Konstantin Ivanov 2023-05-23 15:02:53 +02:00 committed by uburoiubu
parent 9cd1008c0a
commit 16f6666ad9
No known key found for this signature in database
GPG key ID: C8FB80E0A595FBB6
37 changed files with 642 additions and 221 deletions

View file

@ -85,6 +85,8 @@ import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.editor.DocumentExternalEventReducer
@ -263,6 +265,11 @@ open class EditorTestSetup {
@Mock
lateinit var objectToCollection: ConvertObjectToCollection
@Mock
lateinit var fileLimitsEventChannel: FileLimitsEventChannel
lateinit var interceptFileLimitEvents: InterceptFileLimitEvents
val root: String = "rootId123"
val workspaceId = MockDataFactory.randomString()
@ -367,6 +374,8 @@ open class EditorTestSetup {
workspaceManager.setCurrentWorkspace(workspaceId)
}
interceptFileLimitEvents = InterceptFileLimitEvents(fileLimitsEventChannel, dispatchers)
TestEditorFragment.testViewModelFactory = EditorViewModelFactory(
openPage = openPage,
closeObject = closePage,
@ -452,7 +461,8 @@ open class EditorTestSetup {
tableDelegate = tableDelegate,
workspaceManager = workspaceManager,
getObjectTypes = getObjectTypes,
objectToCollection = objectToCollection
objectToCollection = objectToCollection,
interceptFileLimitEvents = interceptFileLimitEvents
)
}

View file

@ -94,6 +94,8 @@ import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
@ -224,6 +226,14 @@ object EditorSessionModule {
storage = storage
)
@JvmStatic
@Provides
@PerScreen
fun provideFileLimitEvents(
channel: FileLimitsEventChannel,
dispatchers: AppCoroutineDispatchers
) : InterceptFileLimitEvents = InterceptFileLimitEvents(channel, dispatchers)
@JvmStatic
@Provides
fun providePageViewModelFactory(
@ -260,7 +270,8 @@ object EditorSessionModule {
tableDelegate: EditorTableDelegate,
workspaceManager: WorkspaceManager,
getObjectTypes: GetObjectTypes,
objectToCollection: ConvertObjectToCollection
objectToCollection: ConvertObjectToCollection,
interceptFileLimitEvents: InterceptFileLimitEvents
): EditorViewModelFactory = EditorViewModelFactory(
openPage = openPage,
closeObject = closePage,
@ -295,7 +306,8 @@ object EditorSessionModule {
tableDelegate = tableDelegate,
workspaceManager = workspaceManager,
getObjectTypes = getObjectTypes,
objectToCollection = objectToCollection
objectToCollection = objectToCollection,
interceptFileLimitEvents = interceptFileLimitEvents
)

View file

@ -3,6 +3,7 @@ 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.device.BuildProvider
import com.anytypeio.anytype.di.common.ComponentDependencies
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.block.repo.BlockRepository
@ -11,6 +12,9 @@ import com.anytypeio.anytype.domain.device.ClearFileCache
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.InterceptFileLimitEvents
import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel
import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider
import com.anytypeio.anytype.ui.settings.FilesStorageFragment
@ -65,6 +69,22 @@ object FilesStorageModule {
@PerScreen
fun clearFileCache(repo: BlockRepository): ClearFileCache = ClearFileCache(repo)
@JvmStatic
@Provides
@PerScreen
fun provideSpaceUsage(
repo: BlockRepository,
dispatchers: AppCoroutineDispatchers
): FileSpaceUsage = FileSpaceUsage(repo, dispatchers)
@JvmStatic
@Provides
@PerScreen
fun provideFileLimitEvents(
channel: FileLimitsEventChannel,
dispatchers: AppCoroutineDispatchers
) : InterceptFileLimitEvents = InterceptFileLimitEvents(channel, dispatchers)
@Module
interface Declarations {
@ -81,4 +101,6 @@ interface FilesStorageDependencies : ComponentDependencies {
fun analytics(): Analytics
fun configStorage(): ConfigStorage
fun channel(): SubscriptionEventChannel
fun fileEventsChannel(): FileLimitsEventChannel
fun buildProvider(): BuildProvider
}

View file

@ -22,6 +22,8 @@ import com.anytypeio.anytype.data.auth.repo.block.BlockRemoteDataStore
import com.anytypeio.anytype.data.auth.repo.unsplash.UnsplashDataRepository
import com.anytypeio.anytype.data.auth.repo.unsplash.UnsplashRemote
import com.anytypeio.anytype.data.auth.types.DefaultObjectTypesProvider
import com.anytypeio.anytype.device.BuildProvider
import com.anytypeio.anytype.device.DefaultBuildProvider
import com.anytypeio.anytype.device.DefaultPathProvider
import com.anytypeio.anytype.domain.`object`.ObjectTypesProvider
import com.anytypeio.anytype.domain.auth.repo.AuthRepository
@ -284,6 +286,11 @@ object DataModule {
)
//endregion
@JvmStatic
@Provides
@Singleton
fun provideBuildProvider(): BuildProvider = DefaultBuildProvider()
@Module
interface Bindings {

View file

@ -4,6 +4,7 @@ import com.anytypeio.anytype.data.auth.account.AccountStatusDataChannel
import com.anytypeio.anytype.data.auth.account.AccountStatusRemoteChannel
import com.anytypeio.anytype.data.auth.event.EventDataChannel
import com.anytypeio.anytype.data.auth.event.EventRemoteChannel
import com.anytypeio.anytype.data.auth.event.FileLimitsDataChannel
import com.anytypeio.anytype.data.auth.event.SubscriptionDataChannel
import com.anytypeio.anytype.data.auth.event.SubscriptionEventRemoteChannel
import com.anytypeio.anytype.data.auth.status.ThreadStatusDataChannel
@ -12,6 +13,8 @@ import com.anytypeio.anytype.domain.account.AccountStatusChannel
import com.anytypeio.anytype.domain.event.interactor.EventChannel
import com.anytypeio.anytype.domain.search.SubscriptionEventChannel
import com.anytypeio.anytype.domain.status.ThreadStatusChannel
import com.anytypeio.anytype.data.auth.event.FileLimitsRemoteChannel
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.middleware.EventProxy
import com.anytypeio.anytype.middleware.interactor.*
import dagger.Binds
@ -110,6 +113,20 @@ object EventModule {
proxy: EventProxy
): SubscriptionEventRemoteChannel = MiddlewareSubscriptionEventChannel(events = proxy)
@JvmStatic
@Provides
@Singleton
fun provideFileLimitsRemoteChannel(
proxy: EventProxy
): FileLimitsRemoteChannel = FileLimitsMiddlewareChannel(events = proxy)
@JvmStatic
@Provides
@Singleton
fun provideFileLimitsDataChannel(
channel: FileLimitsRemoteChannel
): FileLimitsEventChannel = FileLimitsDataChannel(channel = channel)
@Module
interface Bindings {

View file

@ -17,7 +17,9 @@ import com.anytypeio.anytype.R
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.ext.toast
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.FilesStorageViewModel
import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel.Event
@ -61,6 +63,17 @@ class FilesStorageFragment : BaseBottomSheetComposeFragment() {
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) {

View file

@ -0,0 +1,22 @@
package com.anytypeio.anytype.core_models
data class FileLimits(
val filesCount: Long?,
val cidsCount: Long?,
val bytesUsage: Long?,
val bytesLeft: Long?,
val bytesLimit: Long?,
val localBytesUsage: Long?
) {
companion object {
fun empty(): FileLimits = FileLimits(
filesCount = null,
cidsCount = null,
bytesUsage = null,
bytesLeft = null,
bytesLimit = null,
localBytesUsage = null
)
}
}

View file

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

View file

@ -0,0 +1,5 @@
package com.anytypeio.anytype.core_models
enum class FileSyncStatus(val value: Int) {
UNKNOWN(0), SYNCED(1), NOT_SYNCED(2)
}

View file

@ -46,6 +46,7 @@ object Relations {
const val SOURCE_OBJECT = "sourceObject"
const val INTERNAL_FLAGS = "internalFlags"
const val SIZE_IN_BYTES = "sizeInBytes"
const val FILE_SYNC_STATUS = "fileSyncStatus"
const val PAGE_COVER = "pageCover"

View file

@ -395,7 +395,7 @@ fun getDeviceName(): String {
}
}
fun bytesToHumanReadableSize(bytes: Double): String = when {
fun bytesToHumanReadableSize(bytes: Long): String = when {
bytes >= 1 shl 30 -> "%d GB".format((bytes / (1 shl 30)).toInt())
bytes >= 1 shl 20 -> "%d MB".format((bytes / (1 shl 20)).toInt())
bytes >= 1 shl 10 -> "%d kB".format((bytes / (1 shl 10)).toInt())

View file

@ -0,0 +1,14 @@
package com.anytypeio.anytype.data.auth.event
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import kotlinx.coroutines.flow.Flow
class FileLimitsDataChannel(
private val channel: FileLimitsRemoteChannel
) : FileLimitsEventChannel {
override fun observe(): Flow<List<FileLimitsEvent>> {
return channel.observe()
}
}

View file

@ -0,0 +1,8 @@
package com.anytypeio.anytype.data.auth.event
import com.anytypeio.anytype.core_models.FileLimitsEvent
import kotlinx.coroutines.flow.Flow
interface FileLimitsRemoteChannel {
fun observe(): Flow<List<FileLimitsEvent>>
}

View file

@ -8,6 +8,7 @@ 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.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
@ -796,4 +797,8 @@ class BlockDataRepository(
override suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload {
return remote.setQueryToSet(command)
}
override suspend fun fileSpaceUsage(): FileLimits {
return remote.fileSpaceUsage()
}
}

View file

@ -8,6 +8,7 @@ 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.ObjectType
@ -348,4 +349,5 @@ interface BlockDataStore {
suspend fun sortDataViewViewRelation(command: Command.SortRelations): Payload
suspend fun addObjectToCollection(command: Command.AddObjectToCollection): Payload
suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload
suspend fun fileSpaceUsage(): FileLimits
}

View file

@ -8,6 +8,7 @@ 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.ObjectType
@ -342,4 +343,5 @@ 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(): FileLimits
}

View file

@ -8,6 +8,7 @@ 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.ObjectType
@ -727,4 +728,8 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
override suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload {
return remote.setQueryToSet(command)
}
override suspend fun fileSpaceUsage(): FileLimits {
return remote.fileSpaceUsage()
}
}

View file

@ -8,6 +8,7 @@ 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.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Key
@ -392,4 +393,5 @@ 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(): FileLimits
}

View file

@ -0,0 +1,8 @@
package com.anytypeio.anytype.domain.workspace
import com.anytypeio.anytype.core_models.FileLimitsEvent
import kotlinx.coroutines.flow.Flow
interface FileLimitsEventChannel {
fun observe() : Flow<List<FileLimitsEvent>>
}

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.domain.workspace
import com.anytypeio.anytype.core_models.FileLimits
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,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<Unit, FileLimits>(dispatchers.io) {
override suspend fun doWork(params: Unit): FileLimits {
return repo.fileSpaceUsage()
}
}

View file

@ -0,0 +1,16 @@
package com.anytypeio.anytype.domain.workspace
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import kotlinx.coroutines.flow.Flow
class InterceptFileLimitEvents(
private val fileLimitsEventChannel: FileLimitsEventChannel,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<Unit, Flow<List<FileLimitsEvent>>>(dispatchers.io) {
override suspend fun doWork(params: Unit): Flow<List<FileLimitsEvent>> {
return fileLimitsEventChannel.observe()
}
}

View file

@ -9,6 +9,7 @@ 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.ObjectType
@ -760,4 +761,8 @@ class BlockMiddleware(
override suspend fun setQueryToSet(command: Command.SetQueryToSet): Payload {
return middleware.setQueryToSet(command)
}
override suspend fun fileSpaceUsage(): FileLimits {
return middleware.fileSpaceUsage()
}
}

View file

@ -0,0 +1,44 @@
package com.anytypeio.anytype.middleware.interactor
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.data.auth.event.FileLimitsRemoteChannel
import com.anytypeio.anytype.middleware.EventProxy
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.mapNotNull
class FileLimitsMiddlewareChannel(
private val events: EventProxy
) : FileLimitsRemoteChannel {
override fun observe(): Flow<List<FileLimitsEvent>> =
events.flow()
.mapNotNull { emission ->
emission.messages.mapNotNull { message ->
when {
message.fileSpaceUsage != null -> {
val event = message.fileSpaceUsage
checkNotNull(event)
FileLimitsEvent.SpaceUsage(
bytesUsage = event.bytesUsage
)
}
message.fileLocalUsage != null -> {
val event = message.fileLocalUsage
checkNotNull(event)
FileLimitsEvent.LocalUsage(
bytesUsage = event.localBytesUsage
)
}
message.fileLimitReached!= null -> {
val event = message.fileLimitReached
checkNotNull(event)
FileLimitsEvent.FileLimitReached(
fileId = event.fileId,
spaceId = event.spaceId
)
}
else -> null
}
}
}
}

View file

@ -13,6 +13,7 @@ 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.ObjectType
@ -2120,6 +2121,15 @@ class Middleware(
return response.event.toPayload()
}
@Throws(Exception::class)
fun fileSpaceUsage(): FileLimits {
val request = Rpc.File.SpaceUsage.Request()
if (BuildConfig.DEBUG) logRequest(request)
val response = service.spaceUsage(request)
if (BuildConfig.DEBUG) logResponse(response)
return response.toCoreModel()
}
private fun logRequest(any: Any) {
logger.logRequest(any)
}

View file

@ -20,6 +20,7 @@ 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.ObjectOrder
import com.anytypeio.anytype.core_models.ObjectType
import com.anytypeio.anytype.core_models.ObjectView
@ -27,6 +28,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.Response
import com.anytypeio.anytype.core_models.restrictions.DataViewRestriction
import com.anytypeio.anytype.core_models.restrictions.DataViewRestrictions
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
@ -716,4 +718,15 @@ fun MDVViewCardSize.toCodeModels(): DVViewerCardSize = when (this) {
MDVViewCardSize.Small -> DVViewerCardSize.SMALL
MDVViewCardSize.Medium -> DVViewerCardSize.MEDIUM
MDVViewCardSize.Large -> DVViewerCardSize.LARGE
}
fun Rpc.File.SpaceUsage.Response.toCoreModel(): FileLimits {
return FileLimits(
filesCount = usage?.filesCount,
cidsCount = usage?.cidsCount,
bytesUsage = usage?.bytesUsage,
bytesLeft = usage?.bytesLeft,
bytesLimit = usage?.bytesLimit,
localBytesUsage = usage?.localBytesUsage
)
}

View file

@ -172,6 +172,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun fileDownload(request: Rpc.File.Download.Request): Rpc.File.Download.Response
@Throws(Exception::class)
fun spaceUsage(request: Rpc.File.SpaceUsage.Request): Rpc.File.SpaceUsage.Response
//endregion
//region UNSPLASH commands

View file

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

View file

@ -0,0 +1,18 @@
package com.anytypeio.anytype.device
import android.os.Build
interface BuildProvider {
fun getManufacturer(): String
fun getModel(): String
}
class DefaultBuildProvider() : BuildProvider {
override fun getManufacturer(): String {
return Build.MANUFACTURER
}
override fun getModel(): String {
return Build.MODEL
}
}

View file

@ -17,6 +17,7 @@ import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVFilterCondition
import com.anytypeio.anytype.core_models.Document
import com.anytypeio.anytype.core_models.Event
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.InternalFlags
import com.anytypeio.anytype.core_models.Key
@ -83,6 +84,7 @@ import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.common.Action
@ -274,7 +276,8 @@ class EditorViewModel(
private val featureToggles: FeatureToggles,
private val tableDelegate: EditorTableDelegate,
private val workspaceManager: WorkspaceManager,
private val getObjectTypes: GetObjectTypes
private val getObjectTypes: GetObjectTypes,
private val interceptFileLimitEvents: InterceptFileLimitEvents
) : ViewStateViewModel<ViewState>(),
PickerListener,
SupportNavigation<EventWrapper<AppNavigation.Command>>,
@ -966,6 +969,9 @@ class EditorViewModel(
.filter { it.context == context }
.collect { orchestrator.proxies.payloads.send(it) }
}
observeFileLimitsEvents()
val startTime = System.currentTimeMillis()
viewModelScope.launch {
openPage.execute(id).fold(
@ -6830,7 +6836,20 @@ class EditorViewModel(
}
}
}
//endregion
//region FILE LIMITS
private fun observeFileLimitsEvents() {
jobs += viewModelScope.launch {
interceptFileLimitEvents
.run(Unit)
.collect { event ->
if (event.any { it is FileLimitsEvent.FileLimitReached }) {
_toasts.emit("You exceeded file limit upload")
}
}
}
}
//endregion
}

View file

@ -30,6 +30,7 @@ import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
@ -75,7 +76,8 @@ open class EditorViewModelFactory(
private val tableDelegate: EditorTableDelegate,
private val workspaceManager: WorkspaceManager,
private val getObjectTypes: GetObjectTypes,
private val objectToCollection: ConvertObjectToCollection
private val objectToCollection: ConvertObjectToCollection,
private val interceptFileLimitEvents: InterceptFileLimitEvents
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
@ -83,15 +85,15 @@ open class EditorViewModelFactory(
return EditorViewModel(
openPage = openPage,
closePage = closeObject,
createBlockLinkWithObject = createBlockLinkWithObject,
createObjectAsMentionOrLink = createObjectAsMentionOrLink,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
updateLinkMarks = updateLinkMarks,
removeLinkMark = removeLinkMark,
createBlockLinkWithObject = createBlockLinkWithObject,
reducer = documentEventReducer,
urlBuilder = urlBuilder,
renderer = renderer,
createObjectAsMentionOrLink = createObjectAsMentionOrLink,
orchestrator = orchestrator,
analytics = analytics,
dispatcher = dispatcher,
@ -108,13 +110,14 @@ open class EditorViewModelFactory(
templateDelegate = editorTemplateDelegate,
createObject = createObject,
objectToSet = objectToSet,
objectToCollection = objectToCollection,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
featureToggles = featureToggles,
tableDelegate = tableDelegate,
workspaceManager = workspaceManager,
getObjectTypes = getObjectTypes,
objectToCollection = objectToCollection
interceptFileLimitEvents = interceptFileLimitEvents
) as T
}
}

View file

@ -5,6 +5,7 @@ import com.anytypeio.anytype.core_models.DVFilter
import com.anytypeio.anytype.core_models.DVFilterCondition
import com.anytypeio.anytype.core_models.DVSort
import com.anytypeio.anytype.core_models.DVSortType
import com.anytypeio.anytype.core_models.FileSyncStatus
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Marketplace.MARKETPLACE_ID
import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
@ -785,6 +786,11 @@ object ObjectSearchConstants {
relation = Relations.WORKSPACE_ID,
condition = DVFilterCondition.EQUAL,
value = workspaceId
),
DVFilter(
relation = Relations.FILE_SYNC_STATUS,
condition = DVFilterCondition.EQUAL,
value = FileSyncStatus.SYNCED.value.toDouble()
)
)
}

View file

@ -1,16 +1,18 @@
package com.anytypeio.anytype.presentation.settings
import android.os.Build
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import com.anytypeio.anytype.analytics.base.Analytics
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.analytics.base.sendEvent
import com.anytypeio.anytype.core_models.FileLimitsEvent
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_utils.ext.bytesToHumanReadableSize
import com.anytypeio.anytype.core_utils.ext.cancel
import com.anytypeio.anytype.core_utils.ext.throttleFirst
import com.anytypeio.anytype.device.BuildProvider
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.BaseUseCase
import com.anytypeio.anytype.domain.base.Interactor
@ -23,17 +25,21 @@ 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.domain.workspace.FileSpaceUsage
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
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.collect
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import timber.log.Timber
@ -45,32 +51,80 @@ class FilesStorageViewModel(
private val clearFileCache: ClearFileCache,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider,
private val appCoroutineDispatchers: AppCoroutineDispatchers
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val fileSpaceUsage: FileSpaceUsage,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val buildProvider: BuildProvider
) : ViewModel() {
val events = MutableSharedFlow<Event>(replay = 0)
val commands = MutableSharedFlow<Command>(replay = 0)
private val isClearFileCacheInProgress = MutableStateFlow(false)
private val _state = MutableStateFlow<FilesStorageScreenState>(FilesStorageScreenState.Idle)
val state: StateFlow<FilesStorageScreenState> = _state
private val _state = MutableStateFlow(ScreenState.empty())
val state: StateFlow<ScreenState> = _state
val toasts = MutableSharedFlow<String>(replay = 0)
private val profileId = configStorage.get().profile
private val workspaceId = configStorage.get().workspace
private val jobs = mutableListOf<Job>()
init {
subscribeToViewEvents()
}
fun onStart() {
subscribeToSpace()
subscribeToFileLimitEvents()
}
fun onStop() {
viewModelScope.launch {
storelessSubscriptionContainer.unsubscribe(listOf(SPACE_STORAGE_SUBSCRIPTION_ID))
}
jobs.cancel()
}
private fun subscribeToViewEvents() {
events
.throttleFirst()
.onEach { event -> dispatchAnalyticsEvent(event) }
.onEach { event -> dispatchCommand(event) }
.onEach { event ->
dispatchAnalyticsEvent(event)
dispatchCommand(event)
}
.launchIn(viewModelScope)
subscribeToSpace()
viewModelScope.launch { analytics.sendSettingsStorageEvent() }
}
private fun subscribeToFileLimitEvents() {
jobs += viewModelScope.launch {
interceptFileLimitEvents.run(Unit)
.onEach { events ->
val currentState = _state.value
val newState = currentState.updateState(events)
_state.value = newState
}
.collect()
}
}
private fun ScreenState.updateState(events: List<FileLimitsEvent>): ScreenState {
var newState = this
events.forEach { event ->
newState = when (event) {
is FileLimitsEvent.LocalUsage -> newState.copy(
localUsage = bytesToHumanReadableSize(event.bytesUsage)
)
is FileLimitsEvent.SpaceUsage -> newState.copy(
spaceUsage = bytesToHumanReadableSize(event.bytesUsage)
)
else -> newState
}
}
return newState
}
fun onClearFileCacheAccepted() {
Timber.d("onClearFileCacheAccepted")
viewModelScope.launch {
jobs += viewModelScope.launch {
analytics.sendSettingsStorageOffloadEvent()
}
viewModelScope.launch {
@ -82,7 +136,7 @@ class FilesStorageViewModel(
is Interactor.Status.Error -> {
isClearFileCacheInProgress.value = false
Timber.e(status.throwable, "Error while clearing file cache")
// TODO send toast
toasts.emit("Error while clearing the file cache")
}
Interactor.Status.Success -> {
viewModelScope.sendEvent(
@ -104,7 +158,7 @@ class FilesStorageViewModel(
private suspend fun dispatchCommand(event: Event) {
when (event) {
Event.OnManageFilesClicked -> {
commands.emit(Command.OpenRemoteStorageScreen(subscription = Subscription.Files.id))
commands.emit(Command.OpenRemoteStorageScreen(Subscription.Files.id))
analytics.sendSettingsStorageManageEvent()
}
Event.OnOffloadFilesClicked -> {
@ -132,40 +186,52 @@ class FilesStorageViewModel(
}
private fun subscribeToSpace() {
viewModelScope.launch {
storelessSubscriptionContainer.subscribe(
StoreSearchByIdsParams(
subscription = SPACE_SUBSCRIPTION_ID,
targets = listOf(workspaceId, profileId),
keys = listOf(
Relations.ID,
Relations.NAME,
Relations.ICON_EMOJI,
Relations.ICON_IMAGE,
Relations.ICON_OPTION
)
val workspaceId = configStorage.get().workspace
val profileId = configStorage.get().profile
jobs += viewModelScope.launch {
val subscribeParams = StoreSearchByIdsParams(
subscription = SPACE_STORAGE_SUBSCRIPTION_ID,
targets = listOf(workspaceId, profileId),
keys = listOf(
Relations.ID,
Relations.NAME,
Relations.ICON_EMOJI,
Relations.ICON_IMAGE,
Relations.ICON_OPTION
)
).map { result ->
)
combine(
fileSpaceUsage.asFlow(Unit),
storelessSubscriptionContainer.subscribe(subscribeParams)
) { spaceUsage, result ->
val workspace = result.find { it.id == workspaceId }
val spaceUsage = (500 * 1024 * 1024).toDouble()
val spaceLimit = (1024 * 1024 * 1024).toDouble()
val localUsage = (104 * 1024 * 1024).toDouble()
FilesStorageScreenState.SpaceData(
spaceName = workspace?.name,
val bytesUsage = spaceUsage.bytesUsage
val bytesLimit = spaceUsage.bytesLimit
val percentUsage =
if (bytesUsage != null && bytesLimit != null && bytesLimit != 0L) {
(bytesUsage.toFloat() / bytesLimit.toFloat())
} else {
null
}
ScreenState(
spaceName = workspace?.name.orEmpty(),
spaceIcon = workspace?.spaceIcon(urlBuilder, spaceGradientProvider),
spaceUsage = bytesToHumanReadableSize(spaceUsage),
percentUsage = (spaceUsage / spaceLimit).toFloat(),
spaceUsage = bytesUsage?.let { bytesToHumanReadableSize(it) }.orEmpty(),
percentUsage = percentUsage,
device = getDeviceName(),
localUsage = bytesToHumanReadableSize(localUsage),
spaceLimit = bytesToHumanReadableSize(spaceLimit)
localUsage = spaceUsage.localBytesUsage?.let { bytesToHumanReadableSize(it) }
.orEmpty(),
spaceLimit = bytesLimit?.let { bytesToHumanReadableSize(it) }.orEmpty(),
)
}.flowOn(appCoroutineDispatchers.io).collect { _state.value = it }
}
.flowOn(appCoroutineDispatchers.io)
.collect { _state.value = it }
}
}
private fun getDeviceName(): String {
val manufacturer = Build.MANUFACTURER.capitalize()
val model = Build.MODEL.capitalize()
val manufacturer = buildProvider.getManufacturer().capitalize()
val model = buildProvider.getModel().capitalize()
return if (model.startsWith(manufacturer, ignoreCase = true)) {
model
} else {
@ -190,7 +256,10 @@ class FilesStorageViewModel(
private val clearFileCache: ClearFileCache,
private val urlBuilder: UrlBuilder,
private val spaceGradientProvider: SpaceGradientProvider,
private val appCoroutineDispatchers: AppCoroutineDispatchers
private val appCoroutineDispatchers: AppCoroutineDispatchers,
private val fileSpaceUsage: FileSpaceUsage,
private val interceptFileLimitEvents: InterceptFileLimitEvents,
private val buildProvider: BuildProvider
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(
@ -198,26 +267,36 @@ class FilesStorageViewModel(
): T = FilesStorageViewModel(
analytics = analytics,
storelessSubscriptionContainer = storelessSubscriptionContainer,
clearFileCache = clearFileCache,
configStorage = configStorage,
clearFileCache = clearFileCache,
urlBuilder = urlBuilder,
spaceGradientProvider = spaceGradientProvider,
appCoroutineDispatchers = appCoroutineDispatchers
appCoroutineDispatchers = appCoroutineDispatchers,
fileSpaceUsage = fileSpaceUsage,
interceptFileLimitEvents = interceptFileLimitEvents,
buildProvider = buildProvider
) as T
}
}
sealed class FilesStorageScreenState {
object Idle : FilesStorageScreenState()
data class SpaceData(
data class ScreenState(
val spaceIcon: SpaceIconView?,
val spaceName: String?,
val spaceName: String,
val spaceLimit: String,
val spaceUsage: String,
val percentUsage: Float,
val percentUsage: Float?,
val device: String?,
val localUsage: String
): FilesStorageScreenState()
) {
companion object {
fun empty() = ScreenState(
spaceIcon = null,
spaceName = "",
spaceLimit = "",
spaceUsage = "",
percentUsage = null,
device = null,
localUsage = ""
)
}
}
}

View file

@ -48,7 +48,7 @@ class MainSettingsViewModel(
val workspaceAndAccount = storelessSubscriptionContainer.subscribe(
StoreSearchByIdsParams(
subscription = SPACE_SUBSCRIPTION_ID,
subscription = SPACE_STORAGE_SUBSCRIPTION_ID,
targets = listOf(workspaceId, profileId),
keys = listOf(
Relations.ID,
@ -151,7 +151,7 @@ class MainSettingsViewModel(
super.onCleared()
viewModelScope.launch {
storelessSubscriptionContainer.unsubscribe(
listOf(SPACE_SUBSCRIPTION_ID)
listOf(SPACE_STORAGE_SUBSCRIPTION_ID)
)
}
}
@ -240,5 +240,5 @@ class MainSettingsViewModel(
}
const val SPACE_SUBSCRIPTION_ID = "settings_space_subscription"
const val SPACE_STORAGE_SUBSCRIPTION_ID = "settings_space_storage_subscription"
const val STOP_SUBSCRIPTION_TIMEOUT = 1_000L

View file

@ -59,7 +59,7 @@ sealed class Subscription(
object Files : Subscription(
id = Subscriptions.SUBSCRIPTION_FILES,
keys = SUBSCRIPTION_DEFAULT_KEYS + Relations.SIZE_IN_BYTES + Relations.FILE_MIME_TYPE + Relations.FILE_EXT,
keys = SUBSCRIPTION_DEFAULT_KEYS + Relations.SIZE_IN_BYTES + Relations.FILE_MIME_TYPE + Relations.FILE_EXT + Relations.FILE_SYNC_STATUS,
sorts = listOf(
DVSort(
relationKey = Relations.SIZE_IN_BYTES,

View file

@ -86,6 +86,8 @@ import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.BuildConfig
import com.anytypeio.anytype.presentation.MockBlockFactory
@ -337,6 +339,10 @@ open class EditorViewModelTest {
private lateinit var updateDetail: UpdateDetail
@Mock
lateinit var fileLimitsEventChannel: FileLimitsEventChannel
lateinit var interceptFileLimitEvents: InterceptFileLimitEvents
lateinit var vm: EditorViewModel
private lateinit var builder: UrlBuilder
@ -3890,6 +3896,7 @@ open class EditorViewModelTest {
downloadUnsplashImage = DownloadUnsplashImage(unsplashRepo)
clearBlockContent = ClearBlockContent(repo)
clearBlockStyle = ClearBlockStyle(repo)
interceptFileLimitEvents = InterceptFileLimitEvents(fileLimitsEventChannel, dispatchers)
workspaceManager = WorkspaceManager.DefaultWorkspaceManager()
runBlocking {
@ -3900,7 +3907,7 @@ open class EditorViewModelTest {
vm = EditorViewModel(
openPage = openPage,
closePage = closePage,
createObject = createObject,
createBlockLinkWithObject = createBlockLinkWithObject,
createObjectAsMentionOrLink = createObjectAsMentionOrLink,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
@ -3972,15 +3979,16 @@ open class EditorViewModelTest {
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
templateDelegate = editorTemplateDelegate,
createBlockLinkWithObject = createBlockLinkWithObject,
featureToggles = mock(),
createObject = createObject,
objectToSet = objectToSet,
objectToCollection = convertObjectToCollection,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
featureToggles = mock(),
tableDelegate = tableDelegate,
workspaceManager = workspaceManager,
getObjectTypes = getObjectTypes,
objectToCollection = convertObjectToCollection
interceptFileLimitEvents = interceptFileLimitEvents
)
}

View file

@ -84,6 +84,8 @@ import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.domain.workspace.FileLimitsEventChannel
import com.anytypeio.anytype.domain.workspace.InterceptFileLimitEvents
import com.anytypeio.anytype.domain.workspace.WorkspaceManager
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
@ -336,6 +338,10 @@ open class EditorPresentationTestSetup {
@Mock
lateinit var getObjectTypes: GetObjectTypes
@Mock
lateinit var fileLimitsEventChannel: FileLimitsEventChannel
lateinit var interceptFileLimitEvents: InterceptFileLimitEvents
open fun buildViewModel(urlBuilder: UrlBuilder = builder): EditorViewModel {
val storage = Editor.Storage()
@ -414,6 +420,7 @@ open class EditorPresentationTestSetup {
workspaceManager.setCurrentWorkspace(workspaceId)
}
interceptFileLimitEvents = InterceptFileLimitEvents(fileLimitsEventChannel, dispatchers)
return EditorViewModel(
openPage = openPage,
closePage = closePage,
@ -447,14 +454,15 @@ open class EditorPresentationTestSetup {
setDocImageIcon = setDocImageIcon,
templateDelegate = editorTemplateDelegate,
createObject = createObject,
featureToggles = mock(),
objectToSet = objectToSet,
objectToCollection = convertObjectToCollection,
storeOfRelations = storeOfRelations,
storeOfObjectTypes = storeOfObjectTypes,
featureToggles = mock(),
tableDelegate = tableDelegate,
workspaceManager = workspaceManager,
getObjectTypes = getObjectTypes,
objectToCollection = convertObjectToCollection
interceptFileLimitEvents = interceptFileLimitEvents
)
}

View file

@ -36,7 +36,7 @@ import com.anytypeio.anytype.core_ui.views.ButtonSize
import com.anytypeio.anytype.core_ui.views.PreviewTitle2Medium
import com.anytypeio.anytype.core_ui.views.Relations3
import com.anytypeio.anytype.core_ui.views.Title1
import com.anytypeio.anytype.presentation.settings.FilesStorageScreenState
import com.anytypeio.anytype.presentation.settings.FilesStorageViewModel.ScreenState
import com.anytypeio.anytype.presentation.spaces.SpaceIconView
import com.anytypeio.anytype.ui_settings.R
import com.anytypeio.anytype.ui_settings.fstorage.MockFileStorage.mockData
@ -44,170 +44,165 @@ import com.anytypeio.anytype.ui_settings.main.SpaceImageBlock
@Composable
fun FilesStorageScreen(
data: FilesStorageScreenState,
data: ScreenState,
onOffloadFilesClicked: () -> Unit,
onManageFilesClicked: () -> Unit
) {
when (data) {
FilesStorageScreenState.Idle -> {
}
is FilesStorageScreenState.SpaceData -> {
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(16.dp),
backgroundColor = colorResource(id = R.color.background_primary)
Card(
modifier = Modifier.fillMaxSize(),
shape = RoundedCornerShape(16.dp),
backgroundColor = colorResource(id = R.color.background_primary)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp),
) {
Box(
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Dragger()
}
Header()
Spacer(
modifier = Modifier.height(36.dp)
)
Text(
text = stringResource(id = R.string.remote_storage),
style = Title1,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.you_can_store, data.spaceLimit),
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
color = colorResource(R.color.text_primary),
style = BodyCalloutRegular
)
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier
.height(48.dp)
) {
SpaceImageBlock(
icon = data.spaceIcon,
onSpaceIconClick = {},
mainSize = 48.dp,
emojiSize = 24.dp,
gradientSize = 36.dp
)
Column(
modifier = Modifier
.fillMaxSize()
.padding(start = 20.dp, end = 20.dp),
.fillMaxWidth()
.fillMaxHeight()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Box(
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Dragger()
}
Header()
Spacer(
modifier = Modifier.height(36.dp)
)
Text(
text = stringResource(id = R.string.remote_storage),
style = Title1,
text = data.spaceName.orEmpty(),
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.you_can_store, data.spaceLimit),
modifier = Modifier
.fillMaxWidth()
.padding(top = 4.dp),
color = colorResource(R.color.text_primary),
style = BodyCalloutRegular
)
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier
.height(48.dp)
) {
SpaceImageBlock(
icon = data.spaceIcon,
onSpaceIconClick = {},
mainSize = 48.dp,
emojiSize = 24.dp,
gradientSize = 36.dp
)
Column(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = data.spaceName.orEmpty(),
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(
id = R.string.space_usage,
data.spaceUsage,
data.spaceLimit
),
style = Relations3,
color = if (data.percentUsage >= 0.9F) {
colorResource(id = R.color.palette_system_red)
} else {
colorResource(id = R.color.text_secondary)
},
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(20.dp))
DefaultLinearProgressIndicator(progress = data.percentUsage)
Spacer(modifier = Modifier.height(20.dp))
ButtonSecondary(
text = stringResource(id = R.string.manage_files),
onClick = onManageFilesClicked,
size = ButtonSize.XSmall.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
Spacer(modifier = Modifier.height(44.dp))
Text(
text = stringResource(id = R.string.local_storage),
style = Title1,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.in_order_to_save),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(4.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = "\uD83D\uDCF1",
style = TextStyle(fontSize = 28.sp)
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = data.device.orEmpty(),
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(
id = R.string.local_storage_used,
data.localUsage
),
style = Relations3,
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(18.dp))
ButtonSecondary(
text = stringResource(id = R.string.offload_files),
onClick = onOffloadFilesClicked,
size = ButtonSize.XSmall.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
text = stringResource(
id = R.string.space_usage,
data.spaceUsage,
data.spaceLimit
),
style = Relations3,
color = if (data.percentUsage != null && data.percentUsage!! >= 0.9F) {
colorResource(id = R.color.palette_system_red)
} else {
colorResource(id = R.color.text_secondary)
},
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(20.dp))
data.percentUsage?.let {
DefaultLinearProgressIndicator(progress = it)
}
Spacer(modifier = Modifier.height(20.dp))
ButtonSecondary(
text = stringResource(id = R.string.manage_files),
onClick = onManageFilesClicked,
size = ButtonSize.XSmall.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
Spacer(modifier = Modifier.height(44.dp))
Text(
text = stringResource(id = R.string.local_storage),
style = Title1,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(id = R.string.in_order_to_save),
style = BodyCalloutRegular,
color = colorResource(id = R.color.text_primary),
modifier = Modifier
.padding(top = 4.dp)
.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier
.wrapContentHeight(),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(48.dp)
.background(
color = colorResource(id = R.color.shape_tertiary),
shape = RoundedCornerShape(4.dp)
),
contentAlignment = Alignment.Center
) {
Text(
text = "\uD83D\uDCF1",
style = TextStyle(fontSize = 28.sp)
)
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(start = 12.dp),
verticalArrangement = Arrangement.Center
) {
Text(
text = data.device.orEmpty(),
style = PreviewTitle2Medium,
color = colorResource(id = R.color.text_primary),
modifier = Modifier.fillMaxWidth(),
)
Text(
text = stringResource(
id = R.string.local_storage_used,
data.localUsage
),
style = Relations3,
color = colorResource(id = R.color.text_secondary),
modifier = Modifier.fillMaxWidth()
)
}
}
Spacer(modifier = Modifier.height(18.dp))
ButtonSecondary(
text = stringResource(id = R.string.offload_files),
onClick = onOffloadFilesClicked,
size = ButtonSize.XSmall.apply {
contentPadding = PaddingValues(12.dp, 7.dp, 12.dp, 7.dp)
}
)
}
}
}
@ -250,11 +245,11 @@ object MockFileStorage {
val mockSpaceIcon = SpaceIconView.Gradient(from = "#EEDEAE", to = "#B93252")
val mockSpaceName = "Antons space"
val mockSpaceInfraUsage = "212 MB of 1 GB used"
val mockSpaceInfraPercent = 89F
val mockSpaceInfraPercent = 0.9F
val mockDevice = "iPhone 13 Pro"
val mockSpaceLocalUsage = "518 MB used"
val mockInfraMax = "14 GB"
val mockData = FilesStorageScreenState.SpaceData(
val mockData = ScreenState(
spaceIcon = mockSpaceIcon,
spaceName = mockSpaceName,
spaceUsage = mockSpaceInfraUsage,