2.20.1
@@ -21,5 +21,6 @@ package me.proton.drive.android.settings.domain.entity
|
||||
sealed interface UserOverlay {
|
||||
data object Onboarding : UserOverlay
|
||||
data class WhatsNew(val key: WhatsNewKey) : UserOverlay
|
||||
data class Subcription(val key: String) : UserOverlay
|
||||
data object RatingBooster : UserOverlay
|
||||
}
|
||||
|
||||
@@ -21,6 +21,5 @@ package me.proton.drive.android.settings.domain.entity
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
|
||||
enum class WhatsNewKey(val limit: TimestampS) {
|
||||
PUBLIC_SHARING(limit = TimestampS(1743465600)), // end of march 2025
|
||||
ALBUMS(limit = TimestampS(1753920000)), // end of July 2025
|
||||
}
|
||||
|
||||
|
||||
@@ -70,6 +70,7 @@ driveModule(
|
||||
implementation(libs.bundles.accompanist)
|
||||
implementation(libs.bundles.core)
|
||||
implementation(libs.google.play.review)
|
||||
implementation(libs.lottie.compose)
|
||||
implementation(libs.material)
|
||||
implementation(libs.okhttp)
|
||||
implementation(libs.plumber)
|
||||
|
||||
@@ -23,7 +23,6 @@ import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.android.drive.ui.annotation.Scenario
|
||||
import me.proton.android.drive.ui.test.AbstractBaseTest
|
||||
import me.proton.android.drive.ui.test.BaseTest
|
||||
import me.proton.core.accountmanager.domain.AccountManager
|
||||
import me.proton.core.accountmanager.domain.getPrimaryAccount
|
||||
@@ -42,8 +41,13 @@ import me.proton.core.drive.share.user.domain.usecase.ConvertExternalInvitation
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetExternalInvitationsFlow
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetInvitationsFlow
|
||||
import me.proton.core.drive.share.user.domain.usecase.InviteMembers
|
||||
import me.proton.core.test.quark.Quark.GenKeys
|
||||
import me.proton.core.test.quark.data.User
|
||||
import me.proton.core.test.quark.v2.command.userCreate
|
||||
import me.proton.core.test.quark.response.CreateUserQuarkResponse
|
||||
import me.proton.core.test.quark.v2.QuarkCommand
|
||||
import me.proton.core.test.quark.v2.command.CreateAddress
|
||||
import me.proton.core.test.quark.v2.command.USERS_CREATE
|
||||
import me.proton.core.test.quark.v2.toEncodedArgs
|
||||
import me.proton.core.test.rule.annotation.PrepareUser
|
||||
import me.proton.core.util.kotlin.random
|
||||
import org.junit.Assert.assertEquals
|
||||
@@ -130,3 +134,31 @@ class ExternalInvitationTest : BaseTest() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove with version 33 of core
|
||||
fun QuarkCommand.userCreate(
|
||||
user: User = User(),
|
||||
createAddress: CreateAddress? = CreateAddress.WithKey(GenKeys.Curve25519)
|
||||
): CreateUserQuarkResponse {
|
||||
val args = listOf(
|
||||
"--external" to if (user.isExternal) "true" else "",
|
||||
"--external-email" to if (user.isExternal) user.email else "",
|
||||
"-N" to user.name,
|
||||
"-p" to user.password,
|
||||
"-m" to user.passphrase,
|
||||
"-r" to user.recoveryEmail,
|
||||
"-c" to if (createAddress is CreateAddress.NoKey) "true" else "",
|
||||
"-k" to if (createAddress is CreateAddress.WithKey) createAddress.genKeys.name else "",
|
||||
"--format" to "json"
|
||||
).toEncodedArgs(ignoreEmpty = true)
|
||||
|
||||
val response =
|
||||
route(USERS_CREATE)
|
||||
.args(args)
|
||||
.build()
|
||||
.let {
|
||||
client.executeQuarkRequest(it)
|
||||
}
|
||||
|
||||
return json.decodeFromString(response.body!!.string())
|
||||
}
|
||||
|
||||
@@ -23,8 +23,10 @@ import androidx.test.rule.GrantPermissionRule
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import me.proton.android.drive.initializer.MainInitializer
|
||||
import me.proton.android.drive.ui.MainActivity
|
||||
import me.proton.android.drive.ui.rules.OverlayRule
|
||||
import me.proton.android.drive.ui.rules.SlowTestRule
|
||||
import me.proton.android.drive.ui.test.AbstractBaseTest
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.test.rule.ProtonRule
|
||||
import me.proton.core.test.rule.extension.protonAndroidComposeRule
|
||||
import org.junit.Rule
|
||||
@@ -44,18 +46,20 @@ open class ConfigurableTest : AbstractBaseTest() {
|
||||
additionalRules = linkedSetOf(
|
||||
IntentsRule(),
|
||||
SlowTestRule(),
|
||||
configurationRule
|
||||
configurationRule,
|
||||
OverlayRule(this),
|
||||
),
|
||||
beforeHilt = {
|
||||
configureFusion()
|
||||
},
|
||||
afterHilt = {
|
||||
MainInitializer.init(it.targetContext)
|
||||
setOverlaysDisplayStateAfterLogin()
|
||||
},
|
||||
logoutBefore = false
|
||||
)
|
||||
|
||||
override val mainUserId: UserId get() = configurationRule.mainUserId
|
||||
|
||||
@get:Rule(order = 2)
|
||||
val ruleChain: RuleChain = RuleChain
|
||||
.outerRule(permissionRule)
|
||||
|
||||
@@ -23,6 +23,7 @@ import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonArray
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import me.proton.android.drive.ui.test.AbstractBaseTest.Companion.loginTestHelper
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import org.junit.rules.TestRule
|
||||
import org.junit.runner.Description
|
||||
import org.junit.runners.model.Statement
|
||||
@@ -30,6 +31,7 @@ import org.junit.runners.model.Statement
|
||||
class ConfigurationRule : TestRule {
|
||||
|
||||
lateinit var configuration: ConfigurationRoot
|
||||
lateinit var mainUserId: UserId
|
||||
|
||||
fun getArgString(key: String): String {
|
||||
return configuration.info.args.getValue(key).jsonPrimitive.content
|
||||
@@ -61,7 +63,7 @@ class ConfigurationRule : TestRule {
|
||||
object : Statement() {
|
||||
override fun evaluate() {
|
||||
configuration.user.run {
|
||||
loginTestHelper.login(username, password)
|
||||
mainUserId = loginTestHelper.login(username, password).userId
|
||||
}
|
||||
|
||||
base.evaluate()
|
||||
|
||||
@@ -23,10 +23,12 @@ import me.proton.android.drive.lock.domain.exception.LockException
|
||||
import me.proton.core.drive.base.data.entity.LoggerLevel
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.exception.DriveException
|
||||
import me.proton.core.drive.link.domain.exception.LinksResultException
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.exception.ShareException
|
||||
import me.proton.android.drive.lock.presentation.extension.getDefaultMessage as lockGetDefaultMessage
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.drive.link.presentation.extension.getDefaultMessage as linkGetDefaultMessage
|
||||
|
||||
fun DriveException.getDefaultMessage(context: Context): String = when (val exception = this) {
|
||||
is ShareException.ShareLocked -> when (exception.shareType) {
|
||||
@@ -42,6 +44,7 @@ fun DriveException.getDefaultMessage(context: Context): String = when (val excep
|
||||
else -> context.getString(I18N.string.error_creating_share_not_allowed)
|
||||
}
|
||||
is LockException -> exception.lockGetDefaultMessage(context)
|
||||
is LinksResultException -> exception.linkGetDefaultMessage(context)
|
||||
else -> error("Default message for exception is missing")
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +63,7 @@ class MainInitializer : Initializer<Unit> {
|
||||
AccountReadyObserverInitializer::class.java,
|
||||
FirstAppUsageInitializer::class.java,
|
||||
UploadInitializer::class.java,
|
||||
PhotoShareMigrationManagerInitializer::class.java,
|
||||
)
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.initializer
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.startup.Initializer
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import me.proton.core.accountmanager.domain.AccountManager
|
||||
import me.proton.core.accountmanager.presentation.observe
|
||||
import me.proton.core.accountmanager.presentation.onAccountReady
|
||||
import me.proton.core.accountmanager.presentation.onAccountRemoved
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.drivelink.photo.domain.manager.PhotoShareMigrationManager
|
||||
import me.proton.core.presentation.app.AppLifecycleProvider
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
|
||||
class PhotoShareMigrationManagerInitializer : Initializer<Unit> {
|
||||
private val scopes = mutableMapOf<UserId, CoroutineScope>()
|
||||
|
||||
override fun create(context: Context) {
|
||||
EntryPointAccessors.fromApplication(
|
||||
context.applicationContext,
|
||||
PhotoShareMigrationManagerInitializerEntryPoint::class.java
|
||||
).run {
|
||||
accountManager.observe(appLifecycleProvider.lifecycle, Lifecycle.State.RESUMED)
|
||||
.onAccountReady { account ->
|
||||
val userId = account.userId
|
||||
val scope = scopes.getOrPut(userId) {
|
||||
CoroutineScope(Dispatchers.IO + Job())
|
||||
}
|
||||
photoShareMigrationManager.initialize(
|
||||
userId,
|
||||
scope,
|
||||
appLifecycleProvider.state.map { state -> state == AppLifecycleProvider.State.Foreground },
|
||||
)
|
||||
photoShareMigrationManager
|
||||
.status
|
||||
.onEach { status ->
|
||||
CoreLogger.d(LogTag.PHOTO, "On migration status: ${status.name}")
|
||||
}
|
||||
.launchIn(scope)
|
||||
}
|
||||
.onAccountRemoved { account ->
|
||||
scopes.remove(account.userId)?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun dependencies(): List<Class<out Initializer<*>>> = listOf(
|
||||
LoggerInitializer::class.java,
|
||||
)
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface PhotoShareMigrationManagerInitializerEntryPoint {
|
||||
val accountManager: AccountManager
|
||||
val appLifecycleProvider: AppLifecycleProvider
|
||||
val photoShareMigrationManager: PhotoShareMigrationManager
|
||||
}
|
||||
@@ -32,7 +32,7 @@ class DriveApiClient @Inject constructor(
|
||||
override val appVersionHeader: String
|
||||
get() = configurationProvider.appVersionHeader
|
||||
override val enableDebugLogging: Boolean
|
||||
get() = true
|
||||
get() = BuildConfig.DEBUG || BuildConfig.FLAVOR == BuildConfig.FLAVOR_ALPHA
|
||||
override val shouldUseDoh: Boolean
|
||||
get() = false
|
||||
override val userAgent: String
|
||||
|
||||
@@ -28,7 +28,7 @@ import me.proton.core.drive.base.domain.entity.ClientUid
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.ParentId
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.usecase.GetShare
|
||||
import javax.inject.Inject
|
||||
@@ -40,17 +40,17 @@ class BridgeFindDuplicatesRepository @Inject constructor(
|
||||
private val photo: PhotoFindDuplicatesRepository,
|
||||
) : FindDuplicatesRepository {
|
||||
override suspend fun findDuplicates(
|
||||
folderId: FolderId,
|
||||
parentId: ParentId,
|
||||
nameHashes: List<String>,
|
||||
clientUids: List<ClientUid>,
|
||||
): List<BackupDuplicate> {
|
||||
val share = getShare(folderId.shareId)
|
||||
val share = getShare(parentId.shareId)
|
||||
.filterSuccessOrError().mapSuccessValueOrNull().first()
|
||||
requireNotNull(share) { "Cannot find share for folder: ${folderId.id.logId()}" }
|
||||
requireNotNull(share) { "Cannot find share for folder: ${parentId.id.logId()}" }
|
||||
return if (share.type == Share.Type.PHOTO) {
|
||||
photo.findDuplicates(folderId, nameHashes, clientUids)
|
||||
photo.findDuplicates(parentId, nameHashes, clientUids)
|
||||
} else {
|
||||
folder.findDuplicates(folderId, nameHashes, clientUids)
|
||||
folder.findDuplicates(parentId, nameHashes, clientUids)
|
||||
}.let { backupDuplicates ->
|
||||
if (configurationProvider.allowBackupDeletedFilesEnabled) {
|
||||
backupDuplicates.filterNot { duplicate ->
|
||||
|
||||
@@ -67,6 +67,7 @@ import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import me.proton.android.drive.extension.deepLinkBaseUrl
|
||||
import me.proton.android.drive.lock.data.provider.BiometricPromptProvider
|
||||
@@ -91,7 +92,6 @@ import me.proton.core.crypto.common.keystore.KeyStoreCrypto
|
||||
import me.proton.core.drive.announce.event.domain.usecase.AnnounceEvent
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.ListenToBroadcastMessages
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.messagequeue.domain.ActionProvider
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.thumbnail.presentation.coil.ThumbnailEnabled
|
||||
@@ -120,7 +120,6 @@ class MainActivity : FragmentActivity() {
|
||||
@Inject lateinit var activityLauncher: ActivityLauncher
|
||||
@Inject lateinit var announceEvent: AnnounceEvent
|
||||
@Inject lateinit var showRatingBooster: ShowRatingBooster
|
||||
@Inject lateinit var albumsFeatureFlag: AlbumsFeatureFlag
|
||||
|
||||
lateinit var configurationProvider: ConfigurationProvider
|
||||
private val accountViewModel: AccountViewModel by viewModels()
|
||||
@@ -183,6 +182,7 @@ class MainActivity : FragmentActivity() {
|
||||
navigateToBugReport = bugReportViewModel::sendBugReport,
|
||||
navigateToSubscription = plansViewModel::showCurrentPlans,
|
||||
navigateToRatingBooster = { showRatingBooster(this@MainActivity) },
|
||||
navigateToUpgradePlan = plansViewModel::startUpgrade,
|
||||
) { isOpen ->
|
||||
isDrawerOpen = isOpen
|
||||
}
|
||||
@@ -241,17 +241,10 @@ class MainActivity : FragmentActivity() {
|
||||
@Composable
|
||||
private fun photosRoute(): State<String?> =
|
||||
remember {
|
||||
accountViewModel.primaryAccount.flatMapLatest { account ->
|
||||
accountViewModel.primaryAccount.mapLatest { account ->
|
||||
account?.let {
|
||||
albumsFeatureFlag(account.userId)
|
||||
.map { enabled ->
|
||||
if (enabled) {
|
||||
Screen.PhotosAndAlbums.route
|
||||
} else {
|
||||
Screen.Photos.route
|
||||
}
|
||||
}
|
||||
} ?: flowOf(null)
|
||||
Screen.PhotosAndAlbums.route
|
||||
}
|
||||
}
|
||||
}.collectAsState(initial = null)
|
||||
|
||||
|
||||
@@ -19,14 +19,16 @@ package me.proton.android.drive.ui.common
|
||||
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
|
||||
fun DriveLink.onClick(
|
||||
navigateToFolder: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit
|
||||
) = when (this) {
|
||||
is DriveLink.Folder -> navigateToFolder(id, if (isNameEncrypted) null else name)
|
||||
is DriveLink.File -> navigateToPreview(id)
|
||||
is DriveLink.Album -> error("TODO") // navigateToAlbum?
|
||||
is DriveLink.Album -> navigateToAlbum(id)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.component
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.drive.base.presentation.component.IllustratedMessage
|
||||
import me.proton.core.drive.base.presentation.extension.conditional
|
||||
import me.proton.core.drive.base.presentation.extension.isLandscape
|
||||
import me.proton.core.drive.base.presentation.extension.isPortrait
|
||||
|
||||
@Composable
|
||||
fun PromoContainer(
|
||||
titleResId: Int,
|
||||
descriptionResId: Int,
|
||||
imageResId: Int,
|
||||
actionText: String,
|
||||
dismissActionText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
onAction: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
PromoContainer(
|
||||
modifier = modifier,
|
||||
title = stringResource(id = titleResId),
|
||||
description = stringResource(id = descriptionResId),
|
||||
image = {
|
||||
Image(painter = painterResource(id = imageResId), contentDescription = null)
|
||||
},
|
||||
actionText = actionText,
|
||||
dismissActionText = dismissActionText,
|
||||
onAction = onAction,
|
||||
onCancel = onCancel
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PromoContainer(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String,
|
||||
description: String,
|
||||
image: @Composable () -> Unit,
|
||||
actionText: String,
|
||||
dismissActionText: String,
|
||||
onAction: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
PromoContainer(
|
||||
actionText = actionText,
|
||||
dismissActionText = dismissActionText,
|
||||
bodyContent = {
|
||||
IllustratedMessage(
|
||||
imageContent = image,
|
||||
title = title,
|
||||
description = description,
|
||||
)
|
||||
},
|
||||
onAction = onAction,
|
||||
onCancel = onCancel,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PromoContainer(
|
||||
actionText: String,
|
||||
dismissActionText: String,
|
||||
modifier: Modifier = Modifier,
|
||||
bodyContent: @Composable () -> Unit,
|
||||
onAction: () -> Unit,
|
||||
onCancel: () -> Unit,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = ProtonDimens.DefaultSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(ProtonDimens.DefaultSpacing),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
bodyContent()
|
||||
val buttonModifier = Modifier
|
||||
.conditional(isPortrait) {
|
||||
fillMaxWidth()
|
||||
}
|
||||
.conditional(isLandscape) {
|
||||
widthIn(min = ButtonMinWidth)
|
||||
}
|
||||
.heightIn(min = ProtonDimens.ListItemHeight)
|
||||
ProtonSolidButton(
|
||||
onClick = onAction,
|
||||
modifier = buttonModifier,
|
||||
) {
|
||||
Text(
|
||||
text = actionText,
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
ProtonTextButton(
|
||||
onClick = onCancel,
|
||||
modifier = buttonModifier
|
||||
) {
|
||||
Text(
|
||||
text = dismissActionText,
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ButtonMinWidth = 300.dp
|
||||
@@ -18,8 +18,8 @@
|
||||
|
||||
package me.proton.android.drive.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -36,7 +36,6 @@ import androidx.lifecycle.flowWithLifecycle
|
||||
import me.proton.android.drive.photos.presentation.extension.details
|
||||
import me.proton.android.drive.ui.viewmodel.AlbumOptionsViewModel
|
||||
import me.proton.core.compose.component.bottomsheet.BottomSheetContent
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.component.BottomSheetEntry
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
@@ -55,6 +54,7 @@ fun AlbumOptions(
|
||||
navigateToManageAccess: (linkId: LinkId) -> Unit,
|
||||
navigateToRename: (linkId: LinkId) -> Unit,
|
||||
navigateToDelete: (AlbumId) -> Unit,
|
||||
navigateToLeave: (AlbumId) -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -70,6 +70,7 @@ fun AlbumOptions(
|
||||
navigateToManageAccess = navigateToManageAccess,
|
||||
navigateToRename = navigateToRename,
|
||||
navigateToDelete = navigateToDelete,
|
||||
navigateToLeave = navigateToLeave,
|
||||
dismiss = dismiss,
|
||||
).flowWithLifecycle(
|
||||
lifecycle = lifecycle,
|
||||
@@ -153,10 +154,9 @@ fun AlbumOptionsHeader(
|
||||
subtitle = details,
|
||||
modifier = modifier,
|
||||
) { contentModifier ->
|
||||
Icon(
|
||||
painter = painterResource(id = BasePresentation.drawable.ic_proton_images),
|
||||
Image(
|
||||
painter = painterResource(id = BasePresentation.drawable.ic_folder_album),
|
||||
contentDescription = null,
|
||||
tint = ProtonTheme.colors.iconNorm,
|
||||
modifier = contentModifier,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.dialog
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.photos.presentation.component.ConfirmLeaveAlbumDialogContent
|
||||
import me.proton.android.drive.ui.viewmodel.ConfirmLeaveAlbumDialogViewModel
|
||||
|
||||
@Composable
|
||||
fun ConfirmLeaveAlbumDialog(
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<ConfirmLeaveAlbumDialogViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(
|
||||
initialValue = viewModel.initialViewState
|
||||
)
|
||||
val lifecycle = LocalLifecycleOwner.current.lifecycle
|
||||
val viewEvent = remember(lifecycle) {
|
||||
viewModel.viewEvent(onDismiss)
|
||||
}
|
||||
ConfirmLeaveAlbumDialogContent(
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
modifier = modifier
|
||||
.testTag(ConfirmLeaveAlbumDialogTestTag.confirmLeaveAlbum),
|
||||
)
|
||||
}
|
||||
|
||||
object ConfirmLeaveAlbumDialogTestTag {
|
||||
const val confirmLeaveAlbum = "confirm leave album"
|
||||
}
|
||||
@@ -159,7 +159,7 @@ fun FileOrFolderOptions(
|
||||
.testTag(FileFolderOptionsDialogTestTag.folderOptions),
|
||||
)
|
||||
}
|
||||
is DriveLink.Album -> error("TODO")// AlbumOptions
|
||||
is DriveLink.Album -> error("On Album we should invoke AlbumOptions")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ fun MultipleFileOrFolderOptions(
|
||||
runAction: RunAction,
|
||||
navigateToMove: (selectionId: SelectionId, folderId: FolderId?) -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToShareMultiplePhotosOptions: (SelectionId) -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -54,6 +55,7 @@ fun MultipleFileOrFolderOptions(
|
||||
runAction = runAction,
|
||||
navigateToMove = navigateToMove,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToShareMultiplePhotosOptions = navigateToShareMultiplePhotosOptions,
|
||||
dismiss = dismiss,
|
||||
).flowWithLifecycle(
|
||||
lifecycle = lifecycle,
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.airbnb.lottie.compose.LottieAnimation
|
||||
import com.airbnb.lottie.compose.LottieCompositionSpec
|
||||
import com.airbnb.lottie.compose.LottieConstants
|
||||
import com.airbnb.lottie.compose.animateLottieCompositionAsState
|
||||
import com.airbnb.lottie.compose.rememberLottieComposition
|
||||
import me.proton.android.drive.ui.component.PromoContainer
|
||||
import me.proton.android.drive.ui.viewmodel.PhotosImportantUpdatesViewModel
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.compose.theme.defaultHighlightNorm
|
||||
import me.proton.core.drive.base.presentation.component.IllustratedMessage
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Composable
|
||||
fun PhotosImportantUpdates(
|
||||
runAction: RunAction,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosImportantUpdatesViewModel>()
|
||||
val viewEvent = remember {
|
||||
viewModel.viewEvent(
|
||||
runAction = runAction,
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit){
|
||||
onDispose {
|
||||
viewEvent.onRemindMeLater()
|
||||
}
|
||||
}
|
||||
PhotosImportantUpdates(
|
||||
modifier = modifier
|
||||
.navigationBarsPadding()
|
||||
.systemBarsPadding(),
|
||||
onStart = viewEvent.onStart,
|
||||
onRemindMeLater = viewEvent.onRemindMeLater,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotosImportantUpdates(
|
||||
modifier: Modifier = Modifier,
|
||||
onStart: () -> Unit,
|
||||
onRemindMeLater: () -> Unit,
|
||||
) {
|
||||
Column {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(ProtonDimens.DefaultSpacing),
|
||||
text = stringResource(I18N.string.photos_important_updates_title),
|
||||
style = ProtonTheme.typography.headline,
|
||||
)
|
||||
PromoContainer(
|
||||
bodyContent = { PhotosImportantUpdatesBody() },
|
||||
actionText = stringResource(I18N.string.photos_important_updates_start_action),
|
||||
dismissActionText = stringResource(I18N.string.photos_important_updates_cancel_action),
|
||||
onAction = onStart,
|
||||
onCancel = onRemindMeLater,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotosImportantUpdatesBody(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = ProtonDimens.DefaultSpacing),
|
||||
) {
|
||||
IllustratedMessage(
|
||||
imageContent = { PhotosImportantUpdatesIllustration() },
|
||||
titleContent = {
|
||||
Text(
|
||||
text = stringResource(I18N.string.photos_important_updates_subtitle),
|
||||
style = ProtonTheme.typography.defaultHighlightNorm.copy(textAlign = TextAlign.Center),
|
||||
modifier = Modifier.padding(
|
||||
top = ProtonDimens.DefaultSpacing,
|
||||
)
|
||||
)
|
||||
},
|
||||
description = stringResource(I18N.string.photos_important_updates_description),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotosImportantUpdatesIllustration(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val composition by rememberLottieComposition(
|
||||
LottieCompositionSpec.RawRes(BasePresentation.raw.file_transfer_animation)
|
||||
)
|
||||
val progress by animateLottieCompositionAsState(
|
||||
composition = composition,
|
||||
iterations = LottieConstants.IterateForever,
|
||||
)
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = ProtonDimens.DefaultSpacing),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
LottieAnimation(
|
||||
composition = composition,
|
||||
progress = { progress },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun PhotosImportantUpdatesPreview() {
|
||||
ProtonTheme {
|
||||
PhotosImportantUpdates(
|
||||
onStart = {},
|
||||
onRemindMeLater = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.photos.presentation.component.AlbumListItem
|
||||
import me.proton.android.drive.photos.presentation.component.AlbumOptionsSection
|
||||
import me.proton.android.drive.photos.presentation.state.AlbumsItem
|
||||
import me.proton.android.drive.ui.viewevent.ShareMultiplePhotosOptionsViewEvent
|
||||
import me.proton.android.drive.ui.viewmodel.ShareMultiplePhotosOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewstate.ShareMultiplePhotosOptionsViewState
|
||||
import me.proton.core.drive.base.presentation.component.BottomSheetEntry
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.files.presentation.entry.OptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
|
||||
@Composable
|
||||
fun ShareMultiplePhotosOptions(
|
||||
runAction: RunAction,
|
||||
modifier: Modifier = Modifier,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<ShareMultiplePhotosOptionsViewModel>()
|
||||
val viewState = viewModel.initialViewState
|
||||
val viewEvent = remember {
|
||||
viewModel.viewEvent(
|
||||
runAction = runAction,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
)
|
||||
}
|
||||
ShareMultiplePhotosOptions(
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
modifier = modifier
|
||||
.testTag(ShareMultiplePhotosOptionsTestTag.screen)
|
||||
.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareMultiplePhotosOptions(
|
||||
viewState: ShareMultiplePhotosOptionsViewState,
|
||||
viewEvent: ShareMultiplePhotosOptionsViewEvent,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val sharedAlbums by viewState.sharedAlbums.collectAsStateWithLifecycle(emptyList())
|
||||
ShareMultiplePhotosOptions(
|
||||
shareOptionsSectionTitle = stringResource(viewState.shareOptionsSectionTitleResId),
|
||||
shareOptions = viewState.shareOptions,
|
||||
sharedAlbumsSectionTitle = stringResource(viewState.sharedAlbumsSectionTitleResId),
|
||||
sharedAlbums = sharedAlbums,
|
||||
modifier = modifier,
|
||||
onSharedAlbum = viewEvent.onSharedAlbum,
|
||||
onScroll = viewEvent.onScroll,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ShareMultiplePhotosOptions(
|
||||
shareOptionsSectionTitle: String,
|
||||
shareOptions: List<OptionEntry<Unit>>,
|
||||
sharedAlbumsSectionTitle: String,
|
||||
sharedAlbums: List<AlbumsItem.Listing>,
|
||||
modifier: Modifier = Modifier,
|
||||
onSharedAlbum: (AlbumId) -> Unit,
|
||||
onScroll: (Set<LinkId>) -> Unit,
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val firstVisibleItemIndex by remember(
|
||||
listState
|
||||
) { derivedStateOf { listState.firstVisibleItemIndex } }
|
||||
LaunchedEffect(firstVisibleItemIndex, sharedAlbums) {
|
||||
onScroll(
|
||||
sharedAlbums
|
||||
.takeIf { list -> list.isNotEmpty() && list.size > firstVisibleItemIndex }
|
||||
?.let { list ->
|
||||
val sizeRange = IntRange(0, list.size - 1)
|
||||
val fromIndex = (firstVisibleItemIndex - 10).coerceIn(sizeRange)
|
||||
val toIndex = (firstVisibleItemIndex + 20).coerceIn(sizeRange)
|
||||
list.subList(fromIndex, toIndex + 1)
|
||||
.flatMap { albumListing ->
|
||||
listOfNotNull(albumListing.id, albumListing.album?.coverLinkId)
|
||||
}
|
||||
.toSet()
|
||||
} ?: emptySet(),
|
||||
)
|
||||
}
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = modifier
|
||||
) {
|
||||
item(
|
||||
key = shareOptionsSectionTitle
|
||||
) {
|
||||
AlbumOptionsSection(
|
||||
title = shareOptionsSectionTitle
|
||||
)
|
||||
}
|
||||
items(
|
||||
count = shareOptions.size,
|
||||
key = { shareOptions[it].label },
|
||||
) {
|
||||
val option = shareOptions[it]
|
||||
BottomSheetEntry(
|
||||
leadingIcon = option.icon,
|
||||
trailingIcon = null,
|
||||
title = stringResource(option.label),
|
||||
onClick = { option.onClick(Unit) },
|
||||
)
|
||||
}
|
||||
if (sharedAlbums.isNotEmpty()) {
|
||||
item(
|
||||
key = sharedAlbumsSectionTitle
|
||||
) {
|
||||
AlbumOptionsSection(
|
||||
title = sharedAlbumsSectionTitle
|
||||
)
|
||||
}
|
||||
items(
|
||||
count = sharedAlbums.size,
|
||||
key = { "${sharedAlbums[it].id.shareId.id}.${sharedAlbums[it].id.id}" },
|
||||
) {
|
||||
val albumsItem = sharedAlbums[it]
|
||||
AlbumListItem(
|
||||
albumsItem = albumsItem,
|
||||
onClick = onSharedAlbum,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object ShareMultiplePhotosOptionsTestTag {
|
||||
const val screen = "share multiple photos options screen"
|
||||
}
|
||||
@@ -20,4 +20,5 @@ package me.proton.android.drive.ui.effect
|
||||
|
||||
sealed interface PhotosEffect {
|
||||
data object ShowUpsell : PhotosEffect
|
||||
data object ShowImportantUpdates: PhotosEffect
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ import me.proton.android.drive.extension.isCurrentDestination
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.android.drive.extension.require
|
||||
import me.proton.android.drive.extension.requireArguments
|
||||
import me.proton.android.drive.extension.requireSerializable
|
||||
import me.proton.android.drive.extension.runFromRoute
|
||||
import me.proton.android.drive.lock.presentation.component.AppLock
|
||||
import me.proton.android.drive.log.DriveLogTag
|
||||
@@ -72,6 +71,7 @@ import me.proton.android.drive.ui.dialog.ComputerOptions
|
||||
import me.proton.android.drive.ui.dialog.ConfirmDeleteAlbumDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmDeletionDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmEmptyTrashDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmLeaveAlbumDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmSkipIssuesDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmStopAllSharingDialog
|
||||
import me.proton.android.drive.ui.dialog.ConfirmStopLinkSharingDialog
|
||||
@@ -81,12 +81,14 @@ import me.proton.android.drive.ui.dialog.LogOptions
|
||||
import me.proton.android.drive.ui.dialog.MultipleFileOrFolderOptions
|
||||
import me.proton.android.drive.ui.dialog.Onboarding
|
||||
import me.proton.android.drive.ui.dialog.ParentFolderOptions
|
||||
import me.proton.android.drive.ui.dialog.PhotosImportantUpdates
|
||||
import me.proton.android.drive.ui.dialog.ProtonDocsInsertImageOptions
|
||||
import me.proton.android.drive.ui.dialog.SendFileDialog
|
||||
import me.proton.android.drive.ui.dialog.ShareExternalInvitationOptions
|
||||
import me.proton.android.drive.ui.dialog.ShareInvitationOptions
|
||||
import me.proton.android.drive.ui.dialog.ShareLinkPermissions
|
||||
import me.proton.android.drive.ui.dialog.ShareMemberOptions
|
||||
import me.proton.android.drive.ui.dialog.ShareMultiplePhotosOptions
|
||||
import me.proton.android.drive.ui.dialog.SortingList
|
||||
import me.proton.android.drive.ui.dialog.SystemAccessDialog
|
||||
import me.proton.android.drive.ui.dialog.WhatsNew
|
||||
@@ -103,6 +105,7 @@ import me.proton.android.drive.ui.screen.AppAccessScreen
|
||||
import me.proton.android.drive.ui.screen.BackupIssuesScreen
|
||||
import me.proton.android.drive.ui.screen.CreateNewAlbumScreen
|
||||
import me.proton.android.drive.ui.screen.DefaultHomeTabScreen
|
||||
import me.proton.android.drive.ui.screen.SubscriptionPromoScreen
|
||||
import me.proton.android.drive.ui.screen.FileInfoScreen
|
||||
import me.proton.android.drive.ui.screen.GetMoreFreeStorageScreen
|
||||
import me.proton.android.drive.ui.screen.HomeScreen
|
||||
@@ -113,7 +116,7 @@ import me.proton.android.drive.ui.screen.OfflineScreen
|
||||
import me.proton.android.drive.ui.screen.PhotosBackupScreen
|
||||
import me.proton.android.drive.ui.screen.PhotosUpsellScreen
|
||||
import me.proton.android.drive.ui.screen.PickerAlbumScreen
|
||||
import me.proton.android.drive.ui.screen.PickerPhotosAndAlbumsScreen
|
||||
import me.proton.android.drive.ui.screen.PickerPhotosScreen
|
||||
import me.proton.android.drive.ui.screen.PreviewScreen
|
||||
import me.proton.android.drive.ui.screen.SettingsScreen
|
||||
import me.proton.android.drive.ui.screen.SigningOutScreen
|
||||
@@ -169,6 +172,7 @@ fun AppNavGraph(
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
navigateToUpgradePlan: () -> Unit,
|
||||
onDrawerStateChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
val navController = rememberAnimatedNavController(keyStoreCrypto)
|
||||
@@ -234,6 +238,7 @@ fun AppNavGraph(
|
||||
navigateToBugReport = navigateToBugReport,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
navigateToRatingBooster = navigateToRatingBooster,
|
||||
navigateToUpgradePlan = navigateToUpgradePlan,
|
||||
onDrawerStateChanged = onDrawerStateChanged,
|
||||
)
|
||||
}
|
||||
@@ -256,6 +261,7 @@ fun AppNavGraph(
|
||||
navigateToBugReport: () -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
navigateToUpgradePlan: () -> Unit,
|
||||
onDrawerStateChanged: (Boolean) -> Unit,
|
||||
) {
|
||||
DriveNavHost(
|
||||
@@ -388,8 +394,12 @@ fun AppNavGraph(
|
||||
addAlbum(navController)
|
||||
addAlbumOptions(navController)
|
||||
addConfirmDeleteAlbumDialog(navController)
|
||||
addPickerPhotosAndAlbums(navController)
|
||||
addPickerPhotos(navController)
|
||||
addPickerAlbum(navController)
|
||||
addSubscriptionPromoScreen(navigateToUpgradePlan)
|
||||
addConfirmLeaveAlbumDialog(navController)
|
||||
addShareMultiplePhotosOptions(navController)
|
||||
addPhotosImportantUpdates(navController)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -493,7 +503,11 @@ fun NavGraphBuilder.addFileOrFolderOptions(
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
navArgument(Screen.FileOrFolderOptions.KEY_ALBUM_SHARE_ID) {
|
||||
navArgument(Screen.FileOrFolderOptions.ALBUM_SHARE_ID) {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
navArgument(Screen.FileOrFolderOptions.SELECTION_ID) {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
@@ -587,6 +601,11 @@ fun NavGraphBuilder.addMultipleFileOrFolderOptions(
|
||||
popUpTo(Screen.MultipleFileOrFolderOptions.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
navigateToShareMultiplePhotosOptions = { selectionId ->
|
||||
navController.navigate(Screen.ShareMultiplePhotosOptions(userId, selectionId)) {
|
||||
popUpTo(Screen.MultipleFileOrFolderOptions.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
dismiss = {
|
||||
navController.popBackStack(
|
||||
route = Screen.MultipleFileOrFolderOptions.route,
|
||||
@@ -701,6 +720,7 @@ fun NavGraphBuilder.addUserInvitation(
|
||||
route = Screen.UserInvitation.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.UserInvitation.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.UserInvitation.ALBUMS_ONLY) { type = NavType.BoolType },
|
||||
),
|
||||
) {
|
||||
UserInvitationScreen (onBack = {
|
||||
@@ -887,6 +907,9 @@ internal fun NavGraphBuilder.addHome(
|
||||
navigateToPreview = { fileId, pagerType ->
|
||||
navController.navigate(Screen.PagerPreview(pagerType, userId, fileId))
|
||||
},
|
||||
navigateToPreviewWithTag = { fileId, pagerType, photoTag ->
|
||||
navController.navigate(Screen.PagerPreview(pagerType, userId, fileId, photoTag))
|
||||
},
|
||||
navigateToSorting = { sorting ->
|
||||
navController.navigate(
|
||||
Screen.Sorting(userId, currentFolderId, sorting.by, sorting.direction)
|
||||
@@ -895,9 +918,13 @@ internal fun NavGraphBuilder.addHome(
|
||||
navigateToSettings = {
|
||||
navController.navigate(Screen.Settings(userId))
|
||||
},
|
||||
navigateToFileOrFolderOptions = { linkId ->
|
||||
navigateToFileOrFolderOptions = { linkId, selectionId ->
|
||||
navController.navigate(
|
||||
Screen.FileOrFolderOptions(userId, linkId)
|
||||
Screen.FileOrFolderOptions(
|
||||
userId = userId,
|
||||
linkId = linkId,
|
||||
selectionId = selectionId,
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToMultipleFileOrFolderOptions = { selectionId ->
|
||||
@@ -961,8 +988,8 @@ internal fun NavGraphBuilder.addHome(
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToUserInvitation = {
|
||||
navController.navigate(Screen.UserInvitation(userId))
|
||||
navigateToUserInvitation = { albumsOnly ->
|
||||
navController.navigate(Screen.UserInvitation(userId, albumsOnly))
|
||||
},
|
||||
navigateToCreateNewAlbum = {
|
||||
navController.navigate(Screen.PhotosAndAlbums.CreateNewAlbum(userId))
|
||||
@@ -970,6 +997,14 @@ internal fun NavGraphBuilder.addHome(
|
||||
navigateToAlbum = { albumId ->
|
||||
navController.navigate(Screen.Album(albumId))
|
||||
},
|
||||
navigateToSubscriptionPromo = { key ->
|
||||
navController.navigate(Screen.Promo.Subscription(userId, key))
|
||||
},
|
||||
navigateToPhotosImportantUpdates = {
|
||||
navController.navigate(
|
||||
Screen.Photos.ImportantUpdates(userId)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
@@ -1323,6 +1358,11 @@ fun NavGraphBuilder.addOffline(navController: NavHostController) = composable(
|
||||
Screen.PagerPreview(pagerType, userId, fileId)
|
||||
)
|
||||
},
|
||||
navigateToAlbum = { albumId ->
|
||||
navController.navigate(
|
||||
Screen.Album(albumId)
|
||||
)
|
||||
},
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.OfflineFiles.route,
|
||||
@@ -1363,6 +1403,10 @@ fun NavGraphBuilder.addPagerPreview(navController: NavHostController) = composab
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
navArgument(Screen.PagerPreview.PHOTO_TAG) {
|
||||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
),
|
||||
) { navBackStackEntry ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.Files.USER_ID))
|
||||
@@ -1949,6 +1993,22 @@ fun NavGraphBuilder.addPhotosUpsell(
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addSubscriptionPromoScreen(
|
||||
navigateToSubscription: () -> Unit,
|
||||
) = modalBottomSheet(
|
||||
route = Screen.Promo.Subscription.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Settings.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Promo.Subscription.PROMO_KEY) { type = NavType.StringType },
|
||||
),
|
||||
) { _, runAction ->
|
||||
SubscriptionPromoScreen(
|
||||
runAction = runAction,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addComputerOptions(
|
||||
navController: NavHostController,
|
||||
) = modalBottomSheet(
|
||||
@@ -2166,10 +2226,12 @@ fun NavGraphBuilder.addCreateNewAlbum(navController: NavHostController) = compos
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
popExitTransition = defaultPopExitSlideTransition { true },
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Log.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.PhotosAndAlbums.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.PhotosAndAlbums.SHARED_ALBUM) { type = NavType.BoolType },
|
||||
),
|
||||
) { navBackStackEntry ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.PhotosAndAlbums.USER_ID))
|
||||
val sharedAlbum: Boolean = navBackStackEntry.get(Screen.PhotosAndAlbums.SHARED_ALBUM) ?: false
|
||||
CreateNewAlbumScreen(
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
@@ -2179,11 +2241,16 @@ fun NavGraphBuilder.addCreateNewAlbum(navController: NavHostController) = compos
|
||||
},
|
||||
navigateToAlbum = { albumId: AlbumId ->
|
||||
navController.navigate(Screen.Album(albumId)) {
|
||||
popUpTo(route = Screen.PhotosAndAlbums.CreateNewAlbum.route) { inclusive = true }
|
||||
popUpTo(route = Screen.PhotosAndAlbums.CreateNewAlbum.route) {
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
if (sharedAlbum) {
|
||||
navController.navigate(Screen.ShareViaInvitations(userId, albumId))
|
||||
}
|
||||
},
|
||||
navigateToPicker = {
|
||||
navController.navigate(Screen.Picker.PhotosAndAlbums(userId = userId))
|
||||
navController.navigate(Screen.Picker.Photos(userId = userId))
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -2206,9 +2273,9 @@ fun NavGraphBuilder.addAlbum(navController: NavHostController) = composable(
|
||||
navigateToAlbumOptions = { albumId ->
|
||||
navController.navigate(Screen.AlbumOptions(userId, albumId))
|
||||
},
|
||||
navigateToPhotosOptions = { linkId, albumId ->
|
||||
navigateToPhotosOptions = { linkId, albumId, selectionId ->
|
||||
navController.navigate(
|
||||
Screen.FileOrFolderOptions(userId, linkId, albumId)
|
||||
Screen.FileOrFolderOptions(userId, linkId, albumId, selectionId)
|
||||
)
|
||||
},
|
||||
navigateToMultiplePhotosOptions = { selectionId, albumId ->
|
||||
@@ -2225,7 +2292,13 @@ fun NavGraphBuilder.addAlbum(navController: NavHostController) = composable(
|
||||
))
|
||||
},
|
||||
navigateToPicker = { albumId ->
|
||||
navController.navigate(Screen.Picker.PhotosAndAlbums(destinationAlbumId = albumId))
|
||||
navController.navigate(Screen.Picker.Photos(destinationAlbumId = albumId))
|
||||
},
|
||||
navigateToShareViaInvitations = { albumId ->
|
||||
navController.navigate(Screen.ShareViaInvitations(userId, albumId))
|
||||
},
|
||||
navigateToManageAccess = { albumId ->
|
||||
navController.navigate(Screen.ManageAccess(userId, albumId))
|
||||
},
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
@@ -2242,7 +2315,7 @@ fun NavGraphBuilder.addAlbumOptions(
|
||||
route = Screen.AlbumOptions.route,
|
||||
viewState = ModalBottomSheetViewState(dismissOnAction = false),
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Files.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.AlbumOptions.SHARE_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.AlbumOptions.ALBUM_ID) { type = NavType.StringType },
|
||||
),
|
||||
@@ -2270,6 +2343,11 @@ fun NavGraphBuilder.addAlbumOptions(
|
||||
popUpTo(Screen.AlbumOptions.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
navigateToLeave = { albumId ->
|
||||
navController.navigate(Screen.Album.Dialogs.ConfirmLeaveAlbum(albumId)) {
|
||||
popUpTo(Screen.AlbumOptions.route) { inclusive = true }
|
||||
}
|
||||
},
|
||||
dismiss = {
|
||||
navController.popBackStack(
|
||||
route = Screen.AlbumOptions.route,
|
||||
@@ -2283,7 +2361,7 @@ fun NavGraphBuilder.addAlbumOptions(
|
||||
fun NavGraphBuilder.addConfirmDeleteAlbumDialog(navController: NavHostController) = dialog(
|
||||
route = Screen.Album.Dialogs.ConfirmDeleteAlbum.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Files.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Album.Dialogs.ConfirmDeleteAlbum.ALBUM_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Album.Dialogs.ConfirmDeleteAlbum.SHARE_ID) { type = NavType.StringType },
|
||||
),
|
||||
@@ -2299,8 +2377,8 @@ fun NavGraphBuilder.addConfirmDeleteAlbumDialog(navController: NavHostController
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addPickerPhotosAndAlbums(navController: NavHostController) = composable(
|
||||
route = Screen.Picker.PhotosAndAlbums.route,
|
||||
fun NavGraphBuilder.addPickerPhotos(navController: NavHostController) = composable(
|
||||
route = Screen.Picker.Photos.route,
|
||||
enterTransition = defaultEnterSlideTransition { true },
|
||||
exitTransition = { ExitTransition.None },
|
||||
popEnterTransition = { EnterTransition.None },
|
||||
@@ -2318,18 +2396,10 @@ fun NavGraphBuilder.addPickerPhotosAndAlbums(navController: NavHostController) =
|
||||
},
|
||||
),
|
||||
) {
|
||||
PickerPhotosAndAlbumsScreen(
|
||||
navigateToAlbum = { albumId, destinationAlbumId ->
|
||||
val route = if (destinationAlbumId != null) {
|
||||
Screen.Picker.Album(albumId, destinationAlbumId)
|
||||
} else {
|
||||
Screen.Picker.Album(albumId)
|
||||
}
|
||||
navController.navigate(route)
|
||||
},
|
||||
PickerPhotosScreen(
|
||||
navigateBack = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Picker.PhotosAndAlbums.route,
|
||||
route = Screen.Picker.Photos.route,
|
||||
inclusive = true,
|
||||
)
|
||||
}
|
||||
@@ -2367,13 +2437,73 @@ fun NavGraphBuilder.addPickerAlbum(navController: NavHostController) = composabl
|
||||
},
|
||||
onAddToAlbumDone = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Picker.PhotosAndAlbums.route,
|
||||
route = Screen.Picker.Photos.route,
|
||||
inclusive = true,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
fun NavGraphBuilder.addConfirmLeaveAlbumDialog(navController: NavHostController) = dialog(
|
||||
route = Screen.Album.Dialogs.ConfirmLeaveAlbum.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Album.Dialogs.ConfirmLeaveAlbum.ALBUM_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.Album.Dialogs.ConfirmLeaveAlbum.SHARE_ID) { type = NavType.StringType },
|
||||
),
|
||||
) {
|
||||
ConfirmLeaveAlbumDialog(
|
||||
onDismiss = {
|
||||
navController.popBackStack(
|
||||
route = Screen.Album.Dialogs.ConfirmLeaveAlbum.route,
|
||||
inclusive = true,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun NavGraphBuilder.addShareMultiplePhotosOptions(
|
||||
navController: NavHostController,
|
||||
) = modalBottomSheet(
|
||||
route = Screen.ShareMultiplePhotosOptions.route,
|
||||
viewState = ModalBottomSheetViewState(dismissOnAction = false),
|
||||
arguments = listOf(
|
||||
navArgument(Screen.ShareMultiplePhotosOptions.USER_ID) { type = NavType.StringType },
|
||||
navArgument(Screen.ShareMultiplePhotosOptions.SELECTION_ID) { type = NavType.StringType },
|
||||
),
|
||||
) { navBackStackEntry, runAction ->
|
||||
val userId = UserId(navBackStackEntry.require(Screen.ShareMultiplePhotosOptions.USER_ID))
|
||||
ShareMultiplePhotosOptions(
|
||||
runAction = runAction,
|
||||
navigateToCreateNewAlbum = {
|
||||
navController.navigate(
|
||||
Screen.PhotosAndAlbums.CreateNewAlbum(
|
||||
userId = userId,
|
||||
sharedAlbum = true,
|
||||
)
|
||||
)
|
||||
},
|
||||
navigateToAlbum = { albumId ->
|
||||
navController.navigate(Screen.Album(albumId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
fun NavGraphBuilder.addPhotosImportantUpdates(
|
||||
navController: NavHostController,
|
||||
) = modalBottomSheet(
|
||||
route = Screen.Photos.ImportantUpdates.route,
|
||||
arguments = listOf(
|
||||
navArgument(Screen.Photos.USER_ID) { type = NavType.StringType },
|
||||
),
|
||||
) { _, runAction ->
|
||||
PhotosImportantUpdates(
|
||||
runAction = runAction,
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun CoroutineScope.announceScreen(
|
||||
announceEvent: AnnounceEvent,
|
||||
primaryAccount: Flow<Account?>,
|
||||
|
||||
@@ -48,6 +48,7 @@ import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
@@ -62,8 +63,9 @@ fun HomeNavGraph(
|
||||
startDestination: String,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPreview: (fileId: FileId, pagerType: PagerType) -> Unit,
|
||||
navigateToPreviewWithTag: (fileId: FileId, pagerType: PagerType, photoTag: PhotoTag) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId, SelectionId?) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
@@ -73,9 +75,10 @@ fun HomeNavGraph(
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
) = DriveNavHost(
|
||||
navController = homeNavController,
|
||||
startDestination = startDestination
|
||||
@@ -87,7 +90,7 @@ fun HomeNavGraph(
|
||||
homeScaffoldState,
|
||||
{ fileId -> navigateToPreview(fileId, PagerType.FOLDER) },
|
||||
navigateToSorting,
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId) },
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId, null) },
|
||||
{ selectionId -> navigateToMultipleFileOrFolderOptions(selectionId) },
|
||||
navigateToParentFolderOptions,
|
||||
)
|
||||
@@ -97,8 +100,14 @@ fun HomeNavGraph(
|
||||
arguments,
|
||||
homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale,
|
||||
navigateToPhotosPreview = { fileId -> navigateToPreview(fileId, PagerType.PHOTO) },
|
||||
navigateToPhotosOptions = { fileId -> navigateToFileOrFolderOptions(fileId) },
|
||||
navigateToPhotosPreview = { fileId, photoTag ->
|
||||
if (photoTag == null) {
|
||||
navigateToPreview(fileId, PagerType.PHOTO)
|
||||
} else {
|
||||
navigateToPreviewWithTag(fileId, PagerType.PHOTO, photoTag)
|
||||
}
|
||||
},
|
||||
navigateToPhotosOptions = { fileId, selectionId -> navigateToFileOrFolderOptions(fileId, selectionId) },
|
||||
navigateToMultiplePhotosOptions = { selectionId ->
|
||||
navigateToMultipleFileOrFolderOptions(selectionId)
|
||||
},
|
||||
@@ -107,6 +116,7 @@ fun HomeNavGraph(
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
)
|
||||
addPhotosAndAlbums(
|
||||
homeNavController,
|
||||
@@ -114,8 +124,14 @@ fun HomeNavGraph(
|
||||
arguments,
|
||||
homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale,
|
||||
navigateToPhotosPreview = { fileId -> navigateToPreview(fileId, PagerType.PHOTO) },
|
||||
navigateToPhotosOptions = { fileId -> navigateToFileOrFolderOptions(fileId) },
|
||||
navigateToPhotosPreview = { fileId, photoTag ->
|
||||
if (photoTag == null) {
|
||||
navigateToPreview(fileId, PagerType.PHOTO)
|
||||
} else {
|
||||
navigateToPreviewWithTag(fileId, PagerType.PHOTO, photoTag)
|
||||
}
|
||||
},
|
||||
navigateToPhotosOptions = { fileId, selectionId -> navigateToFileOrFolderOptions(fileId, selectionId) },
|
||||
navigateToMultiplePhotosOptions = { selectionId ->
|
||||
navigateToMultipleFileOrFolderOptions(selectionId)
|
||||
},
|
||||
@@ -126,6 +142,8 @@ fun HomeNavGraph(
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
)
|
||||
addComputers(
|
||||
homeNavController,
|
||||
@@ -134,7 +152,7 @@ fun HomeNavGraph(
|
||||
homeScaffoldState,
|
||||
{ fileId -> navigateToPreview(fileId, PagerType.FOLDER) },
|
||||
navigateToSorting,
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId) },
|
||||
{ linkId -> navigateToFileOrFolderOptions(linkId, null) },
|
||||
{ selectionId -> navigateToMultipleFileOrFolderOptions(selectionId) },
|
||||
navigateToParentFolderOptions,
|
||||
navigateToComputerOptions,
|
||||
@@ -146,8 +164,9 @@ fun HomeNavGraph(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFolderPreview = { fileId -> navigateToPreview(fileId, PagerType.FOLDER) },
|
||||
navigateToSinglePreview = { fileId -> navigateToPreview(fileId, PagerType.SINGLE) },
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToSorting = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = { linkId -> navigateToFileOrFolderOptions(linkId) },
|
||||
navigateToFileOrFolderOptions = { linkId -> navigateToFileOrFolderOptions(linkId, null) },
|
||||
navigateToMultipleFileOrFolderOptions = { selectionId ->
|
||||
navigateToMultipleFileOrFolderOptions(selectionId)
|
||||
},
|
||||
@@ -165,7 +184,7 @@ fun NavGraphBuilder.addFiles(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPreview: (linkId: FileId) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
) = composable(
|
||||
@@ -237,14 +256,15 @@ fun NavGraphBuilder.addPhotos(
|
||||
arguments: Bundle,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId, photoTag: PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
) = composable(
|
||||
route = Screen.Photos.route,
|
||||
arguments = listOf(
|
||||
@@ -271,6 +291,7 @@ fun NavGraphBuilder.addPhotos(
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
)
|
||||
} ?: let {
|
||||
val userId = UserId(requireNotNull(arguments.getString(Screen.Photos.USER_ID)))
|
||||
@@ -292,8 +313,8 @@ fun NavGraphBuilder.addPhotosAndAlbums(
|
||||
arguments: Bundle,
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId, PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
@@ -302,6 +323,8 @@ fun NavGraphBuilder.addPhotosAndAlbums(
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
) = composable(
|
||||
route = Screen.PhotosAndAlbums.route,
|
||||
arguments = listOf(
|
||||
@@ -325,6 +348,8 @@ fun NavGraphBuilder.addPhotosAndAlbums(
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
)
|
||||
} ?: let {
|
||||
val userId = UserId(requireNotNull(arguments.getString(Screen.PhotosAndAlbums.USER_ID)))
|
||||
@@ -481,11 +506,12 @@ fun NavGraphBuilder.addSharedTabs(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToFolderPreview: (fileId: FileId) -> Unit,
|
||||
navigateToSinglePreview: (fileId: FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
) = composable(
|
||||
route = Screen.SharedTabs.route,
|
||||
enterTransition = defaultEnterSlideTransition {
|
||||
@@ -560,6 +586,7 @@ fun NavGraphBuilder.addSharedTabs(
|
||||
},
|
||||
navigateToPreview = navigateToSinglePreview,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,16 +30,21 @@ import me.proton.android.drive.ui.viewmodel.AlbumOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.AlbumViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ComputerOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ConfirmDeleteAlbumDialogViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ConfirmLeaveAlbumDialogViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ConfirmStopAllSharingDialogViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.CreateNewAlbumViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.FileOrFolderOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.MoveToFolderViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.MultipleFileOrFolderOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ParentFolderOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.PhotosPickerAndSelectionViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.PickerPhotosAndAlbumsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.PickerPhotosViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ShareInvitationOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ShareMemberOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.SubscriptionPromoViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.ShareMultiplePhotosOptionsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.UploadToViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.UserInvitationViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.WhatsNewViewModel
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
@@ -53,6 +58,7 @@ import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.domain.extension.userId
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.notification.presentation.viewmodel.NotificationPermissionRationaleViewModel
|
||||
@@ -114,23 +120,33 @@ sealed class Screen(val route: String) {
|
||||
}
|
||||
|
||||
data object FileOrFolderOptions : Screen(
|
||||
"options/link/{userId}/shares/{shareId}/linkId={linkId}?albumShareId={albumShareId}&albumId={albumId}"
|
||||
"options/link/{userId}/shares/{shareId}/linkId={linkId}?albumShareId={albumShareId}&albumId={albumId}&selectionId={selectionId}"
|
||||
) {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
linkId: LinkId,
|
||||
albumId: AlbumId? = null
|
||||
albumId: AlbumId? = null,
|
||||
selectionId: SelectionId? = null,
|
||||
) = buildString {
|
||||
append("options/link/${userId.id}/shares/${linkId.shareId.id}/linkId=${linkId.id}")
|
||||
if (albumId != null) {
|
||||
append("?albumShareId=${albumId.shareId.id}&albumId=${albumId.id}")
|
||||
}
|
||||
if (selectionId != null) {
|
||||
if (albumId != null) {
|
||||
append("&")
|
||||
} else {
|
||||
append("?")
|
||||
}
|
||||
append("selectionId=${selectionId.id}")
|
||||
}
|
||||
}
|
||||
|
||||
const val SHARE_ID = FileOrFolderOptionsViewModel.KEY_SHARE_ID
|
||||
const val LINK_ID = FileOrFolderOptionsViewModel.KEY_LINK_ID
|
||||
const val ALBUM_ID = FileOrFolderOptionsViewModel.KEY_ALBUM_ID
|
||||
const val KEY_ALBUM_SHARE_ID = FileOrFolderOptionsViewModel.KEY_ALBUM_SHARE_ID
|
||||
const val ALBUM_SHARE_ID = FileOrFolderOptionsViewModel.KEY_ALBUM_SHARE_ID
|
||||
const val SELECTION_ID = FileOrFolderOptionsViewModel.KEY_SELECTION_ID
|
||||
}
|
||||
|
||||
data object MultipleFileOrFolderOptions : Screen(
|
||||
@@ -196,6 +212,18 @@ sealed class Screen(val route: String) {
|
||||
const val ALBUM_ID = AlbumOptionsViewModel.KEY_ALBUM_ID
|
||||
}
|
||||
|
||||
data object ShareMultiplePhotosOptions : Screen(
|
||||
"options/multiple/sharePhotos/{userId}/selectionId={selectionId}"
|
||||
) {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
selectionId: SelectionId,
|
||||
) = "options/multiple/sharePhotos/${userId.id}/selectionId=${selectionId.id}"
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val SELECTION_ID = ShareMultiplePhotosOptionsViewModel.SELECTION_ID
|
||||
}
|
||||
|
||||
data object Info : Screen("info/{userId}/shares/{shareId}/files?linkId={linkId}") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
@@ -351,6 +379,10 @@ sealed class Screen(val route: String) {
|
||||
operator fun invoke(userId: UserId) = "home/${userId.id}/photos/upsell"
|
||||
}
|
||||
|
||||
data object ImportantUpdates : Screen("home/{userId}/photos/importantUpdates") {
|
||||
operator fun invoke(userId: UserId) = "home/${userId.id}/photos/importantUpdates"
|
||||
}
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val SHARE_ID = "shareId"
|
||||
}
|
||||
@@ -358,11 +390,15 @@ sealed class Screen(val route: String) {
|
||||
|
||||
override fun invoke(userId: UserId) = "home/${userId.id}/photos"
|
||||
|
||||
data object CreateNewAlbum : Screen("home/{userId}/photos/new_album") {
|
||||
operator fun invoke(userId: UserId) = "home/${userId.id}/photos/new_album"
|
||||
data object CreateNewAlbum : Screen("home/{userId}/photos/new_album?sharedAlbum={sharedAlbum}") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
sharedAlbum: Boolean = false,
|
||||
) = "home/${userId.id}/photos/new_album?sharedAlbum=${sharedAlbum}"
|
||||
}
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val SHARED_ALBUM = CreateNewAlbumViewModel.SHARED_ALBUM
|
||||
}
|
||||
|
||||
data object Album : Screen("photos/{userId}/shares/{shareId}/albums/{albumId}") {
|
||||
@@ -371,12 +407,20 @@ sealed class Screen(val route: String) {
|
||||
|
||||
object Dialogs {
|
||||
data object ConfirmDeleteAlbum : Screen("delete/photos/{userId}/shares/{shareId}/albums/{albumId}/confirm") {
|
||||
operator fun invoke(albumId: LinkId) =
|
||||
operator fun invoke(albumId: AlbumId) =
|
||||
"delete/photos/${albumId.userId.id}/shares/${albumId.shareId.id}/albums/${albumId.id}/confirm"
|
||||
|
||||
const val SHARE_ID = ConfirmDeleteAlbumDialogViewModel.SHARE_ID
|
||||
const val ALBUM_ID = ConfirmDeleteAlbumDialogViewModel.ALBUM_ID
|
||||
}
|
||||
|
||||
data object ConfirmLeaveAlbum : Screen("leave/photos/{userId}/shares/{shareId}/albums/{albumId}/confirm") {
|
||||
operator fun invoke(albumId: AlbumId) =
|
||||
"leave/photos/${albumId.userId.id}/shares/${albumId.shareId.id}/albums/${albumId.id}/confirm"
|
||||
|
||||
const val SHARE_ID = ConfirmLeaveAlbumDialogViewModel.SHARE_ID
|
||||
const val ALBUM_ID = ConfirmLeaveAlbumDialogViewModel.ALBUM_ID
|
||||
}
|
||||
}
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
@@ -385,7 +429,7 @@ sealed class Screen(val route: String) {
|
||||
}
|
||||
|
||||
object Picker {
|
||||
data object PhotosAndAlbums : Screen("picker/{userId}/photos/destination?inPickerMode={inPickerMode}&destinationShareId={destinationShareId}&destinationAlbumId={destinationAlbumId}") {
|
||||
data object Photos : Screen("picker/{userId}/photos/destination?inPickerMode={inPickerMode}&destinationShareId={destinationShareId}&destinationAlbumId={destinationAlbumId}") {
|
||||
operator fun invoke(userId: UserId) = "picker/${userId.id}/photos/destination?inPickerMode=true"
|
||||
|
||||
operator fun invoke(destinationAlbumId: AlbumId) =
|
||||
@@ -403,8 +447,8 @@ sealed class Screen(val route: String) {
|
||||
const val IN_PICKER_MODE = PhotosPickerAndSelectionViewModel.IN_PICKER_MODE
|
||||
const val SHARE_ID = AlbumViewModel.SHARE_ID
|
||||
const val ALBUM_ID = AlbumViewModel.ALBUM_ID
|
||||
const val DESTINATION_SHARE_ID = PickerPhotosAndAlbumsViewModel.DESTINATION_SHARE_ID
|
||||
const val DESTINATION_ALBUM_ID = PickerPhotosAndAlbumsViewModel.DESTINATION_ALBUM_ID
|
||||
const val DESTINATION_SHARE_ID = PickerPhotosViewModel.DESTINATION_SHARE_ID
|
||||
const val DESTINATION_ALBUM_ID = PickerPhotosViewModel.DESTINATION_ALBUM_ID
|
||||
}
|
||||
|
||||
data object BackupIssues : Screen("backup/issues/{userId}/shares/{shareId}/folder/{folderId}") {
|
||||
@@ -462,12 +506,18 @@ sealed class Screen(val route: String) {
|
||||
filesBrowsableBuildRoute("offline", userId, folderId, folderName)
|
||||
}
|
||||
|
||||
data object PagerPreview : Screen("pager/{pagerType}/preview/{userId}/shares/{shareId}/files/{fileId}?albumShareId={albumShareId}&albumId={albumId}") {
|
||||
data object PagerPreview : Screen("pager/{pagerType}/preview/{userId}/shares/{shareId}/files/{fileId}?photoTag={photoTag}&albumShareId={albumShareId}&albumId={albumId}") {
|
||||
operator fun invoke(
|
||||
pagerType: PagerType,
|
||||
userId: UserId,
|
||||
fileId: FileId,
|
||||
) = "pager/${pagerType.type}/preview/${userId.id}/shares/${fileId.shareId.id}/files/${fileId.id}"
|
||||
operator fun invoke(
|
||||
pagerType: PagerType,
|
||||
userId: UserId,
|
||||
fileId: FileId,
|
||||
photoTag: PhotoTag,
|
||||
) = "pager/${pagerType.type}/preview/${userId.id}/shares/${fileId.shareId.id}/files/${fileId.id}?photoTag=${photoTag.value}"
|
||||
operator fun invoke(
|
||||
pagerType: PagerType,
|
||||
userId: UserId,
|
||||
@@ -480,6 +530,7 @@ sealed class Screen(val route: String) {
|
||||
const val FILE_ID = "fileId"
|
||||
const val ALBUM_SHARE_ID = "albumShareId"
|
||||
const val ALBUM_ID = "albumId"
|
||||
const val PHOTO_TAG = "photoTag"
|
||||
const val PAGER_TYPE = "pagerType"
|
||||
}
|
||||
|
||||
@@ -646,12 +697,14 @@ sealed class Screen(val route: String) {
|
||||
const val MEMBER_ID = ShareMemberOptionsViewModel.KEY_MEMBER_ID
|
||||
}
|
||||
|
||||
data object UserInvitation : Screen("shareViaInvitations/{userId}/invitations") {
|
||||
data object UserInvitation : Screen("shareViaInvitations/{userId}/invitations?albumsOnly={albumsOnly}") {
|
||||
operator fun invoke(
|
||||
userId: UserId,
|
||||
) = "shareViaInvitations/${userId.id}/invitations"
|
||||
albumsOnly: Boolean = false,
|
||||
) = "shareViaInvitations/${userId.id}/invitations?albumsOnly=$albumsOnly"
|
||||
|
||||
const val USER_ID = Screen.USER_ID
|
||||
const val ALBUMS_ONLY = UserInvitationViewModel.ALBUMS_ONLY
|
||||
}
|
||||
|
||||
data object ShareLinkPermissions : Screen("shareViaLink/{userId}/shares/{shareId}/linkId/{linkId}/permissions") {
|
||||
@@ -788,6 +841,12 @@ sealed class Screen(val route: String) {
|
||||
}
|
||||
}
|
||||
}
|
||||
data object Promo {
|
||||
data object Subscription : Screen("home/{userId}/promo/subscription?key={key}"){
|
||||
operator fun invoke(userId: UserId, key: String) = "home/${userId.id}/promo/subscription?key=$key"
|
||||
const val PROMO_KEY = SubscriptionPromoViewModel.PROMO_KEY
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavHostController.navigate(screen: Screen, builder: NavOptionsBuilder.() -> Unit) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import me.proton.android.drive.ui.common.FolderEntry
|
||||
import me.proton.android.drive.ui.common.folderEntry
|
||||
import me.proton.core.compose.component.bottomsheet.RunAction
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.extension.isViewerOrEditorOnly
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.hasShareLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isPhoto
|
||||
@@ -34,6 +35,7 @@ import me.proton.core.drive.files.presentation.entry.DownloadEntry
|
||||
import me.proton.core.drive.files.presentation.entry.DownloadFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.FileInfoEntry
|
||||
import me.proton.core.drive.files.presentation.entry.FileOptionEntry
|
||||
import me.proton.core.drive.files.presentation.entry.LeaveAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ManageAccessEntry
|
||||
import me.proton.core.drive.files.presentation.entry.MoveEntry
|
||||
import me.proton.core.drive.files.presentation.entry.MoveFileEntry
|
||||
@@ -42,9 +44,12 @@ import me.proton.core.drive.files.presentation.entry.RemoveFromAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.RemoveFromAlbumFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.RemoveMeEntry
|
||||
import me.proton.core.drive.files.presentation.entry.RenameFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.SaveSharePhotoEntry
|
||||
import me.proton.core.drive.files.presentation.entry.SendFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.SetAsAlbumCoverEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ShareMultiplePhotosEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ShareViaInvitationsEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleFavoriteFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleOfflineEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleTrashEntry
|
||||
import me.proton.core.drive.files.presentation.entry.TrashEntry
|
||||
@@ -54,6 +59,7 @@ import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.extension.isProtonCloudFile
|
||||
import me.proton.core.drive.link.domain.extension.isSharedUrlExpired
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
|
||||
@@ -84,7 +90,7 @@ sealed class Option(
|
||||
data object CreateFolder : Option(
|
||||
ApplicableQuantity.Single,
|
||||
setOf(ApplicableTo.FOLDER),
|
||||
setOf(State.NOT_TRASHED, State.SHARED, State.NOT_SHARED),
|
||||
setOf(State.NOT_TRASHED) + State.ANY_SHARED,
|
||||
) {
|
||||
fun build(
|
||||
runAction: RunAction,
|
||||
@@ -143,6 +149,19 @@ sealed class Option(
|
||||
}
|
||||
}
|
||||
|
||||
data object FavoriteToggle : Option(
|
||||
ApplicableQuantity.Single,
|
||||
setOf(ApplicableTo.FILE_PHOTO),
|
||||
setOf(State.NOT_TRASHED) + State.ANY_SHARED,
|
||||
) {
|
||||
fun build(
|
||||
runAction: RunAction,
|
||||
toggleFavorite: suspend (DriveLink.File) -> Unit,
|
||||
) = ToggleFavoriteFileEntry { driveLink ->
|
||||
runAction { toggleFavorite(driveLink) }
|
||||
} as FileOptionEntry<DriveLink>
|
||||
}
|
||||
|
||||
data object Info : Option(
|
||||
ApplicableQuantity.Single,
|
||||
setOf(ApplicableTo.FOLDER) + ApplicableTo.ANY_FILE,
|
||||
@@ -240,6 +259,22 @@ sealed class Option(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
data object SaveSharePhoto : Option(
|
||||
ApplicableQuantity.Single,
|
||||
setOf(
|
||||
ApplicableTo.FILE_PHOTO,
|
||||
),
|
||||
setOf(State.NOT_TRASHED) + State.ANY_SHARED,
|
||||
) {
|
||||
fun build(
|
||||
runAction: RunAction,
|
||||
saveSharePhoto: (link: DriveLink.File) -> Unit,
|
||||
) = SaveSharePhotoEntry { driveLink ->
|
||||
runAction { saveSharePhoto(driveLink) }
|
||||
} as FileOptionEntry<DriveLink>
|
||||
}
|
||||
|
||||
data object SendFile : Option(
|
||||
ApplicableQuantity.Single,
|
||||
ApplicableTo.ANY_DOWNLOADABLE_FILE,
|
||||
@@ -336,6 +371,19 @@ sealed class Option(
|
||||
}
|
||||
}
|
||||
|
||||
data object ShareMultiplePhotos : Option(
|
||||
ApplicableQuantity.All,
|
||||
setOf(ApplicableTo.FILE_PHOTO),
|
||||
setOf(State.NOT_TRASHED) + State.ANY_SHARED,
|
||||
) {
|
||||
fun build(
|
||||
runAction: RunAction,
|
||||
navigateToShareMultiplePhotosOptions: () -> Unit,
|
||||
) = ShareMultiplePhotosEntry {
|
||||
runAction { navigateToShareMultiplePhotosOptions() }
|
||||
}
|
||||
}
|
||||
|
||||
data object CreateAlbum : Option(
|
||||
ApplicableQuantity.All,
|
||||
setOf(ApplicableTo.FILE_PHOTO),
|
||||
@@ -385,6 +433,21 @@ sealed class Option(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data object LeaveAlbum : Option(
|
||||
ApplicableQuantity.Single,
|
||||
setOf(ApplicableTo.ALBUM),
|
||||
setOf(State.NOT_TRASHED) + State.ANY_SHARED,
|
||||
) {
|
||||
fun build(
|
||||
runAction: RunAction,
|
||||
leaveAlbum: suspend (DriveLink.Album) -> Unit,
|
||||
) = LeaveAlbumEntry { album ->
|
||||
runAction {
|
||||
leaveAlbum(album)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ApplicableQuantity(open val quantity: Long) {
|
||||
@@ -460,6 +523,7 @@ fun Iterable<Option>.filterRoot(driveLink: DriveLink, featureFlag: FeatureFlag)
|
||||
fun Iterable<Option>.filterShareMember(isMember: Boolean) = filter { option ->
|
||||
if (!isMember) {
|
||||
when (option) {
|
||||
Option.LeaveAlbum -> false
|
||||
Option.RemoveMe -> false
|
||||
else -> true
|
||||
}
|
||||
@@ -479,16 +543,20 @@ fun Iterable<Option>.filterPermissions(
|
||||
Option.DeletePermanently -> permissions.canWrite
|
||||
Option.Download -> permissions.canRead
|
||||
Option.Info -> permissions.canRead
|
||||
Option.LeaveAlbum -> permissions.canRead
|
||||
Option.ManageAccess -> permissions.isAdmin
|
||||
Option.Move -> permissions.canWrite
|
||||
Option.OfflineToggle -> permissions.canRead
|
||||
Option.FavoriteToggle -> permissions.canWrite
|
||||
Option.OpenInBrowser -> permissions.canRead
|
||||
Option.Rename -> permissions.canWrite
|
||||
Option.RemoveFromAlbum -> permissions.isAdmin
|
||||
Option.RemoveFromAlbum -> permissions.canWrite
|
||||
Option.RemoveMe -> permissions.canRead
|
||||
Option.SaveSharePhoto -> permissions.isViewerOrEditorOnly
|
||||
Option.SendFile -> permissions.canRead
|
||||
Option.SetAsAlbumCover -> permissions.isAdmin
|
||||
Option.ShareViaInvitations -> permissions.isAdmin
|
||||
Option.ShareMultiplePhotos -> permissions.isAdmin
|
||||
Option.TakeAPhoto -> permissions.canWrite
|
||||
Option.Trash -> permissions.isAdmin
|
||||
Option.UploadFile -> permissions.canWrite
|
||||
@@ -504,15 +572,40 @@ fun Iterable<Option>.filterProtonDocs(killSwitch: FeatureFlag) = filter { option
|
||||
}
|
||||
|
||||
fun Iterable<Option>.filterAlbums(
|
||||
featureFlagOn: Boolean,
|
||||
isEnabled: Boolean,
|
||||
killSwitch: FeatureFlag,
|
||||
albumId: AlbumId? = null,
|
||||
) = filter { option ->
|
||||
val featureEnabled = featureFlagOn && killSwitch.off
|
||||
val featureEnabled = isEnabled && killSwitch.off
|
||||
when (option) {
|
||||
Option.CreateAlbum -> featureEnabled
|
||||
Option.CreateAlbum -> featureEnabled && albumId == null
|
||||
Option.ShareMultiplePhotos -> featureEnabled && albumId == null
|
||||
Option.RemoveFromAlbum -> featureEnabled && albumId != null
|
||||
Option.SetAsAlbumCover -> featureEnabled && albumId != null
|
||||
Option.Trash -> albumId == null
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun Iterable<Option>.filterPhotoFavorite(
|
||||
isEnabled: Boolean,
|
||||
killSwitch: FeatureFlag,
|
||||
) = filter { option ->
|
||||
val featureEnabled = isEnabled && killSwitch.off
|
||||
when (option) {
|
||||
Option.FavoriteToggle -> featureEnabled
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun Iterable<Option>.filterShare(
|
||||
shareTempDisabledOn: Boolean,
|
||||
albumId: AlbumId? = null,
|
||||
) = filter { option ->
|
||||
when (option) {
|
||||
Option.ShareViaInvitations -> albumId == null && !shareTempDisabledOn
|
||||
Option.ManageAccess -> albumId == null && !shareTempDisabledOn
|
||||
Option.ShareMultiplePhotos -> !shareTempDisabledOn
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,10 +41,12 @@ import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
@Composable
|
||||
fun AlbumScreen(
|
||||
navigateToAlbumOptions: (AlbumId) -> Unit,
|
||||
navigateToPhotosOptions: (FileId, AlbumId?) -> Unit,
|
||||
navigateToPhotosOptions: (FileId, AlbumId?, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId, AlbumId?) -> Unit,
|
||||
navigateToPreview: (FileId, AlbumId) -> Unit,
|
||||
navigateToPicker: (AlbumId) -> Unit,
|
||||
navigateToShareViaInvitations: (AlbumId) -> Unit,
|
||||
navigateToManageAccess: (AlbumId) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
@@ -58,6 +60,8 @@ fun AlbumScreen(
|
||||
navigateToMultiplePhotosOptions = navigateToMultiplePhotosOptions,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToPicker = navigateToPicker,
|
||||
navigateToShareViaInvitations = navigateToShareViaInvitations,
|
||||
navigateToManageAccess = navigateToManageAccess,
|
||||
navigateBack = navigateBack,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
|
||||
@@ -46,6 +46,7 @@ import me.proton.android.drive.photos.presentation.state.PhotosItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.CreateNewAlbumViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewstate.CreateNewAlbumViewState
|
||||
import me.proton.android.drive.ui.viewmodel.CreateNewAlbumViewModel
|
||||
import me.proton.core.compose.activity.KeepScreenOn
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.component.protonTextButtonColors
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
@@ -95,6 +96,9 @@ fun CreateNewAlbumScreen(
|
||||
driveLinksFlow: Flow<Map<LinkId, DriveLink>>,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (viewState.isCreationInProgress) {
|
||||
KeepScreenOn()
|
||||
}
|
||||
Column(
|
||||
modifier = modifier,
|
||||
) {
|
||||
@@ -136,7 +140,7 @@ fun TopAppBar(
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = I18N.string.common_done_action),
|
||||
text = stringResource(id = viewState.doneButtonLabelResId),
|
||||
style = ProtonTheme.typography.headlineSmallNorm,
|
||||
color = ProtonTheme.colors.interactionNorm(viewState.isDoneEnabled),
|
||||
)
|
||||
@@ -156,6 +160,7 @@ private fun CreateNewAlbumScreenPreview() {
|
||||
isAlbumNameEnabled = true,
|
||||
isAddEnabled = true,
|
||||
isRemoveEnabled = true,
|
||||
doneButtonLabelResId = I18N.string.common_done_action,
|
||||
name = emptyFlow(),
|
||||
hint = stringResource(I18N.string.albums_new_album_name_hint),
|
||||
),
|
||||
|
||||
@@ -67,10 +67,10 @@ import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawer
|
||||
import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.drive.android.settings.domain.entity.WhatsNewKey
|
||||
|
||||
@Composable
|
||||
@@ -88,9 +88,10 @@ fun HomeScreen(
|
||||
navigateToTrash: () -> Unit,
|
||||
navigateToOffline: () -> Unit,
|
||||
navigateToPreview: (fileId: FileId, pagerType: PagerType) -> Unit,
|
||||
navigateToPreviewWithTag: (fileId: FileId, pagerType: PagerType, photoTag: PhotoTag) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToSettings: () -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId, SelectionId?) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
@@ -104,9 +105,11 @@ fun HomeScreen(
|
||||
navigateToWhatsNew: (WhatsNewKey) -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToSubscriptionPromo: (String) -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
setLocalSnackbarPadding(BottomNavigationHeight)
|
||||
@@ -140,6 +143,7 @@ fun HomeScreen(
|
||||
navigateToOnboarding = navigateToOnboarding,
|
||||
navigateToWhatsNew = navigateToWhatsNew,
|
||||
navigateToRatingBooster = navigateToRatingBooster,
|
||||
navigateToSubscriptionPromo = navigateToSubscriptionPromo,
|
||||
)
|
||||
}
|
||||
viewState?.let { currentViewState ->
|
||||
@@ -149,6 +153,7 @@ fun HomeScreen(
|
||||
startDestination = startDestination,
|
||||
onDrawerStateChanged = onDrawerStateChanged,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToPreviewWithTag = navigateToPreviewWithTag,
|
||||
navigateToSorting = navigateToSorting,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToMultipleFileOrFolderOptions = navigateToMultipleFileOrFolderOptions,
|
||||
@@ -163,6 +168,7 @@ fun HomeScreen(
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
arguments = arguments,
|
||||
viewState = currentViewState,
|
||||
viewEvent = viewEvent,
|
||||
@@ -186,8 +192,9 @@ internal fun Home(
|
||||
startDestination: String,
|
||||
onDrawerStateChanged: (Boolean) -> Unit,
|
||||
navigateToPreview: (fileId: FileId, pagerType: PagerType) -> Unit,
|
||||
navigateToPreviewWithTag: (fileId: FileId, pagerType: PagerType, photoTag: PhotoTag) -> Unit,
|
||||
navigateToSorting: (sorting: Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId, SelectionId?) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToParentFolderOptions: (folderId: FolderId) -> Unit,
|
||||
arguments: Bundle,
|
||||
@@ -201,9 +208,10 @@ internal fun Home(
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToComputerOptions: (deviceId: DeviceId) -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
) {
|
||||
val homeScaffoldState = rememberHomeScaffoldState()
|
||||
val isDrawerOpen = with(homeScaffoldState.scaffoldState.drawerState) {
|
||||
@@ -288,6 +296,7 @@ internal fun Home(
|
||||
startDestination,
|
||||
homeScaffoldState,
|
||||
navigateToPreview,
|
||||
navigateToPreviewWithTag,
|
||||
navigateToSorting,
|
||||
navigateToFileOrFolderOptions,
|
||||
navigateToMultipleFileOrFolderOptions,
|
||||
@@ -302,6 +311,7 @@ internal fun Home(
|
||||
navigateToUserInvitation,
|
||||
navigateToCreateNewAlbum,
|
||||
navigateToAlbum,
|
||||
navigateToPhotosImportantUpdates,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import me.proton.android.drive.ui.viewmodel.OfflineViewModel
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.files.presentation.component.DriveLinksFlow
|
||||
import me.proton.core.drive.files.presentation.component.Files
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -43,6 +44,7 @@ import me.proton.core.drive.sorting.domain.entity.Sorting
|
||||
fun OfflineScreen(
|
||||
navigateToFiles: (folderId: FolderId, folderName: String?) -> Unit,
|
||||
navigateToPreview: (pagerType: PagerType, fileId: FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToSortingDialog: (Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
@@ -56,6 +58,7 @@ fun OfflineScreen(
|
||||
viewModel.viewEvent(
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToSortingDialog = navigateToSortingDialog,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateBack = navigateBack,
|
||||
|
||||
@@ -20,9 +20,14 @@ package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
@@ -41,6 +46,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -88,14 +94,16 @@ import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.base.presentation.component.Title
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.UserInvitationBanner
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Composable
|
||||
fun PhotosAndAlbumsScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId, photoTag: PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
@@ -104,6 +112,8 @@ fun PhotosAndAlbumsScreen(
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosAndAlbumsViewModel>()
|
||||
@@ -135,6 +145,7 @@ fun PhotosAndAlbumsScreen(
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToNotificationPermissionRationale = navigateToNotificationPermissionRationale,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
defaultTitle = defaultTitle,
|
||||
)
|
||||
|
||||
@@ -142,6 +153,7 @@ fun PhotosAndAlbumsScreen(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
defaultTitle = defaultTitle,
|
||||
)
|
||||
}
|
||||
@@ -153,6 +165,7 @@ fun AlbumsTab(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
defaultTitle: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
@@ -165,8 +178,12 @@ fun AlbumsTab(
|
||||
viewModel.viewEvent(
|
||||
navigateToCreateNewAlbum = navigateToCreateNewAlbum,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
}
|
||||
val userInvitationViewState by viewModel.userInvitationBannerViewState.collectAsStateWithLifecycle(
|
||||
initialValue = null
|
||||
)
|
||||
|
||||
viewModel.HandleHomeEffect(homeScaffoldState)
|
||||
|
||||
@@ -190,7 +207,25 @@ fun AlbumsTab(
|
||||
viewEvent = viewEvent,
|
||||
items = viewModel.albumItems,
|
||||
modifier = modifier.fillMaxSize(),
|
||||
)
|
||||
) {
|
||||
Box(Modifier.defaultMinSize(minHeight = 1.dp)) {
|
||||
// minHeight: always draw to have the header visible in the lazy list
|
||||
AnimatedVisibility(
|
||||
visible = userInvitationViewState != null,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
userInvitationViewState?.let { viewState ->
|
||||
UserInvitationBanner(
|
||||
description = viewState.description,
|
||||
showSectionHeader = false,
|
||||
onClick = viewEvent.onUserInvitation,
|
||||
modifier = Modifier.padding(bottom = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -198,14 +233,15 @@ fun PhotosTab(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
modifier: Modifier = Modifier,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId, photoTag: PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
defaultTitle: @Composable (Modifier) -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosViewModel>()
|
||||
@@ -221,6 +257,7 @@ fun PhotosTab(
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
@@ -236,6 +273,9 @@ fun PhotosTab(
|
||||
PhotosEffect.ShowUpsell -> launch(Dispatchers.Main) {
|
||||
viewEvent.onShowUpsell()
|
||||
}
|
||||
PhotosEffect.ShowImportantUpdates -> launch(Dispatchers.Main) {
|
||||
viewEvent.onShowImportantUpdates()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
@@ -334,23 +374,29 @@ fun Tabs(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TabItem(
|
||||
modifier = Modifier.padding(end = ProtonDimens.SmallSpacing),
|
||||
modifier = Modifier
|
||||
.testTag(PhotosAndAlbumsScreenTestTag.photosTitleTab)
|
||||
.padding(end = ProtonDimens.SmallSpacing),
|
||||
tab = Tab.PHOTOS,
|
||||
text = I18N.string.photos_title,
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
)
|
||||
VerticalDivider(
|
||||
modifier = Modifier.padding(vertical = ProtonDimens.DefaultSpacing),
|
||||
color = ProtonTheme.colors.separatorNorm,
|
||||
)
|
||||
TabItem(
|
||||
modifier = Modifier.padding(start = ProtonDimens.SmallSpacing),
|
||||
tab = Tab.ALBUMS,
|
||||
text = I18N.string.albums_title,
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
)
|
||||
if (viewState.isAlbumsTabVisible) {
|
||||
VerticalDivider(
|
||||
modifier = Modifier.padding(vertical = ProtonDimens.DefaultSpacing),
|
||||
color = ProtonTheme.colors.separatorNorm,
|
||||
)
|
||||
TabItem(
|
||||
modifier = Modifier
|
||||
.testTag(PhotosAndAlbumsScreenTestTag.albumsTitleTab)
|
||||
.padding(start = ProtonDimens.SmallSpacing),
|
||||
tab = Tab.ALBUMS,
|
||||
text = I18N.string.albums_title,
|
||||
viewState = viewState,
|
||||
viewEvent = viewEvent,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -403,8 +449,16 @@ fun TabsPreview() {
|
||||
ProtonTheme {
|
||||
Tabs(
|
||||
modifier = Modifier.height(ProtonDimens.DefaultButtonMinHeight),
|
||||
viewState = PhotosAndAlbumsViewState(selectedTab = Tab.PHOTOS),
|
||||
viewState = PhotosAndAlbumsViewState(
|
||||
selectedTab = Tab.PHOTOS,
|
||||
isAlbumsTabVisible = true,
|
||||
),
|
||||
viewEvent = object : PhotosAndAlbumsViewEvent {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object PhotosAndAlbumsScreenTestTag {
|
||||
const val photosTitleTab = "photos title tab"
|
||||
const val albumsTitleTab = "albums title tab"
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ import me.proton.core.drive.base.presentation.effect.ListEffect
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
|
||||
@Composable
|
||||
@@ -66,14 +67,15 @@ fun PhotosScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
modifier: Modifier = Modifier,
|
||||
navigateToPhotosPermissionRationale: () -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosPreview: (fileId: FileId, photoTag: PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToNotificationPermissionRationale: () -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PhotosViewModel>()
|
||||
val viewState by rememberFlowWithLifecycle(flow = viewModel.viewState)
|
||||
@@ -88,6 +90,7 @@ fun PhotosScreen(
|
||||
navigateToPhotosIssues = navigateToPhotosIssues,
|
||||
navigateToPhotosUpsell = navigateToPhotosUpsell,
|
||||
navigateToBackupSettings = navigateToBackupSettings,
|
||||
navigateToPhotosImportantUpdates = navigateToPhotosImportantUpdates,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
@@ -100,6 +103,9 @@ fun PhotosScreen(
|
||||
PhotosEffect.ShowUpsell -> launch(Dispatchers.Main) {
|
||||
viewEvent.onShowUpsell()
|
||||
}
|
||||
PhotosEffect.ShowImportantUpdates -> launch(Dispatchers.Main) {
|
||||
viewEvent.onShowImportantUpdates()
|
||||
}
|
||||
}
|
||||
}.launchIn(this)
|
||||
}
|
||||
|
||||
@@ -18,38 +18,22 @@
|
||||
|
||||
package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosUpsellViewEvent
|
||||
import me.proton.android.drive.ui.component.PromoContainer
|
||||
import me.proton.android.drive.ui.viewmodel.PhotosUpsellViewModel
|
||||
import me.proton.core.compose.component.ProtonSolidButton
|
||||
import me.proton.core.compose.component.ProtonTextButton
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
|
||||
import me.proton.core.drive.base.presentation.component.IllustratedMessage
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.base.presentation.extension.conditional
|
||||
import me.proton.core.drive.base.presentation.extension.isLandscape
|
||||
import me.proton.core.drive.base.presentation.extension.isPortrait
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
|
||||
@Composable
|
||||
fun PhotosUpsellScreen(
|
||||
@@ -70,70 +54,43 @@ fun PhotosUpsellScreen(
|
||||
}
|
||||
}
|
||||
PhotosUpsell(
|
||||
viewEvent = viewEvent,
|
||||
modifier = modifier
|
||||
.systemBarsPadding(),
|
||||
modifier = modifier,
|
||||
onMoreStorage = { viewEvent.onMoreStorage() },
|
||||
onCancel = { viewEvent.onCancel() },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PhotosUpsell(
|
||||
viewEvent: PhotosUpsellViewEvent,
|
||||
modifier: Modifier = Modifier,
|
||||
private fun PhotosUpsell(
|
||||
modifier: Modifier,
|
||||
onMoreStorage: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.padding(horizontal = ProtonDimens.DefaultSpacing),
|
||||
verticalArrangement = Arrangement.spacedBy(ProtonDimens.DefaultSpacing),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
IllustratedMessage(
|
||||
imageResId = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.img_upsell_drive_light,
|
||||
dark = BasePresentation.drawable.img_upsell_drive_dark,
|
||||
dayNight = BasePresentation.drawable.img_upsell_drive_daynight,
|
||||
),
|
||||
titleResId = I18N.string.photos_upsell_title,
|
||||
descriptionResId = I18N.string.photos_upsell_description,
|
||||
)
|
||||
val buttonModifier = Modifier
|
||||
.conditional(isPortrait) {
|
||||
fillMaxWidth()
|
||||
}
|
||||
.conditional(isLandscape) {
|
||||
widthIn(min = ButtonMinWidth)
|
||||
}
|
||||
.heightIn(min = ProtonDimens.ListItemHeight)
|
||||
ProtonSolidButton(
|
||||
onClick = { viewEvent.onMoreStorage() },
|
||||
modifier = buttonModifier,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = I18N.string.photos_upsell_get_storage_action),
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
ProtonTextButton(
|
||||
onClick = { viewEvent.onCancel() },
|
||||
modifier = buttonModifier
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(id = I18N.string.photos_upsell_dismiss_action),
|
||||
modifier = Modifier.padding(horizontal = ProtonDimens.DefaultSpacing)
|
||||
)
|
||||
}
|
||||
}
|
||||
PromoContainer(
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
titleResId = I18N.string.photos_upsell_title,
|
||||
descriptionResId = I18N.string.photos_upsell_description,
|
||||
imageResId = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.img_upsell_drive_light,
|
||||
dark = BasePresentation.drawable.img_upsell_drive_dark,
|
||||
dayNight = BasePresentation.drawable.img_upsell_drive_daynight,
|
||||
),
|
||||
actionText = stringResource(id = I18N.string.photos_upsell_get_storage_action),
|
||||
dismissActionText = stringResource(id = I18N.string.photos_upsell_dismiss_action),
|
||||
onAction = { onMoreStorage() },
|
||||
onCancel = { onCancel() },
|
||||
)
|
||||
}
|
||||
|
||||
private val ButtonMinWidth = 300.dp
|
||||
|
||||
@Preview
|
||||
@Preview(widthDp = 600, heightDp = 360)
|
||||
@Composable
|
||||
private fun PhotosUpsellPreview() {
|
||||
ProtonTheme {
|
||||
PhotosUpsell(
|
||||
viewEvent = object : PhotosUpsellViewEvent {},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onMoreStorage = {},
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.ui.viewmodel.PickerPhotosAndAlbumsViewModel
|
||||
import me.proton.android.drive.ui.viewmodel.PickerPhotosViewModel
|
||||
import me.proton.core.drive.base.presentation.extension.shadow
|
||||
|
||||
@Composable
|
||||
@@ -38,7 +38,7 @@ fun PickerAlbumScreen(
|
||||
onAddToAlbumDone: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PickerPhotosAndAlbumsViewModel>()
|
||||
val viewModel = hiltViewModel<PickerPhotosViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(initialValue = null)
|
||||
val lifecycle = LocalLifecycleOwner.current
|
||||
val viewEvent = remember(lifecycle) {
|
||||
@@ -77,10 +77,12 @@ fun PickerAlbumScreen(
|
||||
) {
|
||||
AlbumScreen(
|
||||
navigateToAlbumOptions = {},
|
||||
navigateToPhotosOptions = { _, _ -> },
|
||||
navigateToPhotosOptions = { _, _, _ -> },
|
||||
navigateToMultiplePhotosOptions = { _, _ -> },
|
||||
navigateToPreview = { _, _ -> },
|
||||
navigateToPicker = { _ -> },
|
||||
navigateToShareViaInvitations = {},
|
||||
navigateToManageAccess = {},
|
||||
navigateBack = onBack,
|
||||
)
|
||||
BottomActions(
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBarsPadding
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.photos.presentation.component.AddToAlbumButton
|
||||
import me.proton.android.drive.ui.viewmodel.PickerPhotosViewModel
|
||||
import me.proton.android.drive.ui.viewstate.rememberHomeScaffoldState
|
||||
import me.proton.core.compose.activity.KeepScreenOn
|
||||
import me.proton.core.compose.theme.ProtonDimens
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.extension.shadow
|
||||
import me.proton.core.drive.base.presentation.component.TopAppBar as BaseTopAppBar
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
|
||||
@Composable
|
||||
fun PickerPhotosScreen(
|
||||
navigateBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<PickerPhotosViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(initialValue = null)
|
||||
val lifecycle = LocalLifecycleOwner.current
|
||||
val viewEvent = remember(lifecycle) {
|
||||
viewModel.viewEvent(
|
||||
navigateBack = navigateBack,
|
||||
onAddToAlbumDone = navigateBack,
|
||||
)
|
||||
}
|
||||
viewState?.let { viewState ->
|
||||
PickerPhotosScreen(
|
||||
addToAlbumTitle = viewState.addToAlbumButtonTitle,
|
||||
isAddToAlbumButtonEnabled = viewState.isAddToAlbumButtonEnabled,
|
||||
isAddToAlbumInProgress = viewState.isAddingInProgress,
|
||||
isResetButtonEnabled = viewState.isResetButtonEnabled,
|
||||
onTopAppBarNavigationIcon = viewEvent.onBackPressed,
|
||||
onReset = viewEvent.onReset,
|
||||
onAddToAlbum = viewEvent.onAddToAlbum,
|
||||
modifier = modifier.navigationBarsPadding(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PickerPhotosScreen(
|
||||
addToAlbumTitle: String,
|
||||
isAddToAlbumButtonEnabled: Boolean,
|
||||
isAddToAlbumInProgress: Boolean,
|
||||
isResetButtonEnabled: Boolean,
|
||||
onTopAppBarNavigationIcon: () -> Unit,
|
||||
onReset: () -> Unit,
|
||||
onAddToAlbum: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
PickerPhotos(
|
||||
title = { titleModifier ->
|
||||
Text(
|
||||
modifier = titleModifier,
|
||||
text = stringResource(I18N.string.photos_title),
|
||||
)
|
||||
},
|
||||
onTopAppBarNavigationIcon = onTopAppBarNavigationIcon,
|
||||
onReset = onReset,
|
||||
onAddToAlbum = onAddToAlbum,
|
||||
addToAlbumTitle = addToAlbumTitle,
|
||||
isAddToAlbumButtonEnabled = isAddToAlbumButtonEnabled,
|
||||
isAddToAlbumInProgress = isAddToAlbumInProgress,
|
||||
isResetButtonEnabled = isResetButtonEnabled,
|
||||
modifier = modifier
|
||||
.testTag(PickerPhotosScreenTestTag.screen),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PickerPhotos(
|
||||
title: @Composable (Modifier) -> Unit,
|
||||
addToAlbumTitle: String,
|
||||
isAddToAlbumButtonEnabled: Boolean,
|
||||
isAddToAlbumInProgress: Boolean,
|
||||
isResetButtonEnabled: Boolean,
|
||||
onTopAppBarNavigationIcon: () -> Unit,
|
||||
onReset: () -> Unit,
|
||||
onAddToAlbum: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
if (isAddToAlbumInProgress) {
|
||||
KeepScreenOn()
|
||||
}
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
) {
|
||||
TopAppBar(
|
||||
title = title,
|
||||
onNavigationIcon = onTopAppBarNavigationIcon,
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
val homeScaffoldState = rememberHomeScaffoldState().apply {
|
||||
drawerGesturesEnabled.value = false
|
||||
bottomNavigationEnabled.value = false
|
||||
}
|
||||
PhotosTab(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToPhotosPermissionRationale = {},
|
||||
navigateToPhotosPreview = { _, _ -> },
|
||||
navigateToPhotosOptions = { _, _ -> },
|
||||
navigateToMultiplePhotosOptions = {},
|
||||
navigateToSubscription = {},
|
||||
navigateToPhotosIssues = {},
|
||||
navigateToPhotosUpsell = {},
|
||||
navigateToBackupSettings = {},
|
||||
navigateToNotificationPermissionRationale = {},
|
||||
navigateToPhotosImportantUpdates = {},
|
||||
defaultTitle = {},
|
||||
)
|
||||
BottomActions(
|
||||
addToAlbumTitle = addToAlbumTitle,
|
||||
isAddToAlbumButtonEnabled = isAddToAlbumButtonEnabled,
|
||||
isAddToAlbumInProgress = isAddToAlbumInProgress,
|
||||
isResetButtonEnabled = isResetButtonEnabled,
|
||||
onReset = onReset,
|
||||
onAddToAlbum = onAddToAlbum,
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
.shadow(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TopAppBar(
|
||||
title: @Composable (Modifier) -> Unit,
|
||||
onNavigationIcon: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
BaseTopAppBar(
|
||||
navigationIcon = painterResource(CorePresentation.drawable.ic_proton_close),
|
||||
onNavigationIcon = onNavigationIcon,
|
||||
title = title,
|
||||
modifier = modifier.statusBarsPadding(),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BottomActions(
|
||||
addToAlbumTitle: String,
|
||||
isAddToAlbumButtonEnabled: Boolean,
|
||||
isAddToAlbumInProgress: Boolean,
|
||||
isResetButtonEnabled: Boolean,
|
||||
onReset: () -> Unit,
|
||||
onAddToAlbum: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.padding(all = ProtonDimens.DefaultSpacing)
|
||||
.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
IconButton(
|
||||
modifier = Modifier
|
||||
.testTag(PickerPhotosScreenTestTag.resetButton)
|
||||
.border(1.dp, ProtonTheme.colors.separatorNorm, CircleShape)
|
||||
.background(ProtonTheme.colors.backgroundNorm, CircleShape),
|
||||
onClick = onReset,
|
||||
enabled = isResetButtonEnabled,
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = CorePresentation.drawable.ic_proton_close),
|
||||
contentDescription = null,
|
||||
modifier = modifier.padding(4.dp),
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.size(32.dp))
|
||||
AddToAlbumButton(
|
||||
addToAlbumTitle = addToAlbumTitle,
|
||||
modifier = Modifier
|
||||
.testTag(PickerPhotosScreenTestTag.addToAlbumButton)
|
||||
.widthIn(min = 200.dp),
|
||||
enabled = isAddToAlbumButtonEnabled,
|
||||
loading = isAddToAlbumInProgress,
|
||||
onClick = onAddToAlbum,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object PickerPhotosScreenTestTag {
|
||||
const val screen = "picker photos and albums screen"
|
||||
const val resetButton = "reset button"
|
||||
const val addToAlbumButton = "add to album button"
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import me.proton.android.drive.ui.viewmodel.SharedByMeViewModel
|
||||
import me.proton.android.drive.ui.viewstate.HomeScaffoldState
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.Shared
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -40,6 +41,7 @@ fun SharedByMeScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
@@ -50,7 +52,9 @@ fun SharedByMeScreen(
|
||||
viewModel.viewEvent(
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = {},
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ import me.proton.android.drive.ui.viewstate.SharedTab
|
||||
import me.proton.android.drive.ui.viewstate.SharedTabsViewState
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.component.ProtonTab
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -56,8 +57,9 @@ fun SharedTabsScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<SharedTabsViewModel>()
|
||||
@@ -82,6 +84,7 @@ fun SharedTabsScreen(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
@@ -94,8 +97,9 @@ fun SharedTabs(
|
||||
viewEvent: SharedTabsViewEvent,
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
@@ -124,6 +128,7 @@ fun SharedTabs(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
)
|
||||
@@ -131,6 +136,7 @@ fun SharedTabs(
|
||||
homeScaffoldState = homeScaffoldState,
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -18,8 +18,12 @@
|
||||
|
||||
package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -36,6 +40,7 @@ import me.proton.android.drive.ui.viewstate.HomeScaffoldState
|
||||
import me.proton.core.compose.flow.rememberFlowWithLifecycle
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.Shared
|
||||
import me.proton.core.drive.drivelink.shared.presentation.component.UserInvitationBanner
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -45,9 +50,10 @@ fun SharedWithMeScreen(
|
||||
homeScaffoldState: HomeScaffoldState,
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToUserInvitation: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val viewModel = hiltViewModel<SharedWithMeViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(initialValue = viewModel.initialViewState)
|
||||
@@ -59,7 +65,9 @@ fun SharedWithMeScreen(
|
||||
viewModel.viewEvent(
|
||||
navigateToFiles = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
navigateToFileOrFolderOptions = navigateToFileOrFolderOptions,
|
||||
navigateToUserInvitation = navigateToUserInvitation,
|
||||
lifecycle = lifecycle,
|
||||
)
|
||||
}
|
||||
@@ -77,11 +85,17 @@ fun SharedWithMeScreen(
|
||||
headerContent = {
|
||||
Box(Modifier.defaultMinSize(minHeight = 1.dp)) {
|
||||
// minHeight: always draw to have the header visible in the lazy list
|
||||
userInvitationViewState?.let { viewState ->
|
||||
UserInvitationBanner(
|
||||
description = viewState.description,
|
||||
onClick = navigateToUserInvitation,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = userInvitationViewState != null,
|
||||
enter = expandVertically() + fadeIn(),
|
||||
exit = shrinkVertically() + fadeOut(),
|
||||
) {
|
||||
userInvitationViewState?.let { viewState ->
|
||||
UserInvitationBanner(
|
||||
description = viewState.description,
|
||||
onClick = viewEvent.onUserInvitation,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.screen
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.testTag
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import me.proton.android.drive.R
|
||||
import me.proton.android.drive.ui.component.PromoContainer
|
||||
import me.proton.android.drive.ui.viewmodel.SubscriptionPromoViewModel
|
||||
import me.proton.android.drive.ui.viewstate.SubscriptionPromoViewState
|
||||
import me.proton.core.compose.theme.ProtonTheme
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@Composable
|
||||
fun SubscriptionPromoScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
runAction: RunAction,
|
||||
navigateToSubscription: () -> Unit,
|
||||
) {
|
||||
val viewModel = hiltViewModel<SubscriptionPromoViewModel>()
|
||||
val viewState by viewModel.viewState.collectAsStateWithLifecycle(null)
|
||||
val viewEvent = remember {
|
||||
viewModel.viewEvent(
|
||||
runAction = runAction,
|
||||
navigateToSubscription = navigateToSubscription,
|
||||
)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
viewEvent.onDismiss()
|
||||
}
|
||||
}
|
||||
viewState?.let { viewState ->
|
||||
SubscriptionPromo(
|
||||
modifier = modifier.testTag(SubscriptionPromoTestTag.content),
|
||||
viewState = viewState,
|
||||
onGetDriveLite = { viewEvent.onGetSubscription() },
|
||||
onCancel = { viewEvent.onCancel() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SubscriptionPromo(
|
||||
modifier: Modifier,
|
||||
viewState: SubscriptionPromoViewState,
|
||||
onGetDriveLite: () -> Unit,
|
||||
onCancel: () -> Unit
|
||||
) {
|
||||
PromoContainer(
|
||||
modifier = modifier.systemBarsPadding(),
|
||||
title = viewState.title,
|
||||
description = viewState.description,
|
||||
image = {
|
||||
Image(
|
||||
modifier = Modifier.defaultMinSize(minWidth = 320.dp),
|
||||
painter = painterResource(id = viewState.image),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.FillWidth
|
||||
)
|
||||
},
|
||||
actionText = viewState.actionText,
|
||||
dismissActionText = stringResource(id = I18N.string.photos_upsell_dismiss_action),
|
||||
onAction = { onGetDriveLite() },
|
||||
onCancel = { onCancel() },
|
||||
)
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Preview(widthDp = 600, heightDp = 360)
|
||||
@Composable
|
||||
private fun PhotosUpsellPreview() {
|
||||
ProtonTheme {
|
||||
SubscriptionPromo(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
viewState = SubscriptionPromoViewState(
|
||||
image = R.drawable.img_drive_lite,
|
||||
title ="More storage for only \$1.00",
|
||||
description ="When you need a little more storage, but not a lot. Introducing Drive Lite, featuring 20 GB storage for only \$1.00 a month. ",
|
||||
actionText ="Get Drive Lite",
|
||||
),
|
||||
onGetDriveLite = {},
|
||||
onCancel = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object SubscriptionPromoTestTag {
|
||||
const val content = "subscription promo content"
|
||||
}
|
||||
@@ -19,28 +19,46 @@
|
||||
package me.proton.android.drive.ui.test
|
||||
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import me.proton.android.drive.usecase.MarkOnboardingAsShown
|
||||
import me.proton.android.drive.usecase.MarkRatingBoosterAsShown
|
||||
import me.proton.android.drive.usecase.MarkWhatsNewAsShown
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore.Keys.subscriptionLastUpdate
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.drive.android.settings.data.datastore.AppUiSettingsDataStore
|
||||
import me.proton.drive.android.settings.domain.entity.WhatsNewKey
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
class UiTestHelper @Inject constructor(
|
||||
val configurationProvider: ConfigurationProvider,
|
||||
private val appUiSettingsDataStore: AppUiSettingsDataStore
|
||||
private val appUiSettingsDataStore: AppUiSettingsDataStore,
|
||||
private val getUserDataStore: GetUserDataStore,
|
||||
) {
|
||||
suspend fun doNotShowOnboardingAfterLogin() {
|
||||
appUiSettingsDataStore.onboardingShown = 1L
|
||||
}
|
||||
|
||||
suspend fun doNotShowWhatsNewAfterLogin() {
|
||||
appUiSettingsDataStore.WhatsNew(WhatsNewKey.PUBLIC_SHARING.name).shown = 1L
|
||||
appUiSettingsDataStore.WhatsNew(WhatsNewKey.ALBUMS.name).shown = 1L
|
||||
}
|
||||
|
||||
suspend fun doNotShowRatingBoosterAfterLogin() {
|
||||
appUiSettingsDataStore.ratingBooster = 1L
|
||||
}
|
||||
|
||||
suspend fun doNotShowDrivePlusPromoAfterLogin(userId: UserId) {
|
||||
getUserDataStore(userId).edit { preferences ->
|
||||
preferences[subscriptionLastUpdate("drive2022")] = 1L
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun doNotShowDriveLitePromoAfterLogin(userId: UserId) {
|
||||
getUserDataStore(userId).edit { preferences ->
|
||||
preferences[subscriptionLastUpdate("drivelite2024")] = 1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewevent
|
||||
|
||||
interface DriveLitePopupViewEvent {
|
||||
val onGetSubscription: () -> Unit get() = {}
|
||||
val onCancel: () -> Unit get() = {}
|
||||
val onDismiss: () -> Unit get() = {}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewevent
|
||||
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
|
||||
interface ShareMultiplePhotosOptionsViewEvent {
|
||||
val onSharedAlbum: (AlbumId) -> Unit
|
||||
val onScroll: (Set<LinkId>) -> Unit
|
||||
}
|
||||
@@ -25,17 +25,22 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.ui.options.Option
|
||||
import me.proton.android.drive.ui.options.filter
|
||||
import me.proton.android.drive.ui.options.filterPermissions
|
||||
import me.proton.android.drive.ui.options.filterRoot
|
||||
import me.proton.android.drive.ui.options.filterShare
|
||||
import me.proton.android.drive.ui.options.filterShareMember
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.extension.mapWithPrevious
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
@@ -43,6 +48,13 @@ import me.proton.core.drive.base.presentation.extension.require
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isShareMember
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsTempDisabledOnRelease
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveSharingDevelopment
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.FileOptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -54,12 +66,17 @@ import javax.inject.Inject
|
||||
class AlbumOptionsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getDriveLink: GetDecryptedDriveLink,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val albumId = AlbumId(
|
||||
shareId = ShareId(userId, savedStateHandle.require(KEY_SHARE_ID)),
|
||||
id = savedStateHandle.require(KEY_ALBUM_ID)
|
||||
)
|
||||
private val shareTempDisabled = getFeatureFlagFlow(driveAlbumsTempDisabledOnRelease(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsTempDisabledOnRelease(userId), NOT_FOUND))
|
||||
private val sharingDevelopment = getFeatureFlagFlow(driveSharingDevelopment(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveSharingDevelopment(userId), NOT_FOUND))
|
||||
private var dismiss: (() -> Unit)? = null
|
||||
val driveLink: StateFlow<DriveLink.Album?> = getDriveLink(albumId = albumId)
|
||||
.mapSuccessValueOrNull()
|
||||
@@ -89,33 +106,41 @@ class AlbumOptionsViewModel @Inject constructor(
|
||||
navigateToManageAccess: (linkId: LinkId) -> Unit,
|
||||
navigateToRename: (linkId: LinkId) -> Unit,
|
||||
navigateToDelete: (AlbumId) -> Unit,
|
||||
navigateToLeave: (AlbumId) -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
): Flow<List<FileOptionEntry<DriveLink.Album>>> = driveLink
|
||||
.filterNotNull()
|
||||
.map { driveLink ->
|
||||
options
|
||||
.filter(driveLink)
|
||||
//.filterShareMember(driveLink.isShareMember)
|
||||
//.filterPermissions(driveLink.sharePermissions ?: Permissions.owner)
|
||||
.map { option ->
|
||||
when (option) {
|
||||
is Option.OfflineToggle -> option.build(runAction) { driveLink ->
|
||||
notYetImplemented()
|
||||
}
|
||||
is Option.ShareViaInvitations -> option.build(runAction, navigateToShareViaInvitations)
|
||||
is Option.ManageAccess -> option.build(runAction, navigateToManageAccess)
|
||||
is Option.Rename -> option.build(runAction, navigateToRename)
|
||||
is Option.DeleteAlbum -> option.build(runAction = runAction) { albumId ->
|
||||
navigateToDelete(albumId)
|
||||
}
|
||||
else -> error(
|
||||
"Option ${option.javaClass.simpleName} is not found. Did you forget to add it?"
|
||||
)
|
||||
): Flow<List<FileOptionEntry<DriveLink.Album>>> = combine(
|
||||
driveLink.filterNotNull(),
|
||||
shareTempDisabled,
|
||||
sharingDevelopment,
|
||||
) { driveLink, shareTempDisabled, sharingDevelopment ->
|
||||
options
|
||||
.filter(driveLink)
|
||||
.filterShareMember(driveLink.isShareMember)
|
||||
.filterPermissions(driveLink.sharePermissions ?: Permissions.owner)
|
||||
.filterShare(shareTempDisabled.on)
|
||||
.filterRoot(driveLink, sharingDevelopment)
|
||||
.map { option ->
|
||||
when (option) {
|
||||
is Option.OfflineToggle -> option.build(runAction) { driveLink ->
|
||||
notYetImplemented()
|
||||
}
|
||||
}.also {
|
||||
this.dismiss = dismiss
|
||||
is Option.ShareViaInvitations -> option.build(runAction, navigateToShareViaInvitations)
|
||||
is Option.ManageAccess -> option.build(runAction, navigateToManageAccess)
|
||||
is Option.Rename -> option.build(runAction, navigateToRename)
|
||||
is Option.DeleteAlbum -> option.build(runAction = runAction) { albumId ->
|
||||
navigateToDelete(albumId)
|
||||
}
|
||||
is Option.LeaveAlbum -> option.build(runAction) { album ->
|
||||
navigateToLeave(album.id)
|
||||
}
|
||||
else -> error(
|
||||
"Option ${option.javaClass.simpleName} is not found. Did you forget to add it?"
|
||||
)
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
this.dismiss = dismiss
|
||||
}
|
||||
}
|
||||
|
||||
private fun notYetImplemented() = viewModelScope.launch {
|
||||
broadcastMessages(
|
||||
@@ -130,11 +155,12 @@ class AlbumOptionsViewModel @Inject constructor(
|
||||
const val KEY_ALBUM_ID = "albumId"
|
||||
|
||||
private val options = setOf(
|
||||
Option.OfflineToggle,
|
||||
//Option.OfflineToggle,
|
||||
Option.ShareViaInvitations,
|
||||
Option.ManageAccess,
|
||||
Option.Rename,
|
||||
Option.DeleteAlbum,
|
||||
Option.LeaveAlbum,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,10 +35,9 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
@@ -48,26 +47,36 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
|
||||
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
|
||||
import me.proton.android.drive.photos.domain.usecase.GetAddToAlbumPhotoListings
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotoListingCount
|
||||
import me.proton.android.drive.photos.domain.usecase.RemoveFromAlbumInfo
|
||||
import me.proton.android.drive.photos.presentation.extension.details
|
||||
import me.proton.android.drive.photos.presentation.extension.processAddToStream
|
||||
import me.proton.android.drive.photos.presentation.state.PhotosItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.AlbumViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewstate.AlbumViewState
|
||||
import me.proton.android.drive.ui.common.onClick
|
||||
import me.proton.android.drive.usecase.OnFilesDriveLinkError
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.domain.arch.onSuccess
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.extension.asSuccess
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.extension.isViewerOrEditorOnly
|
||||
import me.proton.core.drive.base.domain.extension.mapWithPrevious
|
||||
import me.proton.core.drive.base.domain.extension.onFailure
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.common.Action
|
||||
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
|
||||
import me.proton.core.drive.base.presentation.effect.ListEffect
|
||||
import me.proton.core.drive.base.presentation.extension.quantityString
|
||||
import me.proton.core.drive.base.presentation.state.ListContentAppendingState
|
||||
@@ -75,10 +84,19 @@ import me.proton.core.drive.base.presentation.state.ListContentState
|
||||
import me.proton.core.drive.base.presentation.viewmodel.onLoadState
|
||||
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isNameEncrypted
|
||||
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPagedAlbumPhotoListingsList
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.SelectAll
|
||||
import me.proton.core.drive.drivelink.shared.presentation.extension.toViewState
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsTempDisabledOnRelease
|
||||
import me.proton.core.drive.feature.flag.domain.extension.off
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -87,12 +105,16 @@ import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.link.selection.domain.usecase.SelectLinks
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.domain.entity.PhotoListing
|
||||
import me.proton.core.drive.share.crypto.domain.usecase.GetPhotoShare
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.share.user.domain.entity.ShareUser
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetShareUsers
|
||||
import me.proton.core.drive.sorting.domain.entity.Direction
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import me.proton.core.drive.base.domain.extension.combine as baseCombine
|
||||
import me.proton.core.drive.base.presentation.R as BasePresentation
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
@@ -110,10 +132,14 @@ class AlbumViewModel @Inject constructor(
|
||||
addToAlbumInfo: AddToAlbumInfo,
|
||||
removeFromAlbumInfo: RemoveFromAlbumInfo,
|
||||
getAddToAlbumPhotoListings: GetAddToAlbumPhotoListings,
|
||||
getPhotoShare: GetPhotoShare,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
getShareUsers: GetShareUsers,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val onFilesDriveLinkError: OnFilesDriveLinkError,
|
||||
private val photoDriveLinks: PhotoDriveLinks,
|
||||
private val getPagedAlbumPhotoListingsList: GetPagedAlbumPhotoListingsList,
|
||||
private val addPhotosToStream: AddPhotosToStream,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
) : PhotosPickerAndSelectionViewModel(
|
||||
@@ -130,6 +156,14 @@ class AlbumViewModel @Inject constructor(
|
||||
override val filterByParentId: Boolean = false
|
||||
private val shareId = ShareId(userId, requireNotNull(savedStateHandle.get<String>(SHARE_ID)))
|
||||
private val albumId = AlbumId(shareId, requireNotNull(savedStateHandle[ALBUM_ID]))
|
||||
private val shareTempDisabled = getFeatureFlagFlow(driveAlbumsTempDisabledOnRelease(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsTempDisabledOnRelease(userId), NOT_FOUND))
|
||||
private val photoShare: StateFlow<Share?> = getPhotoShare(userId)
|
||||
.filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, Eagerly, null)
|
||||
private val driveCopyFeature = getFeatureFlagFlow(FeatureFlagId.driveCopy(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(FeatureFlagId.driveCopy(userId), NOT_FOUND))
|
||||
private var fetchingJob: Job? = null
|
||||
private val listContentState = MutableStateFlow<ListContentState>(ListContentState.Loading)
|
||||
private val listContentAppendingState = MutableStateFlow<ListContentAppendingState>(
|
||||
@@ -145,7 +179,13 @@ class AlbumViewModel @Inject constructor(
|
||||
notificationDotVisible = false,
|
||||
onAction = { viewEvent?.onAlbumOptions?.invoke() },
|
||||
)
|
||||
private val emptyStateImageResId: Int get() = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.empty_albums_light,
|
||||
dark = BasePresentation.drawable.empty_albums_dark,
|
||||
dayNight = BasePresentation.drawable.empty_albums_daynight,
|
||||
)
|
||||
private val topBarActions: MutableStateFlow<Set<Action>> = MutableStateFlow(setOf(albumOptionsAction))
|
||||
private val saveAllLoading: MutableStateFlow<Boolean> = MutableStateFlow(false)
|
||||
private val retryTrigger = MutableSharedFlow<Unit>(replay = 1).apply { tryEmit(Unit) }
|
||||
val driveLink: StateFlow<DriveLink.Album?> = retryTrigger.transformLatest {
|
||||
emitAll(
|
||||
@@ -182,13 +222,42 @@ class AlbumViewModel @Inject constructor(
|
||||
return@mapWithPrevious null
|
||||
}
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
}.stateIn(viewModelScope, Eagerly, null)
|
||||
|
||||
val viewState: Flow<AlbumViewState> = combine(
|
||||
private val shareUsers = driveLink.transformLatest {
|
||||
if((it?.sharePermissions ?: Permissions.owner).isAdmin) {
|
||||
emitAll(getShareUsers(albumId).transformLatest { dataResult ->
|
||||
dataResult.onSuccess { list ->
|
||||
emit(list)
|
||||
}.onFailure { error ->
|
||||
emit(emptyList())
|
||||
if (error.cause !is NoSuchElementException) {
|
||||
error.cause?.log(VIEW_MODEL)
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.WARNING
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
emit(emptyList())
|
||||
}
|
||||
}.stateIn(viewModelScope, Eagerly, emptyList())
|
||||
|
||||
val viewState: Flow<AlbumViewState> = baseCombine(
|
||||
driveLink.filterNotNull(),
|
||||
listContentState,
|
||||
selected,
|
||||
) { album, contentState, selected ->
|
||||
photoShare.filterNotNull(),
|
||||
driveCopyFeature,
|
||||
saveAllLoading,
|
||||
shareUsers,
|
||||
) { album, contentState, selected, photoShare, driveCopy, saveAllLoading, shareUsers ->
|
||||
topBarActions.value = when {
|
||||
inPickerMode -> emptySet()
|
||||
selected.isNotEmpty() -> setOf(
|
||||
@@ -200,7 +269,8 @@ class AlbumViewModel @Inject constructor(
|
||||
}
|
||||
AlbumViewState(
|
||||
name = album.name,
|
||||
details = album.details(appContext),
|
||||
isNameEncrypted = album.isNameEncrypted,
|
||||
details = album.details(appContext, showDisplayName = true),
|
||||
coverLinkId = album.coverLinkId,
|
||||
listContentState = contentState,
|
||||
isRefreshEnabled = contentState !is ListContentState.Loading,
|
||||
@@ -220,6 +290,15 @@ class AlbumViewModel @Inject constructor(
|
||||
selected.size
|
||||
)
|
||||
},
|
||||
showAddAction = (album.sharePermissions ?: Permissions.owner).canWrite && driveCopy.on,
|
||||
addActionEnabled = selected.isEmpty() && !inPickerMode,
|
||||
showSaveAllAction = (album.sharePermissions ?: Permissions.owner).isViewerOrEditorOnly && driveCopy.on
|
||||
&& album.photoCount < configurationProvider.savePhotoToStreamLimit,
|
||||
saveAllActionEnabled = selected.isEmpty() && !inPickerMode,
|
||||
saveAllActionLoading = saveAllLoading,
|
||||
showShareAction = (album.sharePermissions ?: Permissions.owner).isAdmin && shareTempDisabled.value.off,
|
||||
shareActionEnabled = selected.isEmpty() && !inPickerMode,
|
||||
shareUsers = shareUsers.map { shareUser -> shareUser.toViewState(appContext) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -250,10 +329,12 @@ class AlbumViewModel @Inject constructor(
|
||||
|
||||
fun viewEvent(
|
||||
navigateToAlbumOptions: (AlbumId) -> Unit,
|
||||
navigateToPhotosOptions: (FileId, AlbumId?) -> Unit,
|
||||
navigateToPhotosOptions: (FileId, AlbumId?, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId, AlbumId?) -> Unit,
|
||||
navigateToPreview: (FileId, AlbumId) -> Unit,
|
||||
navigateToPicker: (AlbumId) -> Unit,
|
||||
navigateToShareViaInvitations: (AlbumId) -> Unit,
|
||||
navigateToManageAccess: (AlbumId) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
lifecycle: Lifecycle,
|
||||
) : AlbumViewEvent = object : AlbumViewEvent {
|
||||
@@ -263,7 +344,7 @@ class AlbumViewModel @Inject constructor(
|
||||
lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||
flow.take(1).collect { driveLink ->
|
||||
driveLink.onClick(
|
||||
navigateToFolder = { _, _ -> error("Photos should not have folders") },
|
||||
navigateToFolder = { _, _ -> error("Album should not have folders") },
|
||||
navigateToPreview = { linkId ->
|
||||
viewModelScope.launch {
|
||||
navigateToPreview(
|
||||
@@ -272,6 +353,7 @@ class AlbumViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
},
|
||||
navigateToAlbum = { error("Album should not have albums") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -290,7 +372,7 @@ class AlbumViewModel @Inject constructor(
|
||||
coroutineScope = viewModelScope,
|
||||
emptyState = MutableStateFlow(
|
||||
ListContentState.Empty(
|
||||
imageResId = BasePresentation.drawable.ic_proton_images_50,
|
||||
imageResId = emptyStateImageResId,
|
||||
titleId = I18N.string.albums_empty_album_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_album_screen_description,
|
||||
)
|
||||
@@ -324,6 +406,9 @@ class AlbumViewModel @Inject constructor(
|
||||
override val onSelectedOptions =
|
||||
{ onSelectedOptions(navigateToPhotosOptions, navigateToMultiplePhotosOptions, albumId) }
|
||||
override val onBack = { onBack() }
|
||||
override val onShare = { navigateToShareViaInvitations(albumId) }
|
||||
override val onSaveAll = this@AlbumViewModel::onSaveAll
|
||||
override val onShareUsers = { navigateToManageAccess(albumId) }
|
||||
}.also { viewEvent ->
|
||||
this.viewEvent = viewEvent
|
||||
}
|
||||
@@ -342,6 +427,33 @@ class AlbumViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSaveAll() {
|
||||
viewModelScope.launch {
|
||||
saveAllLoading.value = true
|
||||
addPhotosToStream(albumId).onFailure { error ->
|
||||
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${albumId.id.logId()}")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess { result ->
|
||||
result.processAddToStream(appContext) { message, type ->
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
saveAllLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private fun onScroll(driveLinkIds: Set<LinkId>) {
|
||||
if (driveLinkIds.isNotEmpty()) {
|
||||
fetchingJob?.cancel()
|
||||
|
||||
@@ -33,13 +33,17 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
|
||||
import me.proton.android.drive.photos.presentation.R
|
||||
import me.proton.android.drive.photos.presentation.state.AlbumsItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.AlbumsViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewstate.AlbumsFilter
|
||||
@@ -48,6 +52,7 @@ import me.proton.android.drive.ui.effect.HomeEffect
|
||||
import me.proton.android.drive.ui.effect.HomeTabViewModel
|
||||
import me.proton.android.drive.usecase.OnFilesDriveLinkError
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.domain.arch.onSuccess
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
import me.proton.core.drive.base.data.extension.isRetryable
|
||||
@@ -59,6 +64,8 @@ import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.common.Action
|
||||
import me.proton.core.drive.base.presentation.common.getThemeDrawableId
|
||||
import me.proton.core.drive.base.presentation.extension.quantityString
|
||||
import me.proton.core.drive.base.presentation.state.ListContentState
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.base.presentation.viewstate.TagViewState
|
||||
@@ -66,6 +73,7 @@ import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.FetchAndStoreAllAlbumListings
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.GetAllAlbumListings
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewstate.UserInvitationBannerViewState
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.extension.shareId
|
||||
@@ -73,6 +81,7 @@ import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.domain.entity.AlbumListing
|
||||
import me.proton.core.drive.photo.domain.extension.filterBy
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetUserInvitationCountFlow
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
@@ -90,6 +99,7 @@ class AlbumsViewModel @Inject constructor(
|
||||
private val getPhotosDriveLink: GetPhotosDriveLink,
|
||||
private val onFilesDriveLinkError: OnFilesDriveLinkError,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val getUserInvitationCountFlow: GetUserInvitationCountFlow,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle), HomeTabViewModel {
|
||||
private var fetchingJob: Job? = null
|
||||
private val _homeEffect = MutableSharedFlow<HomeEffect>()
|
||||
@@ -112,19 +122,25 @@ class AlbumsViewModel @Inject constructor(
|
||||
)
|
||||
private val topBarActions: MutableStateFlow<Set<Action>> =
|
||||
MutableStateFlow(setOf(addAlbumAction))
|
||||
private val emptyStateImageResId: Int get() = getThemeDrawableId(
|
||||
light = BasePresentation.drawable.empty_albums_light,
|
||||
dark = BasePresentation.drawable.empty_albums_dark,
|
||||
dayNight = BasePresentation.drawable.empty_albums_daynight,
|
||||
)
|
||||
|
||||
val initialViewState: AlbumsViewState =
|
||||
AlbumsViewState(
|
||||
topBarActions = topBarActions,
|
||||
listContentState = listContentState.value,
|
||||
isRefreshEnabled = listContentState.value != ListContentState.Loading,
|
||||
placeholderImageResId = emptyStateImageResId,
|
||||
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
|
||||
filters = listOf(
|
||||
AlbumsFilter(
|
||||
AlbumListing.Filter.ALL,
|
||||
TagViewState(
|
||||
label = appContext.getString(I18N.string.albums_filter_all),
|
||||
icon = CorePresentation.drawable.ic_proton_checkmark,
|
||||
icon = BasePresentation.drawable.ic_folder_album_outline,
|
||||
selected = true,
|
||||
)
|
||||
),
|
||||
@@ -151,6 +167,22 @@ class AlbumsViewModel @Inject constructor(
|
||||
),
|
||||
)
|
||||
)
|
||||
val userInvitationBannerViewState = getUserInvitationCountFlow(
|
||||
userId = userId,
|
||||
albumsOnly = true,
|
||||
refresh = flowOf(true),
|
||||
).filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.filterNotNull()
|
||||
.filter { count -> count > 0 }
|
||||
.map { count ->
|
||||
UserInvitationBannerViewState(
|
||||
appContext.quantityString(
|
||||
I18N.plurals.shared_with_me_album_invitations_banner_description,
|
||||
count
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
private val retryTrigger = MutableSharedFlow<Unit>(replay = 1).apply { tryEmit(Unit) }
|
||||
val driveLink: StateFlow<DriveLink.Folder?> = retryTrigger.transformLatest {
|
||||
@@ -199,11 +231,35 @@ class AlbumsViewModel @Inject constructor(
|
||||
|
||||
is DataResult.Success -> emit(result.value).also {
|
||||
if (result.value.isEmpty()) {
|
||||
listContentState.value = ListContentState.Empty(
|
||||
imageResId = BasePresentation.drawable.empty_albums,
|
||||
titleId = I18N.string.albums_empty_albums_list_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_albums_list_screen_description,
|
||||
)
|
||||
listContentState.value = when (albumListingsFilter.value) {
|
||||
AlbumListing.Filter.ALL -> ListContentState.Empty(
|
||||
imageResId = emptyStateImageResId,
|
||||
titleId = I18N.string.albums_empty_albums_list_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_albums_list_screen_description,
|
||||
actionResId = I18N.string.common_create_album_action
|
||||
)
|
||||
|
||||
AlbumListing.Filter.MY_ALBUMS -> ListContentState.Empty(
|
||||
imageResId = emptyStateImageResId,
|
||||
titleId = I18N.string.albums_empty_albums_list_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_albums_my_albums_screen_description,
|
||||
actionResId = I18N.string.common_create_album_action
|
||||
)
|
||||
|
||||
AlbumListing.Filter.SHARED_BY_ME -> ListContentState.Empty(
|
||||
imageResId = emptyStateImageResId,
|
||||
titleId = I18N.string.albums_empty_albums_shared_by_me_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_albums_shared_by_me_screen_description,
|
||||
actionResId = I18N.string.common_create_album_action
|
||||
)
|
||||
|
||||
AlbumListing.Filter.SHARED_WITH_ME -> ListContentState.Empty(
|
||||
imageResId = emptyStateImageResId,
|
||||
titleId = I18N.string.albums_empty_albums_shared_with_me_screen_title,
|
||||
descriptionResId = I18N.string.albums_empty_albums_shared_with_me_screen_description,
|
||||
actionResId = null
|
||||
)
|
||||
}
|
||||
} else {
|
||||
listContentState.value = ListContentState.Content()
|
||||
}
|
||||
@@ -260,6 +316,7 @@ class AlbumsViewModel @Inject constructor(
|
||||
fun viewEvent(
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
): AlbumsViewEvent = object : AlbumsViewEvent {
|
||||
override val onRefresh = this@AlbumsViewModel::onRefresh
|
||||
override val onScroll = this@AlbumsViewModel::onScroll
|
||||
@@ -273,6 +330,7 @@ class AlbumsViewModel @Inject constructor(
|
||||
}
|
||||
override val onCreateNewAlbum = { navigateToCreateNewAlbum() }
|
||||
override val onFilterSelected = this@AlbumsViewModel::onFilterSelected
|
||||
override val onUserInvitation = { navigateToUserInvitation(true) }
|
||||
}.also { viewEvent ->
|
||||
this.viewEvent = viewEvent
|
||||
}
|
||||
|
||||
@@ -54,6 +54,7 @@ import me.proton.core.drive.drivelink.shared.domain.usecase.SharedDriveLinks
|
||||
import me.proton.core.drive.drivelink.shared.presentation.entity.SharedItem
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewevent.SharedViewEvent
|
||||
import me.proton.core.drive.drivelink.shared.presentation.viewstate.SharedViewState
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -101,7 +102,9 @@ abstract class CommonSharedViewModel(
|
||||
fun viewEvent(
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (fileId: FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateToUserInvitation: (Boolean) -> Unit,
|
||||
lifecycle: Lifecycle,
|
||||
): SharedViewEvent = object : SharedViewEvent {
|
||||
|
||||
@@ -112,6 +115,7 @@ abstract class CommonSharedViewModel(
|
||||
driveLink.onClick(
|
||||
navigateToFolder = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -143,6 +147,8 @@ abstract class CommonSharedViewModel(
|
||||
override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) }
|
||||
|
||||
override val onErrorAction = this@CommonSharedViewModel::onErrorAction
|
||||
|
||||
override val onUserInvitation = { navigateToUserInvitation(false) }
|
||||
}
|
||||
|
||||
private fun onScroll(driveLinkIds: Set<LinkId>) {
|
||||
|
||||
@@ -42,18 +42,26 @@ import me.proton.android.drive.photos.presentation.viewstate.ConfirmDeleteAlbumD
|
||||
import me.proton.android.drive.photos.presentation.viewstate.ConfirmDeleteAlbumViewState
|
||||
import me.proton.android.drive.photos.presentation.viewstate.ConfirmDeleteAlbumWithChildrenViewState
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.data.extension.detailsOrNull
|
||||
import me.proton.core.drive.base.data.workmanager.onProtonHttpException
|
||||
import me.proton.core.drive.base.domain.api.ProtonApiCode
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.usecase.GetDriveLink
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.extension.shareId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.data.api.response.DeleteAlbumErrorResponseDetails
|
||||
import me.proton.core.drive.photo.domain.usecase.DeleteAlbum
|
||||
import me.proton.core.drive.photo.domain.usecase.GetAllAlbumDirectChildren
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.network.domain.ApiResult
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@@ -66,11 +74,13 @@ class ConfirmDeleteAlbumDialogViewModel @Inject constructor(
|
||||
private val saveChildrenAndDeleteAlbum: SaveChildrenAndDeleteAlbum,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val getAllAlbumDirectChildren: GetAllAlbumDirectChildren,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val albumId = AlbumId(
|
||||
ShareId(userId, requireNotNull(savedStateHandle[SHARE_ID])),
|
||||
requireNotNull(savedStateHandle[ALBUM_ID])
|
||||
)
|
||||
private val albumDirectChildren: MutableSet<FileId> = mutableSetOf()
|
||||
private val driveLink: StateFlow<DriveLink.Album?> = getDriveLink(albumId)
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
@@ -147,9 +157,10 @@ class ConfirmDeleteAlbumDialogViewModel @Inject constructor(
|
||||
)
|
||||
.onFailure { error ->
|
||||
isOperationInProgress.value = false
|
||||
error.onProtonHttpException { protonCode: Int ->
|
||||
if (protonCode == ProtonApiCode.RESULTS_IN_DATA_LOSS) {
|
||||
error.onProtonHttpException { protonData ->
|
||||
if (protonData.code == ProtonApiCode.RESULTS_IN_DATA_LOSS) {
|
||||
// store linkIds once available
|
||||
collectAlbumDirectChildren(album.shareId, protonData)
|
||||
showDialog.value = ConfirmDeleteAlbumDialogViewState.Dialog.WITH_CHILDREN
|
||||
}
|
||||
else {
|
||||
@@ -204,9 +215,25 @@ class ConfirmDeleteAlbumDialogViewModel @Inject constructor(
|
||||
private fun onConfirmSaveAndDeleteAlbum() {
|
||||
viewModelScope.launch {
|
||||
isSavingOperationInProgress.value = true
|
||||
val album = driveLink.filterNotNull().first()
|
||||
val children = getAllAlbumDirectChildren(album.volumeId)
|
||||
.onFailure { error ->
|
||||
isSavingOperationInProgress.value = false
|
||||
error.log(VIEW_MODEL, "Failed to get album direct children")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage,
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
.getOrThrow()
|
||||
saveChildrenAndDeleteAlbum(
|
||||
albumId = albumId,
|
||||
children = emptyList(),//TODO get list of Id's
|
||||
children = children,
|
||||
)
|
||||
.onFailure { error ->
|
||||
isSavingOperationInProgress.value = false
|
||||
@@ -227,6 +254,35 @@ class ConfirmDeleteAlbumDialogViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun collectAlbumDirectChildren(
|
||||
albumShareId: ShareId,
|
||||
protonData: ApiResult.Error.ProtonData,
|
||||
) {
|
||||
albumDirectChildren.clear()
|
||||
|
||||
val errorDetails = protonData.detailsOrNull<DeleteAlbumErrorResponseDetails>() ?: return
|
||||
if (!errorDetails.more) {
|
||||
albumDirectChildren.addAll(
|
||||
errorDetails.childLinkIds.map { childPhotoId -> FileId(albumShareId, childPhotoId) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getAllAlbumDirectChildren(volumeId: VolumeId): Result<Set<FileId>> = coRunCatching {
|
||||
if (albumDirectChildren.isEmpty()) {
|
||||
getAllAlbumDirectChildren(
|
||||
volumeId = volumeId,
|
||||
albumId = albumId,
|
||||
includeTrashedChildren = true,
|
||||
)
|
||||
.onSuccess { children ->
|
||||
albumDirectChildren.addAll(children)
|
||||
}
|
||||
.getOrThrow()
|
||||
}
|
||||
albumDirectChildren
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHARE_ID = "shareId"
|
||||
const val ALBUM_ID = "albumId"
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
|
||||
import me.proton.android.drive.photos.presentation.extension.processAddToStream
|
||||
import me.proton.android.drive.photos.presentation.viewevent.ConfirmLeaveAlbumDialogViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewstate.ConfirmLeaveAlbumDialogViewState
|
||||
import me.proton.android.drive.usecase.LeaveShare
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltViewModel
|
||||
class ConfirmLeaveAlbumDialogViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getDecryptedDriveLink: GetDecryptedDriveLink,
|
||||
private val leaveShare: LeaveShare,
|
||||
private val addPhotosToStream: AddPhotosToStream,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val albumId = AlbumId(
|
||||
ShareId(userId, requireNotNull(savedStateHandle[SHARE_ID])),
|
||||
requireNotNull(savedStateHandle[ALBUM_ID])
|
||||
)
|
||||
private val album: StateFlow<DriveLink.Album?> = getDecryptedDriveLink(albumId)
|
||||
.filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
private val isSavingOperationInProgress = MutableStateFlow(false)
|
||||
private val isWithoutSavingOperationInProgress = MutableStateFlow(false)
|
||||
val initialViewState = ConfirmLeaveAlbumDialogViewState(
|
||||
title = appContext.getString(I18N.string.albums_leave_album_dialog_title),
|
||||
description = "",
|
||||
dismissButtonResId = I18N.string.albums_leave_album_dialog_cancel_action,
|
||||
confirmWithoutSavingButtonResId = I18N.string.albums_leave_album_dialog_leave_without_saving_action,
|
||||
confirmSaveAndLeaveButtonResId = I18N.string.albums_leave_album_dialog_save_and_leave_action,
|
||||
isSavingOperationInProgress = false,
|
||||
isWithoutSavingOperationInProgress = false,
|
||||
isSaveAndLeaveButtonVisible = true,
|
||||
)
|
||||
|
||||
val viewState: Flow<ConfirmLeaveAlbumDialogViewState> = combine(
|
||||
album.filterNotNull(),
|
||||
isSavingOperationInProgress,
|
||||
isWithoutSavingOperationInProgress,
|
||||
) { album, savingOperationInProgress, withoutSavingOperationInProgress ->
|
||||
initialViewState.copy(
|
||||
description = appContext.getString(I18N.string.albums_leave_album_dialog_description, album.name),
|
||||
isSavingOperationInProgress = savingOperationInProgress,
|
||||
isWithoutSavingOperationInProgress = withoutSavingOperationInProgress,
|
||||
isSaveAndLeaveButtonVisible = album.photoCount < configurationProvider.savePhotoToStreamLimit,
|
||||
)
|
||||
}
|
||||
|
||||
fun viewEvent(
|
||||
onDismiss: () -> Unit
|
||||
): ConfirmLeaveAlbumDialogViewEvent = object : ConfirmLeaveAlbumDialogViewEvent {
|
||||
override val onDismiss = onDismiss
|
||||
override val onLeaveAlbumWithoutSaving = { leaveAlbumWithoutSaving(onDismiss) }
|
||||
override val onSaveAndLeaveAlbum = { saveAndLeaveAlbum(onDismiss) }
|
||||
}
|
||||
|
||||
private fun leaveAlbumWithoutSaving(dismiss: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
album.value?.let {
|
||||
isWithoutSavingOperationInProgress.value = true
|
||||
leaveShare(it)
|
||||
.onSuccess {
|
||||
dismiss()
|
||||
}
|
||||
isWithoutSavingOperationInProgress.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveAndLeaveAlbum(dismiss: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
album.value?.let {
|
||||
isSavingOperationInProgress.value = true
|
||||
addPhotosToStream(albumId).onFailure { error ->
|
||||
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${albumId.id.logId()}")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess { result ->
|
||||
result.processAddToStream(appContext) { message, type ->
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
leaveShare(it)
|
||||
.onSuccess {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
isSavingOperationInProgress.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHARE_ID = "shareId"
|
||||
const val ALBUM_ID = "albumId"
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,6 @@ import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.paging.CombinedLoadStates
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.map
|
||||
@@ -60,6 +59,8 @@ import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.toVolumePhotoListing
|
||||
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.InvalidLinkName.Empty
|
||||
import me.proton.core.drive.link.domain.entity.InvalidLinkName.ExceedsMaxLength
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import javax.inject.Inject
|
||||
@@ -69,17 +70,18 @@ import me.proton.core.drive.i18n.R as I18N
|
||||
@HiltViewModel
|
||||
class CreateNewAlbumViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getPagedAddToAlbumPhotoListings: GetPagedAddToAlbumPhotoListings,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val updateAlbumName: UpdateAlbumName,
|
||||
private val clearNewAlbum: ClearNewAlbum,
|
||||
private val getNewAlbumName: GetNewAlbumName,
|
||||
private val getPagedAddToAlbumPhotoListings: GetPagedAddToAlbumPhotoListings,
|
||||
private val photoDriveLinks: PhotoDriveLinks,
|
||||
private val createNewAlbum: CreateNewAlbum,
|
||||
private val removeFromAlbumInfo: RemoveFromAlbumInfo,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val isSharedAlbum = savedStateHandle.get<Boolean>(SHARED_ALBUM) ?: false
|
||||
private val isCreationInProgress = MutableStateFlow(false)
|
||||
private val currentAlbumName = MutableStateFlow<String?>(null)
|
||||
private val initialAlbumName = flowOf {
|
||||
@@ -100,6 +102,11 @@ class CreateNewAlbumViewModel @Inject constructor(
|
||||
isAddEnabled = true,
|
||||
isRemoveEnabled = true,
|
||||
isCreationInProgress = isCreationInProgress.value,
|
||||
doneButtonLabelResId = if (isSharedAlbum) {
|
||||
I18N.string.common_share
|
||||
} else {
|
||||
I18N.string.common_done_action
|
||||
},
|
||||
name = initialAlbumName,
|
||||
hint = appContext.getString(I18N.string.albums_new_album_name_hint),
|
||||
)
|
||||
@@ -147,18 +154,12 @@ class CreateNewAlbumViewModel @Inject constructor(
|
||||
viewModelScope.launch {
|
||||
currentAlbumName.value?.let { albumName ->
|
||||
isCreationInProgress.value = true
|
||||
showAddToAlbumStartMessage()
|
||||
createNewAlbum(userId = userId, isLocked = false)
|
||||
.onFailure { error ->
|
||||
isCreationInProgress.value = false
|
||||
error.log(VIEW_MODEL, "Creating new album failed")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage,
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
error.handle()
|
||||
}
|
||||
.onSuccess { albumId ->
|
||||
isCreationInProgress.value = false
|
||||
@@ -215,4 +216,35 @@ class CreateNewAlbumViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Throwable.handle() {
|
||||
val message = when (this) {
|
||||
Empty -> appContext.getString(I18N.string.albums_new_album_create_error_name_is_blank)
|
||||
is ExceedsMaxLength -> appContext.getString(
|
||||
I18N.string.albums_new_album_create_error_name_too_long,
|
||||
this.maxLength
|
||||
)
|
||||
else -> getDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage,
|
||||
)
|
||||
}
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
}
|
||||
|
||||
private fun showAddToAlbumStartMessage() {
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = appContext.getString(I18N.string.albums_add_to_album_start_message),
|
||||
type = BroadcastMessage.Type.INFO,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHARED_ALBUM = "sharedAlbum"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ class DefaultHomeTabViewModel @Inject constructor(
|
||||
private fun TabItem.toHomeTab(): HomeTab = when (route) {
|
||||
Screen.Files.route -> HomeTab.FILES
|
||||
Screen.Photos.route -> HomeTab.PHOTOS
|
||||
Screen.PhotosAndAlbums.route -> HomeTab.PHOTOS
|
||||
Screen.Computers.route -> HomeTab.COMPUTERS
|
||||
Screen.SharedTabs.route -> HomeTab.SHARED
|
||||
else -> error("Unhandled tab item route: $route")
|
||||
|
||||
@@ -27,29 +27,31 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.last
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
|
||||
import me.proton.android.drive.photos.domain.usecase.RemovePhotosFromAlbum
|
||||
import me.proton.android.drive.photos.presentation.extension.processAddToStream
|
||||
import me.proton.android.drive.photos.presentation.extension.processRemove
|
||||
import me.proton.android.drive.ui.options.Option
|
||||
import me.proton.android.drive.ui.options.filter
|
||||
import me.proton.android.drive.ui.options.filterAlbums
|
||||
import me.proton.android.drive.ui.options.filterPermissions
|
||||
import me.proton.android.drive.ui.options.filterPhotoFavorite
|
||||
import me.proton.android.drive.ui.options.filterProtonDocs
|
||||
import me.proton.android.drive.ui.options.filterRoot
|
||||
import me.proton.android.drive.ui.options.filterShare
|
||||
import me.proton.android.drive.ui.options.filterShareMember
|
||||
import me.proton.android.drive.usecase.LeaveShare
|
||||
import me.proton.android.drive.usecase.NotifyActivityNotFound
|
||||
import me.proton.android.drive.usecase.OpenProtonDocumentInBrowser
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.extension.combine
|
||||
import me.proton.core.drive.base.domain.extension.mapWithPrevious
|
||||
import me.proton.core.drive.base.domain.extension.onFailure
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
@@ -62,26 +64,29 @@ import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLin
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isShareMember
|
||||
import me.proton.core.drive.drivelink.offline.domain.usecase.ToggleOffline
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.ToggleFavorite
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.UpdateAlbumCover
|
||||
import me.proton.core.drive.drivelink.shared.domain.extension.sharingDetails
|
||||
import me.proton.core.drive.drivelink.trash.domain.usecase.ToggleTrashState
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsTempDisabledOnRelease
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveDocsDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveSharingDevelopment
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.FileOptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.extension.shareId
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.share.user.domain.usecase.LeaveShare
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import me.proton.core.drive.volume.domain.entity.Volume
|
||||
import me.proton.core.drive.volume.domain.usecase.GetOldestActiveVolume
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@@ -91,7 +96,7 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getDriveLink: GetDecryptedDriveLink,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
albumsFeatureFlag: AlbumsFeatureFlag,
|
||||
getOldestActiveVolume: GetOldestActiveVolume,
|
||||
private val toggleOffline: ToggleOffline,
|
||||
private val toggleTrashState: ToggleTrashState,
|
||||
private val exportTo: ExportTo,
|
||||
@@ -102,7 +107,12 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
private val openProtonDocumentInBrowser: OpenProtonDocumentInBrowser,
|
||||
private val updateAlbumCover: UpdateAlbumCover,
|
||||
private val removePhotosFromAlbum: RemovePhotosFromAlbum,
|
||||
private val deselectLinks: DeselectLinks,
|
||||
private val toggleFavorite: ToggleFavorite,
|
||||
private val addPhotosToStream: AddPhotosToStream,
|
||||
private val hasPhotoVolume: HasPhotoVolume,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val selectionId = savedStateHandle.get<String?>(KEY_SELECTION_ID)?.let { SelectionId(it) }
|
||||
private var dismiss: (() -> Unit)? = null
|
||||
private val linkId: LinkId = FileId(
|
||||
ShareId(userId, savedStateHandle.require(KEY_SHARE_ID)),
|
||||
@@ -133,11 +143,17 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
private val docsKillSwitch = getFeatureFlagFlow(driveDocsDisabled(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveDocsDisabled(userId), NOT_FOUND))
|
||||
|
||||
private val albumsFeature = albumsFeatureFlag(userId)
|
||||
.stateIn(viewModelScope, Eagerly, configurationProvider.albumsFeatureFlag)
|
||||
private val albumsKillSwitch = getFeatureFlagFlow(driveAlbumsDisabled(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsDisabled(userId), NOT_FOUND))
|
||||
|
||||
private val shareTempDisabled = getFeatureFlagFlow(driveAlbumsTempDisabledOnRelease(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsTempDisabledOnRelease(userId), NOT_FOUND))
|
||||
|
||||
private val photoVolume = getOldestActiveVolume(userId, Volume.Type.PHOTO)
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, Eagerly, null)
|
||||
|
||||
|
||||
fun <T : DriveLink> entries(
|
||||
runAction: (suspend () -> Unit) -> Unit,
|
||||
navigateToInfo: (linkId: LinkId) -> Unit,
|
||||
@@ -153,59 +169,99 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
this.driveLink.filterNotNull(),
|
||||
sharingDevelopment,
|
||||
docsKillSwitch,
|
||||
albumsFeature,
|
||||
hasPhotoVolume(userId),
|
||||
albumsKillSwitch,
|
||||
) { driveLink, sharingDevelopment, protonDocsKillSwitch, albumsFeatureFlagOn, albumsKillSwitch ->
|
||||
shareTempDisabled,
|
||||
) { driveLink, sharingDevelopment, protonDocsKillSwitch, hasPhotoVolume, albumsKillSwitch, shareTempDisabled ->
|
||||
options
|
||||
.filter(driveLink)
|
||||
.filterAlbums(albumsFeatureFlagOn, albumsKillSwitch, albumId)
|
||||
.filterAlbums(hasPhotoVolume, albumsKillSwitch, albumId)
|
||||
.filterPhotoFavorite(hasPhotoVolume, albumsKillSwitch)
|
||||
.filterRoot(driveLink, sharingDevelopment)
|
||||
.filterShare(shareTempDisabled.on, albumId)
|
||||
.filterShareMember(driveLink.isShareMember)
|
||||
.filterPermissions(driveLink.sharePermissions ?: Permissions.owner)
|
||||
.filterProtonDocs(protonDocsKillSwitch)
|
||||
.map { option ->
|
||||
when (option) {
|
||||
is Option.DeletePermanently -> option.build(runAction, navigateToDelete)
|
||||
is Option.Info -> option.build(runAction, navigateToInfo)
|
||||
is Option.Move -> option.build(runAction, navigateToMove)
|
||||
is Option.FavoriteToggle -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
toggleFavoriteAction(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.Info -> option.build(runAction) { linkId ->
|
||||
navigateToInfo(linkId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.Move -> option.build(runAction) { linkId, parentId ->
|
||||
navigateToMove(linkId, parentId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.OfflineToggle -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
toggleOffline(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.Rename -> option.build(runAction, navigateToRename)
|
||||
is Option.Rename -> option.build(runAction) { linkId ->
|
||||
navigateToRename(linkId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.Trash -> option.build(
|
||||
runAction = runAction,
|
||||
toggleTrash = {
|
||||
viewModelScope.launch {
|
||||
toggleTrashState(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
)
|
||||
is Option.SendFile -> option.build(runAction, navigateToSendFile)
|
||||
is Option.SendFile -> option.build(runAction) { linkId ->
|
||||
navigateToSendFile(linkId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.Download -> option.build { filename ->
|
||||
showCreateDocumentPicker(filename) { handleActivityNotFound() }
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.ManageAccess -> option.build(runAction) { linkId ->
|
||||
navigateToManageAccess(linkId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.ShareViaInvitations -> option.build(runAction) { linkId ->
|
||||
navigateToShareViaInvitations(linkId)
|
||||
deselectLinks()
|
||||
}
|
||||
is Option.ManageAccess -> option.build(runAction, navigateToManageAccess)
|
||||
is Option.ShareViaInvitations -> option.build(runAction, navigateToShareViaInvitations)
|
||||
is Option.RemoveMe -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
leaveShare(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.OpenInBrowser -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
openProtonDocumentInBrowser(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.SetAsAlbumCover -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
setAsAlbumCover(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.SaveSharePhoto -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
saveSharedPhoto(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
is Option.RemoveFromAlbum -> option.build(runAction) { driveLink ->
|
||||
viewModelScope.launch {
|
||||
removePhotosFromAlbum(driveLink)
|
||||
deselectLinks()
|
||||
}
|
||||
}
|
||||
else -> throw IllegalStateException(
|
||||
@@ -217,6 +273,14 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private fun deselectLinks() {
|
||||
viewModelScope.launch {
|
||||
if (selectionId != null) {
|
||||
deselectLinks(selectionId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun removePhotosFromAlbum(
|
||||
driveLink: DriveLink.File,
|
||||
) {
|
||||
@@ -272,28 +336,62 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun leaveShare(driveLink: DriveLink) {
|
||||
val shareId = driveLink.sharingDetails?.shareId
|
||||
val memberId = driveLink.shareUser?.id
|
||||
if (shareId != null && memberId != null && shareId == driveLink.shareId) {
|
||||
leaveShare(driveLink.volumeId, driveLink.id, memberId).last().onFailure { error ->
|
||||
error.log(LogTag.SHARING, "Cannot leave share")
|
||||
private suspend fun saveSharedPhoto(
|
||||
driveLink: DriveLink.File
|
||||
) {
|
||||
val fileId = driveLink.id
|
||||
addPhotosToStream(
|
||||
photoIds = listOf(fileId),
|
||||
albumId = requireNotNull(albumId),
|
||||
).onFailure { error ->
|
||||
error.log(LogTag.ALBUM, "Cannot copy photo to stream: ${fileId.id.logId()}")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess { result ->
|
||||
result.processAddToStream(appContext) { message, type ->
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
message = message,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
CoreLogger.i(
|
||||
tag = VIEW_MODEL,
|
||||
message = """
|
||||
Skipping leave share (DriveLink.shareId=${driveLink.shareId.id.logId()},
|
||||
SharingDetails.shareId=${shareId?.id?.logId()}, memberId=${memberId?.logId()})
|
||||
""".trimIndent(),
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun toggleFavoriteAction(
|
||||
driveLink: DriveLink.File
|
||||
) {
|
||||
toggleFavorite(driveLink).onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Cannot toggle favorite")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.onSuccess {
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = if(driveLink.isFavorite) {
|
||||
appContext.getString(I18N.string.files_remove_from_favorite_action_success)
|
||||
} else {
|
||||
appContext.getString(
|
||||
if (driveLink.volumeId != photoVolume.value?.id) {
|
||||
I18N.string.files_add_to_favorite_from_foreign_volume_action_success
|
||||
} else {
|
||||
I18N.string.files_add_to_favorite_action_success
|
||||
}
|
||||
)
|
||||
},
|
||||
type = BroadcastMessage.Type.INFO
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -313,11 +411,14 @@ class FileOrFolderOptionsViewModel @Inject constructor(
|
||||
const val KEY_LINK_ID = "linkId"
|
||||
const val KEY_ALBUM_ID = "albumId"
|
||||
const val KEY_ALBUM_SHARE_ID = "albumShareId"
|
||||
const val KEY_SELECTION_ID = "selectionId"
|
||||
|
||||
private val options = setOf(
|
||||
Option.SetAsAlbumCover,
|
||||
Option.RemoveFromAlbum,
|
||||
Option.OfflineToggle,
|
||||
Option.FavoriteToggle,
|
||||
Option.SaveSharePhoto,
|
||||
Option.ShareViaInvitations,
|
||||
Option.ManageAccess,
|
||||
Option.SendFile,
|
||||
|
||||
@@ -326,6 +326,7 @@ class FilesViewModel @Inject constructor(
|
||||
driveLink.onClick(
|
||||
navigateToFolder = navigateToFiles,
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToAlbum = { error("Album is not expected here") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -364,7 +365,7 @@ class FilesViewModel @Inject constructor(
|
||||
override val onMoreOptions = { driveLink: DriveLink -> navigateToFileOrFolderOptions(driveLink.id) }
|
||||
override val onSelectedOptions = {
|
||||
onSelectedOptions(
|
||||
{ linkId: LinkId, _ -> navigateToFileOrFolderOptions(linkId) },
|
||||
{ linkId: LinkId, _, _ -> navigateToFileOrFolderOptions(linkId) },
|
||||
{ selectionId: SelectionId, _ -> navigateToMultipleFileOrFolderOptions(selectionId) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -30,14 +30,10 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.BuildConfig
|
||||
import me.proton.android.drive.ui.navigation.HomeTab
|
||||
@@ -47,22 +43,18 @@ import me.proton.android.drive.ui.viewstate.HomeViewState
|
||||
import me.proton.android.drive.usecase.CanGetMoreFreeStorage
|
||||
import me.proton.android.drive.usecase.GetDynamicHomeTabsFlow
|
||||
import me.proton.android.drive.usecase.ShouldShowOverlay
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.domain.entity.SessionUserId
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.extension.getOrNull
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.base.presentation.component.NavigationTab
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveMobileSharingInvitationsAcceptReject
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawerViewEvent
|
||||
import me.proton.core.drive.navigationdrawer.presentation.NavigationDrawerViewState
|
||||
import me.proton.core.drive.share.user.domain.usecase.HasUserInvitationFlow
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.payment.domain.PaymentManager
|
||||
import me.proton.core.user.domain.UserManager
|
||||
import me.proton.core.user.domain.entity.User
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
@@ -81,24 +73,23 @@ class HomeViewModel @Inject constructor(
|
||||
private val canGetMoreFreeStorage: CanGetMoreFreeStorage,
|
||||
getDynamicHomeTabsFlow: GetDynamicHomeTabsFlow,
|
||||
hasUserInvitationFlow: HasUserInvitationFlow,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val shouldShowOverlay: ShouldShowOverlay,
|
||||
private val paymentManager: PaymentManager,
|
||||
) : ViewModel(), NotificationDotViewModel, UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private var navigateToTab: ((route: String) -> Unit)? = null
|
||||
|
||||
override val notificationDotRequested = getFeatureFlagFlow(
|
||||
driveMobileSharingInvitationsAcceptReject(userId),
|
||||
).transform { featureFlag ->
|
||||
if (featureFlag.on) {
|
||||
emitAll(hasUserInvitationFlow(userId))
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
override val notificationDotRequested = hasUserInvitationFlow(userId)
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
|
||||
private val notificationDotForPhotosTab: StateFlow<Boolean> = hasUserInvitationFlow(userId, true)
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
|
||||
val tabs: StateFlow<Map<out HomeTab, NavigationTab>> = combine(
|
||||
getDynamicHomeTabsFlow(userId),
|
||||
notificationDotRequested,
|
||||
) { dynamicHomeTabs, notificationDotRequested ->
|
||||
notificationDotForPhotosTab,
|
||||
) { dynamicHomeTabs, notificationDotRequested, notificationDotForPhotosTab ->
|
||||
dynamicHomeTabs
|
||||
.filter { dynamicHomeTab -> dynamicHomeTab.isEnabled }
|
||||
.sortedBy { dynamicHomeTab -> dynamicHomeTab.order }
|
||||
@@ -106,8 +97,11 @@ class HomeViewModel @Inject constructor(
|
||||
dynamicHomeTab.screen to NavigationTab(
|
||||
iconResId = dynamicHomeTab.iconResId,
|
||||
titleResId = dynamicHomeTab.titleResId,
|
||||
notificationDotVisible = dynamicHomeTab.screen is Screen.SharedTabs
|
||||
&& notificationDotRequested,
|
||||
notificationDotVisible = when (dynamicHomeTab.screen) {
|
||||
is Screen.SharedTabs -> notificationDotRequested
|
||||
is Screen.PhotosAndAlbums -> notificationDotForPhotosTab
|
||||
else -> false
|
||||
}
|
||||
)
|
||||
}
|
||||
.associateBy({ tab -> tab.first }, { tab -> tab.second })
|
||||
@@ -145,6 +139,7 @@ class HomeViewModel @Inject constructor(
|
||||
navigateToOnboarding: () -> Unit,
|
||||
navigateToWhatsNew: (WhatsNewKey) -> Unit,
|
||||
navigateToRatingBooster: () -> Unit,
|
||||
navigateToSubscriptionPromo: (String) -> Unit,
|
||||
): HomeViewEvent = object : HomeViewEvent {
|
||||
override val onTab = { tab: NavigationTab -> navigateToTab(tab.screen(userId)) }
|
||||
override val onFirstLaunch: () -> Unit = {
|
||||
@@ -153,6 +148,7 @@ class HomeViewModel @Inject constructor(
|
||||
UserOverlay.Onboarding -> navigateToOnboarding()
|
||||
is UserOverlay.WhatsNew -> navigateToWhatsNew(overlay.key)
|
||||
UserOverlay.RatingBooster -> navigateToRatingBooster()
|
||||
is UserOverlay.Subcription -> navigateToSubscriptionPromo(overlay.key)
|
||||
null -> {}
|
||||
}
|
||||
}
|
||||
@@ -189,7 +185,7 @@ class HomeViewModel @Inject constructor(
|
||||
else -> error("Unhandled tab item route: $route")
|
||||
}
|
||||
|
||||
private fun getViewState(
|
||||
private suspend fun getViewState(
|
||||
user: User?,
|
||||
startDestination: String,
|
||||
tabs: Map<out HomeTab, NavigationTab>,
|
||||
@@ -204,6 +200,11 @@ class HomeViewModel @Inject constructor(
|
||||
BuildConfig.VERSION_NAME,
|
||||
currentUser = user,
|
||||
showGetFreeStorage = user?.let { canGetMoreFreeStorage(user) } ?: false,
|
||||
showSubscription = user?.userId?.let { userId ->
|
||||
coRunCatching {
|
||||
paymentManager.isSubscriptionAvailable(userId)
|
||||
}.getOrNull(VIEW_MODEL, "Failed to read subscriptions")
|
||||
} ?: false
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -37,6 +37,8 @@ import me.proton.android.drive.photos.presentation.extension.processRemove
|
||||
import me.proton.android.drive.ui.options.Option
|
||||
import me.proton.android.drive.ui.options.filterAlbums
|
||||
import me.proton.android.drive.ui.options.filterAll
|
||||
import me.proton.android.drive.ui.options.filterPermissions
|
||||
import me.proton.android.drive.ui.options.filterShare
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.extension.getOrNull
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
@@ -48,12 +50,14 @@ import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.documentsprovider.domain.usecase.ExportToDownload
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.isPhoto
|
||||
import me.proton.core.drive.drivelink.domain.extension.lowestCommonPermissions
|
||||
import me.proton.core.drive.drivelink.domain.extension.toVolumePhotoListing
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsTempDisabledOnRelease
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.OptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
@@ -64,6 +68,7 @@ import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.trash.domain.usecase.SendToTrash
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
@@ -71,7 +76,6 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getSelectedDriveLinks: GetSelectedDriveLinks,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
albumsFeatureFlag: AlbumsFeatureFlag,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val sendToTrash: SendToTrash,
|
||||
private val exportToDownload: ExportToDownload,
|
||||
@@ -80,6 +84,7 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
|
||||
private val removePhotosFromAlbum: RemovePhotosFromAlbum,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val hasPhotoVolume: HasPhotoVolume,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val selectionId = SelectionId(requireNotNull(savedStateHandle.get(KEY_SELECTION_ID)))
|
||||
val selectedDriveLinks: Flow<List<DriveLink>> = getSelectedDriveLinks(selectionId)
|
||||
@@ -91,24 +96,28 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
|
||||
albumId
|
||||
)
|
||||
}
|
||||
private val albumsFeature = albumsFeatureFlag(userId)
|
||||
.stateIn(viewModelScope, Eagerly, configurationProvider.albumsFeatureFlag)
|
||||
private val albumsKillSwitch = getFeatureFlagFlow(driveAlbumsDisabled(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsDisabled(userId), NOT_FOUND))
|
||||
private val shareTempDisabled = getFeatureFlagFlow(driveAlbumsTempDisabledOnRelease(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsTempDisabledOnRelease(userId), NOT_FOUND))
|
||||
|
||||
fun entries(
|
||||
driveLinks: List<DriveLink>,
|
||||
runAction: (suspend () -> Unit) -> Unit,
|
||||
navigateToMove: (SelectionId, parentId: FolderId?) -> Unit,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToShareMultiplePhotosOptions: (SelectionId) -> Unit,
|
||||
dismiss: () -> Unit,
|
||||
): Flow<List<OptionEntry<Unit>>> = combine(
|
||||
albumsFeature,
|
||||
hasPhotoVolume(userId),
|
||||
albumsKillSwitch,
|
||||
) { albumsFeatureFlagOn, albumsKillSwitch ->
|
||||
shareTempDisabled,
|
||||
) { hasPhotoVolume, albumsKillSwitch, shareDisabled ->
|
||||
options
|
||||
.filterAll(driveLinks)
|
||||
.filterAlbums(albumsFeatureFlagOn, albumsKillSwitch, albumId)
|
||||
.filterAlbums(hasPhotoVolume, albumsKillSwitch, albumId)
|
||||
.filterShare(shareDisabled.on, albumId)
|
||||
.filterPermissions(driveLinks.lowestCommonPermissions)
|
||||
.map { option ->
|
||||
when (option) {
|
||||
is Option.Trash -> option.build(
|
||||
@@ -165,6 +174,12 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
|
||||
}
|
||||
},
|
||||
)
|
||||
is Option.ShareMultiplePhotos -> option.build(
|
||||
runAction = runAction,
|
||||
navigateToShareMultiplePhotosOptions = {
|
||||
navigateToShareMultiplePhotosOptions(selectionId)
|
||||
},
|
||||
)
|
||||
else -> throw IllegalStateException(
|
||||
"Option ${option.javaClass.simpleName} is not found. Did you forget to add it?"
|
||||
)
|
||||
@@ -212,6 +227,7 @@ class MultipleFileOrFolderOptionsViewModel @Inject constructor(
|
||||
private val options = setOfNotNull(
|
||||
Option.RemoveFromAlbum,
|
||||
Option.CreateAlbum,
|
||||
Option.ShareMultiplePhotos,
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) Option.Download else null,
|
||||
Option.Move,
|
||||
Option.Trash,
|
||||
|
||||
@@ -73,6 +73,7 @@ import me.proton.core.drive.drivelink.download.domain.usecase.GetDownloadProgres
|
||||
import me.proton.core.drive.drivelink.list.domain.usecase.GetPagedDriveLinksList
|
||||
import me.proton.core.drive.drivelink.offline.domain.usecase.GetPagedOfflineDriveLinksList
|
||||
import me.proton.core.drive.files.presentation.state.FilesViewState
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
@@ -206,6 +207,7 @@ class OfflineViewModel @Inject constructor(
|
||||
fun viewEvent(
|
||||
navigateToFiles: (FolderId, String?) -> Unit,
|
||||
navigateToPreview: (PagerType, FileId) -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
navigateToSortingDialog: (Sorting) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: LinkId) -> Unit,
|
||||
navigateBack: () -> Unit,
|
||||
@@ -224,6 +226,7 @@ class OfflineViewModel @Inject constructor(
|
||||
fileId
|
||||
)
|
||||
},
|
||||
navigateToAlbum = navigateToAlbum,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,26 +23,33 @@ import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import me.proton.android.drive.ui.viewevent.PhotosAndAlbumsViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.PhotosAndAlbumsViewState
|
||||
import me.proton.android.drive.ui.viewstate.PhotosAndAlbumsViewState.Tab
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PhotosAndAlbumsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
hasPhotoVolume: HasPhotoVolume,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val selectedTab = MutableStateFlow(Tab.PHOTOS)
|
||||
|
||||
val initialViewState: PhotosAndAlbumsViewState = PhotosAndAlbumsViewState(
|
||||
selectedTab = Tab.PHOTOS,
|
||||
isAlbumsTabVisible = false,
|
||||
)
|
||||
|
||||
val viewState: Flow<PhotosAndAlbumsViewState> = selectedTab.map { selectedTab ->
|
||||
val viewState: Flow<PhotosAndAlbumsViewState> = combine(
|
||||
selectedTab,
|
||||
hasPhotoVolume(userId),
|
||||
) { selectedTab, hasPhotoVolume ->
|
||||
initialViewState.copy(
|
||||
selectedTab = selectedTab,
|
||||
isAlbumsTabVisible = hasPhotoVolume,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosImportantUpdatesViewEvent
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.entity.TimestampMs
|
||||
import me.proton.core.drive.base.domain.log.LogTag.PHOTO
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.photo.domain.manager.PhotoShareMigrationManager
|
||||
import me.proton.core.drive.photo.domain.repository.PhotoShareMigrationRepository
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class PhotosImportantUpdatesViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val photoShareMigrationManager: PhotoShareMigrationManager,
|
||||
private val repository: PhotoShareMigrationRepository,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
fun viewEvent(
|
||||
runAction: RunAction,
|
||||
): PhotosImportantUpdatesViewEvent = object : PhotosImportantUpdatesViewEvent {
|
||||
override val onStart = { Unit.also { onStart(runAction) } }
|
||||
override val onRemindMeLater = { Unit.also { onRemindMeLater(runAction) } }
|
||||
}
|
||||
|
||||
private fun onStart(runAction: RunAction) = viewModelScope.launch {
|
||||
photoShareMigrationManager.start(userId)
|
||||
.onFailure { error ->
|
||||
error.log(PHOTO, "Failed starting photo share migration")
|
||||
}
|
||||
.getOrNull()
|
||||
runAction {}
|
||||
}
|
||||
|
||||
private fun onRemindMeLater(runAction: RunAction) = viewModelScope.launch {
|
||||
repository.setPhotosImportantUpdatesLastShown(userId, TimestampMs())
|
||||
runAction {}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import me.proton.android.drive.photos.domain.usecase.GetPhotoListingCount
|
||||
import me.proton.android.drive.photos.domain.usecase.RemoveFromAlbumInfo
|
||||
import me.proton.core.drive.base.domain.extension.getOrNull
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.toVolumePhotoListing
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
|
||||
@@ -66,11 +67,9 @@ open class PhotosPickerAndSelectionViewModel(
|
||||
override fun onDriveLink(driveLink: DriveLink, nonSelectedBlock: () -> Unit) {
|
||||
if (inPickerMode && driveLink is DriveLink.File) {
|
||||
if (selected.value.contains(driveLink.id)) {
|
||||
removeFromAlbum(driveLink)
|
||||
removeSelected(listOf(driveLink.id))
|
||||
removeFromAlbumAndFromSelected(driveLink)
|
||||
} else {
|
||||
addToAlbum(driveLink)
|
||||
addSelected(listOf(driveLink.id))
|
||||
addToAlbumAndToSelected(driveLink)
|
||||
}
|
||||
} else {
|
||||
super.onDriveLink(driveLink, nonSelectedBlock)
|
||||
@@ -100,22 +99,28 @@ open class PhotosPickerAndSelectionViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun addToAlbum(driveLink: DriveLink.File) = viewModelScope.launch {
|
||||
private fun addToAlbumAndToSelected(driveLink: DriveLink.File) = viewModelScope.launch {
|
||||
val photoListings = setOf(driveLink.toVolumePhotoListing())
|
||||
if (destinationAlbumId == null) {
|
||||
addToAlbumInfo(photoListings)
|
||||
.getOrNull(VIEW_MODEL, "Failed to add to album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
|
||||
} else {
|
||||
addToAlbumInfo(destinationAlbumId, photoListings)
|
||||
.getOrNull(VIEW_MODEL, "Failed to add to album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
|
||||
}
|
||||
addSelected(listOf(driveLink.id))
|
||||
}
|
||||
|
||||
private fun removeFromAlbum(driveLink: DriveLink.File) = viewModelScope.launch {
|
||||
private fun removeFromAlbumAndFromSelected(driveLink: DriveLink.File) = viewModelScope.launch {
|
||||
val photoListings = setOf(driveLink.toVolumePhotoListing())
|
||||
if (destinationAlbumId == null) {
|
||||
removeFromAlbumInfo(photoListings)
|
||||
.getOrNull(VIEW_MODEL, "Failed to remove from album info for new album ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
|
||||
} else {
|
||||
removeFromAlbumInfo(destinationAlbumId, photoListings)
|
||||
.getOrNull(VIEW_MODEL, "Failed to remove from album info ShareId=${driveLink.id.shareId.id.logId()}, LinkId=${driveLink.id.id.logId()}")
|
||||
}
|
||||
removeSelected(listOf(driveLink.id))
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -36,9 +36,10 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.SharingStarted.Companion.Eagerly
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
@@ -48,6 +49,7 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.merge
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.take
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.photos.domain.entity.PhotoBackupState
|
||||
@@ -57,19 +59,25 @@ import me.proton.android.drive.photos.domain.usecase.GetAddToAlbumPhotoListings
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotoListingCount
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotosDriveLink
|
||||
import me.proton.android.drive.photos.domain.usecase.RemoveFromAlbumInfo
|
||||
import me.proton.android.drive.photos.domain.usecase.ShowImportantUpdates
|
||||
import me.proton.android.drive.photos.domain.usecase.ShowUpsell
|
||||
import me.proton.android.drive.photos.presentation.R
|
||||
import me.proton.android.drive.photos.presentation.extension.isSelected
|
||||
import me.proton.android.drive.photos.presentation.extension.toEmptyPhotoTagState
|
||||
import me.proton.android.drive.photos.presentation.extension.toPhotosFilter
|
||||
import me.proton.android.drive.photos.presentation.state.PhotosItem
|
||||
import me.proton.android.drive.photos.presentation.viewevent.PhotosViewEvent
|
||||
import me.proton.android.drive.photos.presentation.viewmodel.BackupPermissionsViewModel
|
||||
import me.proton.android.drive.photos.presentation.viewmodel.BackupStatusFormatter
|
||||
import me.proton.android.drive.photos.presentation.viewmodel.SeparatorFormatter
|
||||
import me.proton.android.drive.photos.presentation.viewstate.PhotosFilter
|
||||
import me.proton.android.drive.photos.presentation.viewstate.PhotosViewState
|
||||
import me.proton.android.drive.ui.common.onClick
|
||||
import me.proton.android.drive.ui.effect.HomeEffect
|
||||
import me.proton.android.drive.ui.effect.HomeTabViewModel
|
||||
import me.proton.android.drive.ui.effect.PhotosEffect
|
||||
import me.proton.android.drive.usecase.OnFilesDriveLinkError
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
import me.proton.core.domain.arch.onSuccess
|
||||
import me.proton.core.drive.backup.domain.entity.BackupPermissions
|
||||
import me.proton.core.drive.backup.domain.entity.BackupStatus
|
||||
@@ -100,15 +108,26 @@ import me.proton.core.drive.base.presentation.extension.quantityString
|
||||
import me.proton.core.drive.base.presentation.state.ListContentAppendingState
|
||||
import me.proton.core.drive.base.presentation.state.ListContentState
|
||||
import me.proton.core.drive.base.presentation.viewmodel.onLoadState
|
||||
import me.proton.core.drive.base.presentation.viewstate.TagViewState
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.photo.domain.extension.isInProgress
|
||||
import me.proton.core.drive.drivelink.photo.domain.extension.isPending
|
||||
import me.proton.core.drive.drivelink.photo.domain.manager.PhotoShareMigrationManager
|
||||
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPagedPhotoListingsList
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.GetPhotoCount
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.SelectAll
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbumsDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.extension.off
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.link.selection.domain.usecase.SelectLinks
|
||||
@@ -117,6 +136,7 @@ import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import me.proton.core.drive.user.domain.entity.UserMessage
|
||||
import me.proton.core.drive.user.domain.usecase.CancelUserMessage
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import me.proton.core.plan.presentation.compose.usecase.ShouldUpgradeStorage
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import java.util.Calendar
|
||||
@@ -152,13 +172,18 @@ class PhotosViewModel @Inject constructor(
|
||||
removeFromAlbumInfo: RemoveFromAlbumInfo,
|
||||
getAddToAlbumPhotoListings: GetAddToAlbumPhotoListings,
|
||||
getPhotoListingCount: GetPhotoListingCount,
|
||||
albumsFeatureFlag: AlbumsFeatureFlag,
|
||||
getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
val backupPermissionsViewModel: BackupPermissionsViewModel,
|
||||
private val photoDriveLinks: PhotoDriveLinks,
|
||||
private val onFilesDriveLinkError: OnFilesDriveLinkError,
|
||||
private val syncFolders: SyncFolders,
|
||||
private val checkMissingFolders: CheckMissingFolders,
|
||||
private val cancelUserMessage: CancelUserMessage,
|
||||
private val photoShareMigrationManager: PhotoShareMigrationManager,
|
||||
private val hasPhotoVolume: HasPhotoVolume,
|
||||
shouldUpgradeStorage: ShouldUpgradeStorage,
|
||||
showImportantUpdates: ShowImportantUpdates,
|
||||
) : PhotosPickerAndSelectionViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
selectLinks = selectLinks,
|
||||
@@ -173,6 +198,15 @@ class PhotosViewModel @Inject constructor(
|
||||
HomeTabViewModel,
|
||||
NotificationDotViewModel by NotificationDotViewModel(shouldUpgradeStorage) {
|
||||
|
||||
private val albumsFeatureFlagOn = combine(
|
||||
albumsFeatureFlag(userId)
|
||||
.stateIn(viewModelScope, Eagerly, configurationProvider.albumsFeatureFlag),
|
||||
getFeatureFlagFlow(driveAlbumsDisabled(userId))
|
||||
.stateIn(viewModelScope, Eagerly, FeatureFlag(driveAlbumsDisabled(userId), NOT_FOUND))
|
||||
) { featureFlagOn, killSwitch ->
|
||||
featureFlagOn && killSwitch.off
|
||||
}
|
||||
|
||||
override val driveLinkFilter = { driveLink: DriveLink -> driveLink !is DriveLink.Album }
|
||||
|
||||
private var viewEvent: PhotosViewEvent? = null
|
||||
@@ -194,11 +228,33 @@ class PhotosViewModel @Inject constructor(
|
||||
emit(PhotosEffect.ShowUpsell)
|
||||
}
|
||||
}
|
||||
private val photosEffectShowImportantUpdates = showImportantUpdates(userId).transform { show ->
|
||||
if (show) {
|
||||
CoreLogger.i(PHOTO, "Showing important updates")
|
||||
emit(PhotosEffect.ShowImportantUpdates)
|
||||
}
|
||||
}
|
||||
val photosEffect: Flow<PhotosEffect> = merge(
|
||||
_photosEffect.asSharedFlow(),
|
||||
photosEffectShowUpsell,
|
||||
photosEffectShowImportantUpdates,
|
||||
)
|
||||
private val photoListingsFilter: MutableStateFlow<PhotoTag?> =
|
||||
MutableStateFlow(null)
|
||||
|
||||
private val photosFilters = listOf(
|
||||
PhotosFilter(
|
||||
filter = null,
|
||||
tagViewState = TagViewState(
|
||||
label = appContext.getString(I18N.string.photos_filter_all),
|
||||
icon = CorePresentation.drawable.ic_proton_image,
|
||||
selected = true,
|
||||
)
|
||||
),
|
||||
PhotoTag.Favorites.toPhotosFilter(appContext),
|
||||
PhotoTag.Videos.toPhotosFilter(appContext),
|
||||
PhotoTag.Raw.toPhotosFilter(appContext),
|
||||
)
|
||||
val initialViewState = PhotosViewState(
|
||||
title = appContext.getString(I18N.string.photos_title),
|
||||
navigationIconResId = CorePresentation.drawable.ic_proton_hamburger,
|
||||
@@ -210,7 +266,11 @@ class PhotosViewModel @Inject constructor(
|
||||
backupStatusViewState = null,
|
||||
selected = selected,
|
||||
isRefreshEnabled = selected.value.isEmpty(),
|
||||
inMultiselect = false
|
||||
inMultiselect = false,
|
||||
filters = emptyList(),
|
||||
showPhotoShareMigrationInProgress = false,
|
||||
showPhotoShareMigrationNeededBanner = false,
|
||||
showStorageBanner = false,
|
||||
)
|
||||
private val retryTrigger = MutableSharedFlow<Unit>(replay = 1).apply { tryEmit(Unit) }
|
||||
val driveLink: StateFlow<DriveLink.Folder?> = retryTrigger.transformLatest {
|
||||
@@ -233,61 +293,65 @@ class PhotosViewModel @Inject constructor(
|
||||
shareType = Share.Type.PHOTO,
|
||||
)
|
||||
error.log(VIEW_MODEL, "Cannot get drive link")
|
||||
if (previous is DataResult.Success) {
|
||||
retryLoadingPhotosDriveLinkFolder()
|
||||
}
|
||||
}
|
||||
return@mapWithPrevious null
|
||||
}
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
}.stateIn(viewModelScope, Eagerly, null)
|
||||
|
||||
val driveLinksMap: Flow<Map<LinkId, DriveLink>> = photoDriveLinks.getDriveLinksMapFlow(userId)
|
||||
|
||||
val driveLinks: Flow<PagingData<PhotosItem>> =
|
||||
parentId
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.transformLatest { _ ->
|
||||
emitAll(
|
||||
getPagedPhotoListingsList(userId)
|
||||
.map { pagingData ->
|
||||
pagingData.map { photoListing ->
|
||||
PhotosItem.PhotoListing(
|
||||
photoListing.linkId,
|
||||
photoListing.captureTime,
|
||||
null
|
||||
)
|
||||
}
|
||||
val driveLinks: Flow<PagingData<PhotosItem>> = combine(
|
||||
parentId.filterNotNull().distinctUntilChanged(),
|
||||
photoListingsFilter,
|
||||
) { _, filter ->
|
||||
filter
|
||||
}.transformLatest { filter ->
|
||||
emit(PagingData.empty())
|
||||
emitAll(getPagedPhotoListingsList(userId, filter)
|
||||
.map { pagingData ->
|
||||
pagingData.map { photoListing ->
|
||||
PhotosItem.PhotoListing(
|
||||
photoListing.linkId,
|
||||
photoListing.captureTime,
|
||||
null
|
||||
)
|
||||
}
|
||||
}
|
||||
.map {
|
||||
it.insertSeparators { before: PhotosItem.PhotoListing?, after: PhotosItem.PhotoListing? ->
|
||||
if (after == null) {
|
||||
null
|
||||
} else if (before == null) {
|
||||
PhotosItem.Separator(
|
||||
value = separatorFormatter.toSeparator(after.captureTime),
|
||||
)
|
||||
} else {
|
||||
val beforeCalendar = Calendar.getInstance().apply {
|
||||
timeInMillis = before.captureTime.value * 1000L
|
||||
}
|
||||
.map {
|
||||
it.insertSeparators { before: PhotosItem.PhotoListing?, after: PhotosItem.PhotoListing? ->
|
||||
if (after == null) {
|
||||
null
|
||||
} else if (before == null) {
|
||||
PhotosItem.Separator(
|
||||
value = separatorFormatter.toSeparator(after.captureTime),
|
||||
)
|
||||
} else {
|
||||
val beforeCalendar = Calendar.getInstance().apply {
|
||||
timeInMillis = before.captureTime.value * 1000L
|
||||
}
|
||||
val afterCalendar = Calendar.getInstance().apply {
|
||||
timeInMillis = after.captureTime.value * 1000L
|
||||
}
|
||||
if (beforeCalendar.get(Calendar.YEAR)
|
||||
!= afterCalendar.get(Calendar.YEAR) ||
|
||||
beforeCalendar.get(Calendar.MONTH)
|
||||
!= afterCalendar.get(Calendar.MONTH)
|
||||
) {
|
||||
PhotosItem.Separator(
|
||||
value = separatorFormatter.toSeparator(after.captureTime),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
val afterCalendar = Calendar.getInstance().apply {
|
||||
timeInMillis = after.captureTime.value * 1000L
|
||||
}
|
||||
)
|
||||
}.cachedIn(viewModelScope)
|
||||
if (beforeCalendar.get(Calendar.YEAR)
|
||||
!= afterCalendar.get(Calendar.YEAR) ||
|
||||
beforeCalendar.get(Calendar.MONTH)
|
||||
!= afterCalendar.get(Calendar.MONTH)
|
||||
) {
|
||||
PhotosItem.Separator(
|
||||
value = separatorFormatter.toSeparator(after.captureTime),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}.cachedIn(viewModelScope)
|
||||
|
||||
private val listContentAppendingState = MutableStateFlow<ListContentAppendingState>(
|
||||
ListContentAppendingState.Idle
|
||||
@@ -328,8 +392,12 @@ class PhotosViewModel @Inject constructor(
|
||||
getPhotoCount(userId = userId),
|
||||
firstVisibleItemIndex,
|
||||
forceStatusExpand,
|
||||
notificationDotRequested
|
||||
) { selected, contentState, backupState, count, firstVisibleItemIndex, forceStatusExpand, notificationDotRequested ->
|
||||
notificationDotRequested,
|
||||
photoListingsFilter,
|
||||
albumsFeatureFlagOn,
|
||||
hasPhotoVolume(userId),
|
||||
photoShareMigrationManager.status,
|
||||
) { selected, contentState, backupState, count, firstVisibleItemIndex, forceStatusExpand, notificationDotRequested, photoListingsFilter, albumsFeatureFlagOn, hasPhotoVolume, photoShareMigrationStatus ->
|
||||
val listContentState = when (contentState) {
|
||||
is ListContentState.Empty -> contentState.copy(
|
||||
imageResId = emptyStateImageResId,
|
||||
@@ -348,6 +416,17 @@ class PhotosViewModel @Inject constructor(
|
||||
((firstVisibleItemIndex?.let { index -> index > 0 } ?: false)
|
||||
|| !isDisableOrRunning)
|
||||
val showHamburgerMenuIcon = selected.isEmpty()
|
||||
val backupStatusViewState = backupStatusFormatter.toViewState(
|
||||
backupState = backupState,
|
||||
count = count.takeIf { configurationProvider.photosSavedCounter },
|
||||
)
|
||||
val filters = photosFilters.map { filter ->
|
||||
filter.copy(
|
||||
tagViewState = filter.tagViewState.copy(
|
||||
selected = filter.filter == photoListingsFilter,
|
||||
),
|
||||
)
|
||||
}
|
||||
initialViewState.copy(
|
||||
title = if (selected.isNotEmpty()) {
|
||||
appContext.quantityString(
|
||||
@@ -368,22 +447,28 @@ class PhotosViewModel @Inject constructor(
|
||||
showEmptyList = backupState.isBackupEnabled || backupState.hasDefaultFolder == false ,
|
||||
showPhotosStateIndicator = showPhotosStateIndicator && !inPickerMode,
|
||||
showPhotosStateBanner = showPhotosStateBanner && !inPickerMode,
|
||||
backupStatusViewState = backupStatusFormatter.toViewState(
|
||||
backupState = backupState,
|
||||
count = count.takeIf { configurationProvider.photosSavedCounter },
|
||||
),
|
||||
backupStatusViewState = backupStatusViewState,
|
||||
isRefreshEnabled = selected.isEmpty(),
|
||||
filters = filters,
|
||||
shouldShowFilters = filters.isNotEmpty()
|
||||
&& hasPhotoVolume
|
||||
&& (listContentState !is ListContentState.Empty || !filters.isSelected(null)),
|
||||
emptyPhotoTagState = photoListingsFilter?.toEmptyPhotoTagState(),
|
||||
showPhotoShareMigrationInProgress = albumsFeatureFlagOn && photoShareMigrationStatus.isInProgress,
|
||||
showPhotoShareMigrationNeededBanner = albumsFeatureFlagOn && photoShareMigrationStatus.isPending,
|
||||
showStorageBanner = !inPickerMode,
|
||||
)
|
||||
}
|
||||
|
||||
fun viewEvent(
|
||||
navigateToPreview: (fileId: FileId) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId) -> Unit,
|
||||
navigateToPreview: (fileId: FileId, photoTag: PhotoTag?) -> Unit,
|
||||
navigateToPhotosOptions: (fileId: FileId, SelectionId?) -> Unit,
|
||||
navigateToMultiplePhotosOptions: (selectionId: SelectionId) -> Unit,
|
||||
navigateToSubscription: () -> Unit,
|
||||
navigateToPhotosIssues: (FolderId) -> Unit,
|
||||
navigateToPhotosUpsell: () -> Unit,
|
||||
navigateToBackupSettings: () -> Unit,
|
||||
navigateToPhotosImportantUpdates: () -> Unit,
|
||||
lifecycle: Lifecycle,
|
||||
): PhotosViewEvent = object : PhotosViewEvent {
|
||||
|
||||
@@ -394,7 +479,10 @@ class PhotosViewModel @Inject constructor(
|
||||
flow.take(1).collect { driveLink ->
|
||||
driveLink.onClick(
|
||||
navigateToFolder = { _, _ -> error("Photos should not have folders") },
|
||||
navigateToPreview = navigateToPreview,
|
||||
navigateToPreview = { fileId, ->
|
||||
navigateToPreview(fileId, photoListingsFilter.value)
|
||||
},
|
||||
navigateToAlbum = { error("Photos should not have albums") },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -427,7 +515,7 @@ class PhotosViewModel @Inject constructor(
|
||||
override val onErrorAction = this@PhotosViewModel::onErrorAction
|
||||
override val onSelectedOptions = {
|
||||
onSelectedOptions(
|
||||
{ linkId: FileId, _ -> navigateToPhotosOptions(linkId) },
|
||||
{ linkId: FileId, _, selectionId -> navigateToPhotosOptions(linkId, selectionId) },
|
||||
{ selectionId: SelectionId, _ -> navigateToMultiplePhotosOptions(selectionId) },
|
||||
)
|
||||
}
|
||||
@@ -456,6 +544,9 @@ class PhotosViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
override val onShowUpsell = navigateToPhotosUpsell
|
||||
override val onFilterSelected = this@PhotosViewModel::onFilterSelected
|
||||
override val onStartPhotoShareMigration = this@PhotosViewModel::onStartPhotoShareMigration
|
||||
override val onShowImportantUpdates = navigateToPhotosImportantUpdates
|
||||
}.also { viewEvent ->
|
||||
this.viewEvent = viewEvent
|
||||
}
|
||||
@@ -605,4 +696,29 @@ class PhotosViewModel @Inject constructor(
|
||||
_listEffect.emit(ListEffect.REFRESH)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFilterSelected(filter: PhotoTag?) {
|
||||
viewModelScope.launch {
|
||||
photoListingsFilter.emit(filter)
|
||||
listContentState.value = ListContentState.Loading
|
||||
removeAllSelected()
|
||||
}
|
||||
}
|
||||
|
||||
private fun onStartPhotoShareMigration() {
|
||||
viewModelScope.launch {
|
||||
photoShareMigrationManager.start(userId)
|
||||
.onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Failed to start photo share migration")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToAlbum
|
||||
import me.proton.android.drive.photos.domain.usecase.GetPhotoListingCount
|
||||
import me.proton.android.drive.photos.domain.usecase.RemoveFromAlbumInfo
|
||||
import me.proton.android.drive.photos.presentation.extension.processAdd
|
||||
import me.proton.android.drive.ui.viewevent.PickerPhotosAndAlbumsViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.PickerPhotosAndAlbumsViewState
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.extension.quantityString
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltViewModel
|
||||
class PickerPhotosViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getPhotoListingCount: GetPhotoListingCount,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val removeFromAlbumInfo: RemoveFromAlbumInfo,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
private val addPhotosToAlbum: AddPhotosToAlbum,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
val destinationAlbumId = savedStateHandle.get<String?>(DESTINATION_SHARE_ID)?.let { destinationShareId ->
|
||||
savedStateHandle.get<String?>(DESTINATION_ALBUM_ID)?.let { destinationAlbumId ->
|
||||
AlbumId(ShareId(userId, destinationShareId), destinationAlbumId)
|
||||
}
|
||||
}
|
||||
private val addingInProgress = MutableStateFlow(false)
|
||||
private val photoListingsCount: StateFlow<Int?> = getPhotoListingCount(userId, destinationAlbumId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val viewState = combine(
|
||||
addingInProgress,
|
||||
photoListingsCount.filterNotNull(),
|
||||
) { inProgress, count ->
|
||||
PickerPhotosAndAlbumsViewState(
|
||||
addToAlbumButtonTitle = if (count == 0) {
|
||||
appContext.getString(I18N.string.albums_add_zero_to_album_button)
|
||||
} else {
|
||||
appContext.quantityString(
|
||||
pluralRes = I18N.plurals.albums_add_non_zero_to_album_button,
|
||||
quantity = count,
|
||||
)
|
||||
},
|
||||
isAddToAlbumButtonEnabled = (count > 0) && !inProgress,
|
||||
isResetButtonEnabled = (count > 0) && !inProgress,
|
||||
isAddingInProgress = inProgress,
|
||||
)
|
||||
}
|
||||
|
||||
fun viewEvent(
|
||||
navigateBack: () -> Unit,
|
||||
onAddToAlbumDone: () -> Unit,
|
||||
): PickerPhotosAndAlbumsViewEvent = object : PickerPhotosAndAlbumsViewEvent {
|
||||
override val onBackPressed = { navigateBack() }
|
||||
override val onReset = this@PickerPhotosViewModel::onReset
|
||||
override val onAddToAlbum = { onAddToAlbum(onAddToAlbumDone) }
|
||||
}
|
||||
|
||||
private fun onAddToAlbum(onDone: () -> Unit) {
|
||||
viewModelScope.launch {
|
||||
if (destinationAlbumId == null) {
|
||||
onDone()
|
||||
} else {
|
||||
// add photos from add to album info into destination album
|
||||
addingInProgress.value = true
|
||||
showAddToAlbumStartMessage()
|
||||
addPhotosToAlbum(destinationAlbumId)
|
||||
.onFailure { error ->
|
||||
addingInProgress.value = false
|
||||
error.log(VIEW_MODEL, "Failed adding photos to album")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
}
|
||||
.onSuccess { result ->
|
||||
addingInProgress.value = false
|
||||
result
|
||||
.processAdd(appContext) { message, type ->
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
onDone()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onReset() {
|
||||
viewModelScope.launch {
|
||||
removeFromAlbumInfo(userId, destinationAlbumId)
|
||||
.onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Failed removing all photo listings from add to album")
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = error.getDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showAddToAlbumStartMessage() {
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = appContext.getString(I18N.string.albums_add_to_album_start_message),
|
||||
type = BroadcastMessage.Type.INFO,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val DESTINATION_SHARE_ID = "destinationShareId"
|
||||
const val DESTINATION_ALBUM_ID = "destinationAlbumId"
|
||||
}
|
||||
}
|
||||
@@ -55,4 +55,11 @@ class PlansViewModel @Inject constructor(
|
||||
plansOrchestrator.showCurrentPlanWorkflow(account.userId)
|
||||
}
|
||||
}
|
||||
|
||||
fun startUpgrade() {
|
||||
viewModelScope.launch {
|
||||
val account = primaryAccount.filterNotNull().first()
|
||||
plansOrchestrator.startUpgradeWorkflow(account.userId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.BuildConfig
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.android.drive.ui.effect.PreviewEffect
|
||||
@@ -104,6 +105,7 @@ import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.Link
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.PhotoTag
|
||||
import me.proton.core.drive.link.domain.extension.isProtonDocument
|
||||
import me.proton.core.drive.link.domain.extension.requireFolderId
|
||||
import me.proton.core.drive.link.domain.extension.rootFolderId
|
||||
@@ -159,6 +161,9 @@ class PreviewViewModel @Inject constructor(
|
||||
id = albumIdString,
|
||||
)
|
||||
}
|
||||
private val photoTag = savedStateHandle.get<String>(Screen.PagerPreview.PHOTO_TAG)?.let { tag ->
|
||||
PhotoTag.fromLong(tag.toLong())
|
||||
}
|
||||
private val trigger = MutableSharedFlow<Trigger>(1).apply {
|
||||
val shareId = savedStateHandle.require<String>(Screen.PagerPreview.SHARE_ID)
|
||||
val fileId = savedStateHandle.require<String>(Screen.PagerPreview.FILE_ID)
|
||||
@@ -206,6 +211,7 @@ class PreviewViewModel @Inject constructor(
|
||||
)
|
||||
PagerType.PHOTO -> PhotoContentProvider(
|
||||
userId = userId,
|
||||
photoTag = photoTag,
|
||||
getDecryptedDriveLink = getDecryptedDriveLink,
|
||||
getPhotoShare = getPhotoShare,
|
||||
photoRepository = photoRepository,
|
||||
@@ -275,7 +281,9 @@ class PreviewViewModel @Inject constructor(
|
||||
},
|
||||
currentIndex = currentIndex.value,
|
||||
)
|
||||
CoreLogger.d(VIEW_MODEL, "$previewViewState")
|
||||
if (BuildConfig.DEBUG) {
|
||||
CoreLogger.d(VIEW_MODEL, "$previewViewState")
|
||||
}
|
||||
emit(previewViewState)
|
||||
}.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
|
||||
|
||||
@@ -715,6 +723,7 @@ class PhotoContentProvider(
|
||||
getPhotoShare: GetPhotoShare,
|
||||
photoRepository: PhotoRepository,
|
||||
userId: UserId,
|
||||
photoTag: PhotoTag?,
|
||||
configurationProvider: ConfigurationProvider,
|
||||
coroutineScope: CoroutineScope,
|
||||
) : PreviewContentProvider {
|
||||
@@ -729,14 +738,20 @@ class PhotoContentProvider(
|
||||
.distinctUntilChanged()
|
||||
.transform { photoShare ->
|
||||
emitAll(
|
||||
photoRepository.getPhotoListingCount(userId, photoShare.volumeId)
|
||||
photoRepository.getPhotoListingCount(userId, photoShare.volumeId, photoTag)
|
||||
.distinctUntilChanged()
|
||||
.transformLatest {
|
||||
emit(
|
||||
pagedList(
|
||||
pageSize = configurationProvider.dbPageSize,
|
||||
) { fromIndex, count ->
|
||||
photoRepository.getPhotoListings(userId, photoShare.volumeId, fromIndex, count)
|
||||
photoRepository.getPhotoListings(
|
||||
userId = userId,
|
||||
volumeId = photoShare.volumeId,
|
||||
fromIndex = fromIndex,
|
||||
count = count,
|
||||
tag = photoTag,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -133,12 +133,12 @@ open class SelectionViewModel(
|
||||
}
|
||||
|
||||
protected inline fun <reified T : LinkId> onSelectedOptions(
|
||||
navigateToFileOrFolderOptions: (linkId: T, albumId: AlbumId?) -> Unit,
|
||||
navigateToFileOrFolderOptions: (linkId: T, albumId: AlbumId?, selectionId: SelectionId?) -> Unit,
|
||||
navigateToMultipleFileOrFolderOptions: (selectionId: SelectionId, albumId: AlbumId?) -> Unit,
|
||||
albumId: AlbumId? = null,
|
||||
) {
|
||||
if (selected.value.size == 1) {
|
||||
navigateToFileOrFolderOptions(selected.value.first() as T, albumId)
|
||||
navigateToFileOrFolderOptions(selected.value.first() as T, albumId, selectionId.value)
|
||||
} else {
|
||||
selectionId.value?.let { selectionId -> navigateToMultipleFileOrFolderOptions(selectionId, albumId) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.extension.getDefaultMessage
|
||||
import me.proton.android.drive.extension.log
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToAlbum
|
||||
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
|
||||
import me.proton.android.drive.photos.presentation.extension.processAdd
|
||||
import me.proton.android.drive.photos.presentation.state.AlbumsItem
|
||||
import me.proton.android.drive.ui.viewevent.ShareMultiplePhotosOptionsViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.ShareMultiplePhotosOptionsViewState
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
import me.proton.core.drive.base.domain.log.LogTag.VIEW_MODEL
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.base.presentation.extension.require
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.domain.extension.toVolumePhotoListing
|
||||
import me.proton.core.drive.drivelink.photo.domain.paging.PhotoDriveLinks
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.GetAllAlbumListings
|
||||
import me.proton.core.drive.drivelink.selection.domain.usecase.GetSelectedDriveLinks
|
||||
import me.proton.core.drive.files.presentation.entry.OptionEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.selection.domain.entity.SelectionId
|
||||
import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.photo.domain.entity.AlbumListing
|
||||
import me.proton.core.drive.share.crypto.domain.usecase.GetPhotoShare
|
||||
import me.proton.core.drive.share.domain.entity.Share
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.core.presentation.R as CorePresentation
|
||||
|
||||
@HiltViewModel
|
||||
class ShareMultiplePhotosOptionsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
getSelectedDriveLinks: GetSelectedDriveLinks,
|
||||
getPhotoShare: GetPhotoShare,
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val getAllAlbumListings: GetAllAlbumListings,
|
||||
private val photoDriveLinks: PhotoDriveLinks,
|
||||
private val deselectLinks: DeselectLinks,
|
||||
private val addToAlbumInfo: AddToAlbumInfo,
|
||||
private val addPhotosToAlbum: AddPhotosToAlbum,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
val selectionId = SelectionId(savedStateHandle.require(SELECTION_ID))
|
||||
private val selectedDriveLinks: Flow<List<DriveLink>> = getSelectedDriveLinks(selectionId)
|
||||
private var fetchingJob: Job? = null
|
||||
private var runAction: RunAction? = null
|
||||
private var navigateToCreateNewAlbum: (() -> Unit)? = null
|
||||
private var navigateToAlbum: ((AlbumId) -> Unit)? = null
|
||||
private val driveLinksMap: StateFlow<Map<LinkId, DriveLink>> =
|
||||
photoDriveLinks.getDriveLinksMapFlow(userId)
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyMap())
|
||||
private val photoShare: StateFlow<Share?> = getPhotoShare(userId)
|
||||
.filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
private val allAlbumListings: StateFlow<List<AlbumListing>> = photoShare
|
||||
.filterNotNull()
|
||||
.distinctUntilChanged()
|
||||
.transform { photoShare ->
|
||||
emitAll(
|
||||
getAllAlbumListings(
|
||||
userId = userId,
|
||||
volumeId = photoShare.volumeId,
|
||||
filterBy = flowOf(AlbumListing.Filter.SHARED_BY_ME),
|
||||
)
|
||||
)
|
||||
}
|
||||
.transform { result ->
|
||||
if (result is DataResult.Success) {
|
||||
emit(result.value)
|
||||
}
|
||||
}
|
||||
.stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
|
||||
private val sharedAlbums: Flow<List<AlbumsItem.Listing>> = combine(
|
||||
allAlbumListings,
|
||||
driveLinksMap,
|
||||
) { listings, links ->
|
||||
listings.map { albumListing ->
|
||||
AlbumsItem.Listing(
|
||||
id = albumListing.albumId,
|
||||
isLocked = albumListing.isLocked,
|
||||
photoCount = albumListing.photoCount,
|
||||
lastActivityTime = albumListing.lastActivityTime,
|
||||
isShared = albumListing.isShared,
|
||||
album = links[albumListing.albumId] as? DriveLink.Album,
|
||||
coverLink = albumListing.coverLinkId?.let { coverLinkId ->
|
||||
links[coverLinkId] as? DriveLink.File
|
||||
},
|
||||
albumDetails = (links[albumListing.albumId] as? DriveLink.Album).details(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val initialViewState: ShareMultiplePhotosOptionsViewState =
|
||||
ShareMultiplePhotosOptionsViewState(
|
||||
shareOptionsSectionTitleResId = I18N.string.albums_share_multiple_photos_options_section_title,
|
||||
shareOptions = listOf(
|
||||
object : OptionEntry<Unit> {
|
||||
override val icon = CorePresentation.drawable.ic_proton_users
|
||||
override val label = I18N.string.albums_share_multiple_photos_options_new_shared_album
|
||||
override val onClick = { _: Unit ->
|
||||
runAction?.invoke { onCreateNewAlbum() }
|
||||
Unit
|
||||
}
|
||||
},
|
||||
),
|
||||
sharedAlbumsSectionTitleResId = I18N.string.albums_share_multiple_photos_options_shared_albums_section_title,
|
||||
sharedAlbums = sharedAlbums,
|
||||
)
|
||||
|
||||
fun viewEvent(
|
||||
runAction: RunAction,
|
||||
navigateToCreateNewAlbum: () -> Unit,
|
||||
navigateToAlbum: (AlbumId) -> Unit,
|
||||
): ShareMultiplePhotosOptionsViewEvent = object : ShareMultiplePhotosOptionsViewEvent {
|
||||
override val onScroll = this@ShareMultiplePhotosOptionsViewModel::onScroll
|
||||
override val onSharedAlbum = { albumId: AlbumId -> runAction { onSharedAlbum(albumId) } }
|
||||
}.also {
|
||||
this.runAction = runAction
|
||||
this.navigateToCreateNewAlbum = navigateToCreateNewAlbum
|
||||
this.navigateToAlbum = navigateToAlbum
|
||||
}
|
||||
|
||||
private fun onScroll(driveLinkIds: Set<LinkId>) {
|
||||
if (driveLinkIds.isNotEmpty()) {
|
||||
fetchingJob?.cancel()
|
||||
fetchingJob = viewModelScope.launch {
|
||||
photoDriveLinks.load(driveLinkIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onSharedAlbum(albumId: AlbumId) {
|
||||
viewModelScope.launch {
|
||||
selectedDriveLinks.firstOrNull()?.let { driveLinks ->
|
||||
driveLinks
|
||||
.filterIsInstance<DriveLink.File>()
|
||||
.map { photo -> photo.toVolumePhotoListing() }
|
||||
.let { photoListings ->
|
||||
addToAlbumInfo(albumId, photoListings.toSet())
|
||||
.onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Failed to add photos to album info")
|
||||
error.broadcast()
|
||||
}
|
||||
.onSuccess {
|
||||
deselectLinks(selectionId)
|
||||
showAddToAlbumStartMessage()
|
||||
addPhotosToAlbum(albumId)
|
||||
.onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Failed to add photos to album")
|
||||
error.broadcast()
|
||||
}
|
||||
.onSuccess { result ->
|
||||
result.processAdd(appContext) { message, type ->
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = message,
|
||||
type = type,
|
||||
)
|
||||
}
|
||||
navigateToAlbum?.invoke(albumId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun onCreateNewAlbum() {
|
||||
viewModelScope.launch {
|
||||
selectedDriveLinks.firstOrNull()?.let { driveLinks ->
|
||||
driveLinks
|
||||
.filterIsInstance<DriveLink.File>()
|
||||
.map { photo -> photo.toVolumePhotoListing() }
|
||||
.let { photoListings ->
|
||||
addToAlbumInfo(photoListings.toSet())
|
||||
.onFailure { error ->
|
||||
error.log(VIEW_MODEL, "Failed to add photos to album info")
|
||||
error.broadcast()
|
||||
}
|
||||
.onSuccess {
|
||||
deselectLinks(selectionId)
|
||||
navigateToCreateNewAlbum?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun DriveLink.Album?.details(): String? = this?.let {
|
||||
appContext.getString(I18N.string.albums_share_multiple_photos_options_album_details)
|
||||
}
|
||||
|
||||
private fun Throwable.broadcast() =
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = getDefaultMessage(
|
||||
context = appContext,
|
||||
useExceptionMessage = configurationProvider.useExceptionMessage,
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR,
|
||||
)
|
||||
|
||||
private fun showAddToAlbumStartMessage() {
|
||||
broadcastMessages(
|
||||
userId = userId,
|
||||
message = appContext.getString(I18N.string.albums_add_to_album_start_message),
|
||||
type = BroadcastMessage.Type.INFO,
|
||||
)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SELECTION_ID = "selectionId"
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,6 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.transform
|
||||
import kotlinx.coroutines.flow.transformLatest
|
||||
import me.proton.core.domain.arch.mapSuccessValueOrNull
|
||||
import me.proton.core.drive.base.domain.extension.filterSuccessOrError
|
||||
@@ -50,9 +49,7 @@ import me.proton.core.drive.drivelink.shared.presentation.viewstate.UserInvitati
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.ENABLED
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State.NOT_FOUND
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveMobileSharingInvitationsAcceptReject
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveSharingDisabled
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.share.user.domain.entity.SharedLinkId
|
||||
import me.proton.core.drive.share.user.domain.usecase.GetAllSharedWithMeIds
|
||||
@@ -116,24 +113,20 @@ class SharedWithMeViewModel @Inject constructor(
|
||||
|
||||
override suspend fun getAllIds(): Result<List<SharedLinkId>> = getAllSharedWithMeIds(userId)
|
||||
|
||||
val userInvitationBannerViewState =
|
||||
getFeatureFlagFlow(driveMobileSharingInvitationsAcceptReject(userId)).transform { featureFlag ->
|
||||
if (featureFlag.on) {
|
||||
emitAll(getUserInvitationCountFlow(userId, refresh = flowOf(true))
|
||||
.filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.filterNotNull()
|
||||
.filter { count -> count > 0 }
|
||||
.map { count ->
|
||||
UserInvitationBannerViewState(
|
||||
appContext.quantityString(
|
||||
I18N.plurals.shared_by_me_invitation_banner_description,
|
||||
count
|
||||
)
|
||||
)
|
||||
}
|
||||
val userInvitationBannerViewState = getUserInvitationCountFlow(
|
||||
userId = userId,
|
||||
refresh = flowOf(true),
|
||||
).filterSuccessOrError()
|
||||
.mapSuccessValueOrNull()
|
||||
.filterNotNull()
|
||||
.filter { count -> count > 0 }
|
||||
.map { count ->
|
||||
UserInvitationBannerViewState(
|
||||
appContext.quantityString(
|
||||
I18N.plurals.shared_by_me_invitation_banner_description,
|
||||
count
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
* Copyright (c) 2023-2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import me.proton.android.drive.R
|
||||
import me.proton.android.drive.log.DriveLogTag.DEFAULT
|
||||
import me.proton.android.drive.ui.viewevent.DriveLitePopupViewEvent
|
||||
import me.proton.android.drive.ui.viewstate.SubscriptionPromoViewState
|
||||
import me.proton.android.drive.usecase.MarkSubscriptionPromoAsShown
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.extension.GiB
|
||||
import me.proton.core.drive.base.presentation.component.RunAction
|
||||
import me.proton.core.drive.base.presentation.extension.asHumanReadableString
|
||||
import me.proton.core.drive.base.presentation.extension.require
|
||||
import me.proton.core.drive.base.presentation.viewmodel.UserViewModel
|
||||
import me.proton.core.plan.domain.entity.DynamicPlan
|
||||
import me.proton.core.plan.domain.usecase.GetDynamicPlansAdjustedPrices
|
||||
import me.proton.core.plan.domain.usecase.ObserveUserCurrency
|
||||
import me.proton.core.plan.presentation.entity.PlanCycle
|
||||
import me.proton.core.presentation.utils.formatCentsPriceDefaultLocale
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
import javax.inject.Inject
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@HiltViewModel
|
||||
class SubscriptionPromoViewModel @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val markSubscriptionPromoAsShown: MarkSubscriptionPromoAsShown,
|
||||
private val getDynamicPlansAdjustedPrices: GetDynamicPlansAdjustedPrices,
|
||||
observeUserCurrency: ObserveUserCurrency,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
val key: String = savedStateHandle.require(PROMO_KEY)
|
||||
|
||||
val viewState = observeUserCurrency(userId).map { userCurrency ->
|
||||
val plans = getDynamicPlansAdjustedPrices(userId)
|
||||
val plan = plans.plans.firstOrNull { plan -> plan.name == key }
|
||||
if (plan != null) {
|
||||
val currency = plan.getCurrency(userCurrency)
|
||||
when (key) {
|
||||
PLAN_NAME_PLUS -> drivePlusViewState(plan, currency, PlanCycle.MONTHLY)
|
||||
PLAN_NAME_LITE -> driveLiteViewState(plan, currency, PlanCycle.MONTHLY)
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun DynamicPlan.getCurrency(
|
||||
userCurrency: String
|
||||
): String {
|
||||
val instanceCurrencies = instances.flatMap { it.value.price.keys }.toSet().toList()
|
||||
val currencies = when {
|
||||
instanceCurrencies.contains(userCurrency) -> listOf(userCurrency) + (instanceCurrencies - userCurrency)
|
||||
else -> instanceCurrencies
|
||||
}
|
||||
val currency = currencies.firstOrNull { it == userCurrency }
|
||||
?: currencies.firstOrNull() ?: userCurrency
|
||||
return currency
|
||||
}
|
||||
|
||||
private fun drivePlusViewState(
|
||||
plan: DynamicPlan,
|
||||
currency: String,
|
||||
planCycle: PlanCycle,
|
||||
): SubscriptionPromoViewState {
|
||||
val price = plan.instances[planCycle.value]?.price?.get(currency)
|
||||
val firstMonthPriceString =
|
||||
(price?.current)?.toDouble()?.formatCentsPriceDefaultLocale(currency).orEmpty()
|
||||
val priceString =
|
||||
(price?.default)?.toDouble()?.formatCentsPriceDefaultLocale(currency).orEmpty()
|
||||
val photoCountString = NumberFormat.getNumberInstance(Locale.getDefault()).format(50_000)
|
||||
val storageString = 200.GiB.asHumanReadableString(appContext, numberOfDecimals = 0)
|
||||
return SubscriptionPromoViewState(
|
||||
image = R.drawable.img_drive_plus,
|
||||
title = appContext.getString(I18N.string.drive_plus_promo_title)
|
||||
.format(storageString, firstMonthPriceString),
|
||||
description = appContext.getString(I18N.string.drive_plus_promo_description)
|
||||
.format(
|
||||
photoCountString,
|
||||
firstMonthPriceString,
|
||||
priceString
|
||||
),
|
||||
actionText = appContext.getString(I18N.string.drive_plus_promo_action)
|
||||
.format(firstMonthPriceString)
|
||||
)
|
||||
}
|
||||
|
||||
private fun driveLiteViewState(
|
||||
plan: DynamicPlan,
|
||||
currency: String,
|
||||
planCycle: PlanCycle,
|
||||
): SubscriptionPromoViewState {
|
||||
val price = plan.instances[planCycle.value]?.price?.get(currency)
|
||||
val priceString =
|
||||
(price?.default)?.toDouble()?.formatCentsPriceDefaultLocale(currency).orEmpty()
|
||||
val name = plan.title
|
||||
val storageString = 20.GiB.asHumanReadableString(appContext, numberOfDecimals = 0)
|
||||
return SubscriptionPromoViewState(
|
||||
image = R.drawable.img_drive_lite,
|
||||
title = appContext.getString(I18N.string.drive_lite_promo_title)
|
||||
.format(priceString),
|
||||
description = appContext.getString(I18N.string.drive_lite_promo_description)
|
||||
.format(name, storageString, priceString),
|
||||
actionText = appContext.getString(I18N.string.drive_lite_promo_action)
|
||||
.format(name)
|
||||
)
|
||||
}
|
||||
|
||||
fun viewEvent(
|
||||
runAction: RunAction,
|
||||
navigateToSubscription: () -> Unit,
|
||||
): DriveLitePopupViewEvent = object : DriveLitePopupViewEvent {
|
||||
override val onGetSubscription = {
|
||||
runAction {
|
||||
navigateToSubscription()
|
||||
}
|
||||
}
|
||||
override val onCancel = {
|
||||
runAction {
|
||||
|
||||
}
|
||||
}
|
||||
override val onDismiss = {
|
||||
viewModelScope.launch {
|
||||
markSubscriptionPromoAsShown(userId, key).onFailure { error ->
|
||||
error.log(DEFAULT, "Cannot mark subscription promo as shown")
|
||||
}
|
||||
}
|
||||
Unit
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val PROMO_KEY = "key"
|
||||
private const val PLAN_NAME_LITE = "drivelite2024"
|
||||
private const val PLAN_NAME_PLUS = "drive2022"
|
||||
}
|
||||
}
|
||||
@@ -182,6 +182,7 @@ class SyncedFoldersViewModel @Inject constructor(
|
||||
driveLink.onClick(
|
||||
navigateToFolder = navigateToFiles,
|
||||
navigateToPreview = { error("Preview is not supported here") },
|
||||
navigateToAlbum = { error("Album is not supported here") },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ import me.proton.core.drive.trash.domain.TrashManager
|
||||
import me.proton.core.drive.trash.domain.usecase.GetEmptyTrashState
|
||||
import me.proton.core.drive.volume.domain.entity.Volume
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.drive.volume.domain.usecase.GetVolumes
|
||||
import me.proton.core.drive.volume.domain.usecase.GetActiveVolumes
|
||||
import me.proton.drive.android.settings.domain.entity.LayoutType
|
||||
import me.proton.drive.android.settings.domain.usecase.GetLayoutType
|
||||
import me.proton.drive.android.settings.domain.usecase.ToggleLayoutType
|
||||
@@ -84,7 +84,7 @@ class TrashViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val toggleLayoutType: ToggleLayoutType,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
getVolumes: GetVolumes,
|
||||
getActiveVolumes: GetActiveVolumes,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
|
||||
private val volumeIdFlow = MutableStateFlow<VolumeId?>(null)
|
||||
@@ -108,7 +108,7 @@ class TrashViewModel @Inject constructor(
|
||||
listContentState,
|
||||
listContentAppendingState,
|
||||
layoutType,
|
||||
getVolumes(userId).mapSuccessValueOrNull(),
|
||||
getActiveVolumes(userId).mapSuccessValueOrNull(),
|
||||
) { sorting, contentState, appendingState, layoutType, volumes ->
|
||||
val listContentState = when (contentState) {
|
||||
is ListContentState.Empty -> contentState.copy(
|
||||
|
||||
@@ -70,6 +70,7 @@ class UserInvitationViewModel @Inject constructor(
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) : ViewModel(), UserViewModel by UserViewModel(savedStateHandle) {
|
||||
private val albumsOnly: Boolean = savedStateHandle[ALBUMS_ONLY] ?: false
|
||||
|
||||
private val emptyState = ListContentState.Empty(
|
||||
imageResId = getThemeDrawableId(
|
||||
@@ -77,7 +78,11 @@ class UserInvitationViewModel @Inject constructor(
|
||||
dark = R.drawable.empty_shared_with_me_dark,
|
||||
dayNight = R.drawable.empty_shared_with_me_daynight,
|
||||
),
|
||||
titleId = I18N.string.shared_user_invitations_title_empty,
|
||||
titleId = if (albumsOnly) {
|
||||
I18N.string.shared_user_album_invitations_title_empty
|
||||
} else {
|
||||
I18N.string.shared_user_invitations_title_empty
|
||||
},
|
||||
descriptionResId = I18N.string.shared_user_invitations_description_empty
|
||||
)
|
||||
|
||||
@@ -85,7 +90,7 @@ class UserInvitationViewModel @Inject constructor(
|
||||
MutableStateFlow(ListContentState.Loading)
|
||||
|
||||
val userInvitations: StateFlow<List<UserInvitation>?> =
|
||||
getDecryptedUserInvitationsFlow(userId).transform { result ->
|
||||
getDecryptedUserInvitationsFlow(userId, albumsOnly).transform { result ->
|
||||
when (result) {
|
||||
is DataResult.Processing -> listContentState.value = ListContentState.Loading
|
||||
is DataResult.Error -> {
|
||||
@@ -111,7 +116,13 @@ class UserInvitationViewModel @Inject constructor(
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
|
||||
|
||||
val initialViewState = UserInvitationViewState(
|
||||
title = appContext.getString(I18N.string.shared_user_invitations_title).format(0),
|
||||
title = appContext.getString(
|
||||
if (albumsOnly) {
|
||||
I18N.string.shared_user_album_invitations_title
|
||||
} else {
|
||||
I18N.string.shared_user_invitations_title
|
||||
}
|
||||
).format(0),
|
||||
navigationIconResId = CorePresentation.drawable.ic_proton_arrow_back,
|
||||
listContentState = listContentState.value,
|
||||
)
|
||||
@@ -121,8 +132,13 @@ class UserInvitationViewModel @Inject constructor(
|
||||
userInvitations,
|
||||
) { state, userInvitations ->
|
||||
initialViewState.copy(
|
||||
title = appContext.getString(I18N.string.shared_user_invitations_title)
|
||||
.format(userInvitations.orEmpty().size),
|
||||
title = appContext.getString(
|
||||
if (albumsOnly) {
|
||||
I18N.string.shared_user_album_invitations_title
|
||||
} else {
|
||||
I18N.string.shared_user_invitations_title
|
||||
}
|
||||
).format(userInvitations.orEmpty().size),
|
||||
listContentState = state,
|
||||
)
|
||||
}
|
||||
@@ -151,7 +167,13 @@ class UserInvitationViewModel @Inject constructor(
|
||||
}.onSuccess {
|
||||
broadcastMessages(
|
||||
userId,
|
||||
appContext.getString(I18N.string.shared_user_invitations_accept_success),
|
||||
appContext.getString(
|
||||
if (albumsOnly) {
|
||||
I18N.string.shared_user_album_invitations_accept_success
|
||||
} else {
|
||||
I18N.string.shared_user_invitations_accept_success
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -175,9 +197,19 @@ class UserInvitationViewModel @Inject constructor(
|
||||
}.onSuccess {
|
||||
broadcastMessages(
|
||||
userId,
|
||||
appContext.getString(I18N.string.shared_user_invitations_decline_success),
|
||||
appContext.getString(
|
||||
if (albumsOnly) {
|
||||
I18N.string.shared_user_album_invitations_decline_success
|
||||
} else {
|
||||
I18N.string.shared_user_invitations_decline_success
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ALBUMS_ONLY = "albumsOnly"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,12 +49,13 @@ class WhatsNewViewModel @Inject constructor(
|
||||
|
||||
val viewState: Flow<WhatsNewViewState?> = flowOf {
|
||||
when (key) {
|
||||
WhatsNewKey.PUBLIC_SHARING -> WhatsNewViewState(
|
||||
title = appContext.getString(I18N.string.whats_new_public_sharing_title),
|
||||
description = appContext.getString(I18N.string.whats_new_public_sharing_description),
|
||||
action = appContext.getString(I18N.string.whats_new_public_sharing_action),
|
||||
image = BasePresentation.drawable.img_whats_new_public_sharing,
|
||||
WhatsNewKey.ALBUMS -> WhatsNewViewState(
|
||||
title = appContext.getString(I18N.string.whats_new_albums_title),
|
||||
description = appContext.getString(I18N.string.whats_new_albums_description),
|
||||
action = appContext.getString(I18N.string.whats_new_albums_action),
|
||||
image = BasePresentation.drawable.img_whats_new_albums,
|
||||
)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ package me.proton.android.drive.ui.viewstate
|
||||
|
||||
data class PhotosAndAlbumsViewState(
|
||||
val selectedTab: Tab,
|
||||
val isAlbumsTabVisible: Boolean,
|
||||
) {
|
||||
enum class Tab {
|
||||
PHOTOS, ALBUMS
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewstate
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import me.proton.android.drive.photos.presentation.state.AlbumsItem
|
||||
import me.proton.core.drive.files.presentation.entry.OptionEntry
|
||||
|
||||
data class ShareMultiplePhotosOptionsViewState(
|
||||
val shareOptionsSectionTitleResId: Int,
|
||||
val shareOptions: List<OptionEntry<Unit>>,
|
||||
val sharedAlbumsSectionTitleResId: Int,
|
||||
val sharedAlbums: Flow<List<AlbumsItem.Listing>>,
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewstate
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
|
||||
data class SubscriptionPromoViewState(
|
||||
@DrawableRes val image: Int,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val actionText: String,
|
||||
)
|
||||
@@ -19,14 +19,12 @@
|
||||
package me.proton.android.drive.usecase
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
import me.proton.android.drive.log.DriveLogTag
|
||||
import me.proton.android.drive.ui.navigation.Screen
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.domain.extension.getOrNull
|
||||
import me.proton.core.drive.base.domain.usecase.HasBusinessPlan
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.drive.android.settings.domain.entity.DynamicHomeTab
|
||||
import me.proton.drive.android.settings.domain.entity.HomeTab
|
||||
import me.proton.drive.android.settings.domain.usecase.GetHomeTab
|
||||
@@ -37,17 +35,15 @@ import me.proton.core.presentation.R as CorePresentation
|
||||
class GetDynamicHomeTabsFlow @Inject constructor(
|
||||
private val getHomeTab: GetHomeTab,
|
||||
private val hasBusinessPlan: HasBusinessPlan,
|
||||
private val albumsFeatureFlag: AlbumsFeatureFlag,
|
||||
) {
|
||||
operator fun invoke(userId: UserId): Flow<List<DynamicHomeTab>> = getHomeTab(userId)
|
||||
.map { userDefaultHomeTab ->
|
||||
val areAlbumsEnabled = albumsFeatureFlag(userId).firstOrNull() ?: false
|
||||
HomeTab
|
||||
.entries
|
||||
.map { homeTab: HomeTab ->
|
||||
DynamicHomeTab(
|
||||
id = homeTab,
|
||||
route = homeTab.route(areAlbumsEnabled),
|
||||
route = homeTab.route(),
|
||||
order = homeTab.ordinal,
|
||||
iconResId = homeTab.iconResId(),
|
||||
titleResId = homeTab.titleResId,
|
||||
@@ -61,9 +57,9 @@ class GetDynamicHomeTabsFlow @Inject constructor(
|
||||
.ifNoDefaultMakeFirstDefault()
|
||||
}
|
||||
|
||||
private fun HomeTab.route(areAlbumsEnabled: Boolean): String = when (this) {
|
||||
private fun HomeTab.route(): String = when (this) {
|
||||
HomeTab.FILES -> Screen.Files.route
|
||||
HomeTab.PHOTOS -> if (areAlbumsEnabled) Screen.PhotosAndAlbums.route else Screen.Photos.route
|
||||
HomeTab.PHOTOS -> Screen.PhotosAndAlbums.route
|
||||
HomeTab.COMPUTERS -> Screen.Computers.route
|
||||
HomeTab.SHARED -> Screen.SharedTabs.route
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.usecase
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.last
|
||||
import me.proton.core.drive.base.data.extension.getDefaultMessage
|
||||
import me.proton.core.drive.base.data.extension.log
|
||||
import me.proton.core.drive.base.domain.extension.toResult
|
||||
import me.proton.core.drive.base.domain.log.LogTag
|
||||
import me.proton.core.drive.base.domain.log.logId
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.shared.domain.extension.sharingDetails
|
||||
import me.proton.core.drive.link.domain.extension.shareId
|
||||
import me.proton.core.drive.link.domain.extension.userId
|
||||
import me.proton.core.drive.messagequeue.domain.entity.BroadcastMessage
|
||||
import me.proton.core.drive.share.user.domain.usecase.LeaveShare
|
||||
import me.proton.core.util.kotlin.CoreLogger
|
||||
import javax.inject.Inject
|
||||
|
||||
class LeaveShare @Inject constructor(
|
||||
@ApplicationContext private val appContext: Context,
|
||||
private val leaveShare: LeaveShare,
|
||||
private val broadcastMessages: BroadcastMessages,
|
||||
private val configurationProvider: ConfigurationProvider,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(driveLink: DriveLink): Result<Boolean> = coRunCatching {
|
||||
val shareId = driveLink.sharingDetails?.shareId
|
||||
val memberId = driveLink.shareUser?.id
|
||||
if (shareId != null && memberId != null && shareId == driveLink.shareId) {
|
||||
leaveShare(driveLink.volumeId, driveLink.id, memberId).last().toResult()
|
||||
.onFailure { error ->
|
||||
error.log(LogTag.SHARING, "Cannot leave share")
|
||||
broadcastMessages(
|
||||
userId = driveLink.userId,
|
||||
message = error.getDefaultMessage(
|
||||
appContext,
|
||||
configurationProvider.useExceptionMessage
|
||||
),
|
||||
type = BroadcastMessage.Type.ERROR
|
||||
)
|
||||
}.getOrThrow()
|
||||
true
|
||||
} else {
|
||||
CoreLogger.i(
|
||||
tag = LogTag.SHARING,
|
||||
message = """
|
||||
Skipping leave share (DriveLink.shareId=${driveLink.shareId.id.logId()},
|
||||
SharingDetails.shareId=${shareId?.id?.logId()}, memberId=${memberId?.logId()})
|
||||
""".trimIndent(),
|
||||
)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.usecase
|
||||
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore.Keys.subscriptionLastUpdate
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
|
||||
class MarkSubscriptionPromoAsShown @Inject constructor(
|
||||
private val getUserDataStore: GetUserDataStore,
|
||||
) {
|
||||
suspend operator fun invoke(userId: UserId, key: String) = coRunCatching {
|
||||
getUserDataStore(userId).edit { preferences ->
|
||||
preferences[subscriptionLastUpdate(key)] = Date().time
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,22 +30,27 @@ class ShouldShowOverlay @Inject constructor(
|
||||
private val shouldShowOnboarding: ShouldShowOnboarding,
|
||||
private val shouldShowWhatsNew: ShouldShowWhatsNew,
|
||||
private val shouldShowRatingBooster: ShouldShowRatingBooster,
|
||||
private val shouldShowSubscriptionPromo: ShouldShowSubscriptionPromo,
|
||||
private val repository: UiSettingsRepository,
|
||||
) {
|
||||
suspend operator fun invoke(userId: UserId): Result<UserOverlay?> = coRunCatching {
|
||||
if (repository.hasShownOverlay()) {
|
||||
null
|
||||
} else if (shouldShowOnboarding(userId).getOrThrow()) {
|
||||
UserOverlay.Onboarding
|
||||
return@coRunCatching null
|
||||
}
|
||||
if (shouldShowOnboarding(userId).getOrThrow()) {
|
||||
return@coRunCatching UserOverlay.Onboarding
|
||||
}
|
||||
val subscriptionPromo = shouldShowSubscriptionPromo(userId).getOrThrow()
|
||||
if (subscriptionPromo != null) {
|
||||
return@coRunCatching subscriptionPromo
|
||||
}
|
||||
val whatsNewKey = shouldShowWhatsNew(userId).getOrThrow()
|
||||
if (whatsNewKey != null) {
|
||||
UserOverlay.WhatsNew(whatsNewKey)
|
||||
} else if (shouldShowRatingBooster(userId).getOrThrow()) {
|
||||
UserOverlay.RatingBooster
|
||||
} else {
|
||||
val whatsNewKey = shouldShowWhatsNew(userId).getOrThrow()
|
||||
if (whatsNewKey != null) {
|
||||
UserOverlay.WhatsNew(whatsNewKey)
|
||||
} else if (shouldShowRatingBooster(userId).getOrThrow()) {
|
||||
UserOverlay.RatingBooster
|
||||
} else {
|
||||
null
|
||||
}
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2024 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.usecase
|
||||
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore
|
||||
import me.proton.core.drive.base.data.datastore.GetUserDataStore.Keys.subscriptionLastUpdate
|
||||
import me.proton.core.drive.base.data.extension.get
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.extension.off
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlag
|
||||
import me.proton.core.payment.domain.PaymentManager
|
||||
import me.proton.core.plan.domain.usecase.GetDynamicPlansAdjustedPrices
|
||||
import me.proton.core.plan.presentation.entity.PlanCycle
|
||||
import me.proton.core.user.domain.extension.hasSubscription
|
||||
import me.proton.core.user.domain.usecase.GetUser
|
||||
import me.proton.drive.android.settings.domain.entity.UserOverlay
|
||||
import javax.inject.Inject
|
||||
|
||||
class ShouldShowSubscriptionPromo @Inject constructor(
|
||||
private val getFeatureFlag: GetFeatureFlag,
|
||||
private val getUser: GetUser,
|
||||
private val paymentManager: PaymentManager,
|
||||
private val getDynamicPlansAdjustedPrices: GetDynamicPlansAdjustedPrices,
|
||||
private val getUserDataStore: GetUserDataStore,
|
||||
) {
|
||||
suspend operator fun invoke(userId: UserId): Result<UserOverlay.Subcription?> = coRunCatching {
|
||||
if (!paymentManager.isSubscriptionAvailable(userId)) {
|
||||
return@coRunCatching null
|
||||
}
|
||||
|
||||
if (getFeatureFlag(FeatureFlagId.drivePlusPlanIntro(userId)).on) {
|
||||
val subscription = subscription(userId, PLAN_NAME_PLUS, PlanCycle.MONTHLY)
|
||||
if (subscription != null) {
|
||||
return@coRunCatching subscription
|
||||
}
|
||||
}
|
||||
|
||||
if (getFeatureFlag(FeatureFlagId.driveOneDollarPlanUpsell(userId)).on) {
|
||||
subscription(userId, PLAN_NAME_LITE, PlanCycle.MONTHLY)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun subscription(
|
||||
userId: UserId,
|
||||
name: String,
|
||||
cycle: PlanCycle,
|
||||
): UserOverlay.Subcription? {
|
||||
val isFreeUser = getUser(userId, false).hasSubscription().not()
|
||||
val plans = getDynamicPlansAdjustedPrices(userId).plans
|
||||
val lastUpdate = getUserDataStore(userId).get(subscriptionLastUpdate(name))
|
||||
val hasPlan = plans.any { plan -> plan.name == name && cycle.value in plan.instances.keys }
|
||||
return if (isFreeUser && hasPlan && lastUpdate == null) {
|
||||
UserOverlay.Subcription(name)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private companion object {
|
||||
const val PLAN_NAME_LITE = "drivelite2024"
|
||||
const val PLAN_NAME_PLUS = "drive2022"
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.util.coRunCatching
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAndroidWhatsNew
|
||||
import me.proton.core.drive.feature.flag.domain.extension.on
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.drive.android.settings.domain.entity.WhatsNewKey
|
||||
import javax.inject.Inject
|
||||
@@ -33,6 +34,7 @@ import javax.inject.Singleton
|
||||
class ShouldShowWhatsNew @Inject constructor(
|
||||
private val wasWhatsNewShown: WasWhatsNewShown,
|
||||
private val getFeatureFlagFlow: GetFeatureFlagFlow,
|
||||
private val albumsFeatureFlag: AlbumsFeatureFlag,
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(userId: UserId): Result<WhatsNewKey?> = coRunCatching {
|
||||
@@ -42,7 +44,7 @@ class ShouldShowWhatsNew @Inject constructor(
|
||||
).first().on
|
||||
) {
|
||||
when {
|
||||
canShow(WhatsNewKey.PUBLIC_SHARING) -> WhatsNewKey.PUBLIC_SHARING
|
||||
canShow(WhatsNewKey.ALBUMS) && albumsOn(userId) -> WhatsNewKey.ALBUMS
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
@@ -52,4 +54,6 @@ class ShouldShowWhatsNew @Inject constructor(
|
||||
|
||||
private suspend fun ShouldShowWhatsNew.canShow(key: WhatsNewKey) =
|
||||
TimestampS() < key.limit && wasWhatsNewShown(key).getOrThrow().not()
|
||||
|
||||
private suspend fun albumsOn(userId: UserId): Boolean = albumsFeatureFlag(userId).first()
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ class BackupNotificationBuilder @Inject constructor(
|
||||
): NotificationCompat.Builder = setContentIntent(
|
||||
contentIntent(
|
||||
notificationId = notificationId,
|
||||
uri = "${appContext.deepLinkBaseUrl}/${Screen.Photos(notificationId.channel.userId)}".toUri(),
|
||||
uri = "${appContext.deepLinkBaseUrl}/${Screen.PhotosAndAlbums(notificationId.channel.userId)}".toUri(),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 147 KiB |
|
After Width: | Height: | Size: 306 KiB |
@@ -29,4 +29,5 @@
|
||||
<!-- Show 2 times the Notification Permission Request rational -->
|
||||
<integer name="core_feature_notifications_permission_show_rationale_count">0</integer>
|
||||
<bool name="core_feature_observability_enabled">true</bool>
|
||||
<bool name="core_feature_payments_v5_enabled">true</bool>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.SavedStateHandle
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.core.crypto.common.pgp.VerificationStatus
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.domain.entity.Attributes
|
||||
import me.proton.core.drive.base.domain.entity.Bytes
|
||||
import me.proton.core.drive.base.domain.entity.CryptoProperty
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.asSuccess
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.db.test.volumeId
|
||||
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.DeleteAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.LeaveAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ManageAccessEntry
|
||||
import me.proton.core.drive.files.presentation.entry.RenameFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ShareViaInvitationsEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleOfflineEntry
|
||||
import me.proton.core.drive.link.domain.entity.AlbumId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.Link
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.SharingDetails
|
||||
import me.proton.core.drive.linkdownload.domain.entity.DownloadState
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.share.user.domain.entity.ShareUser
|
||||
import me.proton.core.drive.shareurl.base.domain.entity.ShareUrlId
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class AlbumOptionsViewModelTest {
|
||||
private val savedStateHandle = mockk<SavedStateHandle>()
|
||||
private val getDriveLink = mockk<GetDecryptedDriveLink>()
|
||||
private val getFeatureFlagFlow = mockk<GetFeatureFlagFlow>()
|
||||
private val broadcastMessages = mockk<BroadcastMessages>()
|
||||
private val albumOptionsViewModel get() =
|
||||
AlbumOptionsViewModel(
|
||||
savedStateHandle = savedStateHandle,
|
||||
getDriveLink = getDriveLink,
|
||||
getFeatureFlagFlow = getFeatureFlagFlow,
|
||||
broadcastMessages = broadcastMessages,
|
||||
)
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
coEvery { savedStateHandle.get<String>(any()) } returns "value"
|
||||
coEvery { getFeatureFlagFlow.invoke(any(), any(), any())} answers {
|
||||
val id: FeatureFlagId = arg(0)
|
||||
flowOf(FeatureFlag(id, State.NOT_FOUND))
|
||||
}
|
||||
coEvery { getFeatureFlagFlow.refreshAfterDuration} answers {
|
||||
{ false }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `private album`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<AlbumId>(), any()) } returns flowOf(album.copy(
|
||||
link = albumLink.copy(
|
||||
isShared = false,
|
||||
sharingDetails = null,
|
||||
)
|
||||
).asSuccess)
|
||||
|
||||
// When
|
||||
val entries = albumOptionEntries()
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
listOf(
|
||||
//ToggleOfflineEntry::class,
|
||||
ShareViaInvitationsEntry::class,
|
||||
ManageAccessEntry::class,
|
||||
RenameFileEntry::class,
|
||||
DeleteAlbumEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `album shared by me`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<AlbumId>(), any()) } returns flowOf(album.asSuccess)
|
||||
|
||||
// When
|
||||
val entries = albumOptionEntries()
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
listOf(
|
||||
//ToggleOfflineEntry::class,
|
||||
ShareViaInvitationsEntry::class,
|
||||
ManageAccessEntry::class,
|
||||
RenameFileEntry::class,
|
||||
DeleteAlbumEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `album shared with me as viewer`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<AlbumId>(), any()) } returns flowOf(
|
||||
album.copy(
|
||||
sharePermissions = Permissions.viewer,
|
||||
shareUser = shareUser.copy(permissions = Permissions.viewer)
|
||||
).asSuccess
|
||||
)
|
||||
|
||||
// When
|
||||
val entries = albumOptionEntries()
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
listOf(
|
||||
//ToggleOfflineEntry::class,
|
||||
LeaveAlbumEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `album shared with me as editor`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<AlbumId>(), any()) } returns flowOf(
|
||||
album.copy(
|
||||
sharePermissions = Permissions.viewer,
|
||||
shareUser = shareUser.copy(permissions = Permissions.editor)
|
||||
).asSuccess
|
||||
)
|
||||
|
||||
// When
|
||||
val entries = albumOptionEntries()
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
listOf(
|
||||
//ToggleOfflineEntry::class,
|
||||
LeaveAlbumEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun albumOptionEntries() =
|
||||
albumOptionsViewModel.entries(
|
||||
runAction = {},
|
||||
navigateToShareViaInvitations = { _: LinkId -> },
|
||||
navigateToManageAccess = { _: LinkId -> },
|
||||
navigateToRename = { _: LinkId -> },
|
||||
navigateToDelete = { _: AlbumId -> },
|
||||
navigateToLeave = { _: AlbumId -> },
|
||||
dismiss = {},
|
||||
).filterNotNull().first()
|
||||
|
||||
private val albumLink = Link.Album(
|
||||
id = AlbumId(ShareId(UserId("USER_ID"), "SHARE_ID"), "ID"),
|
||||
parentId = FolderId(ShareId(UserId("USER_ID"), "SHARE_ID"), "PARENT_ID"),
|
||||
size = Bytes(123),
|
||||
lastModified = TimestampS(System.currentTimeMillis() / 1000),
|
||||
mimeType = "video/mp4",
|
||||
numberOfAccesses = 2,
|
||||
isShared = true,
|
||||
uploadedBy = "m4@proton.black",
|
||||
name = "Link name",
|
||||
key = "key",
|
||||
passphrase = "passphrase",
|
||||
passphraseSignature = "signature",
|
||||
attributes = Attributes(0),
|
||||
permissions = Permissions(0),
|
||||
state = Link.State.ACTIVE,
|
||||
nameSignatureEmail = "",
|
||||
hash = "",
|
||||
expirationTime = null,
|
||||
nodeKey = "",
|
||||
nodePassphrase = "",
|
||||
nodePassphraseSignature = "",
|
||||
signatureEmail = "",
|
||||
creationTime = TimestampS(0),
|
||||
trashedTime = null,
|
||||
shareUrlExpirationTime = null,
|
||||
xAttr = null,
|
||||
sharingDetails = SharingDetails(
|
||||
shareId = ShareId(UserId("USER_ID"), "SHARING_ID"),
|
||||
shareUrlId = ShareUrlId(ShareId(UserId("USER_ID"), "SHARING_ID"), "")
|
||||
),
|
||||
nodeHashKey = "nodehashkey",
|
||||
isLocked = false,
|
||||
lastActivityTime = TimestampS(0),
|
||||
photoCount = 2,
|
||||
)
|
||||
|
||||
val album = DriveLink.Album(
|
||||
link = albumLink,
|
||||
volumeId = volumeId,
|
||||
isMarkedAsOffline = true,
|
||||
isAnyAncestorMarkedAsOffline = false,
|
||||
downloadState = DownloadState.Downloaded(),
|
||||
trashState = null,
|
||||
cryptoName = CryptoProperty.Decrypted("Album", VerificationStatus.Success),
|
||||
cryptoXAttr = CryptoProperty.Decrypted(
|
||||
"""{"Common":{"ModificationTime":"2023-07-27T13:52:23.636Z"},"Media":{"Duration":46}}""",
|
||||
VerificationStatus.Success,
|
||||
),
|
||||
shareInvitationCount = null,
|
||||
shareMemberCount = null,
|
||||
shareUser = null,
|
||||
sharePermissions = Permissions.admin
|
||||
)
|
||||
|
||||
private val shareUser = ShareUser.Member(
|
||||
id ="id",
|
||||
inviter = "",
|
||||
email = "",
|
||||
createTime = TimestampS(),
|
||||
permissions = Permissions.owner,
|
||||
keyPacket = "",
|
||||
keyPacketSignature = null,
|
||||
sessionKeySignature = null,
|
||||
)
|
||||
}
|
||||
@@ -26,10 +26,15 @@ import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.android.drive.photos.domain.usecase.AddPhotosToStream
|
||||
import me.proton.android.drive.photos.domain.usecase.RemovePhotosFromAlbum
|
||||
import me.proton.android.drive.ui.viewmodel.FileOrFolderOptionsViewModel.Companion.KEY_ALBUM_ID
|
||||
import me.proton.android.drive.usecase.LeaveShare
|
||||
import me.proton.android.drive.usecase.NotifyActivityNotFound
|
||||
import me.proton.android.drive.usecase.OpenProtonDocumentInBrowser
|
||||
import me.proton.core.crypto.common.pgp.VerificationStatus
|
||||
import me.proton.core.domain.arch.DataResult
|
||||
import me.proton.core.domain.arch.ResponseSource
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.domain.entity.Attributes
|
||||
import me.proton.core.drive.base.domain.entity.Bytes
|
||||
@@ -37,18 +42,20 @@ import me.proton.core.drive.base.domain.entity.CryptoProperty
|
||||
import me.proton.core.drive.base.domain.entity.Permissions
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.drive.base.domain.extension.asSuccess
|
||||
import me.proton.core.drive.base.domain.extension.bytes
|
||||
import me.proton.core.drive.base.domain.provider.ConfigurationProvider
|
||||
import me.proton.core.drive.base.domain.usecase.BroadcastMessages
|
||||
import me.proton.core.drive.db.test.volumeId
|
||||
import me.proton.core.drive.documentsprovider.domain.usecase.ExportTo
|
||||
import me.proton.core.drive.drivelink.crypto.domain.usecase.GetDecryptedDriveLink
|
||||
import me.proton.core.drive.drivelink.domain.entity.DriveLink
|
||||
import me.proton.core.drive.drivelink.offline.domain.usecase.ToggleOffline
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.ToggleFavorite
|
||||
import me.proton.core.drive.drivelink.photo.domain.usecase.UpdateAlbumCover
|
||||
import me.proton.core.drive.drivelink.trash.domain.usecase.ToggleTrashState
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.DownloadFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.FileInfoEntry
|
||||
@@ -61,6 +68,7 @@ import me.proton.core.drive.files.presentation.entry.RenameFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.SendFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.SetAsAlbumCoverEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ShareViaInvitationsEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleFavoriteFileEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleOfflineEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ToggleTrashEntry
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
@@ -68,12 +76,14 @@ import me.proton.core.drive.link.domain.entity.FolderId
|
||||
import me.proton.core.drive.link.domain.entity.Link
|
||||
import me.proton.core.drive.link.domain.entity.LinkId
|
||||
import me.proton.core.drive.link.domain.entity.SharingDetails
|
||||
import me.proton.core.drive.link.selection.domain.usecase.DeselectLinks
|
||||
import me.proton.core.drive.linkdownload.domain.entity.DownloadState
|
||||
import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.share.user.domain.entity.ShareUser
|
||||
import me.proton.core.drive.share.user.domain.usecase.LeaveShare
|
||||
import me.proton.core.drive.shareurl.base.domain.entity.ShareUrlId
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.drive.volume.domain.entity.Volume
|
||||
import me.proton.core.drive.volume.domain.usecase.GetOldestActiveVolume
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -95,10 +105,19 @@ class FileOrFolderOptionsViewModelTest {
|
||||
private val openProtonDocumentInBrowser = mockk<OpenProtonDocumentInBrowser>()
|
||||
private val updateAlbumCover = mockk<UpdateAlbumCover>()
|
||||
private val removePhotosFromAlbum = mockk<RemovePhotosFromAlbum>()
|
||||
private val deselectLinks = mockk<DeselectLinks>()
|
||||
private val toggleFavorite = mockk<ToggleFavorite>()
|
||||
private val getOldestActiveVolume = mockk<GetOldestActiveVolume>()
|
||||
private val addPhotosToStream = mockk<AddPhotosToStream>()
|
||||
private val hasPhotoVolume = mockk<HasPhotoVolume>()
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
coEvery { savedStateHandle.get<String>(any()) } returns "value"
|
||||
coEvery { savedStateHandle.get<String>(any()) } answers {
|
||||
val key: String = arg(0)
|
||||
if (key == KEY_ALBUM_ID) null else "value"
|
||||
}
|
||||
|
||||
coEvery { getFeatureFlagFlow.invoke(any(), any(), any())} answers {
|
||||
val id: FeatureFlagId = arg(0)
|
||||
flowOf(FeatureFlag(id, State.NOT_FOUND))
|
||||
@@ -107,6 +126,20 @@ class FileOrFolderOptionsViewModelTest {
|
||||
{ false }
|
||||
}
|
||||
coEvery { configurationProvider.albumsFeatureFlag} returns true
|
||||
coEvery { deselectLinks(any()) } returns Unit
|
||||
coEvery { getOldestActiveVolume(any(), any()) } returns flowOf(DataResult.Success(
|
||||
source = ResponseSource.Local,
|
||||
value = Volume(
|
||||
id = volumeId,
|
||||
shareId = "",
|
||||
linkId = "",
|
||||
usedSpace = 0.bytes,
|
||||
state = 0,
|
||||
createTime = TimestampS(),
|
||||
type = Volume.Type.PHOTO
|
||||
)
|
||||
))
|
||||
coEvery { hasPhotoVolume.invoke(any()) } returns flowOf(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -198,7 +231,13 @@ class FileOrFolderOptionsViewModelTest {
|
||||
coEvery { getDriveLink.invoke(any<LinkId>(), any()) } returns flowOf(fileDriveLink.asSuccess)
|
||||
coEvery { getFeatureFlagFlow.invoke(any())} answers {
|
||||
val id: FeatureFlagId = arg(0)
|
||||
flowOf(FeatureFlag(id, State.ENABLED))
|
||||
flowOf(
|
||||
if (id == FeatureFlagId.driveAlbumsTempDisabledOnRelease(UserId("value"))) {
|
||||
FeatureFlag(id, State.NOT_FOUND)
|
||||
} else {
|
||||
FeatureFlag(id, State.ENABLED)
|
||||
}
|
||||
)
|
||||
}
|
||||
// When
|
||||
val entries = fileOptionEntries()
|
||||
@@ -283,9 +322,10 @@ class FileOrFolderOptionsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file options on photo share without album feature flag`() = runTest {
|
||||
fun `file options on photo share without photo volume`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<LinkId>(), any()) } returns flowOf(photoDriveLink.asSuccess)
|
||||
coEvery { hasPhotoVolume.invoke(any()) } returns flowOf(false)
|
||||
|
||||
// When
|
||||
val entries = fileOptionEntries()
|
||||
@@ -306,12 +346,13 @@ class FileOrFolderOptionsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `file options on photo share with album feature flag`() = runTest {
|
||||
fun `file options on photo share with photo volume`() = runTest {
|
||||
// Given
|
||||
coEvery { getDriveLink.invoke(any<LinkId>(), any()) } returns flowOf(photoDriveLink.asSuccess)
|
||||
val featureFlagId = FeatureFlagId.driveAlbums(UserId("value"))
|
||||
coEvery { getFeatureFlagFlow(featureFlagId, any(), any()) } returns
|
||||
flowOf( FeatureFlag(featureFlagId, State.ENABLED))
|
||||
coEvery { savedStateHandle.get<String>(KEY_ALBUM_ID) } returns "album-id"
|
||||
|
||||
// When
|
||||
val entries = fileOptionEntries()
|
||||
@@ -322,12 +363,10 @@ class FileOrFolderOptionsViewModelTest {
|
||||
SetAsAlbumCoverEntry::class,
|
||||
RemoveFromAlbumFileEntry::class,
|
||||
ToggleOfflineEntry::class,
|
||||
ShareViaInvitationsEntry::class,
|
||||
ManageAccessEntry::class,
|
||||
ToggleFavoriteFileEntry::class,
|
||||
SendFileEntry::class,
|
||||
DownloadFileEntry::class,
|
||||
FileInfoEntry::class,
|
||||
ToggleTrashEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
@@ -384,13 +423,17 @@ class FileOrFolderOptionsViewModelTest {
|
||||
exportTo = exportTo,
|
||||
notifyActivityNotFound = notifyActivityNotFound,
|
||||
getFeatureFlagFlow = getFeatureFlagFlow,
|
||||
albumsFeatureFlag = AlbumsFeatureFlag(getFeatureFlagFlow, configurationProvider),
|
||||
leaveShare = leaveShare,
|
||||
configurationProvider = configurationProvider,
|
||||
broadcastMessages = broadcastMessages,
|
||||
openProtonDocumentInBrowser = openProtonDocumentInBrowser,
|
||||
updateAlbumCover = updateAlbumCover,
|
||||
removePhotosFromAlbum = removePhotosFromAlbum,
|
||||
deselectLinks = deselectLinks,
|
||||
toggleFavorite = toggleFavorite,
|
||||
getOldestActiveVolume = getOldestActiveVolume,
|
||||
addPhotosToStream = addPhotosToStream,
|
||||
hasPhotoVolume = hasPhotoVolume,
|
||||
)
|
||||
|
||||
private val fileLink = Link.File(
|
||||
@@ -435,7 +478,7 @@ class FileOrFolderOptionsViewModelTest {
|
||||
|
||||
private val fileDriveLink = DriveLink.File(
|
||||
link = fileLink,
|
||||
volumeId = VolumeId("VOLUME_ID"),
|
||||
volumeId = volumeId,
|
||||
isMarkedAsOffline = true,
|
||||
isAnyAncestorMarkedAsOffline = false,
|
||||
downloadState = DownloadState.Downloaded(),
|
||||
|
||||
@@ -28,6 +28,8 @@ import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import me.proton.android.drive.photos.domain.usecase.AddToAlbumInfo
|
||||
import me.proton.android.drive.photos.domain.usecase.RemovePhotosFromAlbum
|
||||
import me.proton.android.drive.ui.viewmodel.FileOrFolderOptionsViewModel.Companion
|
||||
import me.proton.android.drive.ui.viewmodel.MultipleFileOrFolderOptionsViewModel.Companion.KEY_ALBUM_ID
|
||||
import me.proton.core.crypto.common.pgp.VerificationStatus
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.drive.base.domain.entity.Attributes
|
||||
@@ -44,12 +46,12 @@ import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlag.State
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId
|
||||
import me.proton.core.drive.feature.flag.domain.entity.FeatureFlagId.Companion.driveAlbums
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.AlbumsFeatureFlag
|
||||
import me.proton.core.drive.feature.flag.domain.usecase.GetFeatureFlagFlow
|
||||
import me.proton.core.drive.files.presentation.entry.CreateAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.DownloadEntry
|
||||
import me.proton.core.drive.files.presentation.entry.MoveEntry
|
||||
import me.proton.core.drive.files.presentation.entry.RemoveFromAlbumEntry
|
||||
import me.proton.core.drive.files.presentation.entry.ShareMultiplePhotosEntry
|
||||
import me.proton.core.drive.files.presentation.entry.TrashEntry
|
||||
import me.proton.core.drive.link.domain.entity.FileId
|
||||
import me.proton.core.drive.link.domain.entity.FolderId
|
||||
@@ -62,6 +64,7 @@ import me.proton.core.drive.share.domain.entity.ShareId
|
||||
import me.proton.core.drive.shareurl.base.domain.entity.ShareUrlId
|
||||
import me.proton.core.drive.trash.domain.usecase.SendToTrash
|
||||
import me.proton.core.drive.volume.domain.entity.VolumeId
|
||||
import me.proton.core.drive.volume.domain.usecase.HasPhotoVolume
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
@@ -80,10 +83,14 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
private val configurationProvider = mockk<ConfigurationProvider>()
|
||||
private val removePhotosFromAlbum = mockk<RemovePhotosFromAlbum>()
|
||||
private val broadcastMessages = mockk<BroadcastMessages>()
|
||||
private val hasPhotoVolume = mockk<HasPhotoVolume>()
|
||||
|
||||
@Before
|
||||
fun before() {
|
||||
coEvery { savedStateHandle.get<String>(any()) } returns "value"
|
||||
coEvery { savedStateHandle.get<String>(any()) } answers {
|
||||
val key: String = arg(0)
|
||||
if (key == FileOrFolderOptionsViewModel.KEY_ALBUM_ID) null else "value"
|
||||
}
|
||||
coEvery { getSelectedDriveLinks.invoke(any()) } returns flowOf()
|
||||
coEvery { getFeatureFlagFlow.invoke(any(), any(), any())} answers {
|
||||
val id: FeatureFlagId = arg(0)
|
||||
@@ -93,12 +100,13 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
{ false }
|
||||
}
|
||||
coEvery { configurationProvider.albumsFeatureFlag} returns true
|
||||
coEvery { hasPhotoVolume.invoke(any()) } returns flowOf(true)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `files options`() = runTest {
|
||||
// Given
|
||||
val files = listOf(fileDriveLink)
|
||||
val files = listOf(fileDriveLink, fileDriveLink2)
|
||||
|
||||
// When
|
||||
val entries = files.fileOptionEntries()
|
||||
@@ -115,9 +123,10 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `photos options without album feature flag`() = runTest {
|
||||
fun `photos options without photo volume`() = runTest {
|
||||
// Given
|
||||
val files = listOf(photoDriveLink)
|
||||
coEvery { hasPhotoVolume.invoke(any()) } returns flowOf(false)
|
||||
val files = listOf(photoDriveLink, photoDriveLink2)
|
||||
|
||||
// When
|
||||
val entries = files.fileOptionEntries()
|
||||
@@ -133,12 +142,37 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `photos options with album feature flag `() = runTest {
|
||||
fun `photos options with photo volume`() = runTest {
|
||||
// Given
|
||||
val featureFlagId = driveAlbums(UserId("value"))
|
||||
coEvery { getFeatureFlagFlow(featureFlagId, any(), any()) } returns
|
||||
flowOf( FeatureFlag(featureFlagId, State.ENABLED))
|
||||
val files = listOf(photoDriveLink)
|
||||
coEvery { savedStateHandle.get<String>(KEY_ALBUM_ID) } returns null
|
||||
val files = listOf(photoDriveLink, photoDriveLink2)
|
||||
|
||||
// When
|
||||
val entries = files.fileOptionEntries()
|
||||
|
||||
// Then
|
||||
assertEquals(
|
||||
listOf(
|
||||
CreateAlbumEntry::class,
|
||||
ShareMultiplePhotosEntry::class,
|
||||
DownloadEntry::class,
|
||||
TrashEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `photos options within album`() = runTest {
|
||||
// Given
|
||||
val featureFlagId = driveAlbums(UserId("value"))
|
||||
coEvery { getFeatureFlagFlow(featureFlagId, any(), any()) } returns
|
||||
flowOf( FeatureFlag(featureFlagId, State.ENABLED))
|
||||
coEvery { savedStateHandle.get<String>(Companion.KEY_ALBUM_ID) } returns "album-id"
|
||||
val files = listOf(photoDriveLink, photoDriveLink2)
|
||||
|
||||
// When
|
||||
val entries = files.fileOptionEntries()
|
||||
@@ -147,21 +181,19 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
assertEquals(
|
||||
listOf(
|
||||
RemoveFromAlbumEntry::class,
|
||||
CreateAlbumEntry::class,
|
||||
DownloadEntry::class,
|
||||
TrashEntry::class,
|
||||
),
|
||||
entries.map { it.javaClass.kotlin }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
private suspend fun List<DriveLink>.fileOptionEntries() =
|
||||
fileOrFolderOptionsViewModel().entries(
|
||||
driveLinks = this,
|
||||
runAction = {},
|
||||
navigateToMove = { _: SelectionId, _: FolderId? -> },
|
||||
navigateToCreateNewAlbum = { },
|
||||
navigateToCreateNewAlbum = {},
|
||||
navigateToShareMultiplePhotosOptions = {},
|
||||
dismiss = {},
|
||||
).filterNotNull().first()
|
||||
|
||||
@@ -175,9 +207,9 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
addToAlbumInfo = addToAlbumInfo,
|
||||
removePhotosFromAlbum = removePhotosFromAlbum,
|
||||
getFeatureFlagFlow = getFeatureFlagFlow,
|
||||
albumsFeatureFlag = AlbumsFeatureFlag(getFeatureFlagFlow, configurationProvider),
|
||||
broadcastMessages = broadcastMessages,
|
||||
configurationProvider = configurationProvider,
|
||||
hasPhotoVolume = hasPhotoVolume,
|
||||
)
|
||||
|
||||
private val fileLink = Link.File(
|
||||
@@ -238,6 +270,13 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
sharePermissions = Permissions.admin
|
||||
)
|
||||
|
||||
private val fileDriveLink2 = fileDriveLink.copy(
|
||||
link = fileDriveLink.link.copy(
|
||||
id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "ID2"),
|
||||
),
|
||||
cryptoName = CryptoProperty.Decrypted("Link name 2", VerificationStatus.Success),
|
||||
)
|
||||
|
||||
private val photoDriveLink = fileDriveLink.copy(
|
||||
link = fileLink.copy(
|
||||
photoCaptureTime = TimestampS(0),
|
||||
@@ -245,4 +284,10 @@ class MultipleFileOrFolderOptionsViewModelTest {
|
||||
mainPhotoLinkId = "MAIN_ID"
|
||||
)
|
||||
)
|
||||
|
||||
private val photoDriveLink2 = photoDriveLink.copy(
|
||||
link = photoDriveLink.link.copy(
|
||||
id = FileId(ShareId(UserId("USER_ID"), "SHARE_ID"), "ID2"),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,10 @@ fun NodeAssertions.assertIsSharedWithUsers(expectedValue: Boolean) = apply {
|
||||
interaction.assert(SemanticsMatcher.expectValue(DriveLinkSemanticsProperties.IsSharedWithUsers, expectedValue))
|
||||
}
|
||||
|
||||
fun NodeAssertions.assertIsFavorite(expectedValue: Boolean) = apply {
|
||||
interaction.assert(SemanticsMatcher.expectValue(DriveLinkSemanticsProperties.IsFavorite, expectedValue))
|
||||
}
|
||||
|
||||
// Remove after TPE-334 is resolved
|
||||
fun NodeAssertions.doesNotExist() =
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.extension
|
||||
|
||||
import me.proton.core.domain.entity.UserId
|
||||
import me.proton.core.test.rule.ProtonRule
|
||||
|
||||
val ProtonRule.mainUserId: UserId get() = UserId(requireNotNull(testDataRule.mainTestUser?.id))
|
||||
@@ -26,6 +26,9 @@ object AlbumOptionsRobot : Robot {
|
||||
private val albumOptionsScreen get() = node.withTag(AlbumOptionsTestTag.albumOptions)
|
||||
private val renameButton get() = node.withText(I18N.string.common_rename_action)
|
||||
private val deleteAlbumButton get() = node.withText(I18N.string.albums_delete_album_action)
|
||||
private val shareButton get() = node.withText(I18N.string.common_share)
|
||||
private val manageAccessButton get() = node.withText(I18N.string.common_manage_access_action)
|
||||
private val leaveAlbumButton get() = node.withText(I18N.string.common_leave_album_action)
|
||||
|
||||
fun clickRename() = RenameRobot.apply {
|
||||
renameButton.scrollTo().click()
|
||||
@@ -33,6 +36,18 @@ object AlbumOptionsRobot : Robot {
|
||||
|
||||
fun clickDeleteAlbum() = deleteAlbumButton.clickTo(ConfirmDeleteAlbumRobot)
|
||||
|
||||
fun clickShare() = shareButton
|
||||
.hasAncestor(node.withTag(AlbumOptionsTestTag.albumOptions))
|
||||
.clickTo(ShareUserRobot)
|
||||
|
||||
fun clickManageAccess() = manageAccessButton.clickTo(ManageAccessRobot)
|
||||
|
||||
fun clickLeaveAlbum() = leaveAlbumButton.clickTo(ConfirmLeaveAlbumRobot)
|
||||
|
||||
fun assertDeleteAlbumOptionIsDisplayed() {
|
||||
deleteAlbumButton.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
albumOptionsScreen.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -18,32 +18,65 @@
|
||||
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import androidx.compose.ui.test.assertCountEquals
|
||||
import me.proton.android.drive.ui.extension.withItemType
|
||||
import me.proton.android.drive.ui.extension.withLayoutType
|
||||
import me.proton.android.drive.ui.extension.withLinkName
|
||||
import me.proton.android.drive.ui.screen.AlbumScreenTestTag
|
||||
import me.proton.core.drive.files.presentation.extension.ItemType
|
||||
import me.proton.core.drive.files.presentation.extension.LayoutType
|
||||
import me.proton.test.fusion.Fusion.allNodes
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.test.fusion.FusionConfig.targetContext
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
import me.proton.android.drive.photos.presentation.component.ProtonMediaItemTestTags
|
||||
import me.proton.test.fusion.ui.compose.ComposeWaiter.waitFor
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object AlbumRobot : LinksRobot, NavigationBarRobot {
|
||||
private val albumScreen get() = node.withTag(AlbumScreenTestTag.screen)
|
||||
private val moreButton get() = node.withContentDescription(I18N.string.common_more)
|
||||
private val addButton get() = node.withText(I18N.string.common_add_action)
|
||||
private val saveAllButton get() = node.withText(I18N.string.common_save_all_action)
|
||||
private val shareButton get() = node.withText(I18N.string.common_share)
|
||||
private val emptyAlbumText get() = node.withText(I18N.string.albums_empty_album_screen_title)
|
||||
private val mediaItems get() = allNodes.withTag(ProtonMediaItemTestTags.mediaItemPreviewBox)
|
||||
private val favoriteFromForeignVolume
|
||||
get() = node.withText(I18N.string.files_add_to_favorite_from_foreign_volume_action_success)
|
||||
private val addToAlbumStartMessage
|
||||
get() = node.withText(I18N.string.albums_add_to_album_start_message)
|
||||
|
||||
fun clickOnMoreButton() = moreButton.clickTo(AlbumOptionsRobot)
|
||||
|
||||
fun clickOnAdd() = addButton.clickTo(PickerPhotosAndAlbumsRobot)
|
||||
fun clickOnAdd() = addButton.clickTo(PickerPhotosTabRobot)
|
||||
|
||||
fun clickOnSaveAll() = saveAllButton.clickTo(AlbumRobot)
|
||||
|
||||
fun clickOnShare() = shareButton.clickTo(ShareUserRobot)
|
||||
|
||||
fun clickOnPhoto(name: String) =
|
||||
photoWithName(name).clickTo(PreviewRobot)
|
||||
|
||||
fun <T : Robot> clickOnPhoto(name: String, goesTo: T) =
|
||||
photoWithName(name).clickTo(goesTo)
|
||||
|
||||
fun longClickOnPhoto(fileName: String) =
|
||||
photoWithName(fileName).longClickTo(this)
|
||||
|
||||
private fun photoWithName(name: String) = linkWithName(name)
|
||||
.withItemType(ItemType.File)
|
||||
.withLayoutType(LayoutType.Grid)
|
||||
|
||||
fun dismissPhotoSavedToDrive(count: Int) = node.withText(
|
||||
targetContext.resources.getQuantityString(
|
||||
I18N.plurals.in_app_notification_add_to_stream_success,
|
||||
count
|
||||
).format(count)
|
||||
).clickTo(AlbumsTabRobot)
|
||||
|
||||
fun assertMoreButtonIsDisplayed() = moreButton.await { assertIsDisplayed() }
|
||||
|
||||
fun assertAlbumNameIsDisplayed(name: String) = node.withText(name)
|
||||
.await { assertIsDisplayed() }
|
||||
|
||||
@@ -54,11 +87,34 @@ object AlbumRobot : LinksRobot, NavigationBarRobot {
|
||||
).format(count)
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
fun assertVisibleMediaItemsInAlbum(count: Int) {
|
||||
waitUntilMediaItemsCountEquals(count)
|
||||
}
|
||||
|
||||
private fun waitUntilMediaItemsCountEquals(
|
||||
expectedCount: Int,
|
||||
timeout: Duration = 10.seconds,
|
||||
interval: Duration = 1.seconds,
|
||||
) {
|
||||
val nodes = mediaItems
|
||||
.withItemType(ItemType.File)
|
||||
.withLayoutType(LayoutType.Grid)
|
||||
nodes.waitFor(timeout, interval) {
|
||||
nodes.interaction.assertCountEquals(expectedCount)
|
||||
}
|
||||
}
|
||||
|
||||
fun assertCoverAlbum(name: String) = node.withLinkName(name)
|
||||
.withItemType(ItemType.File)
|
||||
.withLayoutType(LayoutType.Cover)
|
||||
.await { assertIsDisplayed() }
|
||||
|
||||
fun assertEmptyAlbum() = emptyAlbumText.await { assertIsDisplayed() }
|
||||
|
||||
fun assertAddToFavoriteFromForeignVolume() = favoriteFromForeignVolume.await { assertIsDisplayed() }
|
||||
|
||||
fun dismissAddToAlbumStartMessage() = addToAlbumStartMessage.clickTo(AlbumRobot)
|
||||
|
||||
override fun robotDisplayed() {
|
||||
albumScreen.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
@@ -18,9 +18,13 @@
|
||||
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import me.proton.android.drive.photos.presentation.component.ProtonPreviewAlbumItemTestTags
|
||||
import me.proton.android.drive.photos.presentation.extension.albumDetails
|
||||
import me.proton.android.drive.ui.screen.PhotosAndAlbumsScreenTestTag
|
||||
import me.proton.android.drive.ui.test.AbstractBaseTest.Companion.targetContext
|
||||
import me.proton.core.drive.base.domain.entity.TimestampS
|
||||
import me.proton.core.test.android.instrumented.utils.StringUtils
|
||||
import me.proton.test.fusion.Fusion.allNodes
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
@@ -31,7 +35,8 @@ object AlbumsTabRobot :
|
||||
HomeRobot,
|
||||
LinksRobot,
|
||||
NavigationBarRobot {
|
||||
private val albumTitle get() = node.withText(I18N.string.albums_title)
|
||||
private val albumTitleTab get() = node.withTag(PhotosAndAlbumsScreenTestTag.albumsTitleTab)
|
||||
private val photosTitleTab get() = node.withTag(PhotosAndAlbumsScreenTestTag.photosTitleTab)
|
||||
|
||||
private val filterAll get() = node.withText(I18N.string.albums_filter_all)
|
||||
private val filterMyAlbums get() = node.withText(I18N.string.albums_filter_my_albums)
|
||||
@@ -41,8 +46,14 @@ object AlbumsTabRobot :
|
||||
private val emptyTitle get() = node.withText(I18N.string.albums_empty_albums_list_screen_title)
|
||||
private val emptyDescription get() = node.withText(I18N.string.albums_empty_albums_list_screen_description)
|
||||
|
||||
private val emptySharedWithMeTitle get() = node.withText(I18N.string.albums_empty_albums_shared_with_me_screen_title)
|
||||
private val emptySharedWithMeDescription get() = node.withText(I18N.string.albums_empty_albums_shared_with_me_screen_description)
|
||||
|
||||
private val plusButton get() = node.withContentDescription(I18N.string.content_description_albums_new)
|
||||
|
||||
private val previewAlbumItems get() = allNodes.withTag(ProtonPreviewAlbumItemTestTags.albumName)
|
||||
|
||||
fun clickOnPhotosTitleTab() = photosTitleTab.clickTo(PhotosTabRobot)
|
||||
|
||||
fun clickOnFilterAll() = apply {
|
||||
filterAll.click()
|
||||
@@ -60,12 +71,28 @@ object AlbumsTabRobot :
|
||||
filterSharedWithMe.click()
|
||||
}
|
||||
|
||||
fun swipeFiltersToEnd() = apply {
|
||||
filterSharedWithMe.onParent().swipeLeft()
|
||||
}
|
||||
|
||||
fun clickPlusButton() = plusButton.clickTo(CreateAlbumTabRobot)
|
||||
|
||||
fun clickUserInvitation(count: Int) = node.withText(
|
||||
StringUtils.pluralStringFromResource(
|
||||
I18N.plurals.shared_with_me_album_invitations_banner_description,
|
||||
count,
|
||||
count
|
||||
)
|
||||
).clickTo(UserInvitationRobot)
|
||||
|
||||
fun assertAlbumIsDisplayed(name: String) {
|
||||
node.withText(name).await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertAtLeastOneAlbumIsDisplayed() {
|
||||
previewAlbumItems.onFirst().await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertAlbumIsDisplayed(
|
||||
name: String,
|
||||
photoCount: Long,
|
||||
@@ -95,9 +122,14 @@ object AlbumsTabRobot :
|
||||
emptyDescription.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
fun assertIsEmptySharedWithMe() {
|
||||
emptySharedWithMeTitle.await { assertIsDisplayed() }
|
||||
emptySharedWithMeDescription.await { assertIsDisplayed() }
|
||||
}
|
||||
|
||||
override fun robotDisplayed() {
|
||||
homeScreenDisplayed()
|
||||
photosTab.assertIsSelected()
|
||||
albumTitle.assertIsDisplayed()
|
||||
albumTitleTab.assertIsDisplayed()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Proton AG.
|
||||
* This file is part of Proton Drive.
|
||||
*
|
||||
* Proton Drive is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Proton Drive is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Proton Drive. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package me.proton.android.drive.ui.robot
|
||||
|
||||
import me.proton.android.drive.ui.dialog.ConfirmLeaveAlbumDialogTestTag
|
||||
import me.proton.core.test.android.instrumented.FusionConfig
|
||||
import me.proton.test.fusion.Fusion.node
|
||||
import me.proton.core.drive.i18n.R as I18N
|
||||
|
||||
object ConfirmLeaveAlbumRobot : Robot {
|
||||
private val dialog get() = node.withTag(ConfirmLeaveAlbumDialogTestTag.confirmLeaveAlbum)
|
||||
|
||||
private val cancelButton = node.withText(I18N.string.common_cancel_action)
|
||||
private val leaveWithoutSavingButton = node.withText(I18N.string.albums_leave_album_dialog_leave_without_saving_action)
|
||||
private val saveAndLeaveButton = node.withText(I18N.string.albums_leave_album_dialog_save_and_leave_action)
|
||||
|
||||
fun <T : Robot> clickOnCancel(goesTo: T) = cancelButton.clickTo(goesTo)
|
||||
fun <T : Robot> clickOnLeaveWithoutSaving(goesTo: T) = leaveWithoutSavingButton.clickTo(goesTo)
|
||||
fun <T : Robot> clickOnSaveAndLeave(goesTo: T) = saveAndLeaveButton.clickTo(goesTo)
|
||||
|
||||
fun assertDescriptionIsDisplayed(album: String) =
|
||||
node.withText(
|
||||
FusionConfig.targetContext().getString(I18N.string.albums_leave_album_dialog_description, album)
|
||||
).await { assertIsDisplayed() }
|
||||
|
||||
override fun robotDisplayed() {
|
||||
dialog.await { assertIsDisplayed() }
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,7 @@ object CreateAlbumTabRobot : Robot {
|
||||
.hasDescendant(node.withText(I18N.string.albums_new_album_name_hint))
|
||||
|
||||
private val doneButton get() = node.withText(I18N.string.common_done_action)
|
||||
private val shareButton get() = node.withText(I18N.string.common_share)
|
||||
private val addButton get() = node.withText(I18N.string.common_add_action)
|
||||
|
||||
fun typeName(text: String) = apply { newAlbumHint.typeText(text) }
|
||||
@@ -42,7 +43,10 @@ object CreateAlbumTabRobot : Robot {
|
||||
|
||||
fun <T : Robot> clickOnDone(goesTo: T) = doneButton.clickTo(goesTo)
|
||||
|
||||
fun clickOnAdd() = addButton.clickTo(PickerPhotosAndAlbumsRobot)
|
||||
fun clickOnAdd() = addButton.clickTo(PickerPhotosTabRobot)
|
||||
|
||||
fun <T : Robot> clickOnShare(goesTo: T) = shareButton.clickTo(goesTo)
|
||||
|
||||
|
||||
override fun robotDisplayed() {
|
||||
newAlbumHint.assertIsDisplayed()
|
||||
|
||||